Converting Street Network Widths Using `graph_to_shapely` in Prettymaps

Use graph_to_shapely() in prettymaps/draw.py to convert OSM street networks into width-buffered Shapely polygons by passing a numeric width or a dictionary mapping highway types to specific widths.

graph_to_shapely is the core function in the marceloprates/prettymaps library that transforms OpenStreetMap street geometries into stylizable polygons. This conversion is essential for creating the distinctive illustrated map aesthetic that prettymaps produces—thin line strings become solid road shapes with thickness proportional to their real-world importance.

How graph_to_shapely Works Internally

Located in prettymaps/draw.py (lines 75–116), the function follows a six-stage pipeline to process street geometries.

Function Signature

def graph_to_shapely(gdf: gp.GeoDataFrame, width: float = 1.0) -> BaseGeometry

The width parameter accepts either:

  • A single float — uniform width for all streets
  • A dictionary — mapping OSM highway tag values to specific widths in meters

Step 1: Map Highway Tags to Widths

The internal highway_to_width helper (lines 88–97) handles three highway tag formats:

def highway_to_width(highway):
    if (type(highway) == str) and (highway in width):
        return width[highway]
    elif isinstance(highway, Iterable):
        for h in highway:
            if h in width:
                return width[h]
        return np.nan
    else:
        return np.nan
  • String tags (e.g., "primary") — direct dictionary lookup
  • List of tags (e.g., ["primary", "secondary"]) — returns width for first match
  • Unrecognized formats — returns NaN (row filtered out later)

Step 2: Annotate the GeoDataFrame

gdf["width"] = (
    gdf["highway"].map(highway_to_width) if type(width) == dict else width
)

This adds a temporary width column containing the buffer distance for each row.

Step 3: Filter Invalid Rows

gdf.drop(gdf[gdf.width.isna()].index, inplace=True)

Streets without a defined width are removed to prevent buffer errors.

Step 4: Buffer and Union Geometries

with warnings.catch_warnings():
    warnings.simplefilter("ignore", shapely.errors.ShapelyDeprecationWarning)
    if not all(gdf.width.isna()):
        gdf.geometry = gdf.apply(
            lambda row: row["geometry"].buffer(row.width), axis=1
        )
return shapely.ops.unary_union(gdf.geometry)

Each line is expanded into a polygon using Shapely's buffer() method, then unified into a single geometry object via unary_union.

Practical Code Examples

Example 1: Per-Highway Width Mapping with gdf_to_shapely

The gdf_to_shapely helper wraps graph_to_shapely with additional layer handling. This is the recommended entry point for most workflows:

import osmnx as ox
from prettymaps.draw import gdf_to_shapely
import matplotlib.pyplot as plt

# Fetch street network for Amsterdam

city = ox.geocode_to_gdf("Amsterdam, Netherlands")
graph = ox.graph_from_polygon(city.geometry.iloc[0], network_type="drive")
gdf = ox.graph_to_gdfs(graph, nodes=False)

# Define realistic road widths in meters

road_widths = {
    "motorway": 12,
    "trunk": 10,
    "primary": 8,
    "secondary": 6,
    "tertiary": 4,
    "residential": 3,
    "service": 2,
}

# Convert to width-buffered Shapely geometry

streets_shape = gdf_to_shapely(
    layer="streets",
    gdf=gdf,
    width=road_widths,  # Dictionary maps highway types to widths

)

# Render

fig, ax = plt.subplots(figsize=(8, 8))
ax.set_aspect("equal")
ax.set_axis_off()
ax.add_patch(plt.Polygon(streets_shape.exterior.coords, facecolor="#2c3e50"))
plt.show()

Example 2: Direct graph_to_shapely Call

Use this when you already have a processed GeoDataFrame and need lower-level control:

from prettymaps.draw import graph_to_shapely

# Uniform 5-meter width for all streets

streets_polygon = graph_to_shapely(gdf, width=5.0)

# Or with per-type widths

road_widths = {
    "motorway": 10,
    "primary": 6,
    "residential": 2.5,
}
streets_polygon = graph_to_shapely(gdf, width=road_widths)

Example 3: High-Level plot_gdf Helper

For rapid visualization without manual Matplotlib setup:

from prettymaps.draw import plot_gdf
import matplotlib.pyplot as plt

fig, ax = plt.subplots(figsize=(10, 10))
plot_gdf(
    layer="streets",
    gdf=gdf,
    ax=ax,
    width=road_widths,    # Passed through to underlying graph_to_shapely

    mode="matplotlib",
    fc="#34495e",         # Fill color

    ec="none",            # No edge color

)
plt.show()

Key Design Decisions in the Source Code

  • Flexible width specification — The type(width) == dict check (line 99) allows seamless switching between uniform and categorical styling without changing function signatures.

  • Defensive filtering — Dropping NaN widths (line 105) prevents Shapely buffer failures on malformed or unclassified highway data.

  • Warning suppression — The catch_warnings() context manager (lines 108–109) silences Shapely deprecation noise during bulk geometry operations.

  • Union by default — Returning unary_union produces a single MultiPolygon, simplifying downstream rendering code that expects one geometry object per layer.

Integration with the Prettymaps Pipeline

graph_to_shapely sits at the center of the drawing stack:

Function Location Role
graph_to_shapely prettymaps/draw.py:75 Core width-to-geometry conversion
gdf_to_shapely prettymaps/draw.py Layer-aware wrapper that calls graph_to_shapely
plot_gdf prettymaps/draw.py End-to-end renderer consuming width parameters
fetch.py utilities prettymaps/fetch.py OSM data retrieval producing input GeoDataFrames

Changes to graph_to_shapely behavior propagate automatically through all higher-level functions.

Summary

  • graph_to_shapely converts OSM street lines to width-buffered polygons via prettymaps/draw.py
  • Accept scalar widths for uniform styling or dictionaries for per-highway-type control
  • Unmatched highway tags are automatically filtered to prevent rendering errors
  • The function unions all geometries into a single Shapely object for efficient plotting
  • Use gdf_to_shapely for layer-aware workflows or plot_gdf for immediate visualization

Frequently Asked Questions

What happens if a highway type isn't in my width dictionary?

Rows with unmapped highway values receive NaN width and are silently dropped before buffering. This prevents Shapely errors but means those streets won't appear in the final output. To include all streets, ensure your dictionary covers all expected highway tags or use a scalar width fallback.

Can I use graph_to_shapely with non-OSM GeoDataFrames?

Yes, provided your GeoDataFrame has a highway column containing string or list values for the width lookup. For non-street data, you may need to rename your categorical column to highway or modify the function's column reference in a fork.

Why are my buffered streets appearing disconnected?

unary_union merges overlapping geometries, but streets separated by gaps remain distinct polygons. For visual continuity, ensure your width values are large enough to create overlap at intersections, or post-process with additional Shapely operations like buffer(..., cap_style='round', join_style='round').

How do I invert the width logic—making major roads thinner than minor roads?

Simply swap your width values so smaller numbers map to major highways. The function has no baked-in hierarchy; it applies whatever numeric values you provide.

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 →