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;