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

poopdeck.gl
Extensions

TimeFilterExtension

The TimeFilterExtension is a deck.gl layer extension that provides GPU-based temporal filtering. It filters and fades features based on their time range relative to the current time, entirely in shaders — the CPU only updates one uniform block per frame. It also implements time-as-height (the "space-time cube" lift).

It works on instanced layers (ScatterplotLayer, PathLayer) and non-instanced ones (SolidPolygonLayer) alike: its attributes are registered with stepMode: 'dynamic', which resolves to per-instance on instanced models and per-vertex on non-instanced ones — the same mechanism as upstream DataFilterExtension.

Installation#

import {
TimeFilterExtension,
relativizeTime,
MAX_RELATIVE_TIME_MS,
NEVER_ENDS,
} from '@poopdeck.gl/layers';

Usage#

The extension is used internally by the STT layers (AnimatedPointLayer, AnimatedPathLayer, …), but can be applied to any deck.gl layer for custom temporal visualizations.

import { ScatterplotLayer } from '@deck.gl/layers';
import { TimeFilterExtension, relativizeTime } from '@poopdeck.gl/layers';
const timeOffset = dataStartMs; // see "The timeOffset contract" below
const layer = new ScatterplotLayer({
id: 'custom-temporal',
data: myData,
extensions: [new TimeFilterExtension()],
getTime: () => timeController.getTime(), // absolute; relativized internally
timeOffset,
timeWindow: 3600000, // 1 hour window
fadeInDuration: 300,
fadeOutDuration: 300,
// Time accessors — must return RELATIVE times (absolute - timeOffset):
getInstanceStartTime: (d) => relativizeTime(d.startTime, timeOffset),
getInstanceEndTime: (d) => relativizeTime(d.endTime, timeOffset),
getPosition: (d) => d.coordinates,
getRadius: 100,
});

Extension Props#

PropertyTypeDefaultDescription
currentTimenumber0Current time (Unix ms).
getTime(() => number) | nullnullDynamic time getter — called every draw() so the layer instance stays cached across animation ticks (only uniforms update per frame). Takes priority over currentTime.
timeOffsetnumber0The per-layer time origin all attribute times are relative to. Critical for f32 precision — see below.
timeWindownumber0Window size in ms (window mode): features within currentTime ± timeWindow/2 are visible.
fadeInDurationnumber0Fade-in duration (ms) for appearing features.
fadeOutDurationnumber0Fade-out duration (ms) for disappearing features (window mode only).
trailLengthnumber0Trail length in ms (trail mode when > 0).
fadeTrailbooleantrueIn trail mode: fade head→tail (the classic comet trail) vs constant opacity along the whole length (a solid snake).
wakeLengthnumber0Wake length in ms (wake mode when > 0).
wakeTailScalenumber0.15Trailing-edge point-size multiplier in wake mode (0..1; head = 1.0).
cumulativebooleanfalseCumulative ("draw and persist") mode.
timeHeightScalenumber0Time-as-height: meters of altitude per sim-ms (0 = off).
timeHeightOriginnumber | nullnullAbsolute time (Unix ms) mapped to altitude 0 in time-as-height mode. null — and a literal 0, treated identically — anchors altitude 0 at the tile's own timeOffset. Pass the dataset's timeRange.start for altitudes that agree across temporal chunks.
segmentTimebooleanfalseTrail mode: re-point instanceEndTime from the feature's end to the NEXT VERTEX's time, so the trail interpolates across each segment instead of stepping (window mode then reads NEVER_ENDS for the feature end). Pair with the pathSegmentTime constructor option, which injects the interpolation itself.

Data Accessors#

PropertyTypeDefaultDescription
getInstanceStartTimeAccessor<number>0Feature start time, RELATIVE to timeOffset.
getInstanceEndTimeAccessor<number>NEVER_ENDSFeature end time, RELATIVE to timeOffset. The default is NEVER_ENDS (3.4028234663852886e38, the f32 maximum), not Infinity — and a custom accessor must likewise return a large FINITE number: deck rejects non-finite constant attribute values and falls back to 0, which hides every feature the moment the playhead passes timeWindow / 2.
getInstanceVertexTimeAccessor<number>0Per-vertex timestamp (trail mode), RELATIVE to timeOffset. The attribute is always registered, so the constant default keeps non-trail layers valid.

Modes#

One mode is active at a time, by precedence: cumulative → wake → trail → window.

Window mode (default)#

Features whose [startTime, endTime] overlaps currentTime ± timeWindow/2 are visible; fadeInDuration ramps alpha at the leading edge, fadeOutDuration at the trailing edge.

Trail mode (trailLength > 0)#

Progressive drawing with a trailing fade — the AnimatedTripsLayer "vehicle moving along route" effect. Uses the per-vertex instanceVertexTime attribute:

visible if: currentTime - trailLength <= vertexTime <= currentTime
alpha = fadeTrail ? 1 - age / trailLength : 1

Unlike upstream TripsLayer, the tail is always culled at currentTime - trailLength; fadeTrail: false only selects a flat alpha, giving a fixed-length solid snake rather than an ever-growing inked route — use cumulative: true for the draw-and-persist look. The cull is shared with trailAlpha() in @poopdeck.gl/core/time-filter, the kernel oracle the three and maplibre backends are pinned against, so it cannot be changed in deck alone.

