Skip to content

Update GeoJSON polygons

Replicates the MapLibre example Update GeoJSON polygons. The YAML declares one named GeoJSON source and the two layers that split it by geometry type. The page seeds five rectangles with feature ids, then animates them with the source’s incremental updateData(), which patches only the features it names instead of re-tiling the whole collection as setData() (and updateLayerData()) does.

Rectangles drift and spin; tile boundaries show which tiles each update touches
update-geojson-polygons.yaml
# yaml-language-server: $schema=https://docs.maplibre-yaml.org/schema/latest/map.schema.json
# Gallery twin of https://maplibre.org/maplibre-gl-js/docs/examples/update-geojson-polygons/
# The YAML half: one named GeoJSON source and the two layers that split
# it by geometry type. The page seeds five rectangles with feature ids,
# then moves them with the source's incremental updateData() — only the
# changed features are re-tiled — see update-geojson-polygons.js.
type: map
id: update-geojson-polygons
config:
center: [-68.13734351262877, 45.137451890638886]
zoom: 5
mapStyle: "https://demotiles.maplibre.org/style.json"
sources:
rectangles:
type: geojson
data: { type: FeatureCollection, features: [] }
layers:
- id: rectangles
type: fill
source: rectangles
filter: ["==", ["geometry-type"], "Polygon"]
paint:
fill-color: ["get", "color"]
fill-opacity: 0.8
- id: rectangles-label
type: symbol
source: rectangles
filter: ["==", ["geometry-type"], "Point"]
layout:
text-field: ["get", "label"]
text-font: ["Noto Sans Regular"]
text-size: 14
text-allow-overlap: true
text-ignore-placement: true
paint:
text-color: "#ffffff"
update-geojson-polygons.js
// The JavaScript half: GeoJSONSource.updateData(), the incremental API.
// setData() (and <ml-map>'s updateLayerData()) re-tiles the whole
// collection; updateData() patches features by id, which is what makes a
// per-frame animation of many features cheap.
const mapEl = document.querySelector("ml-map");
const rectangles = Array.from({ length: 5 }, (_, i) => ({
id: i,
x: -68.13 + (Math.random() - 0.5) * 5,
y: 45.13 + (Math.random() - 0.5) * 5,
vx: (Math.random() - 0.5) * 0.05,
vy: (Math.random() - 0.5) * 0.05,
w: 0.5 + Math.random(),
h: 0.5 + Math.random(),
rotation: Math.random() * 2 * Math.PI,
rotationSpeed: (Math.random() - 0.5) * 0.1,
color: "#008888",
every: Math.round(Math.random() * 100 + 200), // frames between colour changes
}));
const randomColor = () => `#${Math.floor(Math.random() * 0xffffff).toString(16).padStart(6, "0")}`;
function rectangleGeometry({ x, y, w, h, rotation }) {
const c = Math.cos(rotation);
const s = Math.sin(rotation);
const ring = [[-w / 2, -h / 2], [w / 2, -h / 2], [w / 2, h / 2], [-w / 2, h / 2]]
.map(([dx, dy]) => [x + dx * c - dy * s, y + dx * s + dy * c]);
return { type: "Polygon", coordinates: [[...ring, ring[0]]] };
}
mapEl.mapReady().then((map) => {
map.showTileBoundaries = true; // watch which tiles each update touches
const source = map.getSource("rectangles");
const zoom = () => map.getZoom().toFixed(1);
// Seed: features need ids for updateData() to address them.
source.setData({
type: "FeatureCollection",
features: rectangles.flatMap((r) => [
{ type: "Feature", id: r.id, properties: { color: r.color }, geometry: rectangleGeometry(r) },
{ type: "Feature", id: `${r.id}_label`, properties: { label: zoom() }, geometry: { type: "Point", coordinates: [r.x, r.y] } },
]),
});
let count = 0;
function animate() {
count++;
const update = rectangles.flatMap((r) => {
r.x += r.vx;
r.y += r.vy;
r.rotation += r.rotationSpeed;
if (r.x < -75 || r.x > -60) r.vx *= -1;
if (r.y < 40 || r.y > 50) r.vy *= -1;
if (count % r.every === 0) r.color = randomColor();
return [
{ id: r.id, newGeometry: rectangleGeometry(r), addOrUpdateProperties: [{ key: "color", value: r.color }] },
{ id: `${r.id}_label`, newGeometry: { type: "Point", coordinates: [r.x, r.y] }, addOrUpdateProperties: [{ key: "label", value: zoom() }] },
];
});
source.updateData({ update });
requestAnimationFrame(animate);
}
animate();
});
  • updateData addresses features by their top-level id — the seed data must carry ids.
  • Named sources keep their YAML name as the MapLibre source id, so map.getSource("rectangles") finds it directly.