github.com/pyroscope-io/pyroscope@v0.37.3-0.20230725203016-5f6947968bd0/webapp/javascript/components/TimelineChart/Tooltip.plugin.tsx (about)

     1  import React from 'react';
     2  import * as ReactDOM from 'react-dom';
     3  import getFormatLabel from './getFormatLabel';
     4  import clamp from './clamp';
     5  import injectTooltip from './injectTooltip';
     6  import { ITooltipWrapperProps } from './TooltipWrapper';
     7  
     8  const TOOLTIP_WRAPPER_ID = 'explore_tooltip_parent';
     9  
    10  // TooltipCallbackProps refers to the data available for the tooltip body construction
    11  export interface TooltipCallbackProps {
    12    timeLabel: string;
    13    values: Array<{
    14      closest: number[];
    15      color: number[];
    16      // TODO: remove this
    17      tagName: string;
    18    }>;
    19    coordsToCanvasPos?: jquery.flot.axis['p2c'];
    20    canvasX?: number;
    21  }
    22  
    23  (function ($: JQueryStatic) {
    24    function init(plot: jquery.flot.plot & jquery.flot.plotOptions) {
    25      const exploreTooltip = injectTooltip($, TOOLTIP_WRAPPER_ID);
    26  
    27      const params = {
    28        canvasX: -1,
    29        canvasY: -1,
    30        pageX: -1,
    31        pageY: -1,
    32        xToTime: -1,
    33      };
    34  
    35      function onMouseMove(e: { pageX: number; pageY: number; which?: number }) {
    36        const offset = plot.getPlaceholder().offset()!;
    37        const plotOffset = plot.getPlotOffset();
    38  
    39        params.canvasX = clamp(
    40          0,
    41          plot.width(),
    42          e.pageX - offset.left - plotOffset.left
    43        );
    44        params.canvasY = clamp(
    45          0,
    46          plot.height(),
    47          e.pageY - offset.top - plotOffset.top
    48        );
    49        params.pageX = e.pageX;
    50        params.pageY = e.pageY;
    51      }
    52  
    53      function onMouseLeave() {
    54        params.canvasX = -1;
    55        params.canvasY = -1;
    56        params.pageX = -1;
    57        params.pageY = -1;
    58      }
    59  
    60      function onPlotHover(e: unknown, position: { x?: number }) {
    61        if (position.x) {
    62          params.xToTime = position.x;
    63        }
    64      }
    65  
    66      plot.hooks!.drawOverlay!.push(() => {
    67        const options = plot.getOptions() as jquery.flot.plotOptions & {
    68          onHoverDisplayTooltip?: (
    69            data: Omit<ITooltipWrapperProps & TooltipCallbackProps, 'children'>
    70          ) => React.ReactElement;
    71        };
    72        const { onHoverDisplayTooltip } = options;
    73        const { xaxis } = plot.getAxes() as ShamefulAny;
    74        const data = plot.getData();
    75  
    76        if (onHoverDisplayTooltip && exploreTooltip?.length) {
    77          const align = params.canvasX > plot.width() / 2 ? 'left' : 'right';
    78          const { timezone } = options.xaxis!;
    79  
    80          const timeLabel = getFormatLabel({
    81            date: params.xToTime,
    82            xaxis,
    83            timezone,
    84          });
    85  
    86          const values = data?.map((dataSeries, i) => {
    87            // Sometimes we also pass a tagName/color
    88            // Eg in tagExplorer page
    89            // TODO: use generics
    90            const d = dataSeries as jquery.flot.dataSeries & {
    91              tagName: string;
    92              color: { color: number[] };
    93            };
    94  
    95            let closest = null;
    96            let color = null;
    97            let tagName = String(i);
    98  
    99            if (d?.data?.length && params.xToTime && params.pageX > 0) {
   100              color = d?.color?.color;
   101              tagName = d.tagName;
   102              closest = (d?.data || []).reduce(function (prev, curr) {
   103                return Math.abs(curr?.[0] - params.xToTime) <
   104                  Math.abs(prev?.[0] - params.xToTime)
   105                  ? curr
   106                  : prev;
   107              });
   108            }
   109  
   110            return {
   111              closest,
   112              color,
   113              tagName,
   114            };
   115          });
   116  
   117          if (!values?.length) {
   118            return;
   119          }
   120  
   121          // Returns an element
   122          const Tooltip = onHoverDisplayTooltip({
   123            pageX: params.pageX,
   124            pageY: params.pageY,
   125            timeLabel,
   126            values,
   127            align,
   128            canvasX: params.canvasX,
   129  
   130            coordsToCanvasPos: plot.p2c.bind(plot),
   131          });
   132  
   133          // Type checking will be wrong if a React 18 app tries to use this code
   134          ReactDOM.render(Tooltip as ShamefulAny, exploreTooltip?.[0]);
   135        }
   136      });
   137  
   138      plot.hooks!.bindEvents!.push((p, eventHolder) => {
   139        eventHolder.mousemove(onMouseMove);
   140        eventHolder.mouseleave(onMouseLeave);
   141        plot.getPlaceholder().bind('plothover', onPlotHover);
   142      });
   143  
   144      plot.hooks!.shutdown!.push((p, eventHolder) => {
   145        eventHolder.unbind('mousemove', onMouseMove);
   146        eventHolder.unbind('mouseleave', onMouseLeave);
   147        plot.getPlaceholder().unbind('plothover', onPlotHover);
   148      });
   149    }
   150  
   151    $.plot.plugins.push({
   152      init,
   153      options: {},
   154      name: 'rich_tooltip',
   155      version: '1.0',
   156    });
   157  })(jQuery);