diff --git a/app/main.py b/app/main.py index a41397d..bea3866 100644 --- a/app/main.py +++ b/app/main.py @@ -521,6 +521,23 @@ function trendVal(row) { return row.requests; } +// Y-axis step. Counts are people and things, so their ticks must be whole +// numbers (a 1.08 headroom factor alone yields labels like 19.17 people). +// Money gets the classic 1/2/5 x 10^k rounding so ticks read $5.00, not $4.92. +function niceStep(raw) { + if (trendMetric === "spend") { + const mag = Math.pow(10, Math.floor(Math.log10(Math.max(raw, 1e-9)))); + const norm = raw / mag; + const nice = norm <= 1 ? 1 : (norm <= 2 ? 2 : (norm <= 5 ? 5 : 10)); + return nice * mag; + } + // Counts: whole numbers, rounded up to a half power of ten so ticks read + // 3,000 rather than 2,888. Never below 1, so a near-empty series still gets + // an integer axis instead of fractions. + const p = Math.pow(10, Math.floor(Math.log10(Math.max(raw, 1)))); + return Math.max(1, Math.ceil(raw / (p / 2)) * (p / 2)); +} + function trendLabel(v) { if (v === null || v === undefined) return "not measured"; if (trendMetric === "spend") return fmt(v); @@ -544,7 +561,10 @@ function renderTrends() { } const gaps = vals.length - have.length; const maxV = Math.max.apply(null, have); - const top = maxV <= 0 ? 1 : maxV * 1.08; // headroom so the peak clears the frame + // Axis top = one round step times four ticks: labels land on round numbers + // and the peak still clears the frame. + const step = niceStep((maxV <= 0 ? 1 : maxV * 1.08) / 4); + const top = step * 4; const W = 1000, H = 250, padL = 62, padR = 14, padT = 14, padB = 30; const plotW = W - padL - padR, plotH = H - padT - padB; @@ -557,7 +577,7 @@ function renderTrends() { parts.push(''); for (let g = 0; g <= 4; g++) { const yy = baseY - (plotH * g) / 4; - const lab = trendLabel((top * g) / 4); + const lab = trendLabel(step * g); parts.push(''); parts.push('' + lab + ''); }