github.com/pyroscope-io/pyroscope@v0.37.3-0.20230725203016-5f6947968bd0/webapp/javascript/components/Heatmap/HeatmapTooltip.tsx (about) 1 import React, { 2 useRef, 3 useEffect, 4 RefObject, 5 useState, 6 useCallback, 7 } from 'react'; 8 9 import { getFormatter } from '@pyroscope/flamegraph/src/format/format'; 10 import TooltipWrapper from '@webapp/components/TimelineChart/TooltipWrapper'; 11 import type { Heatmap } from '@webapp/services/render'; 12 import { 13 getTimeDataByXCoord, 14 getBucketsDurationByYCoord, 15 timeFormatter, 16 } from './utils'; 17 import { HEATMAP_HEIGHT } from './constants'; 18 19 import styles from './HeatmapTooltip.module.scss'; 20 21 interface HeatmapTooltipProps { 22 dataSourceElRef: RefObject<HTMLCanvasElement>; 23 heatmapW: number; 24 heatmap: Heatmap; 25 timezone: string; 26 sampleRate: number; 27 } 28 29 function HeatmapTooltip({ 30 dataSourceElRef, 31 heatmapW, 32 heatmap, 33 timezone, 34 sampleRate, 35 }: HeatmapTooltipProps) { 36 const tooltipRef = useRef<HTMLDivElement>(null); 37 const [tooltipParams, setTooltipParams] = useState< 38 | { 39 pageX: number; 40 pageY: number; 41 time: string; 42 duration: string; 43 count: number; 44 } 45 | undefined 46 >(); 47 48 const formatter = timeFormatter(heatmap.startTime, heatmap.endTime, timezone); 49 const valueFormatter = getFormatter(heatmap.maxValue, sampleRate, 'samples'); 50 51 const memoizedOnMouseMove = useCallback( 52 (e: MouseEvent) => { 53 if (!tooltipRef || !tooltipRef.current) { 54 throw new Error('Missing tooltipElement'); 55 } 56 const canvas = dataSourceElRef.current as HTMLCanvasElement; 57 const { left, top } = canvas.getBoundingClientRect(); 58 59 const xCursorPosition = e.pageX - left; 60 const yCursorPosition = e.clientY - top; 61 const time = getTimeDataByXCoord(heatmap, heatmapW, xCursorPosition); 62 const bucketsDuration = getBucketsDurationByYCoord( 63 heatmap, 64 yCursorPosition 65 ); 66 const cellW = heatmapW / heatmap.timeBuckets; 67 const cellH = HEATMAP_HEIGHT / heatmap.valueBuckets; 68 69 const matrixCoords = [ 70 Math.trunc(xCursorPosition / cellW), 71 Math.trunc((HEATMAP_HEIGHT - yCursorPosition) / cellH), 72 ]; 73 74 // to fix tooltip on window edge 75 const maxPageX = window.innerWidth - 250; 76 77 setTooltipParams({ 78 pageX: e.pageX < maxPageX ? e.pageX - 10 : maxPageX, 79 pageY: e.pageY + 10, 80 time: formatter(time).toString(), 81 duration: valueFormatter.format(bucketsDuration, sampleRate), 82 count: heatmap.values[matrixCoords[0]][matrixCoords[1]], 83 }); 84 }, 85 [tooltipRef, setTooltipParams, heatmapW, heatmap, timezone, dataSourceElRef] 86 ); 87 88 // to show tooltip when move mouse over selected area 89 const handleWindowMouseMove = (e: MouseEvent) => { 90 if ( 91 (e.target as HTMLCanvasElement).id !== 'selectionCanvas' && 92 (e.target as HTMLCanvasElement).id !== 'selectionArea' 93 ) { 94 window.removeEventListener('mousemove', memoizedOnMouseMove); 95 setTooltipParams(undefined); 96 } else { 97 memoizedOnMouseMove(e); 98 } 99 }; 100 101 const handleMouseEnter = () => { 102 window.addEventListener('mousemove', handleWindowMouseMove); 103 }; 104 105 useEffect(() => { 106 // use closure to "cache" the current dataSourceRef(canvas/table) reference 107 // so that when cleaning up, it points to a valid canvas 108 // (otherwise it would be null) 109 const dataSourceEl = dataSourceElRef.current; 110 if (!dataSourceEl) { 111 return () => {}; 112 } 113 114 dataSourceEl.addEventListener('mouseenter', handleMouseEnter); 115 116 return () => { 117 dataSourceEl.removeEventListener('mouseenter', handleMouseEnter); 118 window.removeEventListener('mousemove', memoizedOnMouseMove); 119 window.removeEventListener('mousemove', handleWindowMouseMove); 120 }; 121 }, [dataSourceElRef.current, memoizedOnMouseMove]); 122 123 return ( 124 <div data-testid="heatmap-tooltip" ref={tooltipRef}> 125 {tooltipParams && ( 126 <TooltipWrapper 127 className={styles.tooltipWrapper} 128 align="right" 129 pageX={tooltipParams.pageX} 130 pageY={tooltipParams.pageY} 131 > 132 <p className={styles.tooltipHeader}>{tooltipParams.time}</p> 133 <div className={styles.tooltipBody}> 134 <div className={styles.dataRow}> 135 <span>Count: </span> 136 <span>{tooltipParams.count} profiles</span> 137 </div> 138 <div className={styles.dataRow}> 139 <span>Duration: </span> 140 <span>{tooltipParams.duration}</span> 141 </div> 142 </div> 143 </TooltipWrapper> 144 )} 145 </div> 146 ); 147 } 148 149 export default HeatmapTooltip;