# Converting Street Network Widths Using `graph_to_shapely` in Prettymaps

> Learn to convert street network widths using graph_to_shapely in prettymaps. Buffer OSM streets into polygons with custom widths or highway-specific styles.

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

---

**Use `graph_to_shapely()` in [`prettymaps/draw.py`](https://github.com/marceloprates/prettymaps/blob/main/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](https://github.com/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`](https://github.com/marceloprates/prettymaps/blob/main/prettymaps/draw.py) (lines 75–116), the function follows a six-stage pipeline to process street geometries.

### Function Signature

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

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

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

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

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

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

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

```python
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`](https://github.com/marceloprates/prettymaps/blob/main/prettymaps/draw.py) | Layer-aware wrapper that calls `graph_to_shapely` |
| `plot_gdf` | [`prettymaps/draw.py`](https://github.com/marceloprates/prettymaps/blob/main/prettymaps/draw.py) | End-to-end renderer consuming width parameters |
| [`fetch.py`](https://github.com/marceloprates/prettymaps/blob/main/fetch.py) utilities | [`prettymaps/fetch.py`](https://github.com/marceloprates/prettymaps/blob/main/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`](https://github.com/marceloprates/prettymaps/blob/main/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.