panel: integer y-axis for people; round money and request ticks
Nathan: the members axis should be whole numbers, since it counts people. It was drawing fractional ticks (19.17 people) because the 1.08 headroom factor fed straight into the label math. Ticks now come from a step, with the axis top set to step x 4, so labels land on round numbers: - counts (members, requests): whole numbers, rounded up to a half power of ten (requests read 3,000, not 2,888). Never below 1, so a near-empty series still gets an integer axis rather than fractions. - money: the 1/2/5 x 10^k rule, so ticks read $10.00 rather than $4.92. Rendered now: members 0 | 20 | 40 | 60 | 80; requests 0 | 3,000 | 6,000 | 9,000 | 12,000; spend $0.00 | $10.00 | $20.00 | $30.00 | $40.00. The node harness gained assertions for this, checked geometrically (tick values ascend while their screen positions descend, the zero tick sits lowest, ticks are evenly spaced) rather than assuming a text order, so a fractional people-axis or a misplaced label fails before a deploy.
This commit is contained in:
1 parent
11ae968889
commit
41b86bcc80
1 file changed
+22
-2
+22
-2
@@ -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('<svg viewBox="0 0 ' + W + ' ' + H + '" role="img" aria-label="' + conf.title + ' over time">');
|
||||
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('<line x1="' + padL + '" y1="' + yy + '" x2="' + (W - padR) + '" y2="' + yy + '" stroke="var(--border)"/>');
|
||||
parts.push('<text x="' + (padL - 8) + '" y="' + (yy + 4) + '" text-anchor="end" font-size="11" fill="var(--muted)">' + lab + '</text>');
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user