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

@heojeongbo/fluxion-render

v0.7.2

Published

High-performance OffscreenCanvas rendering engine for real-time robotics data (charts, LiDAR, streaming).

Readme

@heojeongbo/fluxion-render

High-performance OffscreenCanvas rendering engine for real-time data visualization.

Built for robotics and sensor systems: streaming line charts, LiDAR point clouds, and high-frequency data pipelines up to 120Hz+. Rendering runs entirely in Web Workers — the main thread is never blocked.

npm install @heojeongbo/fluxion-render

Features

  • Worker Pool — 60 charts share 4 workers by default. Zero config required.
  • OffscreenCanvas — all rendering happens off the main thread
  • Zero-copy dataFloat32Array ownership is transferred to the worker, never copied
  • React integration — hooks and components included (/react subpath)
  • Framework-agnostic core — use FluxionHost directly without React

Quick Start

React (recommended)

import {
  axisGridLayer,
  lineLayer,
  useFluxionCanvas,
  useFluxionStream,
} from '@heojeongbo/fluxion-render/react';

function Chart() {
  const timeOrigin = useMemo(() => Date.now(), []);

  const { containerRef, host } = useFluxionCanvas({
    layers: [
      axisGridLayer('axis', {
        xMode: 'time',
        timeWindowMs: 5000,
        timeOrigin,
        yMode: 'auto',
      }),
      lineLayer('signal', { color: '#4fc3f7', lineWidth: 1.5, capacity: 4096 }),
    ],
  });

  useFluxionStream({
    host,
    intervalMs: 1000 / 60,
    setup: (h) => h.line('signal'),
    tick: (tMs, handle) => {
      handle.push({ t: tMs, y: Math.sin(tMs / 500) });
      return 1;
    },
  });

  return <div ref={containerRef} style={{ width: '100%', height: 300 }} />;
}

Vanilla JS

import { FluxionHost } from '@heojeongbo/fluxion-render';

const canvas = document.getElementById('canvas') as HTMLCanvasElement;
const host = new FluxionHost(canvas, { bgColor: '#0b0d12' });

host.addLayer('axis', 'axis-grid', { xMode: 'time', timeWindowMs: 5000, yMode: 'auto' });
const line = host.addLineLayer('signal', { color: '#4fc3f7', capacity: 4096 });

const t0 = Date.now();
setInterval(() => {
  line.push({ t: Date.now() - t0, y: Math.sin(Date.now() / 500) });
}, 1000 / 60);

Worker Pool

Every FluxionHost automatically uses a shared module-level pool of 4 workers — no setup needed. Mounting 60 charts creates 60 hosts but only 4 OS threads.

// No config — 4 workers shared automatically
<FluxionCanvas layers={[...]} />
<FluxionCanvas layers={[...]} />
// ... 60 of these all share the same 4 workers

Adjust pool size (call before creating any host):

import { configureDefaultPool } from '@heojeongbo/fluxion-render';

configureDefaultPool({ size: 2 }); // use 2 workers instead of 4

Scoped pool (React) — useful when a page needs its own isolated pool:

import { useFluxionWorkerPool, FluxionCanvas } from '@heojeongbo/fluxion-render/react';

function Dashboard() {
  const pool = useFluxionWorkerPool({ size: 4 }); // disposed on unmount

  return (
    <>
      {charts.map((id) => (
        <FluxionCanvas key={id} hostOptions={{ pool }} layers={[...]} />
      ))}
    </>
  );
}

Custom worker factory — bypasses the pool entirely (solo mode):

const host = new FluxionHost(canvas, {
  workerFactory: () => new Worker('/my-worker.js', { type: 'module' }),
});

Layer Types

line — Streaming time-series

Appends { t, y } samples to a ring buffer. Ideal for sensor data at 30–120Hz.

lineLayer('signal', {
  color?: string,        // e.g. '#4fc3f7'
  lineWidth?: number,    // default 1
  capacity?: number,     // ring buffer size in samples (explicit)
  retentionMs?: number,  // data retention window in ms
  maxHz?: number,        // expected max sample rate — auto-calculates capacity
  visible?: boolean,     // show/hide without reinitialising the layer (default true)
})

retentionMs + maxHz auto-calculate capacity = ceil(retentionMs/1000 * maxHz * 1.1).
Explicit capacity always takes priority when both are set.

Toggling series visibility — use visible with useLayerConfig to show/hide a layer without reinitialising the host or losing buffered data:

