Replace all 5 direct recharts usages with the shared, token-themed SVG
primitives, lazy-loaded for bundle savings:
- dashboard, usage, users/[id], ops/client-logs, extraction/entity-chart
now render AreaChart/BarChart/Donut from @bytelyst/charts.
- new src/components/charts: next/dynamic wrappers (own chunk, ssr:false)
that dynamic-import a local static re-export (primitives.tsx) — the chart
packages declare only an `import` export condition, so a direct
import('@bytelyst/charts') trips Next's resolver.
- new src/lib/chart-data.ts: pure, finite-safe data mappers (unit-tested).
- recharts removed from package.json + the admin-web lockfile importer entry
(now fully unused). Lockfile delta is importer-only (+charts/+data-viz as
workspace:*, -recharts); no monorepo re-normalization; --frozen-lockfile clean.
- vitest.config: inline @bytelyst/{charts,data-viz} + dedupe react so the
SSR no-NaN render tests use a single React copy.
Fidelity notes (charts are single-series/vertical; StackedBar is charts 0.2.x):
stacked severity chart -> single bars colored by dominant severity; pie charts
-> Donut; horizontal bars -> vertical.
Verify: typecheck+lint+build green (123 routes); vitest 18 files / 159 tests
(+19); format:check no new failures; e2e 11 passed / 80 failed (unchanged vs
UX-1 baseline — failures are environmental, no backend).
Generated with [Devin](https://cli.devin.ai/docs)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
51 lines
1.7 KiB
TypeScript
51 lines
1.7 KiB
TypeScript
/**
|
|
* Pure data-shaping helpers for the `@bytelyst/charts` / `@bytelyst/data-viz`
|
|
* migration (UX-2). Kept framework-free so they are unit-testable in the node
|
|
* vitest env and guarantee finite, NaN-free output before it ever reaches an
|
|
* SVG chart (the chart primitives also filter non-finite values defensively).
|
|
*/
|
|
import type { BarDatum } from '@bytelyst/charts';
|
|
import type { DonutSlice } from '@bytelyst/charts';
|
|
|
|
/** Coerce to a finite number, falling back to 0 for null/NaN/Infinity. */
|
|
export function finite(n: unknown): number {
|
|
const v = typeof n === 'number' ? n : Number(n);
|
|
return Number.isFinite(v) ? v : 0;
|
|
}
|
|
|
|
/** Map a list of rows to a finite numeric series (X = array index). */
|
|
export function seriesValues<T>(rows: readonly T[], key: keyof T): number[] {
|
|
return rows.map(r => finite(r[key]));
|
|
}
|
|
|
|
/**
|
|
* Map dated rows to `BarDatum[]`, showing an X label only every `labelEvery`
|
|
* bars (date `MM-DD`) so dense 30/90-day series don't overlap. Empty string
|
|
* suppresses a label without dropping the bar.
|
|
*/
|
|
export function dateBars<T extends { date: string }>(
|
|
rows: readonly T[],
|
|
valueKey: keyof T,
|
|
labelEvery = 5
|
|
): BarDatum[] {
|
|
return rows.map((r, i) => ({
|
|
id: r.date,
|
|
value: finite(r[valueKey]),
|
|
label: i % labelEvery === 0 ? String(r.date).slice(5) : '',
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Map breakdown rows to `DonutSlice[]`, dropping non-positive slices (a Donut
|
|
* of all-zero data renders an empty muted ring rather than NaN arcs).
|
|
*/
|
|
export function donutSlices<T>(
|
|
rows: readonly T[],
|
|
idKey: keyof T,
|
|
valueKey: keyof T
|
|
): DonutSlice[] {
|
|
return rows
|
|
.map(r => ({ id: String(r[idKey]), label: String(r[idKey]), value: finite(r[valueKey]) }))
|
|
.filter(s => s.value > 0);
|
|
}
|