github.com/pyroscope-io/pyroscope@v0.37.3-0.20230725203016-5f6947968bd0/packages/pyroscope-flamegraph/src/Tooltip/FlamegraphTooltip.tsx (about) 1 import React, { useCallback, RefObject, Dispatch, SetStateAction } from 'react'; 2 import { Maybe } from 'true-myth'; 3 import type { Unwrapped } from 'true-myth/maybe'; 4 import { Units } from '@pyroscope/models/src/units'; 5 import { 6 getFormatter, 7 numberWithCommas, 8 formatPercent, 9 ratioToPercent, 10 diffPercent, 11 } from '../format/format'; 12 import { 13 FlamegraphPalette, 14 DefaultPalette, 15 } from '../FlameGraph/FlameGraphComponent/colorPalette'; 16 17 import { Tooltip, TooltipData } from './Tooltip'; 18 19 type xyToDataSingle = ( 20 x: number, 21 y: number 22 ) => Maybe<{ format: 'single'; name: string; total: number }>; 23 24 type xyToDataDouble = ( 25 x: number, 26 y: number 27 ) => Maybe<{ 28 format: 'double'; 29 name: string; 30 totalLeft: number; 31 totalRight: number; 32 barTotal: number; 33 }>; 34 35 export type FlamegraphTooltipProps = { 36 canvasRef: RefObject<HTMLCanvasElement>; 37 38 units: Units; 39 sampleRate: number; 40 numTicks: number; 41 leftTicks: number; 42 rightTicks: number; 43 44 palette: FlamegraphPalette; 45 } & ( 46 | { format: 'single'; xyToData: xyToDataSingle } 47 | { 48 format: 'double'; 49 leftTicks: number; 50 rightTicks: number; 51 xyToData: xyToDataDouble; 52 } 53 ); 54 55 export default function FlamegraphTooltip(props: FlamegraphTooltipProps) { 56 const { 57 format, 58 canvasRef, 59 xyToData, 60 numTicks, 61 sampleRate, 62 units, 63 leftTicks, 64 rightTicks, 65 palette, 66 } = props; 67 68 const setTooltipContent = useCallback( 69 ( 70 setContent: Dispatch< 71 SetStateAction<{ 72 title: { 73 text: string; 74 diff: { 75 text: string; 76 color: string; 77 }; 78 }; 79 tooltipData: TooltipData[]; 80 }> 81 >, 82 onMouseOut: () => void, 83 e: MouseEvent 84 ) => { 85 const formatter = getFormatter(numTicks, sampleRate, units); 86 const opt = xyToData(e.offsetX, e.offsetY); 87 88 let data: Unwrapped<typeof opt>; 89 90 // waiting on 91 // https://github.com/true-myth/true-myth/issues/279 92 if (opt.isJust) { 93 data = opt.value; 94 } else { 95 onMouseOut(); 96 return; 97 } 98 99 // set the content for tooltip 100 switch (data.format) { 101 case 'single': { 102 const newLeftContent: TooltipData = { 103 percent: formatPercent(data.total / numTicks), 104 samples: 105 units === 'trace_samples' ? '' : numberWithCommas(data.total), 106 units, 107 formattedValue: formatter.format(data.total, sampleRate), 108 tooltipType: 'flamegraph', 109 }; 110 setContent({ 111 title: { 112 text: data.name, 113 diff: { 114 text: '', 115 color: '', 116 }, 117 }, 118 tooltipData: [newLeftContent], 119 }); 120 121 break; 122 } 123 124 case 'double': { 125 if (format === 'single') { 126 throw new Error( 127 "props format is 'single' but it has been mapped to 'double'" 128 ); 129 } 130 131 const d = formatDouble( 132 { 133 formatter, 134 sampleRate, 135 totalLeft: data.totalLeft, 136 leftTicks, 137 totalRight: data.totalRight, 138 rightTicks, 139 title: data.name, 140 units, 141 }, 142 palette 143 ); 144 145 setContent({ 146 title: d.title, 147 tooltipData: d.tooltipData, 148 }); 149 150 break; 151 } 152 default: 153 throw new Error(`Unsupported format:'`); 154 } 155 }, 156 [numTicks, sampleRate, units, leftTicks, rightTicks, palette] 157 ); 158 159 return ( 160 <Tooltip 161 dataSourceRef={canvasRef} 162 clickInfoSide="right" 163 setTooltipContent={setTooltipContent} 164 /> 165 ); 166 } 167 168 interface Formatter { 169 format(samples: number, sampleRate: number): string; 170 } 171 172 export function formatDouble( 173 { 174 formatter, 175 sampleRate, 176 totalLeft, 177 leftTicks, 178 totalRight, 179 rightTicks, 180 title, 181 units, 182 }: { 183 formatter: Formatter; 184 sampleRate: number; 185 totalLeft: number; 186 leftTicks: number; 187 totalRight: number; 188 rightTicks: number; 189 title: string; 190 units: Units; 191 }, 192 palette: FlamegraphPalette = DefaultPalette 193 ): { 194 tooltipData: TooltipData[]; 195 title: { 196 text: string; 197 diff: { 198 text: string; 199 color: string; 200 }; 201 }; 202 } { 203 const leftRatio = totalLeft / leftTicks; 204 const rightRatio = totalRight / rightTicks; 205 206 const leftPercent = ratioToPercent(leftRatio); 207 const rightPercent = ratioToPercent(rightRatio); 208 209 const newLeft: TooltipData = { 210 percent: `${leftPercent}%`, 211 samples: numberWithCommas(totalLeft), 212 units, 213 formattedValue: formatter.format(totalLeft, sampleRate), 214 tooltipType: 'flamegraph', 215 }; 216 217 const newRight: TooltipData = { 218 percent: `${rightPercent}%`, 219 samples: numberWithCommas(totalRight), 220 units, 221 formattedValue: formatter.format(totalRight, sampleRate), 222 tooltipType: 'flamegraph', 223 }; 224 225 const totalDiff = diffPercent(leftPercent, rightPercent); 226 227 let tooltipDiffColor = ''; 228 if (totalDiff > 0) { 229 tooltipDiffColor = palette.badColor.rgb().string(); 230 } else if (totalDiff < 0) { 231 tooltipDiffColor = palette.goodColor.rgb().string(); 232 } 233 234 let tooltipDiffText = ''; 235 if (!totalLeft) { 236 // this is a new function 237 tooltipDiffText = '(new)'; 238 } else if (!totalRight) { 239 // this function has been removed 240 tooltipDiffText = '(removed)'; 241 } else if (totalDiff > 0) { 242 tooltipDiffText = `(+${totalDiff.toFixed(2)}%)`; 243 } else if (totalDiff < 0) { 244 tooltipDiffText = `(${totalDiff.toFixed(2)}%)`; 245 } 246 247 return { 248 title: { 249 text: title, 250 diff: { 251 text: tooltipDiffText, 252 color: tooltipDiffColor, 253 }, 254 }, 255 tooltipData: [newLeft, newRight], 256 }; 257 }