npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

pixi-heatmap

v0.4.1

Published

Blended GPU heatmap layer for PixiJS v8 (WebGL + WebGPU)

Readme

pixi-heatmap

npm i pixi-heatmap pixi.js

Why

  • Two render passes. Points are splatted as instanced quads with additive blending into a single-channel half-float accumulation texture (R = density), then a direct mesh colorizes it through a 256×1 gradient LUT. Static data splats exactly once; unchanged frames cost ~0 ms.
  • Fill-rate frugal. A compact quadratic kernel discards fragments outside its circular support. Adaptive-resolution accumulation scales from 1.0× to 0.25× with bilinear upscale, driven by a frame-time EMA feedback loop with hysteresis.
  • CPU-frugal. Reused typed-array stores, scratch tables, and capped transferable worker buffers keep bulk data allocation-light; staging-only append uploads and grid aggregation cut upload volume and dense instance counts.
  • Dual backend by design. Every shader ships as GLSL ES 3.00 + WGSL. The balanced path uses r16float accumulation (renderable in WebGL2 and renderable + blendable in core WebGPU) with an r8unorm fallback. High precision uses r32float when WebGL exposes float blending and linear float filtering or the WebGPU device enables float32-blendable and float32-filterable. PixiJS 8 supports all three single-channel formats.
  • Battle-tested techniques. The pipeline follows the mapbox-gl / deck.gl heatmap implementations; instance buffers grow geometrically to 1,048,576 points per desktop draw while iOS uses a 512-instance driver-safe ceiling.

Quick start

import { Application } from 'pixi.js';
import { HeatmapLayer } from 'pixi-heatmap';

const app = new Application();
await app.init({ background: '#0b0e14', resizeTo: window });
document.body.appendChild(app.canvas);

const heatmap = new HeatmapLayer({
  width: app.screen.width,
  height: app.screen.height,
  radius: 40,
  minZoom: 0.5,
  maxZoom: 16,
  optimization: 'auto',
});

heatmap.setPoints([
  { x: 120, y: 80, value: 1 },
  { x: 300, y: 200, value: 0.6 },
  // ... up to 500k+ points
]);

app.stage.addChild(heatmap);
app.ticker.add(() => heatmap.update(app.renderer)); // dirty-gated: cheap when nothing changed

Dynamic data (cursor trails, live streams):

const heatmap = new HeatmapLayer({
  width: 800,
  height: 600,
  radius: 25,
  decay: 0.04, // heat fades over time — perfect for trails
});

app.ticker.add(() => {
  heatmap.addPoint({ x: cursor.x, y: cursor.y, value: 1 });
  heatmap.update(app.renderer);
});

Million-record streaming replacements can reuse one validated unit-weight buffer with O(1) ingestion:

const heatmap = new HeatmapLayer({
  width: 800,
  height: 600,
  radius: 8,
  aggregation: false,
  worker: false,
  maxIntensity: 512,
  toneMapping: 'adaptive',
  resolution: 1,
});
const count = 1_000_000;
const points = new Float32Array(count * 3);

for (let index = 0; index < count; index++) {
  points[index * 3 + 2] = 1;
}

const statistics = {
  count,
  totalWeight: count,
  minWeight: 1,
  maxWeight: 1,
  geometricMeanWeight: 1,
};

app.ticker.add(() => {
  updateCoordinates(points);
  heatmap.setRawTrusted(points, statistics);
  heatmap.update(app.renderer);
});

Bounded-memory aggregate fields:

const heatmap = new HeatmapLayer({
  width: 720,
  height: 380,
  radius: 5,
  aggregationCellSize: 2,
  pointStorage: 'aggregate',
  maxIntensity: 1_000_000,
});

// Interleaved x, y, weight triples. A weight of 25,000 represents
// 25,000 coincident unit-point contributions in the summed field.
heatmap.addPoints(batch, batch.length / 3);
heatmap.update(app.renderer);

Manual view controls with Pixi alone:

// Parent-space anchors keep the cursor's world point fixed.
heatmap.moveBy(deltaX, deltaY);
heatmap.zoomBy(1.2, cursorX, cursorY);
heatmap.rotateBy(Math.PI / 12, canvasCenterX, canvasCenterY);
heatmap.setOpacity(0.75);

// Absolute variants are available for reset buttons and saved views.
heatmap.moveTo(0, 0);
heatmap.zoomTo(1);
heatmap.rotateTo(0);

API

new HeatmapLayer(options)