const [enabled, setEnabled] = useState({ s1: true, s2: true, s3: false });

// layers is fixed on mount — never recreated on toggle
const layers = useMemo(() => [
  axisGridLayer('axis', { ... }),
  lineLayer('s1', { color: '#4fc3f7' }),
  lineLayer('s2', { color: '#80ffa0' }),
  lineLayer('s3', { color: '#ffb060' }),
], []);

// only a lightweight CONFIG message is sent to the worker on each toggle
useLayerConfig(host, lineLayer('s1', { visible: enabled.s1 }));
useLayerConfig(host, lineLayer('s2', { visible: enabled.s2 }));
useLayerConfig(host, lineLayer('s3', { visible: enabled.s3 }));
// Keep 10 seconds of data at up to 60Hz → capacity = 660
lineLayer('signal', { retentionMs: 10_000, maxHz: 60 })

Push data via LineLayerHandle:

const handle = host.addLineLayer('signal', { color: '#4fc3f7', capacity: 4096 });

// Single sample
handle.push({ t: tMs, y: value });

// Batch (more efficient at high rates)
handle.pushBatch([{ t: t1, y: v1 }, { t: t2, y: v2 }]);

line-static — One-shot XY plot

Replaces the entire dataset on each push. For pre-computed or snapshot data.

lineStaticLayer('plot', {
  color?: string,
  lineWidth?: number,
  layout?: 'xy' | 'y',  // 'xy': interleaved [x,y,x,y,...], 'y': y-only array
})
const handle = host.addLineStaticLayer('plot', { color: '#80ffa0' });

// XY pairs
handle.pushXy([{ x: 0, y: 0 }, { x: 1, y: 1 }]);

// Y-only (x = index)
handle.pushY([0.1, 0.4, 0.9, 1.6]);

lidar — Point cloud scatter

Efficient batch rendering of large point clouds (30k+ points at 120Hz). Uses counting-sort by intensity to minimize GPU state changes.

lidarLayer('scan', {
  stride?: 2 | 3 | 4,  // points per element: [x,y] | [x,y,z] | [x,y,z,intensity]
  pointSize?: number,
  intensityMax?: number,
  color?: string,       // base color (used when stride < 4)
})
const handle = host.addLidarLayer('scan', { stride: 4, pointSize: 2 });

// Push raw Float32Array: [x, y, z, intensity, x, y, z, intensity, ...]
handle.pushRaw(float32Array);

// Or push structured points
handle.push([{ x: 1.2, y: -0.4, z: 0, intensity: 0.8 }]);

axis-grid — Axes and grid

Controls the viewport bounds for all layers. Does not receive data — configure via axisGridLayer() or host.configLayer().

axisGridLayer('axis', {
  // X axis
  xMode?: 'fixed' | 'time',   // 'fixed': static range, 'time': sliding window
  xRange?: [min, max],        // xMode: 'fixed' only
  timeWindowMs?: number,      // xMode: 'time' only
  timeOrigin?: number,        // Date.now() at stream start (for clock labels)
  xTickFormat?: string | ((v: number) => string), // format string or custom formatter

  // Y axis
  yMode?: 'fixed' | 'auto',   // 'auto': fits to visible data
  yRange?: [min, max],        // yMode: 'fixed' only
  yAutoPadding?: number,      // fractional padding for auto mode (default 0.1)

  // Appearance
  gridColor?: string,
  axisColor?: string,
  labelColor?: string,
  font?: string,
  showXGrid?: boolean,
  showYGrid?: boolean,
  showAxes?: boolean,
  showXLabels?: boolean,
  showYLabels?: boolean,
})

React API

useFluxionCanvas(options)

Creates the canvas, worker, and all layers. Returns a ref to attach to a container <div> and the FluxionHost instance.

const { containerRef, host } = useFluxionCanvas({
  layers: FluxionLayerSpec[],       // layer declarations
  hostOptions?: FluxionHostOptions, // bgColor, pool, workerFactory
  onReady?: (host) => void,         // called once after initialization
});

useFluxionStream(options)

Drives a data loop via setInterval. Returns a measured sample rate.

const { rate } = useFluxionStream({
  host,                   // from useFluxionCanvas
  intervalMs: number,     // e.g. 1000/60 for 60Hz
  setup: (host) => T,     // called once — resolve typed handles here
  tick: (tMs, state) => number, // called every interval, return sample count
});

