skills/geopandas/references/visualization.md
A map is a derived analytical artifact. It can misstate units, hide missing data, expose exact locations, or contact third-party services even when the underlying geometry operations are correct.
Before plotting:
The bundled scripts/sensitive_coordinates_checklist.py provides a conservative
release gate. It does not claim legal or privacy compliance.
GeoPandas .plot() uses Matplotlib:
import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=(8, 6))
gdf.plot(
ax=ax,
column="rate",
cmap="viridis",
legend=True,
missing_kwds={
"color": "lightgrey",
"edgecolor": "black",
"hatch": "///",
"label": "Missing",
},
)
ax.set_title("Synthetic rate by region")
ax.set_axis_off()
fig.savefig("derived/map.png", dpi=300, bbox_inches="tight")
Pin Matplotlib and optional mapping dependencies in the project lock. The GeoPandas 1.1 tagged source tests Matplotlib >=3.7 and mapclassify >=2.5.
State in the title/caption or metadata:
Raw counts over unequal polygon areas often communicate population size rather than rate. Compute an appropriate rate/density first and retain numerator and denominator.
scheme= delegates classification to mapclassify:
ax = gdf.plot(
column="rate",
scheme="quantiles",
k=5,
cmap="viridis",
legend=True,
)
Quantiles balance feature counts but can place almost equal values in different bins. Equal intervals can leave sparse bins; natural breaks are data-dependent and make cross-map comparisons difficult. For comparisons, reuse explicit boundaries and a common normalization.
GeoPandas ignores missing values by default. That can make missing areas look
like background or water. Always count missing values and use missing_kwds
when they are meaningful. Distinguish:
Do not coerce missing values to zero for visual convenience.
Use categorical=True for categories and a qualitative palette. Verify category
order and legend labels; do not imply magnitude with a sequential palette.
GeoPandas 1.1.4 fixed custom categorical/boolean legend_kwds={"labels": ...}
being ignored by explore().
fig, ax = plt.subplots(figsize=(8, 6))
areas.plot(ax=ax, facecolor="none", edgecolor="0.4", zorder=1)
generalized_points.plot(ax=ax, color="black", markersize=8, zorder=2)
facecolor="none" for transparent polygon faces; Python None means
something different.zorder, alpha, and line width deliberately.Choose a map projection for the communication goal:
GeoPandas may set a latitude-dependent aspect for geographic plots, but that is not a replacement for a chosen projection. Exact metric scale bars require a projected CRS with known linear units and limited distortion.
Antimeridian-crossing geometries must be split/wrapped before plotting; changing axis limits does not repair a line drawn across the map.
Tile helpers such as contextily and explore(tiles=...) can send viewport,
zoom, IP, and timing information to a provider and can disclose the study area.
They also introduce attribution, terms-of-use, caching, availability, and
reproducibility requirements.
Do not fetch tiles automatically. If a user explicitly approves a provider:
exploreGeoDataFrame.explore() returns a folium.Map. Background tiles require CRS
metadata and normally cause network requests. Start with a no-tile,
no-attribute local draft:
interactive = generalized_gdf.explore(
tiles=None,
tooltip=False,
popup=False,
style_kwds={"fillOpacity": 0.5, "weight": 1},
)
interactive.save("derived/local-draft.html")
Even tiles=None HTML can reference CDN-hosted JavaScript/CSS depending on the
Folium configuration. Inspect the generated HTML and use an approved offline
asset strategy before sensitive or air-gapped use.
Privacy hazards:
popup=True can expose all columns;For multiple layers, add only generalized/allowlisted columns and explicit names. Do not use the interactive map as a data-access control boundary.
Record:
tiles=None;Raster PNG reduces direct coordinate extraction compared with SVG/HTML but is not anonymization. Check metadata and visual landmarks before release.