patinate

docs

Install it, point it at your rides, render.

01 Install

cargo install patinate

Or 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"
The salt must be at least 16 characters, private, and never changed: it fixes where the hidden zone sits. Any radius above 0 refuses to render without it. offset_m (default 750, minimum 250) bounds how far the zone moves from home. See what the zone hides.

03 Auth

patinate auth

Runs 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 25000

Pulls 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 sync

An 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.svg

Reads 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.webp

07 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.

themepaletteuse when
noir_heatBlack ground, white road hierarchy, hot orange heat.The default poster.
blueprint_heatDeep blueprint blue, pale roads, amber heat.An engineering sheet.
warm_beigeCream paper, sepia roads, terracotta heat.Printing.
cycle_heatPaper palette, steel-blue heat.A web hero on a light page.
newsprintLetterpress black on gray stock, red heat.A print that reads like a page.
verdigrisBronze 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.svg

The 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

flageffect
--theme <name>Theme to apply. Overrides [render].theme.
--themes-dir <dir>Directory of custom *.json themes, searched before the embedded set.
--cyclingOnly 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.
--webInline-embed preset: lighter glow, compact paths, no text plate, minor roads or streams. It does not imply --anonymize.
--transparent-bgSkip the background rect, for a page ground.
--heat-onlySkip the basemap; heat and text only.
--anonymizeStrip 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-refineSkip 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.

variablepurpose
STRAVA_PATINATE_CLIENT_SECRETOAuth client secret.
STRAVA_PATINATE_REFRESH_TOKENLong-lived refresh token.
STRAVA_PATINATE_ACCESS_TOKENOptional 24 h access token.
PATINATE_CONFIGTOML config path.
PATINATE_OSMOSM JSON path (read by render, written by fetch-osm).
PATINATE_THEMES_DIRCustom themes directory.
PATINATE_OVERPASS_ENDPOINTOverpass API URL.
PATINATE_CACHESQLite cache path.
PATINATE_ACTIVITIESActivity JSON path (skips the cache).