# Converting Shapely Geometries to Matplotlib PathPatches with PolygonPatch in prettymaps

> Learn to convert Shapely geometries to Matplotlib PathPatches using prettymaps PolygonPatch. This function simplifies rendering complex shapes in your maps.

- Repository: [Marcelo de Oliveira Rosa Prates/prettymaps](https://github.com/marceloprates/prettymaps)
- Tags: how-to-guide
- Published: 2026-08-20

---

**The `PolygonPatch` class in [`prettymaps/draw.py`](https://github.com/marceloprates/prettymaps/blob/main/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)](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.

```python

# 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:

```python
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:

```python
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`](https://github.com/marceloprates/prettymaps/blob/main/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`](https://github.com/marceloprates/prettymaps/blob/main/prettymaps/utils.py) wraps performance-critical paths, enabling runtime profiling without code modification.

## Summary

- **Primary conversion location**: [`prettymaps/draw.py`](https://github.com/marceloprates/prettymaps/blob/main/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`](https://github.com/marceloprates/prettymaps/blob/main/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`](https://github.com/marceloprates/prettymaps/blob/main/fetch.py) (OSM data retrieval) and preprocessing functions, ensuring geometries arrive in the target projection before patch conversion.