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 { formatChartTimestamp } from "../../lib/chart-formatters"; import { ScoreChartTooltip } from "../../lib/ScoreChartTooltip"; import { ScoreChartLegendContent } from "./ScoreChartLegendContent"; export interface BooleanTimeSeriesChartProps { data: Array<{ timestamp: Date; category: string; count: number; }>; score1Name: string; score2Name?: string; interval: IntervalConfig; timeRange: TimeRange; colors: Record; } /** * Boolean time series chart component * Renders line charts showing counts for each category over time * One line per category with dynamic colors * Uses the same logic as CategoricalTimeSeriesChart */ export function ScoreTimeSeriesBooleanChart({ data, score1Name: _score1Name, score2Name: _score2Name, interval, timeRange, colors, }: BooleanTimeSeriesChartProps) { // Transform categorical data into pivot format for Recharts const { chartData, categories } = useMemo(() => { // Group by timestamp and collect all categories const groupedByTimestamp = new Map>(); const allCategories = new Set(); data.forEach((item) => { const timestampKey = item.timestamp.getTime(); if (!groupedByTimestamp.has(timestampKey)) { groupedByTimestamp.set(timestampKey, new Map()); } const categoryMap = groupedByTimestamp.get(timestampKey)!; categoryMap.set(item.category, item.count); allCategories.add(item.category); }); // Convert to chart data format // Sort by numeric timestamp BEFORE formatting to ensure chronological order const formattedData = Array.from(groupedByTimestamp.entries()) .sort(([tsA], [tsB]) => tsA - tsB) .map(([timestamp, categoryMap]) => { const formattedTimestamp = formatChartTimestamp( new Date(timestamp), interval, timeRange, ); const dataPoint: Record = { time_dimension: formattedTimestamp, }; // Add each category as a separate column allCategories.forEach((category) => { dataPoint[category] = categoryMap.get(category) ?? 0; }); return dataPoint; }); return { chartData: formattedData, categories: Array.from(allCategories).sort(), }; }, [data, interval, timeRange]); // Visibility state for interactive legend const [hiddenKeys, setHiddenKeys] = useState>(new Set()); // Create visibility state object for legend const visibilityState = useMemo(() => { const state: Record = {}; categories.forEach((category) => { state[category] = !hiddenKeys.has(category); }); return state; }, [hiddenKeys, categories]); // 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; }); }, [], ); // Create chart config with colors for each category const config: ChartConfig = useMemo(() => { const cfg: ChartConfig = {}; categories.forEach((category) => { cfg[category] = { label: category, color: colors[category] || Object.values(colors)[0], }; }); return cfg; }, [categories, colors]); if (chartData.length === 0 || categories.length === 0) { return (
No time series data available
); } // Check if all values are zero (no data in the selected time range) const hasAnyData = chartData.some((item) => categories.some((category) => (item[category] as number) > 0), ); if (!hasAnyData) { return (
No data points available for the selected time range
); } return ( value.toLocaleString()} /> {categories.map((category) => { const isHidden = hiddenKeys.has(category); return ( ); })} value.toLocaleString()} /> } /> } /> ); }