Converting Shapely Geometries to Matplotlib PathPatches with PolygonPatch in prettymaps
The PolygonPatch class in prettymaps/draw.py transforms any Shapely BaseGeometry into a Matplotlib PathPatch by extracting exterior and interior ring coordinates, building vertex arrays with proper path codes, and delegating rendering to Matplotlib's native patch system.
The prettymaps library bridges geographic information systems and publication-quality visualization by converting Shapely geometric objects into Matplotlib-compatible patches. This conversion happens transparently during map generation, enabling complex vector data—including multi-polygons with holes—to render correctly with customized styling.
How PolygonPatch Works: The Conversion Pipeline
The PolygonPatch implementation in [prettymaps/draw.py](https://github.com/marceloprates/prettymaps/blob/main/prettymaps/draw.py) (lines 119–168) follows a six-step process to transform Shapely geometries into renderable Matplotlib elements.
Step 1: Geometry Flattening
The __init__ method accepts any Shapely BaseGeometry and normalizes it for processing. Geometries with a geoms attribute—such as MultiPolygon or GeometryCollection—are flattened into individual polygons. Single geometries pass through unchanged.
# Simplified logic from draw.py
geoms = geometry.geoms if hasattr(geometry, "geoms") else [geometry]
Step 2: Ring Extraction
For each polygon, PolygonPatch distinguishes between exterior boundaries and interior holes. The exterior ring comes from poly.exterior.xy, while interior rings iterate over poly.interiors. All coordinates convert to NumPy arrays for efficient manipulation.
Step 3–4: Vertex and Code Assembly
The class builds two parallel structures:
- Vertices: All ring coordinates appended to a list
- Path codes: A sequence of
Path.MOVETO,Path.LINETO, andPath.CLOSEPOLYconstants that instruct Matplotlib how to traverse each ring
The code generation uses a compact lambda pattern that marks the first point with MOVETO, subsequent points with LINETO, and closes with CLOSEPOLY.
Step 5: Path Construction
_VERTICES and _CODES arrays concatenate via np.concatenate, then transpose to the shape Matplotlib's Path constructor expects. The resulting Path object, combined with user-provided styling arguments (ec, fc, lw, etc.), passes to the parent PathPatch initializer.
Step 6: Integration with plot_gdf
The plot_gdf function leverages PolygonPatch when iterating over preprocessed geometries. Each polygon or multi-polygon triggers instantiation, followed by ax.add_patch() to register with the axes. The same patch often renders twice—once filled, once as an outline (fill=False)—to create layered visual effects.
Manual Usage: Creating Patches Directly
For custom visualization workflows outside the high-level plot() API, instantiate PolygonPatch directly:
import matplotlib.pyplot as plt
import numpy as np
from shapely.geometry import Polygon
from prettymaps.draw import PolygonPatch
# Define a pentagon with a triangular hole
outer = [(0, 0), (2, 0), (3, 1.5), (1, 3), (-1, 1.5)]
hole = [(1, 1), (1.5, 1.5), (0.5, 1.5)]
poly = Polygon(outer, [hole])
fig, ax = plt.subplots(figsize=(6, 6))
ax.add_patch(
PolygonPatch(
poly,
ec="#333333", # edge color
fc="#88ccff", # fill color
lw=1.5,
alpha=0.7,
)
)
ax.set_aspect("equal")
ax.set_xlim(-2, 4)
ax.set_ylim(-1, 4)
plt.title("Shapely Geometry → Matplotlib Patch")
plt.tight_layout()
plt.show()
This pattern grants full control over geometry sources, coordinate reference systems, and aesthetic parameters.
Automatic Conversion via the Public API
The plot() function handles conversion transparently when rendering OpenStreetMap data:
from prettymaps import plot
# PolygonPatch conversion occurs automatically for all polygonal layers
plot(
query="Lisbon, Portugal",
layers={
"building": {"tags": {"building": True}, "width": 0},
"water": {"tags": {"natural": "water"}, "width": 0},
},
style={
"building": {"fc": "#e8e8e8", "ec": "#d0d0d0", "lw": 0.5},
"water": {"fc": "#a5bfdd", "ec": "#8aabcc", "lw": 0},
},
radius=800,
figsize=(10, 10),
save="lisbon.svg",
)
Behind this call, fetch.py retrieves OSM data, gdf_to_shapely converts GeoDataFrames to Shapely objects, and plot_gdf instantiates PolygonPatch for each geometry—no manual intervention required.
Performance and Implementation Details
| Aspect | Implementation |
|---|---|
| Input flexibility | Handles Polygon, MultiPolygon, and GeometryCollection via hasattr(geometry, "geoms") check |
| Coordinate handling | NumPy array conversion with np.asarray and np.concatenate for vectorized operations |
| Path code generation | Lambda building Path.MOVETO, Path.LINETO, Path.CLOSEPOLY sequences per ring |
| Parent class | Subclasses matplotlib.patches.PathPatch, inheriting transform and clipping behavior |
| Styling passthrough | **kwargs forwarded unmodified to PathPatch.__init__ |
The log_execution_time decorator from prettymaps/utils.py wraps performance-critical paths, enabling runtime profiling without code modification.
Summary
- Primary conversion location:
prettymaps/draw.py, lines 119–168, defining thePolygonPatchclass - Input handling: Automatic flattening of multi-geometries; extraction of
exterior.xyandpoly.interiors - Matplotlib bridge: Construction of
Pathobjects with properMOVETO/LINETO/CLOSEPOLYcodes - Typical usage: Direct instantiation for custom plots; automatic application via
plot()andplot_gdf - Output: Native Matplotlib
PathPatchcompatible with all axes methods and export formats
Frequently Asked Questions
How does PolygonPatch handle geometries with multiple holes?
PolygonPatch iterates over poly.interiors and appends each ring's coordinates to the vertex list with corresponding path codes. The CLOSEPOLY code terminates each ring independently, ensuring holes render as expected.
Can PolygonPatch process non-polygon geometries like LineString?
No—PolygonPatch expects polygonal inputs. For linear features, prettymaps uses alternative rendering paths in draw.py. Passing a LineString would raise an AttributeError when accessing .exterior.
What styling parameters does PolygonPatch accept?
All keyword arguments pass through to matplotlib.patches.PathPatch. Common parameters include fc (facecolor), ec (edgecolor), lw or linewidth, alpha, linestyle, and hatch.
Where does the coordinate transformation to display CRS happen?
PolygonPatch receives already-transformed coordinates. CRS handling occurs upstream in fetch.py (OSM data retrieval) and preprocessing functions, ensuring geometries arrive in the target projection before patch conversion.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →