pixi-heatmap

Smooth GPU heatmaps for PixiJS v8 — one code path for WebGL + WebGPU, from static datasets to billion-point streams.

Installation

Usage

Create a HeatmapLayer, feed it points, add it to the stage, and call update(renderer) every frame — it's dirty-gated, so unchanged frames cost nothing.

Options

Everything is optional except width and height.

Dynamic updates

Stream points in over time — with decay the heat fades, which is ideal for cursor trails and live data.

Playground

Benchmark the live build: switch renderer backends, scale the NYC dataset from 1k to 150k points, and toggle the worker, aggregation, and adaptive-resolution pipeline stages.

Built-in view controls

The playground uses the layer directly on app.stage. Parent-space anchors keep the point under the cursor stable during zoom and rotation, providing a compact interaction path for scenes that use Pixi alone.

pixi-viewport

Drop the layer into a pixi-viewport and it follows pan, zoom and rotation with zero configuration — the layer tracks its own worldTransform, so no setZoom() calls or event wiring are needed. Below, 150k NYC taxi pickups over a live tile map: ⌘/Ctrl + scroll to zoom, middle-drag to pan (touch: drag and pinch).

Real-world data

The layer doesn't care where points come from — trip records, check-in logs, or POI extracts all reduce to { x, y, value }. Three real datasets below, served as compact binary point clouds:

One billion points

Streaming stress test: raw points are sampled from NASA Black Marble night-lights density, binned client-side into 0.5° cells, and re-fed to the layer as weighted points continuously — the GPU sees O(cells), while ingestion runs at up to 1M points/frame until the counter hits one billion. Pick a rate and watch it climb.

API Reference

HeatmapLayerOptions

widthnumber

Width of the heatmap in pixels. Required.

heightnumber

Height of the heatmap in pixels. Required.

radiusnumber Default is 25

Kernel radius of each point, in pixels.

gradientRecord<number, string> Default is blue → red ramp

Color ramp stops as offset → color, from 0 (cold) to 1 (hot).

maxIntensitynumber | 'auto' Default is 10

Intensity mapped to the top of the ramp; 'auto' fits it to the data.

resolutionnumber | 'adaptive' Default is 'adaptive'

Accumulation buffer scale; 'adaptive' lowers it under load to hold frame rate.

aggregationboolean Default is true

Pre-aggregate nearby points before rasterization.

workerboolean | 'auto' Default is 'auto'

Offload aggregation to a Web Worker; 'auto' enables it for large datasets.

decaynumber Default is 0

Per-frame heat decay; values above 0 fade old heat over time.

aggregationMode'sum' | 'mean' Default is 'sum'

How overlapping point values combine.

minOpacitynumber Default is 0

Opacity floor for faint areas, so sparse data stays visible.

Methods

setPoints(points)(points: HeatmapPoint[]) => void

Replace the dataset; each point is { x, y, value? }.

setRaw(points, count?)(points: Float32Array, count?: number) => void

Replace the dataset from interleaved (x, y, weight) triples; this path keeps binary and streaming inputs allocation-light.

addPoint(point)(point: HeatmapPoint) => void

Append a single point without replacing the dataset.

clear()() => void

Remove all points.

moveTo(x, y) / moveBy(dx, dy)(...values: number[]) => void

Set or offset the layer position in parent coordinates.

zoomTo(scale, x?, y?) / zoomBy(factor, x?, y?)(...values: number[]) => void

Set or multiply display scale while keeping the optional parent-space anchor fixed.

rotateTo(radians, x?, y?) / rotateBy(delta, x?, y?)(...values: number[]) => void

Set or add rotation while keeping the optional parent-space anchor fixed.

setOpacity(opacity)(opacity: number) => void

Set the display alpha from 0 to 1.

setZoom(zoom)(zoom: number) => void

Pin the kernel scaling manually and stop following ancestor transforms. By default the layer auto-tracks its worldTransform scale, which keeps on-screen density stable inside scaled parents such as pixi-viewport.

resumeAutoZoom()() => void

Re-enable worldTransform tracking after a manual setZoom().

resize(width, height)(width: number, height: number) => void

Resize the accumulation buffers.

update(renderer)(renderer: Renderer) => void

Flush pending changes to the GPU; dirty-gated, call every frame.

destroy()() => void

Release all GPU resources.

Credits

Built on PixiJS. The accumulation-and-ramp pipeline borrows techniques from the mapbox-gl and deck.gl heatmap layers.