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  }