|
13 | 13 | import math |
14 | 14 | from typing import Any |
15 | 15 |
|
| 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 | + |
16 | 21 |
|
17 | 22 | def _rgba(hexcolor: str, alpha: float) -> str: |
18 | 23 | h = hexcolor.lstrip("#") |
@@ -136,6 +141,7 @@ def close_map( |
136 | 141 |
|
137 | 142 | g = gdf.to_crs(4326) |
138 | 143 | traces = [] |
| 144 | + coloraxis = None |
139 | 145 | bounds = [_bounds(g)] |
140 | 146 |
|
141 | 147 | # Semi-transparent background fills, drawn first (underneath everything). |
@@ -197,29 +203,46 @@ def close_map( |
197 | 203 |
|
198 | 204 | g = g.reset_index(drop=True) |
199 | 205 | 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}} |
211 | 221 | 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)) |
218 | 239 |
|
219 | 240 | center, zoom = _center_zoom(bounds, buffer) |
220 | 241 | fig = go.Figure(traces) |
221 | 242 | fig.update_layout( |
222 | 243 | mapbox={"style": "carto-positron", "zoom": zoom, "center": center}, |
223 | 244 | margin={"l": 0, "r": 0, "t": 0, "b": 0}, |
224 | 245 | ) |
| 246 | + if coloraxis is not None: |
| 247 | + fig.update_layout(coloraxis=coloraxis) |
225 | 248 | return fig |
0 commit comments