Wake mode (wakeLength > 0)#

One-sided "ship wake" for point layers: a feature is shown only while 0 <= currentTime - startTime <= wakeLength; alpha fades linearly to zero at the trailing edge and (on ScatterplotLayer, via the DECKGL_FILTER_SIZE hook) point radius shrinks to wakeTailScale of the head radius. The host layer must set timeWindow >= 2 × wakeLength so the tile loader actually loads the past half of the wake — the shader filter is independent of the tile-loading window.

Cumulative mode (cumulative: true)#

"Draw and persist": a feature becomes visible once startTime <= currentTime and stays visible for the rest of playback. Ideal for "watch it get built" datasets (e.g. OSM node creations inking a city in). fadeInDuration still applies as an appear ramp; instanceEndTime is ignored. The host layer must keep already-revealed tiles resident (widen the loader's window) — the shader does the progressive reveal, not the loader.

Time-as-height (orthogonal to the modes)#

When timeHeightScale != 0, every vertex is lifted vertically by (featureTime - timeHeightOrigin) × timeHeightScale meters — per-VERTEX time in trail mode (the thread climbs along its length, slope = speed), per-FEATURE start time otherwise. The lift is computed as a clip-space delta between the lifted and unlifted common-space positions, so screen-space offsets the host layer baked in (path-width quads, billboards) are preserved. A single uniform — animating it (the flat-map ↔ cube "squash" morph) costs nothing per frame. MapView only.

The timeOffset contract (f32 precision)#

This is the contract every consumer must honor. Absolute epoch-ms values (~1.7e12) cannot be represented in a Float32Array or f32 uniform without ~131 s quantization — f32's 24-bit mantissa makes integers exact only up to 2^24 (MAX_RELATIVE_TIME_MS = 16,777,216 ms ≈ 4.66 h… per side of the offset).

The scheme, with relativizeTime(absolute, offset) = absolute - offset as the single source of truth:

  • Attributes (instanceStartTime / instanceEndTime / instanceVertexTime) store absoluteTime - timeOffset. STT binary tiles already arrive this way: BinaryFeatures.startTimes etc. are relative to binary.timeOffset, so the STT layers pass tile values through unchanged and set timeOffset: binary.timeOffset per sublayer.
  • The uniform: the extension subtracts the SAME timeOffset from the resolved current time on the CPU before uploading the currentTime uniform (and from timeHeightOrigin).

Both sides of every shader comparison are therefore small numbers that fit exactly in f32. The layer MUST pass the same offset it used to relativize the attributes.

Worked example#

Dataset starts 2024-01-01 00:00 UTC (1704067200000). A tile's earliest feature start is that instant, so the decoder sets binary.timeOffset = 1704067200000.

QuantityAbsolute (ms)Stored / uploaded
Feature start (01:00)1704070800000attribute 3600000
Feature end (01:30)1704072600000attribute 5400000
Playhead (02:00)1704074400000uniform currentTime = 7200000

The shader compares 3600000 ≤ 7200000 ± window/2 — exact in f32. Had the attribute stored the absolute 1704070800000, f32 would quantize it to a multiple of 128 ms and a ±131s-class error would creep into every comparison.

A relative time past MAX_RELATIVE_TIME_MS triggers a one-time console warning ("check that timeOffset matches the tile data") — except in cumulative mode, which intentionally spans years and tolerates the quantization.

Performance Optimization#

For high-performance animation, use the getTime prop instead of currentTime:

// Less efficient — layer props change every frame
new ScatterplotLayer({
currentTime: animationTime /* triggers prop diffing */,
});
// More efficient — layer instance cached, time read in draw()
new ScatterplotLayer({ getTime: () => timeController.getTime() });

Other built-in optimizations: the shader-injection object is memoized per extension instance — deck calls getShaders() once per sublayer (one per visible tile), and while luma keys its shader and pipeline caches on source text (so a fresh literal would not re-link), the memo saves the per-sublayer allocation of the modules array and inject strings plus deck's mergeShaders pass over them. Fully-hidden features are collapsed at the VERTEX stage (degenerate clip-space position ⇒ zero fragments rasterized) in the whole-feature modes.

Constructor options#

OptionTypeDefaultDescription
pathSegmentTimebooleanfalseInject the per-segment time interpolation that turns a staircase trail into a glide. PATH-family hosts only — it reads PathLayer's vPathPosition / vPathLength varyings, and on a ScatterplotLayer or ArcLayer those identifiers do not exist and the shader fails to compile. Pair it with the segmentTime prop, which arms the branch at draw time.
mode'auto' | 'window' | 'trail' | 'both''auto'Reserved for forward-compat with deck.gl 9.4's gl_InstanceID picking path. All values behave identically today.

Limitations#

  • The three time attributes plus a layer's own attributes can brush WebGL2's 16-vertex-attribute guaranteed minimum when stacked with fp64 positions, picking, and CategoryColorExtension. On GPUs reporting exactly 16 slots, deck.gl logs a link warning and falls back to a non-picking shader; rendering proceeds. AnimatedPathLayer/AnimatedTripsLayer avoid this by default via NoPickingPathLayer.
  • TimeFilterExtension works on SolidPolygonLayer directly (no separate polygon extension needed).

Source#

packages/layers/src/extensions/time-filter-extension.ts