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 };