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%
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