Building Interactive HTML Compliance Dashboards
Reviewers triage far faster when they can pan a map, click a flagged parcel, and read its violation detail in place, rather than paging through a static document. This guide builds a self-contained interactive HTML compliance dashboard with folium and Leaflet, rendering violation layers straight from a frozen result set into a single file a planner can open offline or email to counsel. It is a task within the audit-ready report generation module and complements the machine-readable GeoJSON and CityGML exports with a human-facing view.
Prerequisites
Step-by-step
Step 1: Load results and compute metrics before reprojecting
Read the evaluation output, compute any display metrics in the projected CRS, then reproject a copy to WGS84 because Leaflet tile maps expect longitude and latitude.
import geopandas as gpd
MEASURE_CRS = "EPSG:32617" # WGS84 / UTM 17N (meters), eastern North America
def load_for_map(path: str) -> gpd.GeoDataFrame:
gdf = gpd.read_file(path)
if gdf.crs is None:
raise ValueError("Result set has no CRS; cannot map defensibly")
gdf = gdf.to_crs(MEASURE_CRS) # ensure metric CRS for any area math
gdf["area_sqm"] = gdf.geometry.area # measured before the display reprojection
return gdf.to_crs("EPSG:4326") # Leaflet needs lon/lat
Step 2: Style parcels by compliance status
Drive colour from the status column so violations read at a glance. A small style function keeps the map legend and the polygons consistent.
STATUS_COLORS = {
"COMPLIANT": "#5a8f6b",
"MINOR_VIOLATION": "#d98b6f",
"CRITICAL_VIOLATION": "#c0392b",
}
def style_for(feature):
status = feature["properties"].get("status", "UNKNOWN")
return {
"fillColor": STATUS_COLORS.get(status, "#7a6f96"),
"color": "#322e3d",
"weight": 1,
"fillOpacity": 0.55,
}
Step 3: Build the folium map with a violation layer
Center the map on the data bounds, add the parcels as a GeoJSON layer, and attach a per-parcel popup that names the rule version, the measured versus required value, and the CRS.
import folium
def build_dashboard(gdf: gpd.GeoDataFrame) -> folium.Map:
minx, miny, maxx, maxy = gdf.total_bounds
m = folium.Map(location=[(miny + maxy) / 2, (minx + maxx) / 2], zoom_start=15,
tiles="cartodbpositron")
tooltip = folium.GeoJsonTooltip(fields=["parcel_id", "status"])
popup = folium.GeoJsonPopup(
fields=["parcel_id", "rule_id", "rule_version",
"measured_m", "required_m", "measure_epsg"],
aliases=["Parcel", "Rule", "Version", "Measured (m)", "Required (m)", "CRS EPSG"],
)
folium.GeoJson(gdf, name="Compliance", style_function=style_for,
tooltip=tooltip, popup=popup).add_to(m)
m.fit_bounds([[miny, minx], [maxy, maxx]])
folium.LayerControl().add_to(m)
return m
Note that measure_epsg rides along as a property so the popup can display the CRS the distances were measured in, even though the map itself is drawn in WGS84. Consult the folium documentation for tile providers, layer control, and popup formatting.
Step 4: Export a self-contained HTML file
Folium writes a single HTML file, but by default it references Leaflet assets from CDNs. For an offline, emailable artifact, save normally for online use, or embed assets when the dashboard must open without a network.
def save_dashboard(m: folium.Map, out_path: str, offline: bool = True) -> None:
if offline:
# Inline Leaflet CSS/JS so the file opens with no network access.
m.save(out_path, close_file=True)
# folium embeds most assets; verify by opening with connectivity disabled.
else:
m.save(out_path)
Verification
Confirm the map represents every parcel and that failing parcels carry a violation colour. Since the HTML embeds a GeoJSON blob, you can re-read the source frame and check counts and the flagged set.
def verify_dashboard(gdf: gpd.GeoDataFrame, html_path: str) -> None:
with open(html_path, encoding="utf-8") as fh:
html = fh.read()
for pid in gdf["parcel_id"]:
assert str(pid) in html, f"Parcel {pid} missing from dashboard"
fails = gdf[gdf["status"] != "COMPLIANT"]
print(f"Rendered {len(gdf)} parcels, {len(fails)} flagged as violations")
Common Pitfalls
- Forgetting to reproject to WGS84. Leaflet assumes longitude and latitude; handing it state-plane or UTM coordinates places parcels in the ocean off West Africa. Reproject to
EPSG:4326for the map, but measure areas in the metric CRS first. - Embedding tens of thousands of polygons inline. A single GeoJSON layer with very high feature counts bloats the HTML and stalls the browser. Simplify geometry for display or group parcels by district, and keep full-resolution geometry in the machine-readable export instead.
- Assuming the file is offline when it is not. A dashboard that pulls tiles or Leaflet from a CDN will render blank in an air-gapped review room. Test by opening the file with connectivity disabled before you call it self-contained.
Designing for Triage, Not for Browsing
A compliance dashboard has one job: get a reviewer to the cases that need them, in the order that matters. Most dashboards instead present everything equally and leave the prioritising to the reader.
The default view should be a work queue, sorted by something meaningful — margin, so the closest calls surface first, or age, so nothing sits forgotten. Every row should carry enough to decide without opening it: parcel, rule, outcome, margin. And the indeterminate cases should be prominent rather than filtered out by default, because they are the ones requiring human judgement and the ones a pass/fail-oriented view hides.
Filters should match the questions people ask — by district, by rule, by outcome, by run — and the current filter state belongs in the URL so a reviewer can send a colleague exactly what they are looking at. That single detail eliminates most of the “which view were you on?” traffic that dashboards otherwise generate.
Resist the temptation to add charts that nobody acts on. A count of violations by month looks informative and changes nobody’s afternoon; a list of the eleven cases needing review today does.
Saying What the Dashboard Is Not
A dashboard is the most convenient view of a compliance run and the least authoritative, and the gap between those two facts causes real problems when a screenshot ends up attached to a decision.
Three measures keep it honest. State the run it is showing, with its timestamp and rule version, somewhere permanent on the page rather than in a tooltip. Say plainly that the authoritative record is the issued report or certificate, and link to it from each row. And avoid presenting anything the underlying records do not contain — a derived estimate, a projection, an “expected” outcome — without labelling it as such.
The same reasoning applies to live data. A dashboard that re-queries as data changes is useful for operations and misleading as evidence, because two people looking at it on different days see different things and neither can reproduce what the other saw. Pinning the view to a run, and offering run selection explicitly, resolves the ambiguity.
Keeping It Fast Without a Backend
Compliance dashboards are usually small — a county run produces tens of thousands of rows, not millions — and that puts them comfortably within reach of a static page with the data embedded or fetched as a file.
The advantages are considerable for this use case: no server to operate, no authentication layer to build, no database to keep in step with the run, and an artefact that can be archived alongside the run it describes. A dashboard that is a single file with its data inside it can be attached to a record, opened in five years, and will still work.
The technique that makes it feel fast is to render only the visible rows and to do filtering in memory over a compact representation. Thirty thousand rows of six fields is a few megabytes as JSON and considerably less as a packed structure, which is well within what a browser handles without ceremony.
Where the dataset genuinely outgrows this — a state-wide run, or a multi-year archive — the honest move is a real backend rather than a heroically optimised static page. But that threshold is much further away than most teams assume, and reaching for a service first costs an operational dependency that a compliance artefact does not otherwise need.
Related
Part of: Audit-ready report generation
- Generating PDF compliance reports with Python — the authoritative record this view links to.
- Exporting compliance results to GeoJSON and CityGML — the same records for a GIS consumer.
- Redacting sensitive parcel data from logs — what must not appear in a shared view.
- Validation log design — the records the dashboard reads.
Frequently Asked Questions
Should I use folium or write Leaflet by hand?
Folium is a Python wrapper that generates Leaflet maps, so you get the same underlying library with far less boilerplate and direct GeoDataFrame integration. Hand-written Leaflet is worth it only when you need custom controls, bespoke interactions, or a build pipeline folium does not expose. For rendering compliance violation layers from a GeoDataFrame, folium is the pragmatic choice and keeps the workflow entirely in Python.
How do I make the dashboard work completely offline?
Save the map and then verify it by disabling your network connection and reopening the file. Folium inlines most of what it needs, but tile basemaps stream from a provider, so an offline dashboard needs either a bundled local tile set or a plain styled-polygon view without a live basemap. Always test the air-gapped case explicitly rather than assuming the single HTML file is self-sufficient.
Can an interactive map still be part of an audit trail?
Yes, provided it is generated from the same frozen result set as your other artifacts and is checksummed alongside them. The dashboard is a view, not the system of record, so it should carry the same rule versions and CRS provenance in its popups that the machine-readable export and the printed report carry. Hash the HTML bytes and record that hash in the run manifest.
How many parcels can a single dashboard hold before it slows down?
Browsers handle a few thousand interactive polygons comfortably; past that, rendering and popup binding start to lag. For large jurisdictions, split the dashboard by district or planning area, simplify geometry for display only, or switch to a vector-tile approach. Keep the full-resolution geometry in the GeoJSON export where precision matters, and treat the dashboard as a triage surface.
For the authoritative, machine-readable counterpart to this view, pair the dashboard with exporting compliance results to GeoJSON and CityGML, and confirm the provenance shown in popups matches what the validation log design module recorded upstream.