73 lines
2.5 KiB
TypeScript
73 lines
2.5 KiB
TypeScript
"use client";
|
|||
|
|
|
||
|
|
import { CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts";
|
||
|
|
import { ChartContainer, ChartTooltip, ChartTooltipContent, type ChartConfig } from "@/components/ui/chart";
|
||
|
|
|
||
|
|
export interface TimelineRow {
|
||
|
|
bucket: number;
|
||
|
|
sessionDeviceId: number;
|
||
|
|
slotLabel: string;
|
||
|
|
avgValue: number;
|
||
|
|
maxValue: number;
|
||
|
|
}
|
||
|
|
|
||
|
|
const SERIES_COLORS = ["var(--chart-1)", "var(--chart-2)", "var(--chart-3)", "var(--chart-4)", "var(--chart-5)"];
|
||
|
|
|
||
|
|
// Chart config keys become raw CSS custom-property names (--color-<key>), so
|
||
|
|
// they must be safe identifiers - a user-chosen device display name isn't.
|
||
|
|
// Use the numeric session_device_id as the key, slotLabel only as display text.
|
||
|
|
function seriesKey(sessionDeviceId: number): string {
|
||
|
|
return `device_${sessionDeviceId}`;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function SessionTimelineChart({ timeline }: { timeline: TimelineRow[] }) {
|
||
|
|
const series = [...new Map(timeline.map((r) => [r.sessionDeviceId, r.slotLabel])).entries()];
|
||
|
|
const buckets = [...new Set(timeline.map((r) => r.bucket))].sort((a, b) => a - b);
|
||
|
|
|
||
|
|
const data = buckets.map((bucket) => {
|
||
|
|
const row: Record<string, number> = { bucket };
|
||
|
|
for (const [sessionDeviceId] of series) {
|
||
|
|
const match = timeline.find((r) => r.bucket === bucket && r.sessionDeviceId === sessionDeviceId);
|
||
|
|
row[seriesKey(sessionDeviceId)] = match ? Math.round(match.avgValue * 100) : 0;
|
||
|
|
}
|
||
|
|
return row;
|
||
|
|
});
|
||
|
|
|
||
|
|
const config: ChartConfig = Object.fromEntries(
|
||
|
|
series.map(([sessionDeviceId, slotLabel], i) => [
|
||
|
|
seriesKey(sessionDeviceId),
|
||
|
|
{ label: slotLabel, color: SERIES_COLORS[i % SERIES_COLORS.length] },
|
||
|
|
]),
|
||
|
|
);
|
||
|
|
|
||
|
|
if (timeline.length === 0) {
|
||
|
|
return <p className="text-sm text-muted-foreground">No device activity recorded for this session.</p>;
|
||
|
|
}
|
||
|
|
|
||
|
|
return (
|
||
|
|
<ChartContainer config={config} className="h-64 w-full">
|
||
|
|
<LineChart data={data}>
|
||
|
|
<CartesianGrid vertical={false} />
|
||
|
|
<XAxis
|
||
|
|
dataKey="bucket"
|
||
|
|
tickFormatter={(v: number) => `${Math.round(v / 1000)}s`}
|
||
|
|
tickLine={false}
|
||
|
|
axisLine={false}
|
||
|
|
/>
|
||
|
|
<YAxis domain={[0, 100]} tickLine={false} axisLine={false} width={32} />
|
||
|
|
<ChartTooltip content={<ChartTooltipContent />} />
|
||
|
|
{series.map(([sessionDeviceId]) => (
|
||
|
|
<Line
|
||
|
|
key={sessionDeviceId}
|
||
|
|
type="monotone"
|
||
|
|
dataKey={seriesKey(sessionDeviceId)}
|
||
|
|
stroke={`var(--color-${seriesKey(sessionDeviceId)})`}
|
||
|
|
strokeWidth={2}
|
||
|
|
dot={false}
|
||
|
|
/>
|
||
|
|
))}
|
||
|
|
</LineChart>
|
||
|
|
</ChartContainer>
|
||
|
|
);
|
||
|
|
}
|