learning_ai_common_plat/packages/charts/src/utils.ts
saravanakumardb1 2eaec32849 feat(charts): @bytelyst/charts@0.1.0 — Wave 9.A.1-4 LineChart / BarChart / AreaChart / Donut / Gauge
A new package — pure-SVG token-themed chart primitives, zero deps,
SSR-safe. Slots into the existing dataviz family next to
@bytelyst/data-viz (which owns Sparkline + KpiCard + Heatmap).

──────────────────────────────────────────────────────────────────
Components
──────────────────────────────────────────────────────────────────
<LineChart>   Wave 9.A.1
  - Multi-series with per-series colour override
  - Smooth (catmull-rom) or straight polyline
  - 3-tick subtle Y grid + per-tick label
  - SSR-safe title id via useId()

<BarChart>    Wave 9.A.2
  - Negative-value support via configurable baseline
  - Per-bar colour override; per-bar accessible label
  - Compact-K tick labels

<AreaChart>   Wave 9.A.3
  - Single-series with gradient fill + line stroke
  - Zero-baseline included automatically when data >= 0

<Donut>       Wave 9.A.4 (a)
  - Categorical share-of-total ring
  - Token palette walks 6 colours; per-slice override
  - Empty / all-zero data renders a muted ring (no NaN slices)
  - Single near-100% slice collapses to a closed ring
  - centerContent slot via <foreignObject>

<Gauge>       Wave 9.A.4 (b)
  - Half-circle 'fuel-tank' dial
  - NaN / out-of-range clamped safely
  - Caption slot below the dial

──────────────────────────────────────────────────────────────────
Shared utilities (src/utils.ts) — also exported
──────────────────────────────────────────────────────────────────
  - linearScale, extent (NaN-safe), smoothPath, formatNumber

──────────────────────────────────────────────────────────────────
Quality gates
──────────────────────────────────────────────────────────────────
  ✓ pnpm -F @bytelyst/charts test  →  19/19 passing
    - utils:   3 cases (extent / linearScale / smoothPath edge cases)
    - Line:    3 cases (series count, colour override, useId aria)
    - Bar:     3 cases (count, diverging negative, colour)
    - Area:    2 cases (gradient + line, single-point safety)
    - Donut:   4 cases (slice count, empty ring, full-ring collapse,
                        centerContent slot)
    - Gauge:   4 cases (in-domain, clamp >max, NaN→min, caption)
  ✓ pnpm -F @bytelyst/charts build  →  tsc clean
  ✓ No console.log / no Math.random for ids
  ✓ All primitives honour the design-system anti-patterns doc

──────────────────────────────────────────────────────────────────
Deferred to 0.2.x
──────────────────────────────────────────────────────────────────
  - <StackedBar>, <RadarChart> (see roadmap §9.A)

Showcase routes + roadmap flips land in the paired commit.
2026-05-27 17:16:46 -07:00

65 lines
1.8 KiB
TypeScript

/**
* Shared utilities for chart primitives. Pure functions — no React.
*/
/** Linear scale: maps `[d0, d1]` → `[r0, r1]`. */
export function linearScale(
d0: number,
d1: number,
r0: number,
r1: number,
): (v: number) => number {
const dr = d1 - d0;
if (dr === 0) return () => (r0 + r1) / 2;
const mr = r1 - r0;
return (v) => r0 + ((v - d0) / dr) * mr;
}
/** Min/max of a numeric array, ignoring NaN/non-finite. */
export function extent(values: readonly number[]): [number, number] {
let min = Number.POSITIVE_INFINITY;
let max = Number.NEGATIVE_INFINITY;
for (const v of values) {
if (!Number.isFinite(v)) continue;
if (v < min) min = v;
if (v > max) max = v;
}
if (min === Number.POSITIVE_INFINITY) return [0, 1];
if (min === max) return [min - 1, max + 1];
return [min, max];
}
/** Format a number with `Intl.NumberFormat`, falling back to plain `.toString()`. */
export function formatNumber(
value: number,
opts: Intl.NumberFormatOptions = {},
): string {
try {
return new Intl.NumberFormat(undefined, opts).format(value);
} catch {
return String(value);
}
}
/** Build a smooth catmull-rom path through `pts`. */
export function smoothPath(pts: ReadonlyArray<[number, number]>): string {
if (pts.length === 0) return '';
if (pts.length === 1) {
const [x, y] = pts[0]!;
return `M ${x} ${y}`;
}
let d = `M ${pts[0]![0]} ${pts[0]![1]}`;
for (let i = 1; i < pts.length; i++) {
const [x0, y0] = pts[Math.max(0, i - 2)]!;
const [x1, y1] = pts[i - 1]!;
const [x2, y2] = pts[i]!;
const [x3, y3] = pts[Math.min(pts.length - 1, i + 1)]!;
const cp1x = x1 + (x2 - x0) / 6;
const cp1y = y1 + (y2 - y0) / 6;
const cp2x = x2 - (x3 - x1) / 6;
const cp2y = y2 - (y3 - y1) / 6;
d += ` C ${cp1x} ${cp1y}, ${cp2x} ${cp2y}, ${x2} ${y2}`;
}
return d;
}