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 · 104 lines typescript
Raw Blame History
1/**2 * earth-now.co3 * Author:  Simon-Pierre Boucher4 * Contact: contact@spboucher.ai5 * File:    packages/widget/src/render.ts6 * Purpose: Pure, DOM-free widget helpers — config resolution, model payload parsing, display text building (unit-testable)7 */89import {10  type CounterModel,11  type CounterWindow,12  formatRate,13  formatValue,14  rateAt,15  windowValue,16} from "@earth-now/counter";1718/** Default API origin the widget talks to when data-api is not provided. */19export const DEFAULT_API = "https://www.earth-now.co";2021export interface WidgetConfig {22  metricId: string;23  window: CounterWindow;24  lang: string;25  api: string;26}2728/**29 * Resolve a widget config from a DOMStringMap-like object30 * (data-earth-now-metric / data-window / data-lang / data-api).31 * Returns null when no metric id is declared.32 */33export function resolveConfig(dataset: Partial<Record<string, string>>): WidgetConfig | null {34  const metricId = dataset["earthNowMetric"]?.trim();35  if (!metricId) return null;36  const w = dataset["window"];37  const window: CounterWindow =38    w === "today" || w === "ytd" || w === "session" || w === "total" ? w : "total";39  return {40    metricId,41    window,42    lang: dataset["lang"] ?? "en",43    api: (dataset["api"] ?? DEFAULT_API).replace(/\/+$/, ""),44  };45}4647/** URL of the CounterModel endpoint for a config. */48export function modelUrl(config: WidgetConfig): string {49  return `${config.api}/v1/metrics/${encodeURIComponent(config.metricId)}/model`;50}5152/**53 * Validate a fetched JSON payload into a CounterModel.54 * Accepts either the model itself or an `{ model: ... }` envelope.55 * Throws a descriptive error on anything malformed — the caller renders "data pending".56 */57export function parseModelPayload(json: unknown): CounterModel {58  const candidate =59    typeof json === "object" && json !== null && "model" in json60      ? (json as { model: unknown }).model61      : json;62  if (typeof candidate !== "object" || candidate === null)63    throw new Error("earth-now widget: model payload is not an object");64  const m = candidate as Partial<CounterModel>;65  if (typeof m.metricId !== "string") throw new Error("earth-now widget: model missing metricId");66  if (typeof m.anchorValue !== "number" || !Number.isFinite(m.anchorValue))67    throw new Error("earth-now widget: model missing finite anchorValue");68  if (typeof m.anchorTime !== "string" || Number.isNaN(Date.parse(m.anchorTime)))69    throw new Error("earth-now widget: model missing valid anchorTime");70  if (typeof m.rateFn !== "object" || m.rateFn === null || typeof m.rateFn.kind !== "string")71    throw new Error("earth-now widget: model missing rateFn");72  if (typeof m.displayHints !== "object" || m.displayHints === null)73    throw new Error("earth-now widget: model missing displayHints");74  return m as CounterModel;75}7677export interface DisplayText {78  /** Formatted counter value (applySigFigs false — the ticker animates the trailing digits). */79  value: string;80  /** Formatted instantaneous rate incl. cadence suffix, e.g. "4.3/s". */81  rate: string;82  /** Display unit label from the model's display hints. */83  unit: string;84}8586/**87 * Compute the text a widget shows at instant tMs — pure, time is a parameter.88 * "session" windows use the caller-provided session start (widget load time).89 */90export function buildDisplayText(91  model: CounterModel,92  tMs: number,93  config: WidgetConfig,94  sessionStartMs?: number,95): DisplayText {96  const v = windowValue(model, tMs, config.window, sessionStartMs);97  const opts = { locale: config.lang, applySigFigs: false };98  return {99    value: formatValue(v, model.displayHints, opts),100    rate: formatRate(rateAt(model, tMs), model.displayHints, { locale: config.lang }),101    unit: model.displayHints.unit,102  };103}104