Skip to content

Tutorial

A guided, read-it-top-to-bottom walkthrough of roadstyle — the same ground the example notebooks cover, but on one page so you can learn it in the browser without opening Jupyter. Every snippet is copy-paste runnable; they all use a GeoDataFrame of road edges called edges (any road dataset with line geometry and a road-class column works).

How to follow along

Install with pip install -e ".[numeric,basemaps]" and load any road dataset. The notebooks ship a sample (notebooks/data/sodermalm_edges.gpkg, ~4,600 real edges); to reproduce a step exactly, open the matching notebook listed under each section.


1 · Quickstart

roadstyle turns a GeoDataFrame of roads into a styled, interactive map — an OSM-style geometry sandwich (a coloured fill drawn over a wider casing) so junctions and overlaps read cleanly. You need just two things: a road-class column (default highway) and line geometry.

import geopandas as gpd
import roadstyle as rs

edges = gpd.read_file("edges.gpkg")        # any CRS; reprojected to EPSG:4326 for you
print(f"{len(edges):,} edges, CRS {edges.crs}")

One call renders it. The default backend is web (a self-contained MapLibre map); in a notebook the returned map (WebMap, or folium.Map with backend="folium") displays inline; anywhere else, .save() it:

m = rs.render_edges(edges, basemap="dark_matter")   # high-saturation palette on a dark base map
m.save("roads.html")                        # a standalone, self-contained page

That's the whole loop: load → render_edges.save. Everything below adds colour, filtering, and web output on top of it.

Notebook: 01 · Quickstart


2 · Themes & palettes

Three palettes — highsat (high-contrast), carto (classic OSM), and mono (grayscale) — and the backdrop comes from basemap= (any key in rs.BASEMAPS; default voyager from the settings file). Palettes are data files you can override.

rs.render_edges(edges, palette="carto")
rs.render_edges(edges, palette="highsat", basemap="dark_matter")

Themes set the base map and casing colours; palettes set the per-road-class colours. See Palettes for the full reference.

Notebook: 01 · Quickstart, 06 · Customizing the look


3 · Colour roads by your own data

Beyond road class, you can colour and size edges by any column — categorical or numeric — with automatic legends.

Categorical (color_by + colors)

Give each category an explicit colour. Here we derive an importance class from the highway tag:

major  = {"motorway", "trunk", "primary", "motorway_link", "trunk_link", "primary_link"}
medium = {"secondary", "tertiary", "secondary_link", "tertiary_link"}
edges["importance"] = edges["highway"].apply(
    lambda h: "major" if h in major else "medium" if h in medium else "minor"
)

rs.render_edges(
    edges,
    color_by="importance",
    colors={"major": "#ef4444", "medium": "#f59e0b", "minor": "#64748b"},
    legend=True,
)

Numeric (color_by + cmap + width_by)

Map a number to a colour ramp and a line-width ramp. Any branca/matplotlib colormap name works for cmap; vmin/vmax clamp the colour range; width_by=(min, max) scales width with the value.

edges["length_m"] = edges.to_crs(3006).length.round(1)   # metres, via a projected CRS

rs.render_edges(
    edges,
    color_by="length_m",
    cmap="viridis",
    width_by=(1, 6),        # thinnest .. thickest pixel width across the value range
    vmin=0, vmax=2000,
    legend=True,
)

Numeric styling needs an extra

cmap/numeric classification uses mapclassify + matplotlib: pip install "roadstyle[numeric]".

Notebook: 02 · Data-driven styling


4 · Filtering & highlighting

Show only the road types you care about, and highlight a selection on top of the map.

sorted(rs.highway_types(edges))                       # what classes are present?

# keep or drop types — match_links=True (default) keeps *_link ramps with their parent
rs.render_edges(edges, include=["motorway", "trunk", "primary"], basemap="dark_matter")
rs.render_edges(edges, exclude=["service", "footway", "path", "cycleway"])

# highlight a sub-selection with a neon overlay
sel = edges[edges["highway"] == "secondary"]
rs.render_edges(edges, basemap="dark_matter", selected=sel)

Notebook: 03 · Filtering & highlighting


5 · Customizing the look

Base maps

Pick the base map with any key in rs.BASEMAPS (the default comes from the basemap setting), or offer several as a switcher:

print("available base maps:", list(rs.BASEMAPS))
rs.render_edges(edges, basemap="positron")

# a thumbnail base-layer switcher on the map (folium)
rs.render_edges(edges, basemap="dark_matter", basemaps=["dark_matter", "positron", "satellite"])

Any xyzservices provider also works directly as basemap= (install pip install "roadstyle[basemaps]").

Your own colour vocabulary

color_by + colors defines exactly what each category looks like, with nothing to register:

rs.render_edges(
    edges, basemap="dark_matter",
    color_by="congestion",
    colors={"free": "#22c55e", "slow": "#f59e0b", "jam": "#ef4444"},
    legend=True,
)

For reusable palettes shared across projects there's a registry + JSON I/O (register_palette, save_palette, load_palette) — see Palettes.

Notebook: 06 · Customizing the look


6 · Large datasets (GPU / lonboard)

