A Flask server that serves flowmap data as vector tiles for visualization with MapLibre GL JS.
- Dynamic zoom level matching - Automatically selects the best available zoom level from your data
- Styled by data - Point sizes vary by weight, line widths vary by flow count
- Interactive - Click on locations and flows to see details
- Efficient - Uses DuckDB spatial indexing and MVT generation
# Install package
pip install sqlrooms-flowmap
# Install dev dependencies (includes Flask for the server)
uv sync --group dev
# Or install Flask separately
pip install flaskFirst, generate your flowmap data:
cd ../..
uv run sqlrooms-flowmap \
--locations data/locations.csv \
--flows data/flows.csv \
--output-dir examples/dataThen start the server:
cd examples
python server.py locations --output-dir dataOr if using the default output/ directory:
python server.py locationsOpen your browser to http://localhost:5000
The server finds the best matching zoom level for each tile request:
def find_best_zoom(requested_zoom: int) -> int:
"""Find the closest zoom level that is <= requested zoom."""
idx = bisect.bisect_right(available_zooms, requested_zoom)
if idx == 0:
return available_zooms[0]
return available_zooms[idx - 1]For datasets with millions of flows, the server uses Hilbert range filtering to dramatically reduce query time:
- Metadata Export: The processor exports spatial extents used for Hilbert indexing
- Range Computation: For each tile request, compute the Hilbert index range covering flows in that tile
- Fast Pre-filtering: Query uses
WHERE flow_h BETWEEN min AND maxbefore joining with clusters - Index Utilization: The
(z, flow_h)index enables efficient range scans
Performance:
- Scans only 1 column (
flow_h) instead of 4 (coordinates) - Leverages sorted Parquet file structure
- Joins only the filtered subset (typically 0.1-1% of total flows per tile)
- 10-100x faster than spatial-only filtering for large datasets
Uses DuckDB's ST_AsMVT and ST_AsMVTGeom functions to generate Mapbox Vector Tiles:
- Clusters: Points with
id,name,weight,sizeproperties - Flows: Lines with
origin,dest,countproperties
MapLibre GL JS styles vary based on data properties:
Points (clusters):
- Circle radius: 3-15px based on weight
- Color: Blue with white stroke
Lines (flows):
- Width: 1-4px based on count
- Color: Heat map from light orange to dark red based on count
- Opacity: 0.6 for better layering
You can customize the styling in the HTML by modifying the MapLibre GL style layers:
{
id: 'clusters',
type: 'circle',
source: 'clusters',
'source-layer': 'default',
paint: {
'circle-radius': [
'interpolate', ['linear'], ['get', 'weight'],
0, 3, // weight 0 → 3px
1000, 10, // weight 1000 → 10px
10000, 15 // weight 10000 → 15px
]
}
}