`;
+ }
+ return html;
+ }
+ function tipPasture(i, d) {
+ if (!lastScenario) return "";
+ return tipHead(d) + tipMetric("Pasture & 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");
@@ -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")
diff --git a/docs/_static/carbon-dial/export_surrogate.py b/docs/_static/carbon-dial/export_surrogate.py
index 4ac393ad..f3fbf1f8 100644
--- a/docs/_static/carbon-dial/export_surrogate.py
+++ b/docs/_static/carbon-dial/export_surrogate.py
@@ -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.
@@ -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}
diff --git a/docs/_static/carbon-dial/index.html b/docs/_static/carbon-dial/index.html
index 65e92c51..0abedb46 100644
--- a/docs/_static/carbon-dial/index.html
+++ b/docs/_static/carbon-dial/index.html
@@ -78,7 +78,7 @@
What if we put a price on food's carbon?
Cropland use
- colour = main crop group · fade = cropland per land area
+ colour = main crop group · fade = cropland per land area · hover a region for the breakdown
@@ -86,7 +86,7 @@
What if we put a price on food's carbon?
Pasture & grazing land
- shade = pasture per land area
+ shade = pasture per land area · hover a region for details