/** * 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(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( 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( 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); }