Some visualizations animate, pulse, and flash. If you're sensitive to motion or flashing, turn on Reduce motion in your system settings.

poopdeck.gl
deck.gl Layers

AnimatedPointLayer

The AnimatedPointLayer renders time-series point data as circles. It extends SpatioTemporalLayer and provides GPU-accelerated time filtering (window, wake, and cumulative modes) with support for categorical coloring.

Installation#

import { AnimatedPointLayer } from '@poopdeck.gl/layers';

Usage#

import { AnimatedPointLayer } from '@poopdeck.gl/layers';
const layer = new AnimatedPointLayer({
id: 'earthquakes',
data: 'https://example.com/earthquakes/manifest.json',
currentTime: 1672531200000,
timeWindow: 3600000, // 1 hour
fillColor: [255, 128, 0, 255],
radius: 5,
radiusScale: 2,
radiusUnits: 'meters',
});

With Categorical Coloring#

const layer = new AnimatedPointLayer({
id: 'flights',
data: 'https://example.com/flights/manifest.json',
currentTime: Date.now(),
timeWindow: 3600000,
fillColor: 'airline', // categorical property name → GPU palette lookup
colorPalette: [
[31, 119, 180, 255],
[255, 127, 14, 255],
[44, 160, 44, 255],
],
radius: 'altitude', // numeric property name → per-feature radius
});

Wake mode (ship-wake aesthetic)#

const layer = new AnimatedPointLayer({
id: 'vessels',
data: '/data/ais/manifest.json',
currentTime,
wakeLength: 30 * 60 * 1000, // 30 min comet tail behind each point
wakeTailScale: 0.15,
timeWindow: 60 * 60 * 1000, // must be >= 2 × wakeLength (loader window)
});

Cumulative mode ("the map draws itself")#

const layer = new AnimatedPointLayer({
id: 'osm-nodes',
data: '/data/osm-nyc/manifest.json',
currentTime,
cumulative: true,
fadeInDuration: 500, // appear ramp
timeWindow: WHOLE_DATASET_MS, // keep revealed tiles resident
});

Properties#

Inherits all properties from SpatioTemporalLayer.

Render Options#

PropertyTypeDefaultDescription
radiusScalenumber1Global multiplier for point radii.
radiusUnits'pixels' | 'meters' | 'common''pixels'Units for radius. Diverges from upstream ScatterplotLayer's 'meters' — see Deliberate default drift.
radiusMinPixelsnumber0Minimum on-screen radius in pixels.
radiusMaxPixelsnumberMAX_SAFE_INTEGERMaximum on-screen radius in pixels.
filledbooleantrueFill the marker.
strokedbooleanfalseRender an outline stroke around each point.
strokeColorColor[0, 0, 0, 255]Stroke color (constant).
lineWidthUnits'pixels' | 'meters' | 'common''meters'Units for strokeWidth. Deck-parity default — note this differs from radiusUnits, whose STT default is 'pixels'.
lineWidthScalenumber1Global multiplier for stroke widths.
lineWidthMinPixelsnumber0Minimum on-screen stroke width in pixels.
lineWidthMaxPixelsnumberMAX_SAFE_INTEGERMaximum on-screen stroke width in pixels.
billboardbooleanfalseRender markers as billboards (always face the camera in 3D views).
antialiasingbooleantrueSmooth-edge antialiasing; disable to fix blending artifacts under some depth-test parameters.
fadeInDurationnumber300Duration (ms) for points to fade in.
fadeOutDurationnumber300Duration (ms) for points to fade out (window mode).
splatbooleanfalseRender points as soft-gaussian splats instead of hard antialiased disks (installs SplatExtension). Overlapping splats blend into continuous surfaces — a colored point-cloud / "poor-man's-photogrammetry" look rather than a field of discs. Pairs well with rgbColorColumns, a slightly larger radius, some transparency, and billboard: true.

Mode Options#

PropertyTypeDefaultDescription
wakeLengthnumber0When > 0, switches to one-sided "ship wake" rendering: visible only while 0 <= currentTime - startTime <= wakeLength, alpha fades to 0 at the trailing edge, radius shrinks to wakeTailScale × head. Takes precedence over the symmetric window filter. The caller must ensure timeWindow >= 2 × wakeLength so the loader fetches the past half of the wake.
wakeTailScalenumber0.15Trailing-edge size multiplier in wake mode (0..1).
cumulativebooleanfalse"Draw and persist" mode: each point appears at its startTime and stays visible for the rest of playback. fadeInDuration doubles as the appear ramp. Widen the tile loader's window so revealed tiles stay resident.

