import { ProportionalSymbolMap, ChoroplethMap } from "semiotic/geo"
Any geo chart or StreamGeoFrame can overlay data on raster map tiles from OpenStreetMap, Mapbox, Stamen, or any provider that serves{z}/{x}/{y} tiles. Add the tileURL prop and set projection="mercator" (required for web map tiles) to get a fully interactive slippy map with your Semiotic data layers on top.
Note: Tile maps require projection="mercator"because web map tiles use the Mercator (EPSG:3857) tiling scheme. Other projections like "equalEarth" or "naturalEarth"are not compatible with raster tiles.
Earthquake Map with Tiles
A ProportionalSymbolMapwith OpenStreetMap tiles showing notable earthquakes. Circle size encodes magnitude, color encodes tectonic region. Pan and zoom to explore.
JSX
import { ProportionalSymbolMap } from "semiotic/geo" const earthquakes = [ { name: "Tohoku, Japan", lon: 142.4, lat: 38.3, magnitude: 9.1, region: "Pacific" }, { name: "Sumatra, Indonesia", lon: 95.9, lat: 3.3, magnitude: 9.1, region: "Pacific" }, { name: "Maule, Chile", lon: -72.9, lat: -35.8, magnitude: 8.8, region: "Pacific" }, { name: "Gorkha, Nepal", lon: 84.7, lat: 28.2, magnitude: 7.8, region: "Asia" }, // ...more earthquakes ] <ProportionalSymbolMap points={earthquakes} xAccessor="lon" yAccessor="lat" sizeBy="magnitude" sizeRange={[4, 30]} colorBy="region" showLegend projection="mercator" tileURL="https://tile.openstreetmap.org/{z}/{x}/{y}.png" tileAttribution="© OpenStreetMap contributors" zoomable tooltip={(d) => ( <div> <strong>{d.name}</strong><br /> Magnitude: {d.magnitude} | Depth: {d.depth} km </div> )} />
Semi-Transparent Choropleth over Tiles
Layer a ChoroplethMap on top of tiles by reducing the area opacity. The geographic context from the tile layer shows through, providing labels and terrain while the choropleth encodes your data. Use areaOpacityto control transparency.
Loading world map...
JSX
import { ChoroplethMap, mergeData, resolveReferenceGeography } from "semiotic/geo" const worldFeatures = await resolveReferenceGeography("world-110m") const areas = mergeData(worldFeatures, countryGDP, { featureKey: "id", dataKey: "id" }) <ChoroplethMap areas={areas} valueAccessor="gdpPerCapita" colorScheme="viridis" projection="mercator" tileURL="https://tile.openstreetmap.org/{z}/{x}/{y}.png" tileAttribution="© OpenStreetMap contributors" zoomable areaOpacity={0.55} // semi-transparent so tiles show through tooltip />
Streaming Points over Tiles
StreamGeoFrame supports tiles directly, making it easy to stream real-time data onto a map. This example simulates seismic events appearing along tectonic plate boundaries. Points are color-coded by magnitude: green (<3.5), amber (3.5-5), red (>5). Decay and pulse encodings fade old events and animate new arrivals.
JSX
import { useRef, useEffect } from "react" import { StreamGeoFrame } from "semiotic/geo" function StreamingEarthquakes() { const chartRef = useRef() useEffect(() => { const id = setInterval(() => { if (chartRef.current) { // Simulate a random seismic event along the Ring of Fire // and other tectonic boundaries const zones = [ { lonRange: [130, 180], latRange: [20, 50] }, // Japan–Kamchatka { lonRange: [-80, -65], latRange: [-45, 5] }, // South America west coast { lonRange: [-130, -110], latRange: [30, 55] }, // US/Canada west coast { lonRange: [90, 130], latRange: [-10, 15] }, // Indonesia { lonRange: [25, 45], latRange: [30, 42] }, // Turkey–Iran ] const zone = zones[Math.floor(Math.random() * zones.length)] const lon = zone.lonRange[0] + Math.random() * (zone.lonRange[1] - zone.lonRange[0]) const lat = zone.latRange[0] + Math.random() * (zone.latRange[1] - zone.latRange[0]) const magnitude = 2 + Math.random() * 6 chartRef.current.push({ lon, lat, magnitude }) } }, 300) return () => clearInterval(id) }, []) return ( <StreamGeoFrame ref={chartRef} projection="mercator" xAccessor="lon" yAccessor="lat" runtimeMode="streaming" tileURL="https://tile.openstreetmap.org/{z}/{x}/{y}.png" tileAttribution="\u00a9 OpenStreetMap contributors" zoomable size={[700, 400]} pointStyle={(d) => ({ fill: d.magnitude > 5 ? "#ef4444" : d.magnitude > 3.5 ? "#f59e0b" : "#22c55e", r: Math.max(2, d.magnitude * 1.5), stroke: "#fff", strokeWidth: 0.5, })} decay={{ type: "exponential", minOpacity: 0.05 }} pulse={{ duration: 600, color: "rgba(239,68,68,0.5)", glowRadius: 8 }} enableHover /> ) }
Tile Providers
Any provider that serves {z}/{x}/{y} raster tiles works. Here are some common options:
| Provider | URL Template | Notes |
|---|
| OpenStreetMap | https://tile.openstreetmap.org/{z}/{x}/{y}.png | Free, requires attribution |
| Stadia (Stamen Toner Lite) | https://tiles.stadiamaps.com/tiles/stamen_toner_lite/{z}/{x}/{y}.png | Minimal black-and-white style |
| Stadia (Stamen Watercolor) | https://tiles.stadiamaps.com/tiles/stamen_watercolor/{z}/{x}/{y}.jpg | Artistic watercolor style |
| CartoDB (Positron) | https://basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png | Clean light basemap, good for data overlays |
| CartoDB (Dark Matter) | https://basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png | Dark basemap for dashboards |
Production Tile Providers & API Keys
The examples on this page use OpenStreetMap's free tile server, which is fine for development and low-traffic demos. For production applications you should use a commercial tile provider that offers an SLA, higher rate limits, and custom map styles. Most require an API key that you pass as part of the URL template.
Important: Never hard-code API keys in client-side source code. Use environment variables (e.g.VITE_MAPBOX_TOKEN or NEXT_PUBLIC_MAPTILER_KEY) and your bundler's env injection so keys stay out of version control.
Mapbox
Sign up atmapbox.comfor an access token. Mapbox offers a generous free tier and custom Studio styles.
JSX
// Mapbox raster tiles (requires access token) <ProportionalSymbolMap projection="mercator" tileURL={`https://api.mapbox.com/styles/v1/mapbox/light-v11/tiles/{z}/{x}/{y}?access_token=${MAPBOX_TOKEN}`} tileAttribution="© Mapbox © OpenStreetMap" // ...other props />
MapTiler
MapTilerprovides raster and vector tiles with a free tier. Their raster endpoint works directly with Semiotic's tileURL.
JSX
// MapTiler raster tiles (requires API key) <ChoroplethMap projection="mercator" tileURL={`https://api.maptiler.com/maps/streets/256/{z}/{x}/{y}.png?key=${MAPTILER_KEY}`} tileAttribution="© MapTiler © OpenStreetMap contributors" // ...other props />
Stadia Maps (Stamen styles)
Stadia Mapshosts the classic Stamen tile styles (Toner, Watercolor, Terrain). A free tier is available for non-commercial use; production use requires a plan and API key.
JSX
// Stadia Maps — free tier works without a key on localhost // For production, add your API key: <ProportionalSymbolMap projection="mercator" tileURL={`https://tiles.stadiamaps.com/tiles/stamen_toner_lite/{z}/{x}/{y}.png?api_key=${STADIA_KEY}`} tileAttribution="© Stadia Maps © Stamen Design © OpenStreetMap" // ...other props />
Using a function for tileURL
For providers that need dynamic URL construction (e.g. rotating subdomains, retina detection, or token injection), pass a function instead of a string template:
JSX
// Function form — receives (z, x, y, devicePixelRatio) <StreamGeoFrame projection="mercator" tileURL={(z, x, y, dpr) => { const retina = dpr > 1 ? "@2x" : "" return `https://api.mapbox.com/styles/v1/mapbox/dark-v11/tiles/${z}/${x}/${y}${retina}?access_token=${MAPBOX_TOKEN}` }} // ...other props />
Tips
Projection must be Mercator
Web map tiles are pre-rendered in the Mercator projection (EPSG:3857). If you set projection="equalEarth" or any non-Mercator projection, tiles will not align with your data. Always useprojection="mercator" with tileURL.
Pair with zoomable
Tile maps are most useful when users can pan and zoom. Thezoomable prop enables scroll-wheel zoom and drag panning, and the tile layer automatically loads higher-resolution tiles as you zoom in.
Use subtle tile styles for data overlays
When overlaying a choropleth or dense point cloud, use a minimal tile style like CartoDB Positron or Stamen Toner Lite so tiles provide context without competing with your data layer. For choropleths, reduce area opacity via areaOpacity so tiles show through.
Attribution is required
Most tile providers require attribution. Use tileAttributionto display the required text (rendered as a small overlay in the bottom-right corner of the map).
Tile Props
These props are available on all geo components:ProportionalSymbolMap, ChoroplethMap,FlowMap, DistanceCartogram, andStreamGeoFrame.
| Prop | Type | Required | Default | Description |
|---|
tileURL | string | ((z, x, y, dpr) => string) | Yes | — | URL template for raster tiles with {z}/{x}/{y} placeholders, or a function returning a URL. Use {r} for retina suffix (@2x). Example: "https://tile.openstreetmap.org/{z}/{x}/{y}.png". |
tileAttribution | string | — | — | Attribution text displayed in the bottom-right corner of the map. Required by most tile providers (e.g. OpenStreetMap). |
tileCacheSize | number | — | 256 | Maximum number of tile images to cache in memory. LRU eviction removes least-recently-used tiles when the limit is exceeded. |
projection | "mercator" | Yes | — | Must be "mercator" when using tiles. Web map tiles use the Mercator (EPSG:3857) tiling scheme. A dev warning is emitted for non-Mercator projections. |
zoomable | boolean | — | true with tileURL, false otherwise | Enable pan and zoom. Defaults to true when tileURL is set. Can be set to false to disable. |