For very large edge sets, switch the backend to lonboard (deck.gl / WebGL). Same call, same styling — it just renders on the GPU. lonboard is optional: pip install "roadstyle[lonboard]".

rs.render_edges(edges, backend="lonboard", basemap="dark_matter")
rs.render_edges(edges, backend="lonboard", color_by="length_m", cmap="magma", width_by=(1, 5))

Use folium for portable, self-contained HTML and rich interactions; lonboard when edge counts get large enough that Leaflet feels heavy.

Notebook: 07 · Large datasets (lonboard)


7 · Loading data — every input roadstyle accepts

render_edges / to_spec don't only take a GeoDataFrame. Everything funnels through as_edges, which normalises any of the sources below to the one canonical shape (EPSG:4326 line geometry + a class column). Hand roadstyle whatever you already have:

rs.render_edges(gdf)                 # a GeoDataFrame
rs.render_edges("roads.gpkg")        # a file path: GeoPackage / GeoJSON / Shapefile / …
rs.render_edges(feature_collection)  # a GeoJSON mapping, a to_spec() dict, or __geo_interface__
rs.render_edges(arrow_table)         # a pyarrow Table (GeoArrow works directly)

For a plain WKB/WKT geometry column, or to set the source CRS, use the matching from_* helper and pass its result on:

# pyarrow with a WKB column
table = pa.table({
    "highway": edges["highway"].astype(str).tolist(),
    "geometry": [g.wkb for g in edges.geometry],
})
rs.render_edges(rs.from_arrow(table, geometry="geometry", crs=4326))

# DuckDB — select geometry as WKB (ST_AsWKB) or WKT (ST_AsText) so it round-trips.
# DuckDB carries no CRS, so crs= says what the coordinates are in.
e = rs.from_duckdb(con, "SELECT highway, aadt, ST_AsWKB(geom) AS geom FROM roads",
                   geometry="geom", crs=3006)
rs.render_edges(e, color_by="aadt", cmap="YlOrRd")
You have… Just pass it For options use
GeoDataFrame / RoadEdges render_edges(gdf) normalize_edges
a file render_edges("roads.gpkg") load_edges
GeoJSON / spec render_edges(fc) from_geojson
pyarrow Table render_edges(table) from_arrow
DuckDB from_duckdb

Install the optional extras as needed: pip install "roadstyle[duckdb,arrow]".

Notebook: 08 · Loading data


8 · Outputs for the web

The same styled map can leave roadstyle in several shapes. The heart of it is the canonical JSON spec (spec/1): your data plus the baked-in per-edge style, ready for any frontend to draw.

spec = rs.to_spec(edges, basemap="dark_matter", color_by="highway")   # canonical JSON dict
rs.save_spec(spec, "map_data.json")                          # round-trips with load_spec()

gj = rs.to_geojson(edges, color_by="highway")                # just the styled features

rs.save(edges, "standalone.html", basemap="dark_matter")              # a finished HTML file
html = rs.to_iframe(edges, basemap="dark_matter", height="360px")     # an <iframe> string, zero JS

Each feature in the spec carries reserved __rs_* properties (__rs_fill, __rs_w, __rs_casing, …), so the browser never needs roadstyle's styling logic — it just reads them. See Embedding in a website for the property contract and Leaflet / MapLibre snippets.

Notebook: 04 · Outputs for the web


9 · Embed in your own page (roadstyle.js)

The decoupled path: Python computes the styling once (a JSON spec), and the bundled, reusable roadstyle.js draws it in the browser with your own UI on top. No Python at runtime, and you don't write the renderer — it ships inside the package.

Copy the renderer next to your spec:

import shutil
from importlib.resources import files

spec = rs.to_spec(edges, basemap="dark_matter", palette="highsat")
rs.save_spec(spec, "web/map_data.json")
for asset in ("roadstyle.js", "roadstyle.css"):
    shutil.copy(files("roadstyle") / "static" / asset, f"web/{asset}")

Then a minimal page wires it up. RoadStyleMap is headless — it exposes getRoadClasses(), setFilter(), and selection events you react to:

<link rel="stylesheet" href="./roadstyle.css"/>
<div id="map" style="height:100vh"></div>
<script src="https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="./roadstyle.js"></script>
<script>
  // built-in widgets: legend, a road-type filter panel, and a base-layer switcher
  const m = new RoadStyleMap("map", { widgets: { legend: true, filter: true, basemap: true } });
  m.load("./map_data.json").then(() => {
    // build a filter UI from the classes present
    m.getRoadClasses().forEach(cls => { /* add a checkbox, then m.setFilter([...on]) */ });

    // react to clicks — feature.properties carries __rs_class + your original columns
    m.on("select",   f => console.log("selected", f.properties));
    m.on("deselect", () => console.log("cleared"));
  });
</script>

file:// and fetch()

Browsers block fetch() from file://, so loading map_data.json by URL only works when the folder is served (python -m http.server). To make a page that opens with a double-click, inline the spec instead — load() takes a URL or an object. Notebook 05 shows the inlined pattern end to end.

Selection is single: clicking another road replaces it; click the same road again, or the map background, to deselect. Full callback details are in Embedding → Reacting to a selection.

Notebook: 05 · Embedding with roadstyle.js


Where to next