Skip to content

Live data & streaming

A layer can keep its data current while the page is open: by fetching its GeoJSON URL again on a timer (polling), or by receiving updates the server pushes (a stream). Polling suits a feed that changes every minute or so; a stream suits positions that change every second.

Add a refresh: block to the source, beside its url:. This map fetches the USGS feed of the last day’s earthquakes every minute:

live-earthquakes.yaml
type: map
id: live-earthquakes
config:
center: [-120, 37]
zoom: 3
mapStyle: "https://tiles.openfreemap.org/styles/liberty"
layers:
- id: earthquakes
type: circle
source:
type: geojson
url: "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_day.geojson"
refresh:
refreshInterval: 60000 # milliseconds: fetch the feed again every minute
paint:
circle-radius: ["interpolate", ["linear"], ["get", "mag"], 1, 3, 6, 20]
circle-color: "#ef4444"
circle-opacity: 0.7
circle-stroke-width: 1
circle-stroke-color: "#ffffff"
interactive:
hover:
cursor: pointer
click:
popup:
- h4: [{ property: title }]
The last day's earthquakes, fetched again every minute

Edit this map in the playground

Each poll downloads the URL again and redraws the layer with what it returns. Polling pauses while the browser tab is hidden and resumes when the reader comes back. refreshInterval is in milliseconds, at least 1000.

  • The feed must allow other sites to read it (CORS), as the USGS feeds do (Loading data › CORS).
  • A named source under sources: is polled once, however many layers draw it (named sources).
  • Live updates work on GeoJSON sources. A FlatGeobuf or GeoParquet file is read once, and refresh: or stream: on one is a validation error.

The YAML sets the interval; there is no method to pause or force a poll from JavaScript. To replace the data yourself, call updateLayerData(layerId, geojson) on <ml-map>.

updateStrategy in the refresh: block says what happens to the features already on the map when new ones arrive:

updateStrategy:The map then showsUse it for
replace (default)Only the new featuresA feed that returns the whole dataset each time
mergeThe new features, plus every earlier one the update didn’t mentionThings with an id that move or change: vehicles, sensors
append-windowEarlier and new features together, trimmed by count or ageEvent streams: the last 100 events, the last hour

The default, used by the quick start: each response is the whole dataset.

merge matches features by updateKey, a property whose value is different for every feature. A feature whose key is already on the map is replaced by its new version; a new key is added; features the update doesn’t mention stay. Here the USGS hour feed keeps every earthquake seen since the page opened, even after it drops out of the feed’s last hour:

earthquakes-this-session.yaml
type: map
id: earthquakes-this-session
config:
center: [-120, 37]
zoom: 3
mapStyle: "https://tiles.openfreemap.org/styles/liberty"
layers:
- id: earthquakes
type: circle
source:
type: geojson
url: "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_hour.geojson"
refresh:
refreshInterval: 60000
updateStrategy: merge
updateKey: url # a property that is different for every earthquake
paint:
circle-radius: ["interpolate", ["linear"], ["get", "mag"], 1, 3, 6, 20]
circle-color: "#7c3aed"
circle-opacity: 0.7
Earthquakes from the last hour, accumulating while the page is open

Edit this map in the playground

updateKey names a property (inside properties). A feature without that property is matched by its top-level GeoJSON id instead, and one with neither is kept but not matched: each update’s unmatched features replace the previous update’s, and the console says so once. (Core 0.9.0 matched only the property and dropped other features; this example matches on the USGS url property, which works on every version.) Validation refuses merge without updateKey.

For your own feed, say a vehicle API, each response is a FeatureCollection of points whose properties include the key:

source:
type: geojson
url: "https://api.example.com/vehicles" # your own endpoint
refresh:
refreshInterval: 5000
updateStrategy: merge
updateKey: vehicleId
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": { "type": "Point", "coordinates": [-122.41, 37.79] },
"properties": { "vehicleId": "bus-12", "status": "active" }
}
]
}

A vehicle that stops reporting keeps its last position on the map. To show that, have the feed send it with a status such as offline and colour by status, or use replace if the feed always lists every vehicle.

