Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 76 additions & 2 deletions docs/_static/carbon-dial/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ function init(data, geo) {
if (col < 0) { vals[fi] = 0; continue; }
let s = f.mean[col];
for (let c = 0; c < f.nComp; c++) s += scores[c] * f.comp[c * f.nKeys + col];
vals[fi] = s;
vals[fi] = Math.max(0, s); // PCA reconstruction can dip below 0; area >= 0
}
return vals;
}
Expand All @@ -210,7 +210,9 @@ function init(data, geo) {
regionIntensity[i] = Math.max(0, Math.min(1, crop[i] / (meta.cropMaxFrac * landByFeat[i])));
regionPasture[i] = Math.max(0, Math.min(1, past[i] / (meta.pastureMaxFrac * landByFeat[i])));
}
return { regionGroup, regionIntensity, regionPasture };
// crop/past/groupAreas (raw Mha, aligned to geo features) feed the hover
// tooltip's per-region land-use breakdown; the region* arrays drive fills.
return { regionGroup, regionIntensity, regionPasture, cropMha: crop, pastMha: past, groupAreas };
}

// Full scenario object consumed by the panels.
Expand Down Expand Up @@ -245,6 +247,77 @@ function init(data, geo) {
const cropPaths = buildMap("#map");
const pasturePaths = buildMap("#mapPasture");

// ---- per-region hover tooltip (land-use breakdown) ----
// The maps are land-use only (emissions/diet/feed/cost are global scalars),
// so the tooltip reports cropland (split by crop group) and grazing area for
// the hovered region, read live from the last rendered scenario.
const tip = d3.select("body").append("div").attr("class", "map-tip").style("display", "none");
let lastScenario = null;
const featIndex = new Map(geo.features.map((f, i) => [f, i]));
const fmtMha = (v) => (v >= 10 ? v.toFixed(0) : v >= 1 ? v.toFixed(1) : v.toFixed(2));
const pctOf = (v, tot) => (tot > 0 ? Math.round(100 * v / tot) : 0);

const countryNames = meta.countryNames || {};
function tipHead(d) {
const name = countryNames[d.properties.country] || d.properties.country;
return `<div class="map-tip__head">${name}`
+ `<span>${d.properties.region}</span></div>`;
}
function tipMetric(label, v, area) {
const share = isFinite(area) ? ` <span>${pctOf(v, area)}% of land</span>` : "";
return `<div class="map-tip__metric">${label} <b>${fmtMha(v)} Mha</b>${share}</div>`;
}
function tipCropland(i, d) {
if (!lastScenario) return "";
const total = lastScenario.cropMha[i];
const rows = meta.mapGroups
.map((g, gi) => ({ name: g.name, color: g.color, v: lastScenario.groupAreas[gi][i] }))
.filter((r) => r.v > 1e-4).sort((a, b) => b.v - a.v);
let html = tipHead(d) + tipMetric("Cropland", total, landByFeat[i]);
if (rows.length) {
html += `<div class="map-tip__rows">` + rows.map((r) =>
`<div class="map-tip__row"><span class="map-tip__sw" style="background:${r.color}"></span>`
+ `<span class="map-tip__name">${r.name}</span>`
+ `<span class="map-tip__val">${fmtMha(r.v)}</span>`
+ `<span class="map-tip__share">${pctOf(r.v, total)}%</span></div>`).join("") + `</div>`;
} else {
html += `<div class="map-tip__empty">No cropland</div>`;
}
return html;
}
function tipPasture(i, d) {
if (!lastScenario) return "";
return tipHead(d) + tipMetric("Pasture &amp; grazing", lastScenario.pastMha[i], landByFeat[i]);
}
function attachTip(paths, builder) {
// Outline the hovered region: dark stroke + raise so it sits above
// neighbours; restore the thin white border on leave.
let hovered = null;
const clearHover = () => {
if (hovered) d3.select(hovered).attr("stroke", "#fff").attr("stroke-width", 0.2);
hovered = null;
};
paths
.style("cursor", "default")
.on("mousemove", (event, d) => {
const node = event.currentTarget;
if (node !== hovered) {
clearHover();
hovered = node;
d3.select(node).attr("stroke", "#1f2a26").attr("stroke-width", 1.2).raise();
}
tip.html(builder(featIndex.get(d), d)).style("display", "block");
const pad = 14, w = tip.node().offsetWidth, h = tip.node().offsetHeight;
let x = event.clientX + pad, y = event.clientY + pad;
if (x + w > window.innerWidth) x = event.clientX - pad - w;
if (y + h > window.innerHeight) y = event.clientY - pad - h;
tip.style("left", `${x}px`).style("top", `${y}px`);
})
.on("mouseleave", () => { clearHover(); tip.style("display", "none"); });
}
attachTip(cropPaths, tipCropland);
attachTip(pasturePaths, tipPasture);

const legend = d3.select("#legend");
meta.mapGroups.forEach((g) => {
const it = legend.append("div").attr("class", "legend__item");
Expand Down Expand Up @@ -418,6 +491,7 @@ function init(data, geo) {

function render() {
const s = scenario(mode, price, yll);
lastScenario = s; // hover tooltip reads per-region land use from this
priceValue.textContent = Math.round(price);
if (yllValue) yllValue.textContent = yll ? Math.round(yll).toLocaleString() : "–";
dietHint.textContent = (dietUnit === "kcal" ? "kcal / person / day" : "g / person / day")
Expand Down
21 changes: 21 additions & 0 deletions docs/_static/carbon-dial/export_surrogate.py
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,26 @@ def group_total(prefix, factor):
}


def country_names():
"""ISO3 -> human-readable country name for every code in regions.geojson.

The dial's region tooltip shows full names instead of bare alpha-3 codes;
the short ``common_name`` is preferred over the formal ``name`` when present
(e.g. "Bolivia" over "Bolivia, Plurinational State of").
"""
import pycountry

geo = json.loads((OUT_DIR / "regions.geojson").read_text())
codes = {f["properties"]["country"] for f in geo["features"]}
names = {}
for c in codes:
rec = pycountry.countries.get(alpha_3=c)
if rec is None:
raise ValueError(f"no country name for ISO3 code {c!r}")
names[c] = getattr(rec, "common_name", None) or rec.name
return names


def region_land_areas():
"""Per-region land area (Mha) from the widget's regions.geojson.

Expand Down Expand Up @@ -608,6 +628,7 @@ def main():
meta = {k: v for k, v in common.items() if k not in ("pop", "food2group")}
meta["modes"] = list(modes.keys())
meta["regionArea"] = {k: float(v) for k, v in region_area.items()}
meta["countryNames"] = country_names()
meta["cropMaxFrac"] = crop_max_frac
meta["pastureMaxFrac"] = pasture_max_frac
out = {"meta": meta, "modes": modes}
Expand Down
4 changes: 2 additions & 2 deletions docs/_static/carbon-dial/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -78,15 +78,15 @@ <h1>What if we put a price on food's carbon?</h1>
<div class="dial__maps">
<div class="card card--map">
<div class="card__title">Cropland use
<span class="card__hint">colour = main crop group &middot; fade = cropland per land area</span>
<span class="card__hint">colour = main crop group &middot; fade = cropland per land area &middot; hover a region for the breakdown</span>
</div>
<div class="map-wrap"><svg id="map"></svg></div>
<div class="legend" id="legend"></div>
</div>

<div class="card card--map">
<div class="card__title">Pasture &amp; grazing land
<span class="card__hint">shade = pasture per land area</span>
<span class="card__hint">shade = pasture per land area &middot; hover a region for details</span>
</div>
<div class="map-wrap"><svg id="mapPasture"></svg></div>
<div class="legend legend--gradient" id="legendPasture">
Expand Down
29 changes: 29 additions & 0 deletions docs/_static/carbon-dial/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,35 @@ input[type="range"]::-moz-range-thumb {

.map-wrap { width: 100%; }
#map { width: 100%; height: auto; display: block; }
#mapPasture { width: 100%; height: auto; display: block; }

/* per-region hover tooltip: land-use breakdown */
.map-tip {
position: fixed; z-index: 1000; pointer-events: none;
min-width: 150px; max-width: 240px;
background: var(--card); color: var(--ink);
border: 1px solid var(--line); border-radius: 10px;
box-shadow: var(--shadow); padding: 9px 11px;
font-family: var(--sans); font-size: 0.74rem; line-height: 1.35;
}
.map-tip__head {
display: flex; align-items: baseline; justify-content: space-between;
gap: 8px; font-weight: 700; margin-bottom: 5px;
}
.map-tip__head span { font-weight: 500; font-size: 0.66rem; color: #9aa8a2; }
.map-tip__metric { color: var(--muted); margin-bottom: 6px; }
.map-tip__metric b { color: var(--ink); font-variant-numeric: tabular-nums; }
.map-tip__metric span { color: #9aa8a2; }
.map-tip__rows { display: grid; gap: 3px; }
.map-tip__row {
display: grid; grid-template-columns: 11px 1fr auto auto; gap: 6px;
align-items: center; font-variant-numeric: tabular-nums;
}
.map-tip__sw { width: 11px; height: 11px; border-radius: 3px; }
.map-tip__name { color: var(--ink); white-space: nowrap; }
.map-tip__val { color: var(--ink); font-weight: 600; }
.map-tip__share { color: #9aa8a2; min-width: 30px; text-align: right; }
.map-tip__empty { color: #9aa8a2; font-style: italic; }

.legend { display: flex; flex-wrap: wrap; gap: 6px 12px; margin-top: 8px; }
.legend__item { display: flex; align-items: center; gap: 5px; font-size: 0.74rem; color: var(--muted); }
Expand Down
Loading