Files
sexy/components/sessions/SessionTimelineChart.tsx
T

82 lines
3.1 KiB
TypeScript
Raw Permalink Normal View History

"use client";
import { Area, AreaChart, CartesianGrid, 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">
<AreaChart data={data}>
<defs>
{series.map(([sessionDeviceId]) => (
<linearGradient key={sessionDeviceId} id={`fill-${seriesKey(sessionDeviceId)}`} x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={`var(--color-${seriesKey(sessionDeviceId)})`} stopOpacity={0.5} />
<stop offset="95%" stopColor={`var(--color-${seriesKey(sessionDeviceId)})`} stopOpacity={0.03} />
</linearGradient>
))}
</defs>
<CartesianGrid vertical={false} stroke="var(--border)" />
<XAxis
dataKey="bucket"
tickFormatter={(v: number) => `${Math.round(v / 1000)}s`}
tickLine={false}
axisLine={false}
className="bp-readout text-xs"
/>
<YAxis domain={[0, 100]} tickLine={false} axisLine={false} width={32} className="bp-readout text-xs" />
<ChartTooltip content={<ChartTooltipContent />} />
{series.map(([sessionDeviceId]) => (
<Area
key={sessionDeviceId}
type="monotone"
dataKey={seriesKey(sessionDeviceId)}
stroke={`var(--color-${seriesKey(sessionDeviceId)})`}
strokeWidth={2}
fill={`url(#fill-${seriesKey(sessionDeviceId)})`}
/>
))}
</AreaChart>
</ChartContainer>
);
}