Skip to content

Commit 014aa63

Browse files
author
Nathaniel Henry
committed
Draw overlapping filled contours as separate traces so nested isochrones stay hoverable.
1 parent 1393871 commit 014aa63

1 file changed

Lines changed: 40 additions & 17 deletions

File tree

src/closecity/map.py

Lines changed: 40 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@
1313
import math
1414
from typing import Any
1515

16+
# Below this many filled polygons, draw one trace each (largest first) so
17+
# overlapping fills — e.g. nested isochrone contours — each stay hoverable.
18+
# Above it (block maps), a single trace keeps rendering fast.
19+
_OVERLAP_MAX = 12
20+
1621

1722
def _rgba(hexcolor: str, alpha: float) -> str:
1823
h = hexcolor.lstrip("#")
@@ -136,6 +141,7 @@ def close_map(
136141

137142
g = gdf.to_crs(4326)
138143
traces = []
144+
coloraxis = None
139145
bounds = [_bounds(g)]
140146

141147
# Semi-transparent background fills, drawn first (underneath everything).
@@ -197,29 +203,46 @@ def close_map(
197203

198204
g = g.reset_index(drop=True)
199205
g["_id"] = [str(i) for i in range(len(g))]
200-
geojson = json.loads(g[["_id", "geometry"]].to_json())
201-
common = {
202-
"geojson": geojson, "locations": g["_id"].tolist(),
203-
"featureidkey": "properties._id",
204-
"marker": {"opacity": opacity, "line": {"width": 0}},
205-
"text": hover, "hoverinfo": "text", "showlegend": False,
206-
}
207-
if fv is not None:
208-
trace = go.Choroplethmapbox(
209-
z=fv, colorscale=palette, reversescale=reverse,
210-
showscale=True, colorbar={"title": fill}, **common)
206+
207+
if fv is not None and len(g) <= _OVERLAP_MAX:
208+
# Filled polygons that may overlap (nested isochrone contours): one
209+
# trace each, largest first, so every one stays hoverable. A shared
210+
# coloraxis gives them a single colorbar.
211+
order = g.to_crs(3857).geometry.area.sort_values(ascending=False).index
212+
for i in order:
213+
sub = g.loc[[i], ["_id", "geometry"]].reset_index(drop=True)
214+
traces.append(go.Choroplethmapbox(
215+
geojson=json.loads(sub.to_json()), locations=[g.at[i, "_id"]],
216+
featureidkey="properties._id", z=[fv[i]], coloraxis="coloraxis",
217+
marker={"opacity": opacity, "line": {"width": 0}},
218+
text=[hover[i]], hoverinfo="text", showlegend=False))
219+
coloraxis = {"colorscale": palette, "reversescale": reverse,
220+
"cmin": min(fv), "cmax": max(fv), "colorbar": {"title": fill}}
211221
else:
212-
z = [1] * len(g) if hl is None else [int(h) for h in hl]
213-
colorscale = ([[0, color], [1, color]] if hl is None
214-
else [[0, "#888888"], [1, color]])
215-
trace = go.Choroplethmapbox(
216-
z=z, colorscale=colorscale, showscale=False, **common)
217-
traces.append(trace)
222+
geojson = json.loads(g[["_id", "geometry"]].to_json())
223+
common = {
224+
"geojson": geojson, "locations": g["_id"].tolist(),
225+
"featureidkey": "properties._id",
226+
"marker": {"opacity": opacity, "line": {"width": 0}},
227+
"text": hover, "hoverinfo": "text", "showlegend": False,
228+
}
229+
if fv is not None:
230+
traces.append(go.Choroplethmapbox(
231+
z=fv, colorscale=palette, reversescale=reverse,
232+
showscale=True, colorbar={"title": fill}, **common))
233+
else:
234+
z = [1] * len(g) if hl is None else [int(h) for h in hl]
235+
colorscale = ([[0, color], [1, color]] if hl is None
236+
else [[0, "#888888"], [1, color]])
237+
traces.append(go.Choroplethmapbox(
238+
z=z, colorscale=colorscale, showscale=False, **common))
218239

219240
center, zoom = _center_zoom(bounds, buffer)
220241
fig = go.Figure(traces)
221242
fig.update_layout(
222243
mapbox={"style": "carto-positron", "zoom": zoom, "center": center},
223244
margin={"l": 0, "r": 0, "t": 0, "b": 0},
224245
)
246+
if coloraxis is not None:
247+
fig.update_layout(coloraxis=coloraxis)
225248
return fig

0 commit comments

Comments
 (0)