SPB Git forge

spb/rareindex

Public
54commits 1branches 0releases
7.1 MBsize
maindefault branch
10 days agolast push
TypeScript 61.9% HTML 37.2% SQL 0.7%
3.3 KB · 68 lines typescript
Raw Blame History
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