docs
Install it, point it at your rides, render.
01 Install
cargo install patinateOr cargo install --git https://gitlab.com/dunn.dev/patinate to build from source. The build needs a C compiler (build-essential, or xcode-select --install on macOS); there are no runtime dependencies. Signed binaries are on the releases page.
02 Config
Register a Strava app at strava.com/settings/api with the authorization callback domain localhost, then write ~/.config/patinate/config.toml:
[general]
city_name = "Your City"
country = "USA"
center_lat = 42.9634 # downtown
center_lng = -85.6681
radius_m = 25000
[render]
theme = "noir_heat"
viewbox_width = 1200
viewbox_height = 1600
[privacy]
home_lat = 42.9999 # YOUR home, not the map center
home_lng = -85.9999
obfuscation_radius_m = 1000
salt = "output of: openssl rand -hex 16"
[strava]
client_id = "12345"
client_secret = "your-strava-client-secret"offset_m (default 750, minimum 250) bounds how far the zone moves from home. See what the zone hides.03 Auth
patinate authRuns the OAuth flow once with the activity:read_all scope and writes the tokens into config.toml at mode 0600 (parent dir 0700). The dashboard's "Your Access Token" is read-only and will not work for sync.
04 Fetch the basemap
patinate fetch-osm --city "Your City, USA" --radius-m 25000Pulls the OpenStreetMap basemap through Overpass and caches it as JSON. Once per city; fetch again when you move or change the radius. Overpass 429 and 5xx stop the run at once; set PATINATE_OVERPASS_ENDPOINT for a private mirror.
05 Sync
patinate syncAn incremental pull of your activities into a SQLite cache, the one command you run often. The watermark advances only after a full sweep and writes are idempotent, so a partial failure repeats work rather than duplicating it. A Strava 429 honors Retry-After once, capped at 60 s. There are no schema migrations yet: after an upgrade that changes the schema, delete cache.db and sync again.
06 Render
patinate render --theme warm_beige --cycling --out heatmap.svgReads the config and both caches and writes a poster SVG, with no network. The SVG is layered: basemap (water, parks, six road tiers), a glow stack, the heat strokes, and the text plate. Each answers to a flag below.
The web preset
--web trims a render for a page: compact paths, a lighter glow, no text plate, no minor roads or streams. Pair it with --transparent-bg to set it under a page. One way to ship it is to render once and rasterize:
patinate render --theme cycle_heat --cycling --web --transparent-bg \
--radius-m 12000 --viewbox-width 1200 --viewbox-height 720 --fit cover \
--anonymize --out hero.svg
rsvg-convert hero.svg -w 3600 -h 2160 -o /tmp/hero.png
magick /tmp/hero.png -quality 82 hero.webp07 Themes
Six themes are embedded in the binary; patinate list-themes shows them and anything in --themes-dir. The heat core multiplies on a light ground and screens on a dark one; heat.blend (normal, multiply, screen) pins it.
| theme | palette | use when |
|---|---|---|
noir_heat | Black ground, white road hierarchy, hot orange heat. | The default poster. |
blueprint_heat | Deep blueprint blue, pale roads, amber heat. | An engineering sheet. |
warm_beige | Cream paper, sepia roads, terracotta heat. | Printing. |
cycle_heat | Paper palette, steel-blue heat. | A web hero on a light page. |
newsprint | Letterpress black on gray stock, red heat. | A print that reads like a page. |
verdigris | Bronze roads on a dark green ground, verdigris heat. | A dark print for a frame. |
Your own theme
Start one on the press and save the JSON, or copy a shipped theme. Then:
mkdir -p themes && mv ~/Downloads/my_theme.json themes/
patinate list-themes --themes-dir ./themes
patinate render --themes-dir ./themes --theme my_theme --out my_theme.svgThe schema, by example (this is newsprint renamed). The glow keys and heat.blend are optional; without them the renderer uses its defaults.
{
"name": "my_theme",
"bg": "#e8e5dd",
"text": "#141414",
"water": "#bfc5c7",
"parks": "#d6d8cc",
"water_line_width": 1,
"road_motorway": {
"color": "#141414",
"width": 0.9
},
"road_trunk": {
"color": "#2a2a2a",
"width": 0.75
},
"road_primary": {
"color": "#4a4a4a",
"width": 0.6
},
"road_secondary": {
"color": "#7a7a7a",
"width": 0.45
},
"road_tertiary": {
"color": "#a8a8a4",
"width": 0.32
},
"road_residential": {
"color": "#cccbc4",
"width": 0.2
},
"heat": {
"color": "#e8312a",
"width": 0.75,
"alpha": 0.6,
"glow_outer_ratio": 3,
"glow_outer_alpha": 0.08,
"glow_inner_ratio": 1.6,
"glow_inner_alpha": 0.18,
"blend": "multiply"
},
"fade_top": {
"from": "#e8e5dd",
"to": "transparent",
"height_pct": 0
},
"fade_bottom": {
"from": "#e8e5dd",
"to": "transparent",
"height_pct": 0
}
}08 Render flags
| flag | effect |
|---|---|
--theme <name> | Theme to apply. Overrides [render].theme. |
--themes-dir <dir> | Directory of custom *.json themes, searched before the embedded set. |
--cycling | Only Ride and EBikeRide. |
--type <list> | Comma-separated activity types: Ride, EBikeRide, VirtualRide, Run, VirtualRun, Walk, Hike. |
--gear <id> | Strava gear_id filter. Repeatable. |
--activity-id <n> | Render a single activity by Strava ID. |
--min-distance-m <m> | Drop activities shorter than m. Default 1000. |
--web | Inline-embed preset: lighter glow, compact paths, no text plate, minor roads or streams. It does not imply --anonymize. |
--transparent-bg | Skip the background rect, for a page ground. |
--heat-only | Skip the basemap; heat and text only. |
--anonymize | Strip data-* from heat paths and scrub <desc>. For anything public. |
--obfuscation-radius-m <m> | Override the privacy radius for one render. |
--heat-bloom <f> | Multiplier on the glow stack. 0 = sharp, 1 = theme default, 2+ = soft cloud. |
--heat-alpha <f> | Multiplier on the sharp-core stroke opacity. |
--radius-m <m> | Override the configured map radius. |
--viewbox-width <px> / --viewbox-height <px> | Override the viewBox dimensions. |
--fit <contain|cover> | contain (default) letterboxes the radius circle; cover fills the viewBox and crops. The frame never depends on rides or home. |
--no-refine | Skip the OSM filter pipeline. Diagnostic. |
--config <path> | TOML config. Default ~/.config/patinate/config.toml. |
--osm <path> | Pre-fetched OSM JSON. Default: the per-user cache. |
--activities <path> | JSON array of activities. Default: read from the cache. |
--cache <path> | SQLite cache path. Default: per-user. |
--out <path> | Output SVG. Default heatmap.svg. |
09 Before you publish
A default render embeds data-rider, data-bike, data-year and data-type on the heat paths and the exact center coordinates in <desc>. That is useful for a page's filters and identifying in public. For anything public:
patinate render --theme cycle_heat --anonymize --obfuscation-radius-m 1000 --out heatmap.svg--anonymize drops the attributes and the <desc>; a PNG hides no more than that. It also warns when the map center sits within the radius plus offset_m of home, since the frame centers on it. The rendered SVG is yours; the GPS traces are Strava's, under their API terms. Report a privacy bypass through SECURITY.md.
10 Environment
Any config key also reads from PATINATE_<SECTION>__<KEY>, for example PATINATE_RENDER__VIEWBOX_WIDTH=1600.
| variable | purpose |
|---|---|
STRAVA_PATINATE_CLIENT_SECRET | OAuth client secret. |
STRAVA_PATINATE_REFRESH_TOKEN | Long-lived refresh token. |
STRAVA_PATINATE_ACCESS_TOKEN | Optional 24 h access token. |
PATINATE_CONFIG | TOML config path. |
PATINATE_OSM | OSM JSON path (read by render, written by fetch-osm). |
PATINATE_THEMES_DIR | Custom themes directory. |
PATINATE_OVERPASS_ENDPOINT | Overpass API URL. |
PATINATE_CACHE | SQLite cache path. |
PATINATE_ACTIVITIES | Activity JSON path (skips the cache). |