TypeScript 61.9%
HTML 37.2%
SQL 0.7%
1/** Tiny scale/format helpers shared by chart components (client-safe, no deps). */23export const SERIES_COLORS = ['var(--ri-series-1)', 'var(--ri-series-2)', 'var(--ri-series-3)', 'var(--ri-series-4)'] as const;45export function niceTicks(min: number, max: number, count = 4): number[] {6 if (!Number.isFinite(min) || !Number.isFinite(max)) return [];7 if (min === max) {8 const pad = Math.abs(min) * 0.1 || 1;9 min -= pad;10 max += pad;11 }12 const span = max - min;13 const step0 = span / Math.max(1, count);14 const mag = 10 ** Math.floor(Math.log10(step0));15 const norm = step0 / mag;16 const step = (norm >= 5 ? 10 : norm >= 2 ? 5 : norm >= 1 ? 2 : 1) * mag;17 const start = Math.ceil(min / step) * step;18 const out: number[] = [];19 for (let v = start; v <= max + 1e-9; v += step) out.push(Number(v.toFixed(10)));20 return out;21}2223export function compactNumber(v: number, opts: { currency?: boolean } = {}): string {24 const abs = Math.abs(v);25 const sign = v < 0 ? '-' : '';26 const pre = opts.currency ? '$' : '';27 if (abs >= 1e9) return `${sign}${pre}${(abs / 1e9).toFixed(abs >= 1e10 ? 0 : 1)}B`;28 if (abs >= 1e6) return `${sign}${pre}${(abs / 1e6).toFixed(abs >= 1e7 ? 0 : 1)}M`;29 if (abs >= 1e3) return `${sign}${pre}${(abs / 1e3).toFixed(abs >= 1e4 ? 0 : 1)}K`;30 if (abs >= 100) return `${sign}${pre}${abs.toFixed(0)}`;31 if (abs >= 1) return `${sign}${pre}${abs.toFixed(abs % 1 === 0 ? 0 : 2)}`;32 return `${sign}${pre}${abs.toFixed(2)}`;33}3435export function fullNumber(v: number, opts: { currency?: boolean; digits?: number } = {}): string {36 return new Intl.NumberFormat('en-US', { style: opts.currency ? 'currency' : 'decimal', currency: 'USD', maximumFractionDigits: opts.digits ?? (Math.abs(v) >= 1000 ? 0 : 2) }).format(v);37}3839export function parseDay(d: string): number {40 return new Date(d.length === 10 ? `${d}T00:00:00Z` : d).getTime();41}4243export function fmtDay(ms: number, span: number): string {44 const d = new Date(ms);45 if (span > 3 * 365 * 86_400_000) return d.toLocaleDateString('en-US', { year: 'numeric', timeZone: 'UTC' });46 if (span > 120 * 86_400_000) return d.toLocaleDateString('en-US', { month: 'short', year: '2-digit', timeZone: 'UTC' });47 return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', timeZone: 'UTC' });48}4950export function fmtDayFull(ms: number): string {51 return new Date(ms).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric', timeZone: 'UTC' });52}5354/** Diverging colour for percentage change: loss → neutral → gain, symmetric around 0 (clamped at ±limit). */55export function divergingColor(change: number | null | undefined, limit = 0.15): string {56 if (change === null || change === undefined || !Number.isFinite(change)) return 'var(--ri-bg-inset)';57 const t = Math.max(-1, Math.min(1, change / limit));58 const a = Math.abs(t);59 const alpha = 0.12 + a * 0.68;60 return t >= 0 ? `color-mix(in oklab, var(--ri-gain) ${Math.round(alpha * 100)}%, var(--ri-bg-elevated))` : `color-mix(in oklab, var(--ri-loss) ${Math.round(alpha * 100)}%, var(--ri-bg-elevated))`;61}6263/** Sequential colour (single hue) for magnitude in [0,1]. */64export function sequentialColor(t: number): string {65 const a = Math.max(0.08, Math.min(1, t));66 return `color-mix(in oklab, var(--ri-index) ${Math.round(a * 100)}%, var(--ri-bg-elevated))`;67}68