tMs is milliseconds since the first tick (not Date.now()). Use it as the t value for line samples.

useFluxionWorkerPool(options)

Creates a scoped FluxionWorkerPool that is disposed when the component unmounts.

const pool = useFluxionWorkerPool({
  size?: number,              // default 4
  workerFactory: () => Worker, // required
});

useFluxionHistorical(options)

Pushes a full dataset into a line-static layer whenever data changes. Handles are memoized — re-renders that don't change data are free.

useFluxionHistorical({
  host,                // FluxionHost | null — no-op while null
  layerId: string,     // must match a lineStaticLayer id
  data: readonly XyPoint[] | readonly number[] | null | undefined,
  layout?: 'xy' | 'y', // must match layout on lineStaticLayer config (default 'xy')
});
const layers = useMemo(() => [
  axisGridLayer('axis', { xMode: 'fixed', xRange: [0, 100], yMode: 'auto' }),
  lineStaticLayer('plot', { color: '#4fc3f7', layout: 'xy' }),
], []);

const [host, setHost] = useState<FluxionHost | null>(null);

useFluxionHistorical({ host, layerId: 'plot', data: chartData });

return <FluxionCanvas layers={layers} onReady={setHost} />;

<FluxionLegend>

React overlay legend rendered on top of the canvas. Zero performance cost — fully independent of the OffscreenCanvas render loop.

import { FluxionLegend } from '@heojeongbo/fluxion-render/react';

// Always visible
<div style={{ position: 'relative', width: '100%', height: '100%' }}>
  <FluxionCanvas layers={layers} onReady={setHost} />
  <FluxionLegend
    items={[
      { color: '#4fc3f7', label: 'Signal A' },
      { color: '#80ffa0', label: 'Signal B' },
    ]}
    position="top-left"
  />
</div>

// Visible only on container hover
const containerRef = useRef<HTMLDivElement>(null);

<div ref={containerRef} style={{ position: 'relative', width: '100%', height: '100%' }}>
  <FluxionCanvas layers={layers} onReady={setHost} />
  <FluxionLegend
    items={legendItems}
    visibility="hover"
    containerRef={containerRef}
    position="top-right"
  />
</div>

| Prop | Type | Default | Description | |------|------|---------|-------------| | items | LegendItem[] | required | { color: string, label: string }[] | | visibility | 'always' \| 'hover' | 'always' | Always shown, or fade in on hover | | position | 'top-left' \| 'top-right' \| 'bottom-left' \| 'bottom-right' | 'top-right' | Corner anchor | | containerRef | RefObject<HTMLElement> | — | Hover target in 'hover' mode. Falls back to the legend's parent element | | style | CSSProperties | — | Additional styles |

useFluxionTable(options)

Drives a high-frequency data pump (same pattern as useFluxionStream) and throttles React state updates to a configurable low frequency via updateHz. The data tick runs at intervalMs — only the flush into React state triggers a re-render.

const { rows, rate } = useFluxionTable({
  host,                        // FluxionHost | null
  intervalMs: 1000 / 120,      // data tick rate (120 Hz)
  updateHz: 1,                 // React re-render rate (default 1 Hz). 0 = rAF
  maxRows: 20,                 // max rows kept (default 50, oldest trimmed)
  setup: (host) => T,          // called once — resolve handles or per-stream state
  tick: (tMs, state) => R | null, // return a row object to append, or null to skip
});

tick can push to chart handles and return a row in the same call — chart and table share one data pump without doubling work:

const { rows, rate } = useFluxionTable({
  host,
  intervalMs: 1000 / 120,
  updateHz: 2,
  maxRows: 20,
  setup: (h) => ({ line: h.line('signal') }),
  tick: (tMs, { line }) => {
    const y = Math.sin(tMs / 500);
    line.push({ t: tMs, y });          // → chart
    return { t: tMs.toFixed(0), y: y.toFixed(4) }; // → table row
  },
});

| Option | Type | Default | Description | |--------|------|---------|-------------| | host | FluxionHost \| null | required | No-op while null | | intervalMs | number | required | Data tick interval | | updateHz | number | 1 | React re-render frequency. 0 uses requestAnimationFrame | | maxRows | number | 50 | Max rows; oldest are dropped when exceeded | | setup | (host) => T | required | One-shot initializer | | tick | (tMs, state) => R \| null | required | Called every interval; null skips the row |

