import React, { useState, useRef, useMemo, useEffect, RefObject } from 'react'; import useResizeObserver from '@react-hook/resize-observer'; import Color from 'color'; import cl from 'classnames'; import { interpolateViridis } from 'd3-scale-chromatic'; import { getFormatter } from '@pyroscope/flamegraph/src/format/format'; import type { Heatmap as HeatmapType } from '@webapp/services/render'; import { SelectedAreaCoordsType, useHeatmapSelection, } from './useHeatmapSelection.hook'; import HeatmapTooltip from './HeatmapTooltip'; import { HEATMAP_HEIGHT, HEATMAP_COLORS } from './constants'; import { getTicks } from './utils'; // eslint-disable-next-line css-modules/no-unused-class import styles from './Heatmap.module.scss'; interface HeatmapProps { heatmap: HeatmapType; onSelection: ( minV: number, maxV: number, startT: number, endT: number ) => void; sampleRate: number; timezone: string; } export function Heatmap({ heatmap, onSelection, sampleRate, timezone, }: HeatmapProps) { const canvasRef = useRef(null); const heatmapRef = useRef(null); const resizedSelectedAreaRef = useRef(null); const [heatmapW, setHeatmapW] = useState(0); const { selectedCoordinates, selectedAreaToHeatmapRatio, resetSelection } = useHeatmapSelection({ canvasRef, resizedSelectedAreaRef, heatmapW, heatmap, onSelection, }); useEffect(() => { if (heatmapRef.current) { const { width } = heatmapRef.current.getBoundingClientRect(); setHeatmapW(width); } }, []); useResizeObserver(heatmapRef.current, (entry: ResizeObserverEntry) => { if (canvasRef.current) { // Firefox implements `contentBoxSize` as a single content rect, rather than an array const contentBoxSize = Array.isArray(entry.contentBoxSize) ? entry.contentBoxSize[0] : entry.contentBoxSize; canvasRef.current.width = contentBoxSize.inlineSize; setHeatmapW(contentBoxSize.inlineSize); } }); const getColor = useMemo( () => (x: number): string => { if (x === 0) { return Color.rgb(22, 22, 22).toString(); } // from 0 to 1 const colorIndex = (x - heatmap.minDepth) / heatmap.maxDepth; return interpolateViridis(colorIndex); }, [heatmap] ); const getLegendLabel = (index: number): string => { switch (index) { case 0: return heatmap.maxDepth.toString(); case 3: return Math.round( (heatmap.maxDepth - heatmap.minDepth) / 2 + heatmap.minDepth ).toString(); case 6: return heatmap.minDepth.toString(); default: return ''; } }; const heatmapGrid = (() => heatmap.values.map((column, colIndex) => ( // eslint-disable-next-line react/no-array-index-key {column.map((itemsCount: number, rowIndex: number, itemsCountArr) => ( ))} )))(); return (
{heatmapGrid}
Count {HEATMAP_COLORS.map((color, index) => (
{index % 3 === 0 && ( {getLegendLabel(index)} )}
))}
); } interface ResizedSelectedArea { resizedSelectedAreaRef: RefObject; containerW: number; start: SelectedAreaCoordsType; end: SelectedAreaCoordsType; resizeRatio: number; handleClick: () => void; } function ResizedSelectedArea({ resizedSelectedAreaRef, containerW, start, end, resizeRatio, handleClick, }: ResizedSelectedArea) { const top = start.y > end.y ? end.y : start.y; const originalLeftOffset = start.x > end.x ? end.x : start.x; const w = Math.abs(containerW / resizeRatio); const h = Math.abs(end.y - start.y); const left = Math.abs((originalLeftOffset * w) / (end.x - start.x || 1)); return ( <> {h ? (
) : null}
); } interface AxisProps { axis: 'x' | 'y'; min: number; max: number; ticksCount: number; timezone?: string; sampleRate?: number; } function Axis({ axis, max, min, ticksCount, timezone, sampleRate }: AxisProps) { const yAxisformatter = sampleRate && getFormatter(max, sampleRate, 'samples'); let ticks: string[]; ticks = getTicks( min, max, { timezone, formatter: yAxisformatter, ticksCount }, sampleRate ); // There's not enough data to construct the Y axis if (axis === 'y' && min === 0 && max === 0) { ticks = ['0']; } return (
{yAxisformatter ? (
{yAxisformatter.suffix}s
) : null}
{ticks.map((tick) => (
{tick}
))}
{ticks.map((tick) => (
))}
); }