import { useCallback, useEffect, useRef } from 'react'; import uPlot from 'uplot'; export type BuildFn = ( el: HTMLDivElement, w: number, h: number, legendRef: React.RefObject, ) => uPlot | null; /** Converts a data index to the pixel x position uPlot expects for setCursor */ function idxToPixel(plot: uPlot, idx: number): number { const x = plot.data[0]?.[idx]; if (x == null) return -10; return plot.valToPos(x, 'x'); } /** * Manages full uPlot lifecycle: * - builds/rebuilds on dep change or resize * - mounts legend into legendRef container * - pins cursor to last data point on init (legend shows latest values) * - restores last-point legend on mouseleave */ export function usePlot( build: BuildFn, // eslint-disable-next-line @typescript-eslint/no-explicit-any deps: any[], ): { containerRef: React.RefObject; legendRef: React.RefObject } { const containerRef = useRef(null); const legendRef = useRef(null!); const plotRef = useRef(null); const lastIdxRef = useRef(0); const rebuild = useCallback(() => { const el = containerRef.current; if (!el) return; const w = el.clientWidth, h = el.clientHeight; if (w < 10 || h < 10) return; plotRef.current?.destroy(); if (legendRef.current) legendRef.current.innerHTML = ''; const plot = build(el, w, h, legendRef); plotRef.current = plot; if (plot) { const lastIdx = Math.max(0, (plot.data[0]?.length ?? 1) - 1); lastIdxRef.current = lastIdx; // Pin legend to latest data point plot.setCursor({ left: idxToPixel(plot, lastIdx), top: -10 }); // Defer mouseleave — prevents React hydration events firing before lastIdxRef set requestAnimationFrame(() => { plot.over.addEventListener('mouseleave', () => { const p = plotRef.current; if (!p) return; p.setCursor({ left: idxToPixel(p, lastIdxRef.current), top: -10 }); }); }); } // eslint-disable-next-line react-hooks/exhaustive-deps }, deps); useEffect(() => { rebuild(); return () => { plotRef.current?.destroy(); plotRef.current = null; }; }, [rebuild]); useEffect(() => { const el = containerRef.current; if (!el) return; const ro = new ResizeObserver(rebuild); ro.observe(el); return () => ro.disconnect(); }, [rebuild]); return { containerRef, legendRef }; }