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

@matthewjacobson/ess

v0.1.0

Published

Compute evenly-spaced streamlines of a 2D vector field, with per-point nearest-neighbor spacing and an explicit completion signal. ESM + UMD, TypeScript-first.

Readme

ess — evenly-spaced streamlines

Compute evenly-spaced streamlines of a 2D vector field, in the spirit of anvaka/streamlines and the Jobard & Lefer (1997) placement algorithm — rewritten as a modern, TypeScript-first library with two extra capabilities:

  1. Per-point spacing. Every output point carries distanceToNearest: the Euclidean distance to the closest point on any other streamline. Handy for adaptive line width, opacity, glyph density, or detecting where the field is sparsely covered.
  2. An explicit completion signal. computeStreamlines returns a handle with a done promise that resolves with a result ({ finished, reason, ... }), plus an onComplete callback and a cancel() method — so you always know when the process has stopped, and why.

Ships as ESM, CommonJS, and UMD (browser global / CDN), with bundled TypeScript declarations.

Install

npm install @matthewjacobson/ess

Usage

ESM / TypeScript

import { computeStreamlines } from '@matthewjacobson/ess';

const handle = computeStreamlines({
  // Return the flow direction at (x, y). Magnitude is ignored.
  // Return null/undefined for points outside the domain.
  vectorField: (x, y) => ({ x: -y, y: x }),
  boundingBox: { left: -50, top: -50, width: 100, height: 100 },
  seed: { x: 10, y: 0 },
  dSep: 8, // target spacing between adjacent streamlines
});

const result = await handle.done;

console.log(result.reason);           // 'completed'
console.log(result.streamlines.length);

for (const streamline of result.streamlines) {
  for (const p of streamline.points) {
    // p.x, p.y, and the distance to the nearest neighboring streamline:
    drawPoint(p.x, p.y, p.distanceToNearest);
  }
}

CommonJS

const { computeStreamlines } = require('@matthewjacobson/ess');
computeStreamlines({ /* ... */ }).done.then((result) => { /* ... */ });

Browser (UMD via CDN)

<script src="https://unpkg.com/@matthewjacobson/ess"></script>
<script>
  streamlines.computeStreamlines({
    vectorField: (x, y) => ({ x: -y, y: x }),
    boundingBox: { left: -50, top: -50, width: 100, height: 100 },
  }).done.then((result) => console.log(result.reason, result.pointCount));
</script>

Examples

The examples/ folder has runnable demos:

| File | What it shows | | --- | --- | | examples/node-example.mjs | Minimal Node usage. Run npm run build, then node examples/node-example.mjs. | | examples/canvas.html | Self-contained canvas renderer. Streamline line width and color are driven by distanceToNearest — thick/warm where coverage is sparse, thin/cool where lines pack tightly. Selectable fields and an adjustable dSep. Run npm run build, then open the file in a browser (it loads the local dist/streamlines.umd.min.js). | | examples/cdn-demo.html | The same line-width-from-spacing idea, but loading the library straight from a CDN (unpkg) — no build step. Just open it in a browser. |

Knowing when it stopped

The work runs cooperatively on the event loop (it yields periodically so it won't freeze a UI). There are three ways to observe completion:

const handle = computeStreamlines({
  vectorField,
  onComplete: (result) => console.log('done:', result.reason),
});

// 1. Await the promise.
const result = await handle.done;

// 2. Inspect the handle synchronously at any time.
handle.finished; // boolean

// 3. Stop early — `done` still resolves, with reason 'cancelled'.
handle.cancel();

result.reason is 'completed' when the field was fully covered, or 'cancelled' when cancel() was called first. Cancelling still finalizes the distanceToNearest values for whatever was produced.

How distanceToNearest works

While placing streamlines, every committed point is stored in a uniform spatial hash grid tagged with its streamline id. After the run settles, each point is queried against the grid (expanding ring search) for the closest point that belongs to a different streamline.

Because a streamline added later can become a point's new nearest neighbor, the value is only finalized on the resolved result — inside onPointAdded / onStreamlineAdded it is still the placeholder Infinity. A point with no other streamline anywhere in the field keeps Infinity.

API

computeStreamlines(options): StreamlinesHandle

Options

| Option | Type | Default | Description | | --- | --- | --- | --- | | vectorField | (x, y) => {x,y} \| null | — (required) | Flow direction at a point; null = outside the domain. | | boundingBox | {left, top, width, height} | {0,0,100,100} | Region streamlines are confined to. | | seed | {x, y} | center of box | First seed point. | | dSep | number | 10 | Target separation between adjacent streamlines. | | dTest | number | dSep / 2 | Collision distance that stops an in-progress streamline. Must be ≤ dSep. | | stepSize | number | dSep / 2 | RK4 integration step. | | minPointsPerStreamline | number | 2 | Discard streamlines shorter than this. | | maxPointsPerStreamline | number | 10000 | Loop guard. | | timeBudgetMs | number | 16 | How long to run between event-loop yields. | | onStreamlineAdded | (streamline) => void | — | Fires per accepted streamline. | | onPointAdded | (point, streamline) => void | — | Fires per sampled point. | | onComplete | (result) => void | — | Fires once when the run settles. |

Returns — StreamlinesHandle

| Member | Type | Description | | --- | --- | --- | | done | Promise<StreamlinesResult> | Resolves when the run settles. Never rejects on cancellation. | | cancel() | () => void | Request an early stop (idempotent). | | finished | boolean | true once settled. |

StreamlinesResult

interface StreamlinesResult {
  streamlines: Streamline[];          // { id, points: StreamlinePoint[] }
  finished: boolean;                  // true if completed, false if cancelled
  reason: 'completed' | 'cancelled';
  pointCount: number;                 // total points across all streamlines
}

interface StreamlinePoint {
  x: number;
  y: number;
  distanceToNearest: number;          // to the nearest point on another streamline
}

Notes & differences from the original

  • Beyond growing seeds from existing streamlines, this implementation also sweeps a coarse fallback grid of seeds once the growth queue drains, so disconnected sub-domains and regions across field singularities still get covered.
  • Integration uses classic RK4 with normalized field directions, so stepSize is a true spatial step.
  • Self-intersection within a single integration direction is detected; a closed loop formed across the two halves is bounded by maxPointsPerStreamline.

Build outputs

| File | Format | For | | --- | --- | --- | | dist/streamlines.mjs | ESM | import, bundlers | | dist/streamlines.cjs | CommonJS | require | | dist/streamlines.umd.js | UMD | browser <script>, AMD | | dist/streamlines.umd.min.js | UMD (minified) | CDN (unpkg, jsdelivr) | | dist/streamlines.d.ts | Types | TypeScript |

npm run build      # produce dist/
npm test           # run the test suite
npm run typecheck  # tsc --noEmit

Node versions

There are two separate requirements — don't conflate them:

  • Using the package (runtime): Node ≥ 16, declared in engines. The build output is ES2020 and relies only on setTimeout and performance.now (with a Date.now fallback), so it runs anywhere from Node 16 up — and in browsers.
  • Developing the package (build/test toolchain): Node 20 LTS, pinned in .nvmrc. Run nvm use to match it. The test runner (vitest@3) needs Node 18/20/22.

If you later bump the dev tooling to vitest@4, note it drops esbuild (which is why the overrides pin in package.json exists) but requires Node ≥ 20.12 — update .nvmrc accordingly at that point.

License

MIT