| Option | Type | Default | Description | | --- | --- | --- | --- | | width, height | number | — | Layer size in pixels. Required. | | optimization | 'manual' \| 'auto' | 'manual' | Auto coordinates precision, normalization, tone mapping, aggregation, Worker use, and resolution. Explicit options override their individual decisions. | | radius | number | 25 | Kernel radius in px. | | minZoom | number | unset | Lower bound for effective kernel compensation and radius-derived aggregation. | | maxZoom | number | unset | Upper bound for effective kernel compensation and radius-derived aggregation. | | gradient | Record<number, string> | blue → cyan → yellow → red | Color ramp stops (offsets 0–1). | | maxIntensity | number \| 'auto' | 10 | Normalization domain. Auto optimization supplies 'auto', which calibrates snapshot-keyed, accumulation-format-aware kernel peaks across radius. | | toneMapping | 'linear' \| 'adaptive' | 'linear' | Gradient transfer curve. Auto optimization supplies 'adaptive', which compresses dominant peaks and lifts valid weak density. | | accumulationPrecision | 'balanced' \| 'high' | 'balanced' | Auto optimization supplies 'high'; high precision prefers 32-bit float accumulation when the renderer exposes float blending and linear filtering, then falls back through the balanced formats. | | webgpuDensity | 'auto' \| 'raster' \| 'gather' | 'auto' | Auto selects the tiled WebGPU compute gather when its point/pixel/kernel cost model predicts a win. Raster is the cross-backend instanced path. | | resolution | number \| 'adaptive' | 'adaptive' | Accumulation texture scale. | | aggregation | boolean | true | Keep up to 12,000 active radius-following points as direct raw splats, blend gradually through 20,000 points, then use a continuous bilinear grid with sparse outlier support. | | aggregationCellSize | number | radius / 4 | Bin size in px. | | pointStorage | 'retained' \| 'aggregate' | 'retained' | Retain every source record, or fold append batches into a fixed Cloud-in-Cell grid with bounded memory and fixed layer dimensions. | | aggregationMode | 'sum' | 'sum' | Combine overlapping point values by summation. | | worker | boolean \| 'auto' | 'auto' | Offload aggregation to a Web Worker; falls back to inline. | | decay | number | 0 | Per-frame fade factor for streaming/trails (0 = off, no ping-pong RT allocated). | | minOpacity | number | 0 | Alpha floor for faint areas. |

Methods

setPoints(points) · setRaw(interleavedFloat32, count?) · setRawTrusted(interleavedFloat32, statistics) · addPoint(point) · addPoints(pointsOrInterleavedFloat32, count?) · clear() · moveTo(x, y) · moveBy(dx, dy) · zoomTo(scale, anchorX?, anchorY?) · zoomBy(factor, anchorX?, anchorY?) · rotateTo(radians, anchorX?, anchorY?) · rotateBy(delta, anchorX?, anchorY?) · setOpacity(opacity) · setZoom(zoom) (kernel compensation) · resumeAutoZoom() · setRadius(r) · setGradient(g) · setMaxIntensity(v) · setMinOpacity(v) · resize(w, h) · update(renderer) (call every frame, or attach(renderer) once for the prerender hook) · destroy()

optimizationProfile reports the current effective mode, heuristic strategy, precision request, normalization mode, tone-mapping mode and exponent, aggregation choice, resolution policy, and Worker policy. pointCount, totalWeight, renderPointCount, and aggregateStorageBytes report accepted representatives, summed contribution weight, retained GPU splats, and aggregate CPU storage. densityEngine reports the latest complete field as raster or gather; webgpuGatherStats exposes compute dispatch, fallback, grid, and capability diagnostics.

The current auto heuristic selects wide-range-quality for a bounded zoom span of at least 8×, up to 160,000 active points, and a desktop-sized instance chunk. This profile uses direct splats and fixed full resolution. Other sources select adaptive-throughput, which uses radius-following aggregation and adaptive resolution. Auto tone mapping compares the rendered peak with the geometric mean of positive source weights, then applies a monotonic power curve from a 16× soft start. setPoints(), setRaw(), and setRawTrusted() refresh the decision; append streams retain one profile for their source lifetime.

Points are { x: number; y: number; value?: number } (value defaults to 1). Coordinates and values must remain finite after Float32 encoding, values are non-negative, and zero-value records are discarded. setRaw validates and copies interleaved (x, y, weight) triples with the same constraints. setRawTrusted borrows a complete-triple Float32Array, trusts strictly positive records plus exact precomputed weight statistics, and performs O(1) ingestion. The trusted path uses pointStorage: 'retained' with decay: 0; treat its array as layer-owned between calls and apply each mutation immediately before the next call. Aggregate point storage fixes the layer dimensions and ignores contributions outside those bounds; create a new layer when the dimensions change. View anchors use the layer parent's coordinates; a layer on app.stage accepts canvas coordinates directly. minZoom and maxZoom clamp the effective zoom reported by heatmap.zoom; viewport and layer display transforms continue through their full range. This caps the derived kernel radius and aggregation cell size during extreme zoom. Automatic kernel compensation assumes uniform display scaling, including rotation. With non-uniform scale or skew, compensation follows the transformed local x-axis magnitude. Float16 targets apply a power-of-two accumulation scale for legal high weights. The r8 fallback applies per-add quantization-aware scaling and represents high dynamic ranges as a saturated degraded field. High precision doubles accumulation-texture memory relative to r16float; WebGPU applications enable the optional float32-blendable and float32-filterable device features to activate it.

