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