Glide (motion interpolation)#

Point archives carry one row per entity per sample, so a GPU time-window filter shows every sample in the window at once and a moving entity POPS between them. Glide instead pools the loaded samples by idProperty and emits one CPU-interpolated pose per active entity per frame.

PropertyTypeDefaultDescription
interpolatebooleanfalseOpt into the glide path. Active only when interpolate && !reducedMotion && a resolvable idProperty && !cumulative && wakeLength === 0; any condition unmet leaves the GPU window/wake/cumulative path unchanged, at zero cost.
idPropertystring | nullnullPer-entity id column name grouping samples into one track (e.g. aircraft icao24). Reads a categorical column; a numeric one is stringified. Must be an EXACT id — a lossy or quantized id fuses distinct tracks.
maxInterpolationGapnumberInfinityLargest gap (ms) glide interpolates across. A wider gap is a data hole: the entity HOLDS its last known position rather than gliding a straight line it never travelled.
reducedMotionbooleanfalseHonor the viewer's reduced-motion preference: disables glide and degrades to the discrete GPU window path.

Data Accessors#

PropertyTypeDefaultDescription
fillColorColor | string[255, 128, 0, 255]Fill: constant RGBA, or a property name for categorical coloring. Drifts from upstream — see below.
getFillColorColor | string | nullnullUpstream-vocabulary alias of fillColor. When set, it wins. Unset here, so fillColor wins unless you opt in; with neither set the effective constant is [255, 128, 0, 255], not upstream's [0, 0, 0, 255].
radiusnumber | string5Point radius: constant, or a numeric property name. Drifts from upstream — see below.
getRadiusnumber | string | nullnullUpstream-vocabulary alias of radius. Unset here, so radius wins unless you opt in; with neither set the effective constant is 5, not upstream's 1.
getLineColorColor | nullnullUpstream-vocabulary alias of strokeColor (constant only).
strokeWidthnumber | string1Outline width: constant, or a numeric property name.
getLineWidthnumber | string | nullnullUpstream-vocabulary alias of strokeWidth.
colorPaletteColor[]10-color palettePalette for categorical fillColor (GPU path, up to 4096 entries).
colorMappingRecord<string, Color> | nullnullExplicit category-string → color map. Forces the CPU palette path.
colorMappingDefaultColor[0, 0, 0, 0]Fallback for categories absent from colorMapping (transparent).
rgbColorColumns[string, string, string] | nullnullPer-point RGB from three numeric columns (each 0–255).
colorVectorColumnstring | nullnullPer-point RGBA from one interleaved FixedSizeList<UInt8,4> column.
radiusTransform(v: number) => number | nullnullTransform applied to the radius value before GPU upload.
rampPropertystring | nullnullContinuous ramp: name of a baked NUMERIC column to color each point by. Each point's fill is rampColorRamp sampled at its value mapped through rampDomain (clamped). Expanded once per tile at prepare time and uploaded as a u8 RGBA attribute — zero per-frame cost. A tile lacking the column falls through to the normal color path; a categorical column warns once and is ignored.
rampDomain[number, number][0, 1]Value range mapped to the ramp's ends; values outside it clamp. No effect unless rampProperty is set.
rampColorRampColor[][]Low→high stops, evenly spaced across rampDomain. Empty leaves the ramp inert even when rampProperty is set (warns once).

Accessor aliases. The upstream get* names accept a constant or a property-column name — not a function accessor, since binary tiles cannot run per-feature JS. A function warns once and falls back to the plain prop.

Deliberate default drift#

Three defaults intentionally differ from upstream ScatterplotLayer. Porting a deck config that relied on the upstream values will look different until these are passed explicitly.

PropertySTT defaultdeck defaultWhy
radiusUnits'pixels''meters'STT points are overwhelmingly event markers, not ground footprints; a pixel radius keeps them legible across the whole zoom range.
radius / getRadius515 in the pixel units above is a legible marker; 1 px is a near-invisible speck.
fillColor / getFillColor[255, 128, 0, 255][0, 0, 0, 255]Opaque black is invisible on the dark basemaps these demos use.