TypeScript compatibility

TypeScript 5 checks the published declarations directly. With TypeScript 6 or 7, enable "skipLibCheck": true; PixiJS 8.19 still installs @webgpu/types while those TypeScript releases also provide WebGPU globals in lib.dom.

PixiJS WebGPU compatibility

The WebGPU target-format integration is validated with PixiJS 8.19. Runtime startup checks the private renderer shape used by the compatibility shim and reports a clear error when that shape changes.

Performance

The Interaction demo offers Manual and pixi-viewport camera drivers over the same 149,994-point NYC source. Both use optimization: 'auto', the public default gradient, live color-stop editing, and random palette generation. The bounded camera selects the exact high-precision direct path with a fixed accumulation resolution, preserving density detail across rapid zoom reversals.

The Aggregate capacity benchmark feeds every value-1 record through addPoints() and folds its contribution into a fixed 2 px Cloud-in-Cell grid. It continuously sweeps 0.6×–6× zoom while panning and reports native refresh, frame p95/p99, dropped-frame ratio, preparation, ingestion, and layer update separately. The CLI direct profiler retains one source record and one GPU splat per point for raw-path capacity regression.

On the reference M1 Pro at 120 Hz, the corrected 8 px direct probe measures a 500k Static WebGL envelope, a 300k Static WebGPU envelope, and a 300k validated Dynamic envelope on both backends. The WebGPU trusted one-million replacement path measures 0.00 ms median / 0.10 ms p95 ingestion and 0.60 ms median / 1.00 ms p95 for ingestion plus layer update. Source-coordinate mutation adds about 1.90 ms median, and one million accumulation splats reach about 25 ms frame p95 during mixed zoom/pan. The aggregate probe processes 100M actual records in 2.07–2.40 s and 1B actual records in 20.54–23.32 s, retains 69,504 GPU nodes in 3.84 MiB, and measures zero dropped interaction frames on both backends.

At 1920×1080, one r16float RT uses about 3.96 MiB at 1.0×, 0.99 MiB at 0.5×, and 0.25 MiB at 0.25×; r32float uses twice those amounts. Validated retained point storage uses 12 B/point on the CPU, 12 B/point for master GPU instances, and 4 B/point for decay-age metadata. Trusted retained snapshots reuse the caller's 12 B/point source and keep decay-age storage at base capacity. Aggregate point storage retains a fixed grid sized by width × height ÷ aggregationCellSize². The LUT uses 1 KiB. In-flight worker aggregation uses one 12 B/point buffer for raw/centroid jobs or one 24 B/point buffer for continuous jobs, with returned buffers retained under a 64 MiB cap. Aggregation scratch, result pools, and staging capacity add data-dependent CPU/GPU memory. Master buffers grow geometrically and shrink after a fourfold dataset reduction. Decay allocates a second RT when decay > 0.

Docs

The docs/ workspace is a Nuxt 4.5 SSR site centered on the live Interaction drivers, focused Manual and pixi-viewport source examples, the Aggregate capacity benchmark, and the public API:

pnpm install
pnpm build       # build the library
pnpm docs:dev    # http://localhost:3000

Development

pnpm build        # tsdown (ESM + oxc-based d.ts)
pnpm typecheck    # tsc --noEmit (TypeScript 7, native)
pnpm lint         # oxlint
pnpm test         # vitest — data layer, gradient, aggregation, adaptive controller
pnpm docs:check-cursor             # homepage cursor alignment at DPR 2
pnpm docs:check-page-focus         # focused navigation and fully visible interaction source
pnpm docs:check-interaction-drivers # Manual and pixi-viewport camera contracts
pnpm docs:check-viewport-zoom-flicker # 150k pixi-viewport fast round-trip continuity
pnpm docs:check-interaction-palette   # default, editable, random, and responsive palette controls
pnpm docs:check-dynamic-range         # dominant hotspot keeps weak data visible on both backends
pnpm docs:profile-interactive-capacity webgl 1000000 mixed 240 static balanced 1 8
pnpm docs:profile-aggregate-interaction webgl 100000000 240 1000000 2
pnpm docs:check-aggregate-interaction # actual aggregate records and mixed camera contract
pnpm docs:check-update-metric-format # fixed two-decimal update-time labels
pnpm docs:check-zoom-bounds           # viewport scale stays independent from effective zoom bounds

Credits

Built on PixiJS. Pipeline design informed by the heatmap implementations of mapbox-gl and deck.gl, and by pyalot/webgl-heatmap.

License

MIT