Returns { rows: R[], rate: number }.

<FluxionTable>

Unstyled table renderer. Pair with useFluxionTable for throttled rendering.

import { FluxionTable } from '@heojeongbo/fluxion-render/react';

<FluxionTable
  columns={[
    { key: 'id',    header: 'ID' },
    { key: 'value', header: 'Value', render: (v) => <strong>{v}</strong> },
    { key: 'time',  header: 'Time' },
  ]}
  rows={rows}
  classNames={{
    root:  'my-table-wrap',
    table: 'my-table',
    thead: 'my-thead',
    tbody: 'my-tbody',
    tr:    'my-tr',
    th:    'my-th',
    td:    'my-td',
  }}
  style={{ fontSize: 12 }}
/>

| Prop | Type | Description | |------|------|-------------| | columns | FluxionTableColumn<R>[] | { key, header, render? }render receives (value, row) | | rows | R[] | Row data objects | | classNames | FluxionTableClassNames | Per-element CSS class names. All optional | | style | CSSProperties | Applied to the root wrapper <div> |

No default styles are applied — layout and appearance are fully controlled via classNames.

useLayerConfig(host, layerSpec)

Reactively updates a layer's config when the spec changes.

const [windowMs, setWindowMs] = useState(5000);
useLayerConfig(host, axisGridLayer('axis', { timeWindowMs: windowMs }));

<FluxionCanvas>

Declarative wrapper around useFluxionCanvas.

import { FluxionCanvas } from '@heojeongbo/fluxion-render/react';

<FluxionCanvas
  layers={[axisGridLayer('axis', { ... }), lineLayer('s1', { ... })]}
  hostOptions={{ bgColor: '#fff', pool }}
  style={{ width: '100%', height: 300 }}
  onReady={(host) => { /* store ref */ }}
/>

Vanilla JS API

FluxionHost

const host = new FluxionHost(canvas, opts?: FluxionHostOptions);

// Layer management
host.addLayer(id, kind, config?)
host.removeLayer(id)
host.configLayer(id, config)

// Typed helpers — add layer and return a handle
const line   = host.addLineLayer(id, config?)      // → LineLayerHandle
const static = host.addLineStaticLayer(id, config?) // → LineStaticLayerHandle
const lidar  = host.addLidarLayer(id, config?)      // → LidarLayerHandle

// Attach a handle to an already-added layer
const line = host.line(id)
const lidar = host.lidar(id, stride?)

// Canvas
host.resize(width, height, dpr)
host.setBgColor(color)
host.dispose()

FluxionWorkerPool

const pool = new FluxionWorkerPool({
  size?: number,           // default 4, clamped to [1, 16]
  workerFactory: () => Worker, // required
});

// Pass to FluxionHost — called automatically, you rarely need this directly
pool.acquire() // → FluxionWorkerHandle

pool.dispose() // terminate all workers

Data Format

All data is transferred as TypedArray with zero-copy semantics. After calling pushData / push / pushRaw, do not reuse the buffer — ownership is transferred to the worker.

| Layer | Format | Stride | |-------|--------|--------| | line | [t, y, t, y, ...] | 2 | | line-static (xy) | [x, y, x, y, ...] | 2 | | line-static (y) | [y0, y1, y2, ...] | 1 | | lidar stride=2 | [x, y, x, y, ...] | 2 | | lidar stride=3 | [x, y, z, ...] | 3 | | lidar stride=4 | [x, y, z, intensity, ...] | 4 |


Architecture

Main Thread                          Worker Thread(s)
───────────────                      ─────────────────
FluxionHost                          FluxionWorkerPool
  │                                    │
  │──POOL_INIT (OffscreenCanvas)──────►│  Engine (per host)
  │──ADD_LAYER ──────────────────────►│    LayerStack
  │──DATA (Float32Array transfer) ───►│      LineChartLayer
  │──RESIZE ──────────────────────────►│      LidarScatterLayer
  │──DISPOSE ─────────────────────────►│      AxisGridLayer
                                       │
                                       │  Scheduler (rAF)
                                       │    scan pass → draw pass
                                       │    OffscreenCanvas → screen
  • Workers are never blocked by main-thread layout or JS execution
  • ArrayBuffer is transferred (not copied) on every pushData call
  • The Scheduler only renders when data changes (markDirty())
  • Multiple engines share one worker via hostId routing

License

MIT