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.7 KB · 102 lines typescript
Raw Blame History
1/**2 * earth-now.co3 * Author:  Simon-Pierre Boucher4 * Contact: contact@spboucher.ai5 * File:    apps/web/lib/derived.ts6 * Purpose: Pure derived-metric transforms used by LiveCounter — primary window, depletion countdown, model resolution, display hints7 */89import {10  counterValue,11  rateAt,12  windowValue,13  type CounterModel,14  type CounterWindow,15  type DisplayHints,16} from "@earth-now/counter";17import type { Locale, MetricSummary } from "./api";1819/** Mean Gregorian year in seconds (365.2425 × 86 400) — matches YEAR_SECONDS in @earth-now/counter. */20export const YEAR_SECONDS = 31_556_952;2122/**23 * The window a metric's card should display by default:24 * derived op=window → that window; cumulative → ytd; everything else → total.25 */26export function resolvePrimaryWindow(metric: MetricSummary): CounterWindow {27  if (metric.derived?.op === "window" && metric.derived.window !== undefined) {28    return metric.derived.window;29  }30  // The registry declares window priority: windows[0] is the primary reading31  // (e.g. cigarettes/coffee/GDP lead with "today", births with "ytd").32  const declared = metric.windows[0];33  if (declared === "today" || declared === "ytd" || declared === "session" || declared === "total") {34    return declared;35  }36  if (metric.kind === "cumulative") return "ytd";37  return "total";38}3940/**41 * Years until a depleting stock reaches zero at the current instantaneous rate:42 * value(t) / (−rate(t) × YEAR_SECONDS). NaN when the stock is not depleting43 * (rate ≥ 0) — the caller renders the "data pending" fallback.44 */45export function depletionYears(model: CounterModel, tMs: number): number {46  const remaining = counterValue(model, tMs);47  const perSecond = rateAt(model, tMs);48  const perYear = -perSecond * YEAR_SECONDS;49  if (!Number.isFinite(remaining) || !(perYear > 0)) return NaN;50  return remaining / perYear;51}5253/**54 * Model powering a metric's card: its own CounterModel when the API materialized55 * one, otherwise the model of its first derived input (rate-of / depletion-countdown56 * / window metrics are transforms over an input model).57 */58export function resolveModel(59  metric: MetricSummary,60  models: Record<string, CounterModel>,61): CounterModel | undefined {62  const own = models[metric.id];63  if (own !== undefined) return own;64  const inputId = metric.derived?.inputs[0]?.id;65  return inputId !== undefined ? models[inputId] : undefined;66}6768/** DisplayHints for the shared formatters, with the unit localized from the registry. */69export function hintsFor(metric: MetricSummary, locale: Locale): DisplayHints {70  return {71    decimals: metric.display.decimals,72    unit: metric.display.unit[locale],73    ...(metric.display.sigFigs !== undefined ? { sigFigs: metric.display.sigFigs } : {}),74    ...(metric.display.scale !== undefined ? { scale: metric.display.scale } : {}),75  };76}7778/**79 * Raw (unformatted) number a card displays at time t, applying the metric's80 * derived transform and window. Pure — time is a parameter. Returns NaN instead81 * of throwing when a session window has no session start yet.82 */83export function displayRawValue(84  metric: MetricSummary,85  model: CounterModel,86  tMs: number,87  window: CounterWindow,88  sessionStartMs?: number,89): number {90  const op = metric.derived?.op;91  if (op === "rate-of") return rateAt(model, tMs);92  if (op === "depletion-countdown") return depletionYears(model, tMs);93  if (window === "total") return counterValue(model, tMs);94  if (window === "session" && sessionStartMs === undefined) return NaN;95  return windowValue(model, tMs, window, sessionStartMs);96}9798/** Whether a card must carry the mandatory "estimation / estimate" label. */99export function isEstimate(metric: MetricSummary): boolean {100  return metric.uncertaintyFraction !== undefined;101}102