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

@benjamin-small/graph-explorer

v0.7.0

Published

Browser graph explorer — WebGL2 + WebAssembly. Progressive neighbourhood-at-a-time exploration with a force layout. Requires a DOM; not a Node.js library.

Downloads

903

Readme

@benjamin-small/graph-explorer

A typed TypeScript client for a Rust/WebAssembly graph engine. Runs in a browser only — it needs a DOM and WebGL2, and will not work under Node.js.

The engine owns the graph, the layout and the WebGL2 rendering; this package is the framework-agnostic JavaScript surface on top of it — a GraphClient class with typed events, an immutable snapshot suitable for React/Svelte binding, an optional Vim-style key layer, and a built-in requestAnimationFrame loop.

The compiled wasm binary and its glue ship inside this package (pkg/), so there is nothing extra to fetch or configure — importing the /mount entry initializes it.

How to run it in a browser → — the canvas contract, DPI and resizing, bundler and MIME-type requirements, CSP, SSR, context loss, and what each of those looks like when it goes wrong. API reference and live demos →

Status: pre-1.0. The API has only grown so far, but minor versions may still change behaviour; the changelog calls out anything that alters existing output.

Install

npm install @benjamin-small/graph-explorer

A bundler that understands the exports field and can emit .wasm as an asset (Vite, Webpack 5, Rollup, esbuild) is required — the wasm URL is resolved with new URL(..., import.meta.url).

Entry points

| Specifier | Contents | | --- | --- | | @benjamin-small/graph-explorer | Core: GraphClient, Emitter, toStore, initialSnapshot, and all types. No wasm import. | | @benjamin-small/graph-explorer/mount | mountGraph(canvasId) — initializes the wasm module and mounts onto a canvas. This is the entry that pulls in the binary. | | @benjamin-small/graph-explorer/pointer | attachPointer(client, canvas) — click, drag, double-click, wheel zoom, and the CSS-to-physical pixel conversion. | | @benjamin-small/graph-explorer/vim | attachVim, defaultBindings, and their types — an optional host-owned key layer. | | @benjamin-small/graph-explorer/react | <GraphExplorer> and useGraphSnapshot. React is an optional peer dependency. | | @benjamin-small/graph-explorer/testing | FakeEngine, a test double implementing the raw engine interface. Test-only; keep it out of production bundles. |

The core entry is deliberately wasm-free, so importing types or writing framework adapters does not drag the binary into a bundle.

Usage

import { mountGraph } from "@benjamin-small/graph-explorer/mount";
import { attachVim } from "@benjamin-small/graph-explorer/vim";

const client = await mountGraph("graph"); // id of a <canvas> element

client.load(jsonGraph);
client.on("change", (id) => console.log("current node:", id));

attachVim(client);  // optional h/j/k/l navigation
client.start();     // library-owned rAF render loop

// later
client.stop();
client.dispose();

Consumers who want to drive their own scheduling can skip start() and call client.render(now) from their own loop.

React

GraphClient implements the useSyncExternalStore contract directly, and subscribe/getSnapshot are bound, so they can be passed detached:

const snap = useSyncExternalStore(client.subscribe, client.getSnapshot);

The snapshot is reference-stable: it is only replaced when one of currentId, mode, loading or error actually changes.

Svelte

toStore adapts the same snapshot to Svelte's readable-store contract (immediate call on subscribe, value passed to the callback):

import { toStore } from "@benjamin-small/graph-explorer";

const graph = toStore(client);
// in markup: $graph.currentId, $graph.mode, $graph.loading, $graph.error

Neither framework is a dependency of this package.

Multiple explorers on one page

Supported. Mounted explorers are independent: they render side by side, and disposing one does not disturb the others. Mounts are also safe to start concurrently — mountGraph and the engine's own mount both serialize GPU initialization internally, which matters because React's StrictMode double-invokes effects and so issues two overlapping mounts from a single component.

The ceiling is the browser's WebGL context limit, not this library. Chrome allows 16 live contexts per page and force-loses the oldest beyond that; a canvas whose context was lost goes blank. Measured on Chrome:

| Live explorers | Contexts lost | Errors | | --- | --- | --- | | 8 | 0 | 0 | | 16 | 0 | 0 | | 24 | 8 | 0 |

One explorer holds one context, so budget accordingly, and dispose explorers you are no longer showing rather than leaving them mounted off-screen. A discarded canvas does not release its context until it is garbage collected, so churning through mounts can trip the cap even when few are live at once.

Idle cost

The render loop parks when nothing is in motion, and any mutating call on GraphClient restarts it. An untouched explorer costs nothing rather than rebuilding and re-uploading its scene every 16ms forever.

running reports whether you asked for a loop; scheduled reports whether a frame is pending right now. A parked loop is still running.

Three things stop a graph from ever reaching a full idle:

  • Halos pulse by default, and halos mark the current node. A pulsing halo takes a reduced path — the nodes, edges and labels of the previous frame are reused and only the halo buffer is re-uploaded — but it still draws. Set reduced_motion in the AnimationSpec to freeze the pulse and let the loop park completely.
  • An in-flight fetch always keeps the loop awake, because the fetch pump runs per frame. Parking with a request outstanding would strand its result.
  • An idle animation, if you configure one (AnimationSpec.idle, or a setIdleFn callback), keeps the loop alive by design: ambient motion — drift, breathe, or shimmer — that starts after ~2s of quiet, stops the instant anything real happens, and honours reduced_motion. Drift can be draw-time only (mode: "visual", layout truth untouched) or applied to the real positions (mode: "physical"). It runs on a reduced frame tier that skips styling, stylers, fetches and the simulation.

If you drive render(now) yourself instead of calling start(), it returns whether another frame is wanted, so you can idle on the same signal. If you change something the engine cannot observe — a styler callback that starts returning different answers without being re-registered is the realistic case — force a repaint with client.raw.invalidate?.(). That one is on the raw engine rather than on GraphClient, because every method the client does surface already wakes the loop by itself.

Build

npm run build   # tsc -p tsconfig.build.json -> dist/ (JS + .d.ts + maps)

The wasm binary is produced separately with wasm-pack from the repository root (npm run wasm for a dev build, npm run wasm:release for the optimized one) and lands in pkg/.