From a5229103f687f873fd55653d98a079ae4e91ec2f Mon Sep 17 00:00:00 2001 From: Inferencebot Date: Wed, 28 Oct 2026 04:36:13 +0000 Subject: [PATCH] charts: add Energy and Carbon (co-op/support#26) Two more metrics on the same chart, from the portal's new energy_wh and co2e_g daily columns. - Energy in watt-hours, carbon in grams CO2e, both for the day shown. - Continuous metrics use the 1/2/5 axis rule (not the integer rule reserved for counts), so energy ticks read 500 Wh rather than being rounded to whole people-style numbers. - Labels carry units, so a reader never has to guess what the axis measures. - Same measured-vs-estimated provenance as the data layer: GreenPT models use facts fitted from their own impact API, everyone else a labelled proxy. The docs page on estimation is still to be written; the factor table and its sources are in metrics_snapshot.py. Harness extended: one bar per measured day for each, no NaN in markup, and axis labels matching ^[\d,]+(\.\d+)? (Wh|g)$ so a unit prefix or a forced integer axis fails the build. --- app/main.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/app/main.py b/app/main.py index 6e50538..c2388f0 100644 --- a/app/main.py +++ b/app/main.py @@ -370,6 +370,8 @@ PAGE_HTML = """ Show + + @@ -473,8 +475,10 @@ let trendDays = 90; // 0 = all time const TREND_CONF = { members: { kind: "line", title: "Members" }, + requests: { kind: "bar", title: "Requests per day" }, spend: { kind: "bar", title: "Spend per day" }, - requests: { kind: "bar", title: "Requests per day" } + energy: { kind: "bar", title: "Energy per day" }, + co2e: { kind: "bar", title: "Carbon per day" } }; async function loadMetrics() { @@ -496,6 +500,8 @@ function setTrendMetric(m) { trendMetric = m; $("tm-members").className = "range-btn" + (m === "members" ? " active" : ""); $("tm-spend").className = "range-btn" + (m === "spend" ? " active" : ""); + $("tm-energy").className = "range-btn" + (m === "energy" ? " active" : ""); + $("tm-co2e").className = "range-btn" + (m === "co2e" ? " active" : ""); $("tm-requests").className = "range-btn" + (m === "requests" ? " active" : ""); renderTrends(); } @@ -511,6 +517,8 @@ function setTrendDays(d) { function trendVal(row) { if (trendMetric === "members") return row.members_total; if (trendMetric === "spend") return row.spend_usd; + if (trendMetric === "energy") return row.energy_wh; + if (trendMetric === "co2e") return row.co2e_g; return row.requests; } @@ -518,7 +526,7 @@ function trendVal(row) { // 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") { + if (trendMetric === "spend" || trendMetric === "energy" || trendMetric === "co2e") { 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)); @@ -534,6 +542,8 @@ function niceStep(raw) { function trendLabel(v) { if (v === null || v === undefined) return "not measured"; if (trendMetric === "spend") return fmt(v); + if (trendMetric === "energy") return Number(v).toLocaleString() + " Wh"; + if (trendMetric === "co2e") return Number(v).toLocaleString() + " g"; return Number(v).toLocaleString(); }