Explore fitness activity data spatially in QGIS.
qfit is a QGIS plugin that turns synced fitness activities into GeoPackage-backed layers, analysis views, and atlas-ready publish data for mapping and print workflows.
qfit in QGIS, showing synced activity tracks and points with the plugin dock open.
This README is split into two parts:
- Part 1, for human readers using qfit in QGIS
- Part 2, for contributors and AI coding agents changing the codebase
qfit currently supports:
- connecting to Strava with
client_id,client_secret, andrefresh_token - opening the Strava authorize page from the plugin and exchanging an auth code for a refresh token
- fetching activities from Strava in the background
- previewing fetched activities in the dock before writing anything to disk
- storing a canonical local GeoPackage sync store
- loading QGIS layers for tracks, start points, and optional sampled stream points
- filtering by activity type, text search, date range, distance, and detailed-stream availability
- applying visualization presets, optional temporal wiring, and an optional Mapbox basemap
- running analysis workflows such as frequent starting points and activity heatmaps
- generating atlas-ready publish layers and exporting a PDF atlas from the dock
qfit uses a GeoPackage as both local sync store and QGIS data source.
Internal tables
activity_registrysync_state
Visible layers
activity_tracksactivity_startsactivity_points(optional, derived from detailed streams)activity_atlas_pages
Atlas helper tables
atlas_document_summaryatlas_cover_highlightsatlas_page_detail_itemsatlas_toc_entriesatlas_profile_samples
- Configure Strava credentials.
- Fetch activities and preview the result in the dock.
- Choose an output
.gpkgand store the data. - Load the qfit layers into QGIS.
- Apply visualization or analysis workflows.
- Optionally generate atlas-ready publish data and export a PDF atlas.
You need:
client_idclient_secretrefresh_token
qfit includes a built-in OAuth helper in qfit → Configuration for the refresh-token step.
See:
docs/strava-setup.md
qfit can load an optional Mapbox basemap and keep it below the qfit layers in the QGIS layer tree.
For Mapbox basemaps, raster mode is the recommended choice when you need the closest visual match to Mapbox's own rendering. Vector mode is rendered natively by QGIS and should be treated as a practical local approximation: it can be more interactive and inspectable inside QGIS, but fonts, labels, sprites, antialiasing, and zoom interpolation will not be pixel-identical to Mapbox GL JS.
The current visualization flow supports:
- semantic activity styling by activity type
- simpler line-based presets
- track points / start points / heatmap-oriented views
- temporal timestamp wiring when timestamp fields are available
See:
docs/map-style-guide.mddocs/mapbox-outdoors-comparison-harness.mdfor manual Mapbox Outdoors browser-vs-QGIS parity checks
qfit can generate atlas-ready layers and helper tables for print layouts, then export a PDF atlas from the plugin.
The current publish flow supports:
- atlas page extent planning
- cover and summary helper tables
- TOC-ready helper rows
- route-profile sample tables for layout charts
- programmatic PDF export through the atlas/export subsystem
For validation notes and rendering-sensitive workflow details, see:
docs/atlas-validation-harness.md
docs/strava-setup.mddocs/schema.mddocs/map-style-guide.mddocs/mapbox-outdoors-comparison-harness.mddocs/qgis-testing.md
If you are changing internals, start here:
CONTRIBUTING.mddocs/architecture.mddocs/qgis-plugin-architecture-principles.mddocs/refactoring-roadmap.mddocs/qgis-testing.mddocs/atlas-validation-harness.mdfor atlas rendering/export-sensitive workdocs/mapbox-outdoors-comparison-harness.mdfor manual Mapbox Outdoors browser-vs-QGIS visual comparison work
qfit is being evolved as a modular monolith with pragmatic ports-and-adapters boundaries.
Preferred dependency direction:
UI -> application/workflow -> domain + ports -> infrastructure adapters
In practice, that means:
- keep
qfit_dockwidget.pyfocused on UI glue - move workflow orchestration into feature-owned application modules
- keep provider-neutral logic easier to test than QGIS-heavy code
- keep QGIS, Strava, GeoPackage, settings, and PDF assembly details in infrastructure/adapters when that improves clarity
- add ports/gateways only when they earn their keep
Plugin entrypoints and UI host
qfit_plugin.pyqfit_dockwidget.pyqfit_config_dialog.pyqfit_dockwidget_base.ui
Feature-owned packages
activities/for fetch/sync/load workflows and provider-neutral activity logicanalysis/for analysis workflows, request/result shaping, and QGIS-backed analysis adaptersatlas/for publish/export workflows, runtime preparation, and PDF assemblyconfiguration/for settings, connection status, and dock-settings binding helpersproviders/for provider contracts and Strava-backed adaptersui/for dock-widget dependency assembly and UI-only coordination helpersvisualization/for render planning, basemap workflows, temporal wiring, and QGIS layer adapters
Root-level modules
The top-level Python module layer is now mostly limited to:
- plugin/bootstrap entrypoints
- a few small shared helpers such as
polyline_utils.py,time_utils.py,mapbox_config.py, andqfit_cache.py - transitional compatibility shims such as
activity_query.py,activity_classification.py,models.py,activity_storage.py, andlayer_manager.pythat still exist only to cushion package migration
Rule of thumb:
Do not add new feature-specific top-level modules.
If new code belongs to one feature, it should usually live under that feature package.
- Keep thinning
QfitDockWidget. - Preserve strict feature ownership.
- Move policy into application/domain while leaving mechanics in adapters.
- Keep request/result seams explicit where they reduce UI or framework coupling.
- Delete compatibility shims once in-repo callers are migrated.
- Prefer small, reviewable PR-sized slices.
- Every behavior change needs tests.
- New workflow logic should not accumulate in
QfitDockWidget. - Prefer feature-owned modules over generic root-level helpers.
- Prefer explicit request/result dataclasses when they replace long parameter lists or messy widget-state handoff.
- Keep provider-neutral logic free of PyQGIS when practical.
- Rendering/export-sensitive changes need artifact proof, not only green CI.
activities/
activities/domain/holds provider-neutral activity logic.activities/application/owns fetch/sync/load workflows, preview helpers, and task wrappers.- GeoPackage-backed activity persistence belongs under infrastructure-oriented paths.
analysis/
- dock-facing analysis entrypoints are intentionally being thinned behind workflow-oriented application seams
- request building, dispatch, result shaping, and status policy belong in application modules, not in the dock
- QGIS-backed layer creation stays in analysis infrastructure
visualization/
- render planning, temporal intent, and user-facing visualization policy belong in
visualization/application/ - layer mutation, renderer construction, basemap loading, and QGIS project wiring belong in
visualization/infrastructure/
atlas/
atlas/owns publish/export workflows- keep naming and ownership crisp across request building, runtime preparation, task execution, and PDF assembly
- avoid adding abstraction layers that do not clarify responsibility
ui/
ui/is for dock-widget support code, dependency assembly, UI-only coordination, and similar glue- UI modules should call workflows and render results, not absorb more business logic
Run the main test suite with:
python3 -m pytest tests/ -x -q --tb=shortRun unittest discovery with:
python3 -m unittest discover -s tests -vRun the PyQGIS smoke test with:
python3 -m unittest tests.test_qgis_smoke -vOn machines without PyQGIS installed, the smoke test skips automatically.
Install qfit into a local QGIS profile for testing with:
python3 scripts/install_plugin.py --plugins-dir <QGIS plugins dir> --mode copyBuild a release-style plugin archive with:
python -m pip install pypdf
python3 scripts/package_plugin.pyThe release ZIP is written to dist/.
Before treating a change as done:
- run the relevant tests
- keep SonarCloud green
- keep CodeQL/CI green
- address meaningful review feedback
- for export/rendering work, verify the final artifact, not just object construction
If you only remember a few rules, remember these:
- make
QfitDockWidgetthinner, not heavier - keep real feature logic inside feature-owned packages
- keep QGIS-heavy mechanics out of provider-neutral workflow code
- prefer small, behavior-preserving slices
- delete migration shims once callers are gone
GPL-2.0-or-later. See LICENSE.