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, and Path.CLOSEPOLY constants 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 the PolygonPatch class
  • Input handling: Automatic flattening of multi-geometries; extraction of exterior.xy and poly.interiors
  • Matplotlib bridge: Construction of Path objects with proper MOVETO/LINETO/CLOSEPOLY codes
  • Typical usage: Direct instantiation for custom plots; automatic application via plot() and plot_gdf
  • Output: Native Matplotlib PathPatch compatible 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →