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.