learning_ai_common_plat/dashboards/tracker-web/src/components/overview-charts.tsx
saravanakumardb1 f2dfddf944 feat(tracker-web): data-viz overview with charts + KpiCards (UX-4)
Add @bytelyst/charts + @bytelyst/data-viz as workspace:* deps (minimal
importer link entries in the lockfile). Replace the badge-pill breakdowns
on /dashboard with KpiCards (total/open/in-progress/done) and a dynamically
imported chart surface: Donut for By Status + By Type (centered total) and
a BarChart for By Priority, coloured from the bridged --bl-chart-* palette.

Pure transforms live in src/lib/overview-charts.ts (finite-coerced, no NaN
reaches the SVG); the heavy chart surface is split into overview-charts.tsx
and loaded via next/dynamic (ssr:false) to keep it out of the route bundle.
Add overview-charts.test.tsx rendering the surface with mocked stats via
react-dom/server (no NaN in paths) + transform unit tests; dedupe react in
vitest so the SSR render uses a single React instance.

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-05-29 06:51:39 -07:00

79 lines
2.5 KiB
TypeScript

'use client';
import { Donut, BarChart } from '@bytelyst/charts';
import { Card, CardHeader, CardTitle } from '@/components/ui/Primitives';
import { statusSlices, typeSlices, priorityBars } from '@/lib/overview-charts';
import type { TrackerStats } from '@/lib/tracker-client';
/**
* Heavy chart surface for the dashboard overview (UX-4.2). Imported via
* next/dynamic from the overview page so the SVG chart code stays out of the
* initial route bundle.
*/
export default function OverviewCharts({ stats }: { stats: TrackerStats }) {
const statuses = statusSlices(stats);
const types = typeSlices(stats);
const priorities = priorityBars(stats);
return (
<div className="grid gap-4 md:grid-cols-3">
<Card className="flex flex-col items-center p-5">
<CardHeader className="w-full">
<CardTitle className="text-sm font-semibold uppercase tracking-wider text-muted-foreground">
By Status
</CardTitle>
</CardHeader>
<Donut
slices={statuses}
size={180}
ariaLabel="Items by status"
centerContent={<div className="text-2xl font-bold text-foreground">{stats.total}</div>}
/>
<ChartLegend slices={statuses} />
</Card>
<Card className="flex flex-col items-center p-5">
<CardHeader className="w-full">
<CardTitle className="text-sm font-semibold uppercase tracking-wider text-muted-foreground">
By Type
</CardTitle>
</CardHeader>
<Donut slices={types} size={180} ariaLabel="Items by type" />
<ChartLegend slices={types} />
</Card>
<Card className="p-5">
<CardHeader>
<CardTitle className="text-sm font-semibold uppercase tracking-wider text-muted-foreground">
By Priority
</CardTitle>
</CardHeader>
<BarChart
data={priorities}
width={360}
height={200}
ariaLabel="Items by priority"
className="w-full"
/>
</Card>
</div>
);
}
function ChartLegend({ slices }: { slices: { id: string; label?: string; color?: string }[] }) {
return (
<ul className="mt-4 flex w-full flex-wrap justify-center gap-x-4 gap-y-1 text-xs text-muted-foreground">
{slices.map(s => (
<li key={s.id} className="flex items-center gap-1.5">
<span
aria-hidden="true"
className="h-2 w-2 rounded-full"
style={{ background: s.color }}
/>
{s.label ?? s.id}
</li>
))}
</ul>
);
}