diff --git a/examples/osm-equity/README.md b/examples/osm-equity/README.md
index 4a734fbd..4a888797 100644
--- a/examples/osm-equity/README.md
+++ b/examples/osm-equity/README.md
@@ -20,7 +20,7 @@ tests/test_osm_equity.py FFL-compile + unit + end-to-end signal recovery
| 2 | Data acquisition | `ResolveStudyTracts`, `FetchRegionOSM`, `FetchRegionFootprints` (region-level, **update-gated**) |
| 3 | Metric calculation | per-tract `foreach`: `ClipTractOSM` (local), `FetchCensusEquity`, `ComputeAttributeQuality` (intrinsic), `ComputeExtrinsicQuality` (extrinsic, gated) |
| 4 | Statistical & spatial | `SpearmanCorrelation`, `MoransI`, `GeographicallyWeightedRegression`, `TemporalEvolution` — run concurrently over the full tract set |
-| 5 | Actionable reporting | `BuildEquityReport` → HTML + choropleth GeoJSON + ranked data deserts |
+| 5 | Actionable reporting | `BuildEquityReport` → interactive MapLibre heat map + HTML findings report + ranked data deserts |
## Design choices (folded-in improvements)
diff --git a/examples/osm-equity/handlers/shared/equity_utils.py b/examples/osm-equity/handlers/shared/equity_utils.py
index 3aa78906..9a8df365 100644
--- a/examples/osm-equity/handlers/shared/equity_utils.py
+++ b/examples/osm-equity/handlers/shared/equity_utils.py
@@ -649,14 +649,114 @@ def data_deserts(records: list[dict]) -> list[str]:
return [g for g, _ in scored[:cut]]
+def render_maplibre_map(
+ feats: list[dict], title: str, stats_res: dict, deserts: list[str], synthetic: bool = True
+) -> str:
+ """Self-contained MapLibre GL choropleth ("heat map") of tract OSM
+ attribute completeness, styled to match the facetwork-maps gallery:
+ inline GeoJSON, CARTO Voyager raster basemap, click popups, a legend,
+ and a findings banner. Data-desert tracts get a red outline."""
+ fc = json.dumps({"type": "FeatureCollection", "features": feats})
+ xs, ys = [], []
+ for f in feats:
+ for ring in f["geometry"]["coordinates"]:
+ for x, y in ring:
+ xs.append(x)
+ ys.append(y)
+ bounds = [[min(xs), min(ys)], [max(xs), max(ys)]] if xs else [[-124.4, 32.5], [-114.1, 42.0]]
+ rho = stats_res.get("spearman_rho", 0.0)
+ moran = stats_res.get("morans_i", 0.0)
+ trend = stats_res.get("temporal_trend", "n/a")
+ synth = (
+ '
⚠ SYNTHETIC DEMONSTRATION DATA - methodology demo, '
+ "not real OSM/Census values
"
+ if synthetic
+ else ""
+ )
+ return f"""
+
+{title}
+
+
+
+
+
+ {synth}
+
{title}
+
Tract OSM attribute completeness (green = well-mapped, red = under-mapped).
+ Red-outlined tracts are data deserts (bottom quartile).
+
+ Spearman income~POI diversity ρ={rho:.2f} ·
+ Moran's I {moran:.2f} · divide {trend}
+
+
+
+
Attribute completeness
+
+
0.00.51.0
+
+"""
+
+
def build_report(records: list[dict], stats_res: dict, title: str) -> tuple[str, str, list[str]]:
- """Phase 5 - HTML report + a simple GeoJSON choropleth artifact +
- the ranked data-desert list, with intervention recommendations."""
+ """Phase 5 - interactive MapLibre heat map + an HTML findings report +
+ the ranked data-desert list, with intervention recommendations.
+ Returns (report_html_path, map_html_path, deserts)."""
recs = _as_list(records)
deserts = data_deserts(recs)
n_ref = sum(1 for r in recs if r["extrinsic"].get("has_reference"))
- # choropleth artifact: tract polygons coloured by completeness
+ # choropleth features: tract polygons + per-tract properties
feats = []
for r in recs:
feats.append(
@@ -674,8 +774,11 @@ def build_report(records: list[dict], stats_res: dict, title: str) -> tuple[str,
"geometry": mapping(shapely_wkt.loads(r["geometry_wkt"])),
}
)
- map_path = out_dir() / "equity_choropleth.geojson"
- _write_geojson(map_path, feats)
+ _write_geojson(out_dir() / "equity_choropleth.geojson", feats)
+
+ # the interactive heat map (this is the returned map_path)
+ map_path = out_dir() / "equity_map.html"
+ map_path.write_text(render_maplibre_map(feats, title, stats_res, deserts))
rho = stats_res.get("spearman_rho", 0.0)
moran = stats_res.get("morans_i", 0.0)
diff --git a/examples/osm-equity/tests/test_osm_equity.py b/examples/osm-equity/tests/test_osm_equity.py
index 9e1f0215..b77e0e9a 100644
--- a/examples/osm-equity/tests/test_osm_equity.py
+++ b/examples/osm-equity/tests/test_osm_equity.py
@@ -188,6 +188,7 @@ def test_full_pipeline_runs_and_reports(self, tmp_path, monkeypatch):
assert len(records) >= 9
assert os.path.exists(report["report_html"])
assert os.path.exists(report["map_path"])
+ assert "maplibre" in open(report["map_path"]).read().lower() # interactive heat map
html = open(report["report_html"]).read()
assert "Mapping Equity" in html or "Digital Divide" in html or "data desert" in html.lower()
assert isinstance(report["data_deserts"], list) and len(report["data_deserts"]) >= 1