swiftmap
swiftmap is a Python mapping library that keeps Leaflet's ergonomics while replacing its
vector drawing with WebGL pipelines — so the map you build with a few add_* calls keeps
rendering smoothly at millions of points, not thousands.
It targets a gap the existing libraries leave open: folium and ipyleaflet have the ergonomics but hit a ceiling around tens of thousands of features and offer flat layer controls; deck.gl scales but is a different mental model with a thin Python layer. swiftmap keeps the Leaflet mental model, adds WebGL scale, hierarchical layer controls, and bidirectional Python sync.
One rendering core serves four stacks: notebooks (anywhere anywidget runs —
JupyterLab, Jupyter Notebook, VS Code), Shiny for Python,
React, and Streamlit — with map state (viewport,
visibility, clicks, draws, the time slider) syncing reactively in both directions
wherever there is a Python side to sync with. A finished map also exports to a single
static HTML file that opens with no backend at all — see
Sharing a map.
Everything runs offline. Leaflet, glify and Leaflet-Geoman ride inside the bundle that ships in the wheel, so nothing is fetched when a map is viewed — on an air-gapped network the wheel is the whole story.
Live demos and a capability gallery → — every map on that page is real: pan it, toggle it, scrub it, and read the six lines that made it.
Contents · Installation · Quick start (notebook) · Quick start (Shiny) · What you can plot · Basemaps · Imagery · Styling · Color and size from data · Density · The legend · Scale bar · Logo card · The sidebar: hierarchy from your data · Acting on layers after they exist · Drawing & AOIs · Time layers · Popups & Tooltips · Labels · React · Streamlit · Sharing a map: one static file · Performance notes
Installation
pip install swiftmap
Required: anywidget, numpy, and xyzservices (the tile-provider catalogue behind
name-callable basemaps — pure metadata, no compiled bits). Pandas, Polars, GeoPandas,
and geostructures are all supported as data sources but none is a dependency — swiftmap
parses whatever you have installed. The bundled JavaScript ships with the package, so
installing never requires Node — and its map libraries are bundled in, so viewing never
reaches the network either.
Quick start (notebook)
import pandas as pd
from swiftmap import Map
df = pd.DataFrame({
"lat": [36.01, 36.02, 36.03],
"lon": [-5.36, -5.35, -5.34],
"name": ["Point A", "Point B", "Point C"],
"value": [12.4, 8.2, 15.1],
})
m = Map()
m.add_circle_markers(df, name="Sites", color_col="value")
m
Three things worth noticing:
- No column mapping.
lat/lonwere found by name;latitude/longitude,x/y, andlngwork too, andlat_col=/lon_col=override the guess when your names differ. color_col="value"colored each point through a viridis ramp. See Color and size from data.- The view followed the data. A
Map()built withoutcenter/zoomfits itself to whatever is added, and keeps following as layers arrive — until anyone states a view (explicitcenter=/zoom=at construction, afit_bounds()call, or panning the map in the browser), after which it is left alone. Frame something on demand any time withm.fit_bounds(m.bounds_of("Sites")). - Sizing:
Map(height="500px")fixes the widget's height, and assigningm.heightresizes a live map. Unset, it fills whatever container it is in.
Quick start (Shiny)
Build the map once, and drive it from effects:
@render_widget
def map_widget():
m = Map() # no input.* in here, ever
m.add_markers(df, name="name", layer_group=["Points", "status"])
return m
@map_effect(map_widget)
def filter_points(m):
m.select(group=f"Points/{input.status()}", scope="Points")
The one Shiny rule: the @render_widget function must depend on nothing reactive.
It rebuilds the widget whenever a dependency invalidates, which throws the map away and
re-uploads every coordinate buffer. @map_effect (from swiftmap.shiny) resolves the
live widget instead, skips quietly if it has not rendered yet, and batches everything in
its body into one message. It takes event= for what @reactive.event would do -- do
not stack that decorator yourself, as it rejects the map argument at decoration time.
Four runnable apps live in examples/shiny/, from this one to a live feed and a draw-an-AOI filter.
What you can plot
| Method | Draws | Notes |
|---|---|---|
add_markers |
Pin icons | Custom GLSL shader — anti-aliased pins with drop shadows, at scale |
add_circle_markers |
Circle points | radius in pixels; the cheaper primitive, better default for many points |
add_line / add_polyline |
Polylines | Long-format rows (line_id_col, order_col), WKT, GeoJSON, GeoPandas... A MultiLineString is one layer drawn as disjoint parts |
add_polygon (+ add_polygons, add_shape, add_shapes) |
Polygons | Interior holes and MultiPolygons render correctly; one MultiPolygon is one layer |
add_circle |
Geodesic circle | Center plus radius in meters — note the unit difference from circle markers |
add_collection (aliases add_geojson, add_geostructures) |
Every kind in a mixed dataset | One layer per geometry kind, merged under a single sidebar entry; points render as circle markers unless point_type="markers" |
add_basemap |
Tile layer | Any xyzservices catalogue name (~880 providers), a WMS registry name or raw WMS endpoint, or a raw {z}/{x}/{y} template — see Basemaps |
add_heatmap |
Density field | Screen-space blobs, or real H3/geohash cells filled by their sums — see Density |
add_imagery |
Georeferenced raster overlay | Anything GDAL reads, warped into the map's CRS; single band through the house colormaps — see Imagery; requires rasterio (optional) |
Every add_* method accepts the same range of inputs:
- Pandas / Polars DataFrames — lat/lon columns found by name; long format via
shape_id_col/line_id_col+order_col; WKT geometry columns recognised by value (point at one explicitly viashape_id_col/line_id_colwhen its name would not be guessed); H3 cell-id columns, each cell drawn as its hexagon — an aggregated table needs nothing but the cell and value columns it already has (requiresh3, optional); wide vertex columns (lat1, lon1, lat2, lon2, ...) - GeoPandas GeoDataFrames and GeoSeries
- geostructures shapes,
FeatureCollections, andTracks - GeoJSON dicts or strings
- A bare geometry — a WKT string (
m.add_polygon("POLYGON ((...))")), an H3 cell id or a list of them, a shapely geometry, or a geostructures shape — straight in, no table around it - Raw lists and dicts of coordinates, with a range-based heuristic for axis order
(
coord_order="lat_lon"/"lon_lat"to state it explicitly; WKT and GeoJSON declare their own order and are never guessed at)
A method that cannot read what you passed warns and adds nothing rather than raising —
a map is built by a chain of add_* calls, and an exception partway through would discard
the layers already added. Rows with missing coordinates are dropped and counted in a
warning rather than failing the layer.
Basemaps
add_basemap resolves, in order: a preset, a WMS registry name, a raw URL, then the
xyzservices catalogue — ~880 providers, with tolerant lookup ("CartoDB.DarkMatter",
"CartoDB DarkMatter", and "cartodb darkmatter" all land):
m.add_basemap("Esri.WorldImagery", visible=True) # catalogue name
m.add_basemap("USGS Imagery") # WMS, from the registry
m.add_basemap("https://server/wms", wms_layers="0") # raw WMS endpoint
m.add_basemap("https://{s}.tile.../{z}/{x}/{y}.png") # raw XYZ template
m.list_basemaps("dark") searches every accepted name. An unknown name warns and
adds nothing — you never silently get OpenStreetMap when you asked for something
else — and a provider that needs an API key warns by the exact keyword it expects
(accessToken=, apiKey=, ...). WMS requests follow the map's CRS, so an EPSG:4326
map asks in 4326.
Deploying on a network with its own tile and WMS infrastructure — different services, a different catalogue, different defaults — is one swappable module: see DEPLOYING.md.
Imagery
A georeferenced raster — a GeoTIFF, a COG, anything GDAL reads — drops onto the map as an overlay. The raster is reprojected into the map's CRS, so any source projection lands where it belongs, and nodata pixels come out transparent:
m.add_imagery("scene.tif", opacity=0.8) # RGB as-is
m.add_imagery("elevation.tif", colormap="turbo", vmin=0, vmax=1500)
Single-band rasters colour through the same colormaps color_col uses; RGB(A)
rasters keep their own pixels. Large rasters are downsampled to max_size=2048 on
the longest edge on the way in. The overlay behaves like any layer — sidebar
toggle, groups, opacity, static export — and sits above the basemap, below your
vectors. Requires rasterio (pip install rasterio), an optional dependency.
Styling
Every add_* method takes the same styling vocabulary as keyword arguments — color,
fill_color, fill_opacity, weight, opacity, and radius where it applies. Leaflet's
camelCase spellings (fillColor, fillOpacity) work too:
m.add_polygon(zones, color="crimson", fill_color="#f5c4ac", fill_opacity=0.4, weight=2)
Polygons follow Leaflet semantics: the fill reads fill_color and defaults to color
when unset; weight and opacity draw the border. Point layers take their alpha from
the color itself (rgba(...) or #rrggbbaa) rather than from opacity.
Style from your data, row by row. A style property or column is applied per
feature, so one DataFrame can carry its own appearance:
df["style"] = df["kind"].map({"city": "red", "town": "blue"})
m.add_markers(df) # each point takes its own colour
The value is a colour string or a dict of options
({"color": "red", "weight": 5}). Only a column named exactly style is treated this
way — a column that happens to be called color is data, not styling.
Overriding. Precedence runs static_style → explicit keyword → style column →
defaults. static_style={...} forces one appearance for the whole layer regardless of
the data; a plain keyword still beats the column.
Honest warnings. Misspelled options are reported with a suggestion (colour →
color); a real option that this geometry cannot draw says so instead of being silently
ignored. Unrecognised keys that match nothing are passed through to the layer untouched,
which is how custom metadata reaches the frontend.
Color and size from data
Any add_* call can drive color and size from a column, the way the rest of the styling
vocabulary works — no precomputed hex codes, no per-row style dicts:
m.add_circle_markers(df, name="Sensors",
color_col="reading", colormap="plasma",
radius_col="volume", radius_range=(4, 20))
m.add_polygon(districts, name="Districts",
color_col="median_income", colormap="greens") # choropleth:
# fill ramps, border keeps `color`
- Numeric columns ramp between the data's own extremes, or between
vmin/vmaxwhen you fix them.color_bins=[10, 20, 50]classifies into discrete classes instead of a continuous ramp. Missing values (NaN) paint as the layer's base color. - Non-numeric columns are categories: each distinct value takes a palette colour
automatically. Naming a sequential colormap instead spreads it evenly across the
categories. To say exactly which value gets which colour — and the order the legend
reads in — pass a mapping:
colormap={"high": "#d7191c", "medium": "#fdae61", "low": "#1a9641"}; a value the mapping does not name takes the layer colour, and every value it does name is listed in the legend whether or not the data carries it yet, so a feed's legend holds still as it fills in. - Colormaps:
viridis(default),plasma,inferno,magma,turbo,coolwarm,blues,reds,greens,greys; categorical paletteswift10. They are small anchor tables interpolated in RGB — matplotlib-faithful with no matplotlib dependency. Bring your own too: a list of colours, a callablet -> colour(a matplotlibColormapobject works as-is), or"matplotlib:cividis"when matplotlib is installed; andswiftmap.register_colormap("corp", [...])makes a name of it for every call. radius_colsizes points so area is proportional to the value (radius grows with the square root) — a doubled value looks doubled, not quadrupled — acrossradius_rangepixels, default(3, 18).
At scale this matters more than convenience: point colors and radii travel as compact binary buffers beside the coordinates, never as per-feature style dicts in the layer JSON, so coloring five million points does not bloat the payload that every sidebar toggle and view attach has to carry.
The legend below picks all of this up automatically — the ramp, the bins, the
categories, and a stated size row for radius_col (size ∝ volume (10 – 500);
stated, never drawn, because legend pixels are not map pixels at any zoom).
Density
Past a certain count, colouring individual points stops showing anything — five million
markers are a solid blob. add_heatmap is the answer, in two flavours:
m.add_heatmap(df) # screen-space blobs
m.add_heatmap(df, cells="h3", resolution=8) # real hexagons, filled by their sums
m.add_heatmap(df, cells="geohash", length=6, base=32)
m.add_heatmap("Sites", weight_col="volume") # derive from a layer already plotted
Blobs accumulate a Gaussian kernel sized in pixels, so the field recomputes with
every view — no upstream table could equal it. Cells bin in Python at add time and
fill the actual cell polygons through the ramp. (base is required for geohashes: a hash
cannot state its own base, so swiftmap never assumes one.)
Either way the colour is relative to what you are looking at: the scale re-stretches to
the extremes in view on every settled pan or zoom, so zooming into a quiet region spreads
its structure across the whole ramp instead of leaving it uniformly dark. That is why the
legend reads low to high with no numbers — numbers on a view-relative scale would lie
with every pan. Pin the scale with max_intensity (blobs) or vmin/vmax (cells), or
turn the tracking off with auto_normalize=False, and the numbers mean something again.
Heat rides the time slider like any other layer.
Deriving from an existing point layer by name costs a config rather than a re-upload, and
weight_col is read from that layer's own properties.
This is the only place swiftmap aggregates, deliberately. When you want numbers, clicks, or a legend with values on it, bin the data yourself and plot the result as polygons — the map stays a painter:
import swiftmap
cells = swiftmap.hexbin(df, resolution=8) # data in, data out: h3 + count columns
m.add_polygon(cells, color_col="count") # ordinary polygons, popups and all
hexbin takes everything the add_* methods take and returns the flavour you gave it
(pandas in, pandas out; polars in, polars out). Counting is the only statistic built in —
it is the one with no interpretation in it; every other statistic belongs upstream, and
whatever upstream produces paints through the same door. Needs h3.
The legend
One call puts a legend on the map, and it derives from the same layer state the map
renders — glyphs per geometry, sections per sidebar folder, ramps/bins/categories
straight from color_col — so there is nothing to fall out of step:
m.configure_legend(show=True, title="Key", position="bottom-right")
scope='all' (default) lists every layer and dims the hidden ones — a legend is the
map's vocabulary; scope='visible' tracks the screen instead, and dim_hidden=False
gives the print-style static key. Eight anchor positions, shared with the time control.
When derivation isn't what you want to say, legend_add writes your own rows — plain
swatches, ramps, or category lists, optionally bound to a live layer's visibility
(layer=) — and legend_remove is a persistent suppressor that keeps matching across
every re-derivation. configure_legend(auto=False) hands the whole legend over to
manual entries. Exports carry it all.
Scale bar
Off by default, one call away, with the unit maritime work runs on alongside the usual ones:
m.show_scale = True
m.configure_scale(units="nautical") # metric (default) | imperial | both | nautical
It measures through the map's CRS — Leaflet's own control underneath, so there is no pixel math of swiftmap's to get wrong — and exports carry it.
Logo card
Off by default, and the branding is yours — swiftmap ships none:
m.configure_logo(company="assets/logo.png", # URL, data URI, or a local file
parent_company={"url": "...", "alt": "Parent"},
position="bottom-right", height=35, show=True)
A local file is read and embedded as a data URI, so it survives a static export and a
machine with no route to the file's home. A slot you never set does not render, and
False clears one. Corner positions, like the draw toolbar — mind that the legend
defaults to the same bottom-left corner.
The sidebar: hierarchy from your data
Layers organise into a tree of folders in the map's sidebar, and the tree can come from
the data itself — any part of layer_group that matches a column name resolves per row:
m.add_markers(df, name="site", # each point named from its own row
layer_group=["Sensor Feeds", "region", "status"])
# -> Sensor Feeds / EMEA / Active, Sensor Feeds / EMEA / Inactive, ...
Toggling a folder toggles everything nested under it. Configure a folder's behaviour with:
m.configure_group("Sensor Feeds", collapsed=True)
m.configure_group("Basemaps", multi_select=False) # radio buttons, not checkboxes
Acting on layers after they exist
One targeting vocabulary — id or name, types=, exclude_types=, group= — is shared
by everything that operates on existing layers:
m.hide(group="Feeds/Inactive")
m.show("Survey") # every part of a collection
m.select(chosen_ids, scope="Dwells", zoom=True, zoom_offset=-1)
m.highlight("Survey", color="#ffcc00", markers={"radius": 14})
m.set_feature_styles("Sites", {12: {"color": "#ffcc00"}}) # one feature, by index
m.fit_bounds(m.bounds_of(["Dwell 3", "Dwell 7"]), zoom_offset=-1)
A layer's data updates in place too, and that is the live-feed primitive: the layer
keeps its id, name, folder, visibility, time animation and highlight while its points
change under it. append=True grows a point layer with the new rows instead:
m.update_layer("Feed", data=latest_df) # replace the data
m.update_layer("Feed", data=new_rows, append=True) # grow it
color_col/radius_col, labels, the legend and bounds re-derive from the new data; a
refresh never moves a viewport you have chosen. One limitation for now: a layer that was
fanned out from a column (name=<column>, layer_group=[..., <column>]) is one of several
siblings and warns rather than guessing how to re-fan.
select and highlight are declarative and total: each call states the complete
selection, so switching needs no undoing and select(None) / highlight(None) restores
a clean slate. Highlights and per-feature overrides sit in fields of their own above the
layer's styling, so clearing them restores what was underneath with nothing remembered.
A call that matches nothing warns — a mistyped name looks identical to a hidden layer, and silence is the failure you would not notice.
Clicks sync back to Python — on features and on open map alike. A feature click sets
m.clicked_layer_id and m.selected_index (overlaps resolved top-down: points over
lines over polygons); a click on open map clears them and reports m.clicked_latlng
as [lat, lon]. Feature clicks record their location too — a point reports its own
coordinates, not the mouse's. m.click_seq bumps on every click, so one observer
reads "where" and "on what" from a single event.
Drawing & AOIs
configure_draw(show=True) puts a draw toolbar on the map — markers, lines,
rectangles, polygons and circles, with vertex editing, dragging and deletion.
Everything drawn lands in m.drawings as GeoJSON features (a circle carries
properties.kind and its radius, since GeoJSON has no circle), and m.draw_seq
bumps on every create, edit and delete — observe that one trait and read
m.drawings in the handler, the same pattern clicks use:
m.configure_draw(show=True, tools=["rectangle", "polygon"])
aoi = m.drawings[0]["geometry"] # after the analyst draws one
The trait syncs both ways: setting m.drawings from Python seeds AOIs onto the map,
clear_drawings() empties it, and exports carry the drawings and the toolbar with
them. The drawing engine (Leaflet-Geoman) is bundled like Leaflet and glify, so the
toolbar works with no network.
Time layers
Any layer whose features carry timestamps can be animated. Timestamps are read from the
layer's own properties — a DataFrame's timestamp column, or the interval a
geostructures Track already records — so nothing extra is passed in:
m.add_circle_markers(df, name="Vessel") # df has a timestamp column
m.make_time_layer("Vessel", period="PT1H")
Ticks land on period boundaries — 04:00, 05:00 for PT1H, not 03:17, 04:17 — so the
slider reads in round numbers, and data arriving earlier than everything before it
only adds ticks at the front instead of shifting every existing one. Wherever the
playhead is, it stays there: it is an absolute moment, not an index into a series that
can move underneath it.
The automatic probe checks the usual names (times, datetime_start/datetime_end,
timestamp, datetime, time, date). When your column is called something else —
or the probe would pick the wrong one — name it yourself, like any other override:
m.make_time_layer("Vessel", time_field="obs_time", period="PT1H")
m.make_time_layer("Dwells", time_field="arrived", time_end_field="departed")
One slider serves every time layer on the map; animating a second layer joins it to the
same slider rather than adding another control. The slider steps through generated
periods ('P1D', 'PT15M', ...) rather than through the observed timestamps, so a
period in which nothing happened still gets its tick — an empty map at 03:00 is a
finding, not a gap in the slider.
By default each tick shows its own period. Pass duration=None to accumulate history
instead, or an ISO8601 duration ('PT6H') for a fixed trailing window. Add fade=True
to dim point features with age — newest at full opacity, reaching zero at the window's
trailing edge. Playback options are shared:
m.configure_time(period="PT1H", auto_play=True, loop=True, speed=2,
position="bottom-center").
The slider's position syncs both ways: m.time_current holds the current tick (epoch
ms) for Shiny to react to, and setting it from Python jumps the slider to that moment.
m.clear_time_layer() stops animating and removes the control.
For point layers the per-tick filtering runs in the vertex shader — dragging the window across five million points repaints in milliseconds, not seconds.
Popups & Tooltips
By default every property is listed as name: value. Narrow the list with *_fields, and give
columns readable labels with *_names:
m.add_markers(
df,
popup_fields=["city", "pop_2020"],
popup_names=["City", "Population"], # matched to fields by position
tooltip_style="background: white; border: 1px solid #333; font-size: 10px;",
)
If *_names cannot be lined up with *_fields, swiftmap warns and falls back to the raw
column names rather than failing the render.
For images, links, or custom layout, supply a template. {column} inserts a single value and
{*} expands the default field list, so you can add markup without enumerating every column:
m.add_markers(
df,
popup_template="<img src='{photo_url}' width=300><br>{*}<br><a href='{source_url}'>source</a>",
popup_fields=["city", "pop_2020"],
popup_names=["City", "Population"],
popup_max_width=800,
)
Your markup renders as HTML; values pulled from the data are escaped. That means a value
containing <script> displays as text instead of executing — which matters as soon as your map
shows data you didn't author yourself (uploads, shared tables, third-party APIs). Values landing
in an href or src are additionally checked for a safe URL scheme.
If you need raw HTML stored inside the data itself to render as markup, that is an explicit opt-in and should only be used with data you control.
Labels
label= puts permanent text chips on features — a column name labels each feature from
its own value, anything else is the literal text:
m.add_circle_markers(sites, name="Sites", label="site")
m.add_polygon(zones, name="zone_id", label="zone_id")
Every geometry takes one: points anchor at the point, lines at their middle vertex, areas at their centre. Label text is escaped like popup values, and on a time layer the chips appear and vanish with their features. Labels are DOM elements — built for sites and zones, not point clouds — so the point builders warn past a thousand.
React
The same map as a React component: swiftmap-core/react exports <SwiftMap>, one more
host over the same core this widget uses — not a second renderer. Layers can be authored
in JS too, through createMapModel, held byte-identical to Python's Map by a
conformance suite.
npm install swiftmap-core
Props, callbacks, the live-feed hooks, the buffer contract for hand-built configs, and the
host interface are documented in the package's own README:
README.npm.md.
examples/react/ is the working example, and tier 3 drives it.
Streamlit
The fourth stack, and a bidirectional one: st_swiftmap renders the map as a
Streamlit component and returns what the viewer did — clicks, draws, the viewport,
the time slider, layer toggles — on every rerun.
import streamlit as st
from swiftmap import Map
from swiftmap.streamlit import st_swiftmap
@st.cache_resource
def build():
m = Map(height="600px")
m.add_circle_markers(df, name="Sensors", color_col="reading")
return m
events = st_swiftmap(build(), key="map")
if events["clicked_layer_id"]:
st.write(events["clicked_layer_id"], events["selected_index"])
aoi = events["drawings"]
Build the map once — @st.cache_resource, or st.session_state when each session
mutates its own — and let the component decide: every call carries a change
fingerprint, so a rerun that changed nothing costs the frontend a comparison, not a
rebuild. The returned dict always has the same keys, empty before anyone has
interacted. The component ships Leaflet, glify and Geoman bundled inside the wheel, like the widget
and the export do — no stack needs a network. pip install swiftmap[streamlit];
st.components.v1.html(m.to_html()) remains the static, no-interaction embed.
examples/streamlit/app.py is the working app.
Sharing a map: one static file
m.save("map.html") # or: html = m.to_html()
The export is one self-contained HTML file with the map's whole state baked in — layer configs, every coordinate/time/style buffer (base64, so expect roughly 4/3 of their binary size), the widget bundle and its CSS. Hand the file to anyone: it opens from disk or a static file host with no Python, no kernel, no server. The sidebar and time playback work fully client-side, and the automatic fit rides along so the file opens framed on its data. It is genuinely self-contained: Leaflet, glify and Geoman are inside the file too, so it opens on a machine that has never had a network.
to_html() returns the same document as a string, which is also the Streamlit story:
import streamlit.components.v1 as components
components.html(m.to_html(), height=600)
Interaction in an export stays in the file — there is no Python behind it to sync back to.
Performance notes
The things that keep large maps fast are on by default and mostly invisible:
- Coordinates — points, lines, and polygons alike — travel as binary float64 buffers, never as JSON in the layer configs. Data-driven colors and radii ride the same transport.
- Every mutation after the first render is a small patch, not a resend of the map. A sidebar toggle costs ~100 bytes at any scale.
- Time filtering for points runs on the GPU (per-point times upload once as vertex attributes; the tick is a uniform).
- Every
add_*call already batches internally. Group several calls into one message withwith m.batch(): ...— it is reentrant, so nesting is safe.
sync() pushes a render when auto_sync=False; resync() replaces the client's entire
state, useful if a view may have missed updates.
Full parameter documentation lives in each method's docstring
(e.g. help(Map.add_markers)).
Contributing
Want to add support for a new data source, fix a bug, or improve the docs? See
CONTRIBUTING.md. The JS lives in src/ and must be rebuilt after
changes (npm run build); the committed bundle is what ships.
Development Notes
Claude Code is used during development of this project — for refactoring, building test harnesses, and working through architecture decisions. Direction, review, and acceptance are human-owned: changes are verified against the test suite and exercised in a real application before they land.
Metadata
Release files for swiftmap 0.5.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| swiftmap-0.5.1.tar.gz | 724.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| swiftmap-0.5.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 1.4 MB
Release files / swiftmap-0.5.1.tar.gz
| Download URL | swiftmap-0.5.1.tar.gz |
|---|---|
| Size | 724.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
1e70df500aa34ff60a19426fbb7fb60dd938e1f479655ea204ce4f4b965020a0
|
|
BLAKE2b-256 checksum How to use checksums |
8e7faf8fd829b86ed92a7af76c938d3da62da045e27d45b5032d05808eb756a0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.3
|
Release files / swiftmap-0.5.1-py3-none-any.whl
| Download URL | swiftmap-0.5.1-py3-none-any.whl |
|---|---|
| Size | 683.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
3f44b97241af2312ff292a5dc3187e9774c7893f8eb12b34f497dec3c834092f
|
|
BLAKE2b-256 checksum How to use checksums |
84904ae70321da3ed45e9a312794e43e45bd780e5e8e8932374ce40b43666e33
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.3
|