/** * Minimal scale + tick helpers. No d3 — just the bits dashboards need. */ /** "Nice" rounded domain max so axis ticks land on clean numbers. */ export function niceMax(max: number, ticks = 4): number { if (max <= 0) return ticks const rough = max / ticks const mag = 10 ** Math.floor(Math.log10(rough)) const norm = rough / mag const step = norm >= 5 ? 10 : norm >= 2 ? 5 : norm >= 1 ? 2 : 1 const niceStep = step * mag return Math.ceil(max / niceStep) * niceStep } /** Evenly spaced tick values from min..max (inclusive). */ export function ticks(min: number, max: number, count = 4): number[] { const out: number[] = [] const step = (max - min) / count for (let i = 0; i <= count; i++) out.push(min + step * i) return out } /** Linear scale factory: maps a value in [d0,d1] → pixel in [r0,r1]. */ export function linear(d0: number, d1: number, r0: number, r1: number) { const span = d1 - d0 || 1 return (v: number) => r0 + ((v - d0) / span) * (r1 - r0) } /** Band scale for categorical x: returns center x + band width. */ export function band(count: number, range: number, padding = 0.2) { const step = range / Math.max(1, count) const bandWidth = step * (1 - padding) return { bandWidth, center: (i: number) => step * i + step / 2, start: (i: number) => step * i + (step - bandWidth) / 2, } }