Note that lineWidthUnits does not drift — it keeps deck's 'meters', which is why it differs from radiusUnits in this same layer.

Color precedence. colorVectorColumn wins over everything, then rgbColorColumns, then rampProperty (the continuous ramp), then colorMapping/colorPalette (categorical), then a constant fillColor. Every column path falls through to the next when its column is absent from a tile. The ramp is not applied on the CPU glide (interpolate) path, which colors per-track.

colorMapping is the only way to get stable colors across tiles whose categorical column contains different category subsets — the GPU palette texture cannot look up by string, so setting it forces CPU palette expansion.

rgbColorColumns reads three numeric columns (each 0–255) as [r, g, b, 255], e.g. LIDAR returns colored by projecting them into camera images at build time (waymo_extract.py --colorize). Alpha comes from layer opacity.

colorVectorColumn reads one interleaved column baked by stt-build --vector-group point_rgba=r,g,b,a:u8 and binds the contiguous u8 buffer to getFillColor zero-copy — the GPU-ready analogue of rgbColorColumns. It defaults off on this layer because a truthy default would silently shadow an explicit fillColor on any tile that happened to carry the column; AnimatedPointCloudLayer defaults it to 'point_rgba'.

In cumulative mode a property-column strokeWidth is ignored (slabs don't pack stroke widths); the constant branch still applies.

Column filter#

Wires a baked numeric column into STTDataFilterExtension. Points whose value falls inside filterRange render; the rest are hidden, or soft-faded via filterSoftRange. It composes with the time filter — a point must pass both.

PropertyTypeDefaultDescription
filterPropertystring | nullnullName of the baked numeric column to filter by.
filterRange[number, number] | nullnullInclusive [min, max] bounds.
filterSoftRange[number, number] | nullnullSoft bounds inside filterRange; values between the two fade.
filterEnabledbooleantrueToggle the filter without dropping the bound attribute.

filterProperty is the accessor-alias of deck's getFilterValue: pass a column name, not a function (a function warns once and is ignored). Leaving it unset means the extension is never installed — zero cost. A categorical column cannot be range-filtered and warns once. The filter is ignored in cumulative mode, where slabs bake a fixed schema.

A null filterRange keeps the column bound with no active range, so a range set later animates purely by uniform with no tile re-preparation. filterSoftRange and filterEnabled have no effect unless filterProperty (and for the former, filterRange) is set.

3D props#

PropertyTypeDefaultDescription
elevationPropertystring | nullnullNumeric property supplying per-point elevation (z).
elevationScalenumber1Multiplier applied before the value becomes z.
use3DbooleanfalseAccepted for API compatibility only — has no effect.

Tile geometry is 2D lon/lat. With elevationProperty set, each point's z is baked as column[i] * elevationScale into the position buffer at tile-prepare time, on both the per-tile sublayer path and the cumulative slab path. Negative and zero values pass through unchanged (below-grade to rooftop LIDAR returns). Left unset, z stays 0 — byte-identical to a flat 2D render.

3D is otherwise inferred automatically: tiles whose positionDimensions is 3 ride their z zero-copy, and 2D tiles are padded with z=0 (or the elevationProperty-baked z). There is no separate "3D mode" to opt into beyond setting elevationProperty for 2D tiles, or building the archive with 3D positions in the first place.

Architecture & performance#

  • Geometry-kind guard: tile layers whose geometryType is not Point are skipped with one named console warning. geometryType is the only thing that distinguishes a point tile from a linestring tile once the columns are decoded — this layer would otherwise read the first featureCount vertices of a flattened vertex run as one position per feature: no error, no blank map, just points silently bunched along the first few paths. Tiles predating the geometry-kind tag are trusted, not rejected.
  • Cumulative slabs: in cumulative mode points pack append-only into consolidated slabs rather than one sublayer per tile, which is why a property-column strokeWidth and filterProperty are ignored there (see Data Accessors and Column filter).

The sublayer short id for _subLayerProps overrides is points (covers both per-tile and slab sublayers): _subLayerProps: { points: { type: MyLayer, ... } }.

Source#

packages/layers/src/layers/core/animated-point-layer.ts