Skip to content

Usage

Install

Using the library — from PyPI, no clone:

pip install roadstyle
pip install "roadstyle[studio]"      # + the no-code workbench: `roadstyle studio`

Optional extras pull in heavier backends/inputs only when you ask for them (combine freely, e.g. "roadstyle[numeric,tiles]"):

# studio   — the interactive Streamlit workbench (roadstyle studio)
# numeric  — continuous data-driven styling (mapclassify + matplotlib colormaps)
# tiles    — tiles=True: embedded vector tiles for big networks
# lonboard — the WebGL backend for very large edge sets
# basemaps — any xyzservices tile provider as a base map
# duckdb / arrow — read edges straight from a DuckDB query or an Arrow table
# all      — every extra above in one go: pip install "roadstyle[all]"

The latest unreleased state, still without cloning — extras combine the same way (bare roadstyle @ git+… is core only): pip install "roadstyle[all] @ git+https://github.com/Khoshkhah/roadstyle.git".

Developing on it (clone + editable, from the repo root — a src layout): pip install -e ".[dev]", or with conda conda env create -f environment.yml && conda activate roadstyle && pip install -e ".[dev]".

Command line

No Python needed — roadstyle renders any road file straight from the shell:

roadstyle edges.gpkg -o map.html --basemap dark_matter               # styled interactive map
roadstyle edges.gpkg --include motorway trunk primary       # keep only major roads
roadstyle edges.gpkg --color-by aadt --cmap viridis --width-by 1 6   # colour by your data
roadstyle edges.gpkg -f spec -o map_data.json               # JSON spec for your own frontend

roadstyle studio                                            # the interactive Streamlit workbench
roadstyle studio --server.port 8502                         # extra args are forwarded to streamlit

-f/--format is one of web (self-contained MapLibre map, default), folium, rsjs (roadstyle.js page), spec, geojson; every other flag mirrors a render_edges keyword (the web map also takes --no-arrows/--no-labels/--no-filter/--no-basemap-switcher). See roadstyle --help. roadstyle studio (from the studio extra) launches the workbench and forwards any extra arguments to streamlit run.

Input — what roadstyle expects

roadstyle styles road edges: a GeoDataFrame (or a RoadEdges) with line geometry (LineString/MultiLineString) and at least one column to style by. You load your data however you like; roadstyle normalises it (reproject to EPSG:4326, drop non-line rows, optionally rename your class column):

import geopandas as gpd
import roadstyle as rs

raw = gpd.read_file("edges.gpkg")              # GeoPackage / GeoJSON / Shapefile / PostGIS / …
edges = rs.normalize_edges(raw, class_col="highway", rename={"vagtyp": "highway"})

render_edges (and to_spec, etc.) also accept a plain GeoDataFrame and normalise it for you. Roads already in DuckDB? rs.from_duckdb(con, query) renders straight from a SQL query (select the geometry as WKB with ST_AsWKB) — see examples/roads_from_duckdb.py.

Quick start

edges = gpd.read_file("edges.gpkg")            # needs a `highway` column
rs.render_edges(edges, basemap="dark_matter").save("roads.html")

render_edges returns a WebMap by default (the MapLibre web backend), or a folium.Map with backend="folium", or a lonboard.Map with backend="lonboard" — so you can keep customising it, .save() it, or display it inline in a notebook.

Recipes

# Palettes, base maps, filtering, selection
rs.render_edges(edges, palette="carto",
                include=["motorway", "trunk", "primary"])
rs.render_edges(edges, basemap="satellite")
rs.render_edges(edges, selected=picked_edges)          # neon-violet highlight

# Data-driven: categorical (e.g. congestion levels)
rs.render_edges(edges, color_by="congestion",
                colors={"low": "#11D68F", "moderate": "#FFCF43",
                        "heavy": "#F24E42", "severe": "#A92727"}, legend=True)

# Data-driven: numeric (e.g. traffic volume) with a width ramp + gradient legend
rs.render_edges(edges, color_by="aadt", cmap="viridis",
                vmin=0, vmax=20000, width_by=(1, 8), legend=True)

# Per-edge colour from your own edge_id -> colour table (gray fallback for misses)
rs.render_edges(edges, color_table={"4897…": "#e6194B", "5193…": "#3cb44b"})
rs.render_edges(edges, color_by="color", colors="self")   # colour already a column

