SPB Git forge

spb/countryatlas

Public
20commits 1branches 0releases
268.3 MBsize
maindefault branch
12 days agolast push
TypeScript 57% Python 38.6% JavaScript 3.6% CSS 0.6%
3.0 KB · 87 lines typescript
Raw Blame History
1/** Small geometry helpers for the explorer map (client-safe, pure). */23export interface BBox {4  x: number;5  y: number;6  w: number;7  h: number;8}910/** Bounding box of an SVG path made of absolute M/L/Z commands (what d3 geoPath emits). */11export function pathBBox(d: string): BBox | null {12  const nums = d.match(/-?\d+(?:\.\d+)?/g);13  if (!nums || nums.length < 4) return null;14  let minX = Infinity;15  let minY = Infinity;16  let maxX = -Infinity;17  let maxY = -Infinity;18  for (let i = 0; i + 1 < nums.length; i += 2) {19    const x = Number(nums[i]);20    const y = Number(nums[i + 1]);21    if (!Number.isFinite(x) || !Number.isFinite(y)) continue;22    if (x < minX) minX = x;23    if (x > maxX) maxX = x;24    if (y < minY) minY = y;25    if (y > maxY) maxY = y;26  }27  if (minX === Infinity) return null;28  return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };29}3031/** Class index 0..breaks.length for a value against sorted quantile breaks. */32export function classIndex(value: number, breaks: number[]): number {33  let i = 0;34  while (i < breaks.length && value >= breaks[i]!) i++;35  return i;36}3738/** Map a class index (0..k-1) onto the 7-step sequential ramp (same rule as ChoroplethView). */39export function stepFor(cls: number, k: number): number {40  if (k <= 1) return 4;41  const start = k >= 6 ? 1 : 2;42  return Math.round(start + (cls / (k - 1)) * (7 - start));43}4445/** Median of finite numbers (null when empty). */46export function median(values: Array<number | null | undefined>): number | null {47  const v = values.filter((x): x is number => typeof x === 'number' && Number.isFinite(x)).sort((a, b) => a - b);48  if (!v.length) return null;49  const mid = Math.floor(v.length / 2);50  return v.length % 2 ? v[mid]! : (v[mid - 1]! + v[mid]!) / 2;51}5253/** Equal-width histogram (log10 when `log`), 20 bins by default. */54export function histogramOf(values: number[], bins = 20, log = false): { edges: number[]; counts: number[]; log: boolean } {55  const v = log ? values.filter((x) => x > 0) : values;56  if (!v.length) return { edges: [], counts: [], log };57  const t = log ? v.map((x) => Math.log10(x)) : v;58  let lo = Math.min(...t);59  let hi = Math.max(...t);60  if (lo === hi) {61    const pad = Math.abs(lo) * 0.05 || 0.5;62    lo -= pad;63    hi += pad;64  }65  const counts = new Array<number>(bins).fill(0);66  const w = (hi - lo) / bins;67  for (const x of t) {68    let i = Math.floor((x - lo) / w);69    if (i >= bins) i = bins - 1;70    if (i < 0) i = 0;71    counts[i]!++;72  }73  const edges = Array.from({ length: bins + 1 }, (_, i) => {74    const e = lo + i * w;75    return log ? Math.pow(10, e) : e;76  });77  return { edges, counts, log };78}7980/** Heuristic: a level series spanning > 50× on positive values reads better on a log axis. */81export function wantsLog(spec: { format?: string | null }, values: number[]): boolean {82  if (!['currency', 'number', 'tonnes', 'kwh', 'per_million'].includes(spec.format ?? '')) return false;83  const pos = values.filter((v) => v > 0);84  if (pos.length < 10 || pos.length < values.length * 0.95) return false;85  return Math.max(...pos) / Math.min(...pos) > 50;86}87