|
| 1 | +"""A one-line interactive map for the GeoDataFrames the client returns. |
| 2 | +
|
| 3 | +Built on plotly over a CARTO Positron basemap: points become bright hoverable |
| 4 | +markers, block polygons are filled and can highlight the features meeting a |
| 5 | +criterion. plotly is GDAL-free (unlike leaflet/mapgl in R), and geopandas emits |
| 6 | +the GeoJSON directly, so no extra system dependency. |
| 7 | +""" |
| 8 | + |
| 9 | +from __future__ import annotations |
| 10 | + |
| 11 | +from typing import Any |
| 12 | + |
| 13 | + |
| 14 | +def close_map( |
| 15 | + gdf, |
| 16 | + *, |
| 17 | + color: str = "#e8590c", |
| 18 | + highlight: Any = None, |
| 19 | + label: str = "name", |
| 20 | + size: int = 9, |
| 21 | + zoom: float = 10, |
| 22 | + opacity: float = 0.65, |
| 23 | +): |
| 24 | + """Draw a :class:`geopandas.GeoDataFrame` from a client method as an |
| 25 | + interactive map on a CARTO Positron basemap. |
| 26 | +
|
| 27 | + Points (POIs, places) render as bright hoverable markers; polygons (census |
| 28 | + blocks) are filled, optionally greying the features that do not meet |
| 29 | + ``highlight`` so the ones that matter stand out. |
| 30 | +
|
| 31 | + ``highlight`` is either a boolean sequence (length ``len(gdf)``) or the name |
| 32 | + of a boolean/0-1 column. ``label`` names the hover column. Returns a plotly |
| 33 | + ``Figure``. |
| 34 | + """ |
| 35 | + try: |
| 36 | + import plotly.graph_objects as go |
| 37 | + except ImportError as exc: # pragma: no cover |
| 38 | + raise ImportError( |
| 39 | + "close_map needs plotly: pip install 'closecity[maps]' or plotly" |
| 40 | + ) from exc |
| 41 | + |
| 42 | + g = gdf.to_crs(4326) |
| 43 | + hl = None |
| 44 | + if highlight is not None: |
| 45 | + col = g[highlight] if isinstance(highlight, str) else highlight |
| 46 | + hl = [bool(v) for v in col] |
| 47 | + |
| 48 | + minx, miny, maxx, maxy = g.total_bounds |
| 49 | + center = {"lon": (minx + maxx) / 2, "lat": (miny + maxy) / 2} |
| 50 | + hover = g[label].astype(str).tolist() if label in g.columns else None |
| 51 | + geom_type = g.geom_type.iloc[0] if len(g) else "Point" |
| 52 | + |
| 53 | + if "Point" in geom_type: |
| 54 | + marker_color = color if hl is None else [ |
| 55 | + color if h else "#888888" for h in hl |
| 56 | + ] |
| 57 | + fig = go.Figure(go.Scattermapbox( |
| 58 | + lat = g.geometry.y.tolist(), lon = g.geometry.x.tolist(), |
| 59 | + mode = "markers", |
| 60 | + marker = dict(size = size, color = marker_color), |
| 61 | + text = hover, hoverinfo = "text" if hover else "none", |
| 62 | + )) |
| 63 | + else: |
| 64 | + import json |
| 65 | + |
| 66 | + g = g.reset_index(drop = True) |
| 67 | + g["_id"] = [str(i) for i in range(len(g))] |
| 68 | + z = [1] * len(g) if hl is None else [int(h) for h in hl] |
| 69 | + geojson = json.loads(g[["_id", "geometry"]].to_json()) |
| 70 | + colorscale = ( |
| 71 | + [[0, color], [1, color]] if hl is None |
| 72 | + else [[0, "#888888"], [1, color]] |
| 73 | + ) |
| 74 | + fig = go.Figure(go.Choroplethmapbox( |
| 75 | + geojson = geojson, locations = g["_id"].tolist(), z = z, |
| 76 | + featureidkey = "properties._id", colorscale = colorscale, |
| 77 | + showscale = False, |
| 78 | + marker = dict(opacity = opacity, line = dict(width = 0)), |
| 79 | + text = hover, hoverinfo = "text" if hover else "none", |
| 80 | + )) |
| 81 | + |
| 82 | + fig.update_layout( |
| 83 | + mapbox = dict(style = "carto-positron", zoom = zoom, center = center), |
| 84 | + margin = dict(l = 0, r = 0, t = 0, b = 0), |
| 85 | + ) |
| 86 | + return fig |
0 commit comments