import { useState, useRef, useEffect, useMemo } from 'react'; import { Box, Column, BoxProps } from '@umami/react-zen'; import ChartJS, { LegendItem, ChartOptions, ChartData, UpdateMode } from 'chart.js/auto'; import { Legend } from '@/components/metrics/Legend'; import { DEFAULT_ANIMATION_DURATION } from '@/lib/constants'; ChartJS.defaults.font.family = 'Inter'; export interface ChartProps extends BoxProps { type?: 'bar' | 'bubble' | 'doughnut' | 'pie' | 'line' | 'polarArea' | 'radar' | 'scatter'; chartData?: ChartData & { focusLabel?: string }; chartOptions?: ChartOptions; updateMode?: UpdateMode; animationDuration?: number; onTooltip?: (model: any) => void; } export function Chart({ type, chartData, animationDuration = DEFAULT_ANIMATION_DURATION, updateMode, onTooltip, chartOptions, ...props }: ChartProps) { const canvas = useRef(null); const chart = useRef(null); const [legendItems, setLegendItems] = useState([]); const options = useMemo(() => { return { responsive: true, maintainAspectRatio: false, animation: { duration: animationDuration, resize: { duration: 0, }, active: { duration: 0, }, }, plugins: { legend: { display: false, }, tooltip: { enabled: false, intersect: true, external: onTooltip, }, }, ...chartOptions, }; }, [chartOptions]); const handleLegendClick = (item: LegendItem) => { if (type === 'bar') { const { datasetIndex } = item; const meta = chart.current.getDatasetMeta(datasetIndex); meta.hidden = meta.hidden === null ? !chart.current.data.datasets[datasetIndex]?.hidden : null; } else { const { index } = item; const meta = chart.current.getDatasetMeta(0); const hidden = !!meta?.data?.[index]?.hidden; meta.data[index].hidden = !hidden; chart.current.legend.legendItems[index].hidden = !hidden; } chart.current.update(updateMode); setLegendItems(chart.current.legend.legendItems); }; // Create chart useEffect(() => { if (canvas.current) { chart.current = new ChartJS(canvas.current, { type, data: chartData, options, }); setLegendItems(chart.current.legend.legendItems); } return () => { chart.current?.destroy(); }; }, []); // Update chart useEffect(() => { if (chart.current && chartData) { // Replace labels and datasets *in-place* chart.current.data.labels = chartData.labels; chart.current.data.datasets = chartData.datasets; if (chartData.focusLabel !== null) { chart.current.data.datasets.forEach((ds: { hidden: boolean; label: any }) => { ds.hidden = chartData.focusLabel ? ds.label !== chartData.focusLabel : false; }); } chart.current.options = options; chart.current.update(updateMode); setLegendItems(chart.current.legend.legendItems); } }, [chartData, options, updateMode]); return ( ); }