# GPU backend for very large edge sets
rs.render_edges(big_edges, backend="lonboard", color_by="aadt", cmap="magma")

# self-contained MapLibre map (per-zoom widths, two-way lanes, arrows, names,
# hover/select, base switcher, tunnel/bridge grade separation) — opens offline, no server
rs.render_edges(edges, backend="web", basemap="dark_matter").save("roads.html")

MapLibre web backend

backend="web" renders a finished, zoom-correct MapLibre vector map as one self-contained HTML file (MapLibre + data bundled in — opens from disk with no server, no internet). It adds two-way directional lanes, direction arrows, curved street names, hover/select, a base-layer switcher, and tunnel/bridge grade separation (reads optional tunnel/bridge/layer columns). It returns a WebMap with .save(path).

rs.render_edges(edges, backend="web", basemap="dark_matter",
                offset_frac=0.28, width_frac=0.6, offset_zoom=15).save("roads.html")

Toggle the UI with arrows / labels / filter_control / basemap_switcher (all default True). See MapLibre web backend for the full feature list and parameters. (This is distinct from rs.save / -f rsjs, which writes the roadstyle.js spec page for embedding.)

# Switchable data-driven colouring: a neutral mono base + a "Colour by" dropdown that
# recolours client-side (no re-render). Blank edges keep the base colour, not a flat grey.
rs.render_edges(edges, backend="web", palette="mono",
    color_options={"Class": {}, "AADT": {"color_by": "aadt", "cmap": "viridis"},
                   "Speed": {"color_by": "speed_kph", "cmap": "magma"}}).save("recolor.html")

# Extra overlay layers you bring (zones / POIs / any geometry): drawn under/over the roads,
# clickable, toggled from a Layers control.
rs.render_edges(edges, backend="web",
    overlays=[rs.Overlay(zones, kind="fill", placement="under", color="#6aa9ff",
                         opacity=0.14, label="Zones", popup=["taz_id", "weight"]),
              rs.Overlay(pois, kind="circle", placement="over", color="#ff5d5d",
                         radius=7, label="POIs", popup=["name", "type"])]).save("overlays.html")

Prefer to see these working? The Manual walks through them with the live map embedded after each step.

Settings & the JavaScript API

Every styling default lives in one settings file with a 5-level override ladder — see Palettes → settings; per-call: render_edges(edges, settings={...}). And every in-map control is scriptable from JS (rsSetBasemap, rsQuery id-set verbs, rs:* events) — see the JS API and the copyable ui/ templates.

Custom (non-OSM) road classes

from roadstyle import register_palette, RoadStyle, color_by_class
register_palette("nvdb", {"0": RoadStyle("#00E5FF", 6, 8),
                          "1": RoadStyle("#FF9100", 4.5, 6.5)})
rs.render_edges(edges, style=color_by_class("functional_road_class",
                                            palette="nvdb", normalize_links=False))

Palettes can also be saved/loaded as JSON (save_palette / load_palette) so a non-coder can edit colours, or a web frontend can share the same colour source.

Web output

spec = rs.to_spec(edges, color_by="aadt", cmap="viridis")  # canonical JSON dict
rs.save_spec(edges, "roads.json", color_by="aadt", cmap="viridis")
rs.save(edges, "roads.html", color_by="aadt", cmap="viridis")   # standalone HTML
html = rs.to_iframe(edges, color_by="aadt", cmap="viridis")     # <iframe> string

See Embedding in a website for the JSON spec and frontend snippets, and the Parameter reference for every parameter.

Input formats (what render_edges / to_spec accept)

Everything funnels through as_edges, so the entry points take more than a GeoDataFrame — pass whichever you already have and roadstyle normalises it (to EPSG:4326 line geometry):

import roadstyle as rs

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 / __geo_interface__
rs.render_edges(arrow_table)               # a pyarrow Table

For DuckDB (and to control the geometry column / source CRS), use the matching helper and pass its result on — select the geometry as WKB or WKT so it round-trips:

edges = rs.from_duckdb(
    con,
    "SELECT highway, aadt, ST_AsWKB(geom) AS geom FROM roads",
    geometry="geom", crs=3006,             # DuckDB carries no CRS; say what the coords are in
)
rs.render_edges(edges, color_by="aadt", cmap="YlOrRd")

from_geojson / from_arrow / from_duckdb / load_edges all return a RoadEdges; install the optional duckdb / arrow extras (pip install roadstyle[duckdb,arrow]) as needed.