github.com/pyroscope-io/pyroscope@v0.37.3-0.20230725203016-5f6947968bd0/webapp/javascript/components/Heatmap/useHeatmapSelection.hook.ts (about)

     1  import { useState, useEffect, RefObject } from 'react';
     2  
     3  import type { Heatmap } from '@webapp/services/render';
     4  import { HEATMAP_HEIGHT } from './constants';
     5  import { clearRect, drawRect, getSelectionData } from './utils';
     6  
     7  const DEFAULT_SELECTED_COORDINATES = { start: null, end: null };
     8  let startCoords: SelectedAreaCoordsType | null = null;
     9  let endCoords: SelectedAreaCoordsType | null = null;
    10  let selectedAreaToHeatmapRatio = 1;
    11  
    12  export type SelectedAreaCoordsType = Record<'x' | 'y', number>;
    13  interface SelectedCoordinates {
    14    start: SelectedAreaCoordsType | null;
    15    end: SelectedAreaCoordsType | null;
    16  }
    17  interface UseHeatmapSelectionProps {
    18    canvasRef: RefObject<HTMLCanvasElement>;
    19    resizedSelectedAreaRef: RefObject<HTMLDivElement>;
    20    heatmapW: number;
    21    heatmap: Heatmap;
    22    onSelection: (
    23      minV: number,
    24      maxV: number,
    25      startT: number,
    26      endT: number
    27    ) => void;
    28  }
    29  interface UseHeatmapSelection {
    30    selectedCoordinates: SelectedCoordinates;
    31    selectedAreaToHeatmapRatio: number;
    32    resetSelection: () => void;
    33  }
    34  
    35  export const useHeatmapSelection = ({
    36    canvasRef,
    37    resizedSelectedAreaRef,
    38    heatmapW,
    39    heatmap,
    40    onSelection,
    41  }: UseHeatmapSelectionProps): UseHeatmapSelection => {
    42    const [selectedCoordinates, setSelectedCoordinates] =
    43      useState<SelectedCoordinates>(DEFAULT_SELECTED_COORDINATES);
    44  
    45    const resetSelection = () => {
    46      setSelectedCoordinates(DEFAULT_SELECTED_COORDINATES);
    47      startCoords = null;
    48      endCoords = null;
    49    };
    50  
    51    const handleCellClick = (x: number, y: number) => {
    52      const cellW = heatmapW / heatmap.timeBuckets;
    53      const cellH = HEATMAP_HEIGHT / heatmap.valueBuckets;
    54  
    55      const matrixCoords = [
    56        Math.trunc(x / cellW),
    57        Math.trunc((HEATMAP_HEIGHT - y) / cellH),
    58      ];
    59  
    60      if (heatmap.values[matrixCoords[0]][matrixCoords[1]] === 0) {
    61        return;
    62      }
    63  
    64      // set startCoords and endCoords to draw selection rectangle for single cell
    65      startCoords = {
    66        x: (matrixCoords[0] + 1) * cellW,
    67        y: HEATMAP_HEIGHT - matrixCoords[1] * cellH,
    68      };
    69      endCoords = {
    70        x: matrixCoords[0] * cellW,
    71        y: HEATMAP_HEIGHT - (matrixCoords[1] + 1) * cellH,
    72      };
    73  
    74      const {
    75        selectionMinValue,
    76        selectionMaxValue,
    77        selectionStartTime,
    78        selectionEndTime,
    79      } = getSelectionData(
    80        heatmap,
    81        heatmapW,
    82        startCoords,
    83        endCoords,
    84        startCoords.y === HEATMAP_HEIGHT
    85      );
    86  
    87      onSelection(
    88        selectionMinValue,
    89        selectionMaxValue,
    90        selectionStartTime,
    91        selectionEndTime
    92      );
    93    };
    94  
    95    const startDrawing = (e: MouseEvent) => {
    96      window.addEventListener('mousemove', handleDrawingEvent);
    97      window.addEventListener('mouseup', endDrawing);
    98  
    99      const canvas = canvasRef.current as HTMLCanvasElement;
   100      const { left, top } = canvas.getBoundingClientRect();
   101      resetSelection();
   102  
   103      startCoords = { x: e.clientX - left, y: e.clientY - top };
   104    };
   105  
   106    const endDrawing = (e: MouseEvent) => {
   107      if (startCoords) {
   108        const canvas = canvasRef.current as HTMLCanvasElement;
   109        const { left, top, width, height } = canvas.getBoundingClientRect();
   110        clearRect(canvas);
   111  
   112        const xCursorPosition = e.clientX - left;
   113        const yCursorPosition = e.clientY - top;
   114        let xEnd;
   115        let yEnd;
   116  
   117        if (xCursorPosition < 0) {
   118          xEnd = 0;
   119        } else if (xCursorPosition > width) {
   120          xEnd = width;
   121        } else {
   122          xEnd = xCursorPosition;
   123        }
   124  
   125        if (yCursorPosition < 0) {
   126          yEnd = 0;
   127        } else if (yCursorPosition > height) {
   128          yEnd = parseInt(height.toFixed(0), 10);
   129        } else {
   130          yEnd = yCursorPosition;
   131        }
   132  
   133        endCoords = { x: xEnd, y: yEnd };
   134        const isClickEvent = startCoords.x === xEnd && startCoords.y === yEnd;
   135  
   136        if (isClickEvent) {
   137          handleCellClick(xEnd, yEnd);
   138        } else {
   139          const {
   140            selectionMinValue,
   141            selectionMaxValue,
   142            selectionStartTime,
   143            selectionEndTime,
   144          } = getSelectionData(heatmap, heatmapW, startCoords, endCoords);
   145  
   146          onSelection(
   147            selectionMinValue,
   148            selectionMaxValue,
   149            selectionStartTime,
   150            selectionEndTime
   151          );
   152        }
   153  
   154        window.removeEventListener('mousemove', handleDrawingEvent);
   155        window.removeEventListener('mouseup', endDrawing);
   156  
   157        const selectedAreaW = endCoords.x - startCoords.x;
   158        if (selectedAreaW) {
   159          selectedAreaToHeatmapRatio = Math.abs(width / selectedAreaW);
   160        } else {
   161          selectedAreaToHeatmapRatio = 1;
   162        }
   163      }
   164    };
   165  
   166    const handleDrawingEvent = (e: MouseEvent) => {
   167      const canvas = canvasRef.current as HTMLCanvasElement;
   168  
   169      if (canvas && startCoords) {
   170        const { left, top } = canvas.getBoundingClientRect();
   171  
   172        /**
   173         * Cursor coordinates inside canvas
   174         * @cursorXCoordinate - e.clientX - left
   175         * @cursorYCoordinate - e.clientY - top
   176         */
   177        const width = e.clientX - left - startCoords.x;
   178        const h = e.clientY - top - startCoords.y;
   179  
   180        drawRect(canvas, startCoords.x, startCoords.y, width, h);
   181      }
   182    };
   183  
   184    useEffect(() => {
   185      if (canvasRef.current) {
   186        canvasRef.current.addEventListener('mousedown', startDrawing);
   187      }
   188  
   189      if (resizedSelectedAreaRef.current) {
   190        resizedSelectedAreaRef.current.addEventListener(
   191          'mousedown',
   192          startDrawing
   193        );
   194      }
   195  
   196      return () => {
   197        if (canvasRef.current) {
   198          canvasRef.current.removeEventListener('mousedown', startDrawing);
   199        }
   200  
   201        if (resizedSelectedAreaRef.current) {
   202          resizedSelectedAreaRef.current.removeEventListener(
   203            'mousedown',
   204            startDrawing
   205          );
   206        }
   207  
   208        window.removeEventListener('mousemove', handleDrawingEvent);
   209        window.removeEventListener('mouseup', endDrawing);
   210      };
   211    }, [heatmap, heatmapW]);
   212  
   213    // set coordinates to display resizable selection rectangle (div element)
   214    useEffect(() => {
   215      if (startCoords && endCoords) {
   216        setSelectedCoordinates({
   217          start: { x: startCoords.x, y: startCoords.y },
   218          end: { x: endCoords.x, y: endCoords.y },
   219        });
   220      }
   221    }, [startCoords, endCoords]);
   222  
   223    return {
   224      selectedCoordinates,
   225      selectedAreaToHeatmapRatio,
   226      resetSelection,
   227    };
   228  };