SPB Git

spb/earth-now Public License

earth-now.co — real-time planetary dashboard: live world metrics modeled, not streamed.

TypeScript 93% Shell 2.3% SQL 1.4% JavaScript 1.3% Dockerfile 1.2% CSS 0.8%
3.2 KB · 98 lines typescript
Raw Blame History
1/**2 * earth-now.co3 * Author:  Simon-Pierre Boucher4 * Contact: contact@spboucher.ai5 * File:    packages/counter/src/format.ts6 * Purpose: All UI number formatting — sigFigs honesty cap, Intl.NumberFormat, rates and uncertainty; never inline toFixed elsewhere7 */89import type { DisplayHints } from "./counter-model.js";1011/** Round to n significant digits (the honesty cap: never display precision the model can't justify). */12export function roundToSigFigs(value: number, sigFigs: number): number {13  if (value === 0 || !Number.isFinite(value)) return value;14  const magnitude = Math.floor(Math.log10(Math.abs(value)));15  const factor = 10 ** (sigFigs - 1 - magnitude);16  return Math.round(value * factor) / factor;17}1819export interface FormatOptions {20  locale?: string;21  /**22   * Animated tickers keep the trailing digits moving (the units digit animates)23   * while static contexts (badge alt text, tooltips, share cards) apply the24   * sigFigs cap strictly. Default: true (strict).25   */26  applySigFigs?: boolean;27}2829/** Format a counter value according to its display hints. Fallback text on non-finite input. */30export function formatValue(31  value: number,32  hints: DisplayHints,33  options: FormatOptions = {},34): string {35  if (!Number.isFinite(value)) return "—";36  const { locale = "en", applySigFigs = true } = options;37  const scaled = value * (hints.scale ?? 1);38  const v =39    applySigFigs && hints.sigFigs !== undefined ? roundToSigFigs(scaled, hints.sigFigs) : scaled;40  return new Intl.NumberFormat(locale, {41    minimumFractionDigits: hints.decimals,42    maximumFractionDigits: hints.decimals,43  }).format(v);44}4546/** Format an instantaneous rate at a human-friendly cadence (/s, /min, /h). */47export function formatRate(48  perSecond: number,49  hints: DisplayHints,50  options: FormatOptions = {},51): string {52  if (!Number.isFinite(perSecond)) return "—";53  const { locale = "en" } = options;54  const perSecondScaled = perSecond * (hints.scale ?? 1);55  const abs = Math.abs(perSecondScaled);56  let scaled = perSecondScaled;57  let suffix = "/s";58  if (abs < 1 / 60) {59    scaled = perSecondScaled * 3600;60    suffix = "/h";61  } else if (abs < 1) {62    scaled = perSecondScaled * 60;63    suffix = "/min";64  }65  const formatted = new Intl.NumberFormat(locale, {66    maximumSignificantDigits: Math.min(hints.sigFigs ?? 3, 3),67  }).format(scaled);68  return `${formatted}${suffix}`;69}7071/**72 * Compact display for chart axis ticks (8.31 Md / 8.31B) — never used for the73 * ticking value itself. Honors the display scale and caps significant digits.74 */75export function formatCompact(76  value: number,77  hints: DisplayHints,78  options: FormatOptions = {},79): string {80  if (!Number.isFinite(value)) return "—";81  const { locale = "en" } = options;82  const scaled = value * (hints.scale ?? 1);83  return new Intl.NumberFormat(locale, {84    notation: "compact",85    maximumSignificantDigits: Math.min(hints.sigFigs ?? 3, 4),86  }).format(scaled);87}8889/** Format a 90 % CI as "low – high" with the same hints (for tooltips and the methodology page). */90export function formatUncertainty(91  low: number,92  high: number,93  hints: DisplayHints,94  options: FormatOptions = {},95): string {96  return `${formatValue(low, hints, options)} – ${formatValue(high, hints, options)}`;97}98