append-window adds every update to what is there, then trims: windowSize keeps the newest N features, and windowDuration (milliseconds) drops features older than that, judged by the number in timestampField (milliseconds since 1970, as USGS’s time is). It suits a stream:

source:
type: geojson
data: { type: FeatureCollection, features: [] } # empty until messages arrive
stream:
type: sse
url: "https://stream.example.com/events"
refresh:
updateStrategy: append-window
windowSize: 100 # keep the newest 100 features
windowDuration: 3600000 # and none older than an hour
timestampField: time

With a stream, the server sends updates as they happen instead of waiting to be asked. A stream: block opens a Server-Sent Events (type: sse) or WebSocket (type: websocket) connection:

source:
type: geojson
url: "https://api.example.com/vehicles" # the current state, loaded first
stream:
type: sse # or websocket, with a wss:// url
url: "https://api.example.com/vehicles/stream"
refresh:
updateStrategy: merge
updateKey: vehicleId
  • Each message is one GeoJSON FeatureCollection, as JSON text: an SSE event’s data: or one WebSocket message. Send a single changed feature inside a collection of one. Anything else (a bare Feature, text that isn’t JSON) is skipped and reported as a data error.
  • The source needs a refresh: block beside stream:. The stream connects only when one is there, and its updateStrategy says how each message is applied. Validation warns when it is missing.
  • The source needs somewhere to start: a url: with the current state, or data: with an empty FeatureCollection to start blank. Under merge, messages merge into those starting features.
  • Reconnecting is automatic. After a dropped connection the stream retries up to reconnectMaxAttempts times (10), waiting reconnectDelay (1 second) and doubling the wait each time up to reconnectMaxDelay (30 seconds). An SSE stream is reconnected by the browser itself, which resumes it with Last-Event-ID after the wait the server sends as retry:; those tries count toward reconnectMaxAttempts, and the delays apply once the browser gives up (an HTTP error, say). reconnect: false turns it off.
  • The layer keeps its data when the stream is down. A drop the stream is reconnecting from shows nothing. A stream that can’t connect at first, that runs out of attempts, or that closes with reconnect: false keeps the layer and the data it has, and the map’s notice says once that it isn’t getting live updates (a first connection that fails is still retried). The line goes when the next message arrives.
  • An SSE stream listens for the message event unless eventTypes names others. A WebSocket can ask for sub-protocols.

SSE is the simpler choice for updates that only flow from the server; use a WebSocket when your server already speaks it. Every stream: and refresh: key, with its default, is in the sources reference: stream: and refresh:.

Each poll or stream message that reaches the map fires the layer’s data events, so a page can show when the data last changed or that a request failed:

const mapEl = document.querySelector("ml-map");
mapEl.addEventListener("ml-map:layer-data-loaded", (event) => {
const { layerId, featureCount } = event.detail;
document.querySelector("#updated").textContent =
`${featureCount} features, updated ${new Date().toLocaleTimeString()}`;
});
mapEl.addEventListener("ml-map:layer-data-error", (event) => {
const { layerId, error } = event.detail;
console.error(`${layerId}:`, error.message);
});

A classified layer recomputes its classes after each update. Every event is in <ml-map> events. With the JavaScript API, the same events are renderer.on("layer:data-loaded", …) and renderer.on("layer:data-error", …).

  • The layer never updates. Check the browser’s network tab for a request every interval. No request: refresh: is not inside the source (it sits beside url:), or the tab was hidden. A request that fails: the feed blocks other sites (CORS), or it is down; the map’s notice and ml-map:layer-data-error say which.
  • The stream never connects. The source has stream: but no refresh:; validation warns “stream: connects only when the source also has a refresh: block”.
  • With merge, a note says features “have no updateKey property and no top-level id, so updateStrategy: merge can’t match them”. Check the key’s spelling; the note lists property names the features do have. (On core 0.9.0 the layer empties instead.)
  • Stream messages are ignored. Each message must be a whole FeatureCollection (see above); the console shows “Received data is not a valid GeoJSON FeatureCollection”.

More: Troubleshooting.

The vehicle-tracking walkthrough is now Merge, with a feed that works as printed.

Folded into the sections above: Choose how updates combine and Troubleshooting.