spb/countryatlas
Public
TypeScript 57%
Python 38.6%
JavaScript 3.6%
CSS 0.6%
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