import { useMemo, useState, useCallback } from "react"; import { Line, LineChart, XAxis, YAxis, Legend } from "recharts"; import { ChartContainer, ChartTooltip, type ChartConfig, } from "@/src/components/ui/chart"; import { type IntervalConfig, type TimeRange, } from "@/src/utils/date-range-utils"; import { compactNumberFormatter } from "@/src/utils/numbers"; import { formatChartTimestamp } from "../../lib/chart-formatters"; import { ScoreChartTooltip } from "../../lib/ScoreChartTooltip"; import { ScoreChartLegendContent } from "./ScoreChartLegendContent"; export interface NumericTimeSeriesChartProps { data: Array<{ timestamp: Date; avg1: number | null; avg2: number | null; count: number; }>; score1Name: string; score2Name?: string; interval: IntervalConfig; timeRange: TimeRange; colors: { score1: string; score2?: string }; } /** * Numeric time series chart component * Renders line charts for numeric score time series * - Single score: One line * - Two scores: Two lines for comparison */ export function ScoreTimeSeriesNumericChart({ data, score1Name, score2Name, interval, timeRange, colors, }: NumericTimeSeriesChartProps) { const isComparisonMode = Boolean(score2Name); // Transform data for Recharts const chartData = useMemo(() => { return data.map((item) => { // Format timestamp based on interval and time range const timestamp = formatChartTimestamp( item.timestamp, interval, timeRange, ); if (isComparisonMode) { return { time_dimension: timestamp, [score1Name]: item.avg1, [score2Name!]: item.avg2, }; } return { time_dimension: timestamp, [score1Name]: item.avg1, }; }); }, [data, score1Name, score2Name, interval, timeRange, isComparisonMode]); // Visibility state for interactive legend (comparison mode only) const [hiddenKeys, setHiddenKeys] = useState>(new Set()); // Create visibility state object for legend const visibilityState = useMemo(() => { if (!isComparisonMode) return undefined; return { [score1Name]: !hiddenKeys.has(score1Name), ...(score2Name && { [score2Name]: !hiddenKeys.has(score2Name) }), }; }, [hiddenKeys, score1Name, score2Name, isComparisonMode]); // Toggle handler const handleVisibilityToggle = useCallback( (key: string, visible: boolean) => { setHiddenKeys((prev) => { const next = new Set(prev); if (visible) { next.delete(key); } else { next.add(key); } return next; }); }, [], ); const config: ChartConfig = useMemo(() => { if (isComparisonMode && score2Name) { return { [score1Name]: { label: score1Name, color: colors.score1, }, [score2Name]: { label: score2Name, color: colors.score2, }, }; } return { [score1Name]: { label: score1Name, color: colors.score1, }, }; }, [score1Name, score2Name, isComparisonMode, colors]); if (chartData.length === 0) { return (
No time series data available
); } // Check if all values are null (no data in the selected time range) const hasAnyData = chartData.some((item) => { if (isComparisonMode) { return item[score1Name] !== null || item[score2Name!] !== null; } return item[score1Name] !== null; }); if (!hasAnyData) { return (
No data points available for the selected time range
); } return ( compactNumberFormatter(value)} /> {isComparisonMode && score2Name && ( )} } /> } /> ); }