@vishinvents/aerostat-charts
v0.1.0
Published
WebGL-powered charts with spring-physics transitions. Built on Aerostat.
Maintainers
Readme
@vishinvents/aerostat-charts
WebGL-powered charts with spring-physics transitions. Built on Aerostat.
Key traits: GPU-rendered, handles millions of points via LTTB downsampling, zero-idle-CPU render loop, spring-animated transitions, transport-agnostic streaming API.
Install
npm install @vishinvents/aerostat-chartsQuick Start
import { createChart } from '@vishinvents/aerostat-charts';
const chart = createChart({
container: '#my-chart', // CSS selector or HTMLElement
data: [{
x: new Float64Array([0, 1, 2, 3, 4]),
y: new Float64Array([10, 25, 18, 30, 22]),
}],
});
// Later: clean up
chart.destroy();That's it. The chart auto-fits to the data, renders axes, tooltip, area fill gradient, and a thick anti-aliased line.
API Reference
createChart(options): ChartController / createLineChart(options): ChartController
Creates a line chart with area fill gradient and thick anti-aliased line. createChart is an alias for createLineChart.
createBarChart(options: ChartOptions): ChartController
Creates a bar chart with gradient-filled bars. Uses the same ChartOptions + bar field for bar-specific config.
import { createBarChart } from '@vishinvents/aerostat-charts';
const chart = createBarChart({
container: '#chart',
data: [{ x: [0, 1, 2, 3, 4], y: [10, 25, 18, 30, 22] }],
bar: { color: '#22c55e', widthRatio: 0.7 },
});createLollipopChart(options: ChartOptions): ChartController
Creates a lollipop chart — vertical stems with SDF dot heads. Spatial-ready: monochromatic, background-agnostic, luminance-encoded magnitude.
import { createLollipopChart } from '@vishinvents/aerostat-charts';
const chart = createLollipopChart({
container: '#chart',
data: [{ x: [0, 1, 2, 3, 4], y: [10, 25, 18, 30, 22] }],
theme: 'dark', // 'dark' | 'light' | 'spatial'
lollipop: {
dotShape: 'circle', // 'circle' | 'square' | 'diamond'
dotRadius: 5,
luminanceMagnitude: true, // brighter dots = higher values
},
});createStepChart(options: ChartOptions): ChartController
Creates a step-line chart with ghost hatch fill and double-stroke outlined lines. Step geometry uses 90-degree angles (horizontal-then-vertical). Spatial-ready: procedural hatching, background-agnostic outline.
import { createStepChart } from '@vishinvents/aerostat-charts';
const chart = createStepChart({
container: '#chart',
data: [{ x: timestamps, y: values }],
theme: 'spatial',
step: {
lineWidth: 2.5,
ghostFill: true, // diagonal hatch fill under the step line
hatchSpacing: 6, // stripe spacing in px
hatchWidth: 1.5, // stripe width in px
},
});createHeatmapChart(options: ChartOptions, data: HeatmapData): HeatmapController
Creates a halftone heatmap — a grid of variable-radius SDF circles. Value magnitude is encoded as dot size (larger = higher value). Single draw call for the entire grid. Spatial-ready: halo outline for background agnosticism, AA glow for VR.
import { createHeatmapChart } from '@vishinvents/aerostat-charts';
const chart = createHeatmapChart({
container: '#chart',
theme: 'dark',
heatmap: {
edgeSoftness: 0.06, // increase for VR glow
showAxes: true,
},
}, {
cols: 50,
rows: 30,
values: new Float32Array(1500).map(() => Math.random()),
});
// Update a single cell efficiently
chart.setCell(10, 5, 0.8);createRayChart(options: ChartOptions, data: RayData[]): RayController
Creates a ray chart — radial lines from a center point where ray length encodes value. Ghost rings show scale. Double-stroke rendering for background agnosticism. Rich negative space for AR/VR.
import { createRayChart } from '@vishinvents/aerostat-charts';
const chart = createRayChart({
container: '#chart',
theme: 'spatial',
ray: {
ghostRings: true,
innerRadius: 0.08, // donut-style inner gap
lineWidth: 2.5,
},
}, [
{ label: 'CPU', value: 0.85 },
{ label: 'Memory', value: 0.6 },
{ label: 'Disk', value: 0.3 },
]);createFlowChart(options: ChartOptions, data: FlowData): FlowController
Creates a flow-stream chart — a monochromatic Sankey with animated marching-ants paths. Width encodes volume, animation speed encodes velocity/conversion rate. Spatial-ready: movement shows direction without labels or color.
import { createFlowChart } from '@vishinvents/aerostat-charts';
const chart = createFlowChart({
container: '#chart',
theme: 'dark',
flow: {
dashDensity: 8,
speed: 1,
ghostOpacity: 0.15, // faint paths for low-value flows
},
}, {
nodes: [
{ id: 'a', label: 'Visit', stage: 0 },
{ id: 'b', label: 'Sign Up', stage: 1 },
{ id: 'c', label: 'Paid', stage: 2 },
],
links: [
{ source: 'a', target: 'b', value: 100, speed: 1.5 },
{ source: 'b', target: 'c', value: 40, speed: 2.0 },
],
});ChartOptions
| Property | Type | Default | Description |
|---|---|---|---|
| container | HTMLElement \| string | required | DOM element or CSS selector to mount into |
| data | SeriesData[] | [] | Initial data series |
| width | number | container width | Chart width in CSS pixels |
| height | number | container height | Chart height in CSS pixels |
| padding | Padding | { top: 20, right: 20, bottom: 40, left: 60 } | Inner padding in pixels |
| line | LineOptions | see below | Line rendering config |
| axes | AxesOptions | see below | Axis/grid config |
| tooltip | TooltipOptions | see below | Tooltip config |
| transitionPreset | 'snappy' \| 'bouncy' \| 'smooth' \| 'heavy' | 'snappy' | Spring preset for data transitions |
| viewportPreset | 'snappy' \| 'bouncy' \| 'smooth' \| 'heavy' | 'snappy' | Spring preset for viewport animations |
| transitionSpring | SpringOpts | — | Custom spring config (overrides preset) |
| viewportSpring | SpringOpts | — | Custom spring config (overrides preset) |
| pixelRatio | number | devicePixelRatio | Render resolution multiplier |
| backgroundColor | string | theme-derived | Background color (hex, rgb, rgba) |
| interactive | boolean | false | Enable zoom/pan/pinch. When false, hover/tooltip still work. |
| theme | 'dark' \| 'light' \| 'spatial' | 'dark' | Theme preset — drives default colors for data, grid, labels |
| bar | BarOptions | see below | Bar chart config (used by createBarChart) |
| lollipop | LollipopOptions | see below | Lollipop chart config (used by createLollipopChart) |
| step | StepOptions | see below | Step-line chart config (used by createStepChart) |
| heatmap | HeatmapOptions | see below | Heatmap config (used by createHeatmapChart) |
| ray | RayOptions | see below | Ray chart config (used by createRayChart) |
| flow | FlowOptions | see below | Flow chart config (used by createFlowChart) |
LineOptions
| Property | Type | Default | Description |
|---|---|---|---|
| width | number | 2 | Line width in CSS pixels |
| color | string | '#3b82f6' | Line color (hex, rgb, rgba) |
| antialias | boolean | true | Enable anti-aliasing |
BarOptions
| Property | Type | Default | Description |
|---|---|---|---|
| color | string | '#3b82f6' | Bar color (hex, rgb, rgba) |
| widthRatio | number | 0.8 | Bar width as fraction of available space (0–1) |
| topAlpha | number | 0.9 | Gradient alpha at bar top |
| bottomAlpha | number | 0.3 | Gradient alpha at bar bottom |
| borderColor | string | same as color | Border/outline color |
| borderWidth | number | 0 | Border width in pixels (0 = no border) |
LollipopOptions
| Property | Type | Default | Description |
|---|---|---|---|
| stemColor | string | theme fg | Stem line color |
| stemWidth | number | 1.5 | Stem line width in pixels |
| dotRadius | number | 5 | Dot head radius in pixels |
| dotShape | 'circle' \| 'square' \| 'diamond' | 'circle' | SDF shape for dot heads |
| dotOutline | number | 1.5 | Outline width on dot heads in pixels |
| luminanceMagnitude | boolean | true | Encode value as dot brightness (higher = brighter) |
StepOptions
| Property | Type | Default | Description |
|---|---|---|---|
| color | string | theme fg | Step line color |
| lineWidth | number | 2.5 | Line width in pixels (double-stroke outlined) |
| ghostFill | boolean | true | Enable diagonal hatch fill under the step line |
| hatchSpacing | number | 6 | Hatch stripe spacing in pixels |
| hatchWidth | number | 1.5 | Hatch stripe width in pixels |
| outlineColor | string | theme outline | Outline/border color for double-stroke |
HeatmapOptions
| Property | Type | Default | Description |
|---|---|---|---|
| color | string | theme fg | Dot fill color |
| outlineColor | string | theme outline | Halo/outline color for background agnosticism |
| outlineWidth | number | 0.12 | Outline width as fraction of dot radius (0–1) |
| edgeSoftness | number | 0.04 | Smoothstep AA width. Increase for VR glow. |
| minRadius | number | 0.05 | Minimum dot radius as fraction of cell |
| maxRadius | number | 0.48 | Maximum dot radius as fraction of cell |
| showAxes | boolean | false | Show row/column labels |
| xFormat | (col: number) => string | String(col) | Column label formatter |
| yFormat | (row: number) => string | String(row) | Row label formatter |
HeatmapData
| Property | Type | Description |
|---|---|---|
| cols | number | Number of columns |
| rows | number | Number of rows |
| values | Float32Array \| number[] | Flat row-major array (length = cols × rows). Each value 0–1. |
RayOptions
| Property | Type | Default | Description |
|---|---|---|---|
| color | string | theme fg | Ray line color |
| lineWidth | number | 2.5 | Ray line width in pixels |
| innerRadius | number | 0.08 | Inner radius as fraction of chart radius (0 = full, >0 = donut) |
| ghostRings | boolean | true | Show concentric scale rings |
| ringCount | number | 4 | Number of ghost rings (25%, 50%, 75%, 100%) |
| ringColor | string | theme fgDim | Ghost ring color |
| showLabels | boolean | true | Show category labels around perimeter |
RayData
| Property | Type | Description |
|---|---|---|
| label | string | Category label for this ray |
| value | number | Value 0–1 (1 = full radius) |
FlowOptions
| Property | Type | Default | Description |
|---|---|---|---|
| color | string | theme fg | Path/node color |
| outlineColor | string | theme outline | Outline color for path edges |
| dashDensity | number | 8 | Number of dash cycles along path length |
| speed | number | 1 | Default animation speed multiplier |
| nodeStroke | number | 2 | Node outline width in pixels |
| minPathWidth | number | 2 | Minimum flow path width in pixels |
| maxPathWidth | number | 30 | Maximum flow path width in pixels |
| ghostOpacity | number | 0.15 | Opacity for low-value "ghost" flows |
FlowData
| Property | Type | Description |
|---|---|---|
| nodes | FlowNode[] | Array of nodes. Each has id, label?, stage (column index), and optional dropoff flag. |
| links | FlowLink[] | Array of links. Each has source, target (node ids), value (volume), speed? (animation multiplier). |
Nodes are rendered as SDF circles — radius proportional to sqrt(volume). Dual-tone (white fill + black stroke) for light/dark backgrounds. Drop-off nodes (dropoff: true) render as smaller hollow circles positioned below the main flow, with gravity-curve paths and decreasing dash frequency.
AxesOptions
| Property | Type | Default | Description |
|---|---|---|---|
| x | AxisConfig | — | X-axis config |
| y | AxisConfig | — | Y-axis config |
| gridColor | string | 'rgba(255,255,255,0.06)' | Grid line color |
| labelColor | string | 'rgba(255,255,255,0.5)' | Axis label color |
| font | string | '11px monospace' | Label font |
AxisConfig
| Property | Type | Default | Description |
|---|---|---|---|
| show | boolean | true | Show this axis |
| format | (value: number) => string | auto | Label formatter function |
| ticks | number | ~6 | Approximate tick count |
TooltipOptions
| Property | Type | Default | Description |
|---|---|---|---|
| enabled | boolean | true | Show tooltip on hover |
| stiffness | number | 300 | Spring stiffness for tooltip follow |
| damping | number | 24 | Spring damping for tooltip follow |
| render | (point, x, y) => string | built-in | Custom HTML render function |
SpringOpts
| Property | Type | Default |
|---|---|---|
| stiffness | number | varies by preset |
| damping | number | varies by preset |
| mass | number | 1 |
ChartController
Returned by createChart(). All methods that change the view are spring-animated.
| Method | Signature | Description |
|---|---|---|
| setData | (data: SeriesData[]) => void | Replace all data. Spring-animated transition. |
| appendData | (x: Float64Array \| number[], y: Float64Array \| number[]) => void | Append points to series 0. Auto-scrolls viewport right. |
| setViewport | (vp: Partial<Viewport>) => void | Set viewport bounds (spring-animated). |
| fitData | () => void | Reset viewport to fit all data. |
| resize | (width: number, height: number) => void | Resize chart (CSS pixels). |
| destroy | () => void | Remove from DOM, free GPU resources. |
| getViewport | () => Viewport | Get current animated viewport bounds. |
| getCanvas | () => HTMLCanvasElement | Get the underlying WebGL canvas. |
SeriesData
interface SeriesData {
x: Float64Array | number[]; // X values
y: Float64Array | number[]; // Y values
label?: string; // Series label
color?: string; // Line color override
width?: number; // Line width override
}Performance tip: Use
Float64Arrayfor x/y data. Avoids internal conversion and preserves precision for timestamps.
Viewport
interface Viewport {
xMin: number;
xMax: number;
yMin: number;
yMax: number;
}DataPoint
Passed to tooltip render functions and returned by hover events.
interface DataPoint {
x: number;
y: number;
seriesIndex: number;
pointIndex: number;
}Streaming Data
The appendData(x, y) method is transport-agnostic — it accepts arrays of new points regardless of how they arrive. Below are integration patterns for common transports.
Core Pattern
// This is the only line that touches the chart.
// Everything else is just "how do I get x[] and y[] from my transport?"
chart.appendData(xArray, yArray);WebSocket
Best for high-frequency push (>10 updates/sec). Lowest latency.
import { createChart } from '@vishinvents/aerostat-charts';
const chart = createChart({
container: '#chart',
line: { color: '#22c55e', width: 2 },
});
const ws = new WebSocket('wss://api.example.com/stream');
ws.onmessage = (event) => {
// Expect JSON: { x: number[], y: number[] }
// or adapt to your wire format
const { x, y } = JSON.parse(event.data);
chart.appendData(
Float64Array.from(x),
Float64Array.from(y),
);
};
ws.onclose = () => console.log('Stream ended');
// Cleanup
// ws.close();
// chart.destroy();Binary WebSocket (highest throughput):
ws.binaryType = 'arraybuffer';
ws.onmessage = (event) => {
const buf = new Float64Array(event.data);
const half = buf.length / 2;
const x = buf.subarray(0, half);
const y = buf.subarray(half);
chart.appendData(x, y);
};Server-Sent Events (SSE)
Best for unidirectional server push over HTTP. Auto-reconnects. Works through proxies.
import { createChart } from '@vishinvents/aerostat-charts';
const chart = createChart({
container: '#chart',
line: { color: '#3b82f6', width: 2 },
});
const source = new EventSource('/api/metrics/stream');
source.addEventListener('data', (event) => {
// Expect JSON: { x: number[], y: number[] }
const { x, y } = JSON.parse(event.data);
chart.appendData(
Float64Array.from(x),
Float64Array.from(y),
);
});
source.onerror = () => console.warn('SSE reconnecting...');
// Cleanup
// source.close();
// chart.destroy();Batched SSE (recommended for >1K points/sec):
let xBuf: number[] = [];
let yBuf: number[] = [];
source.addEventListener('point', (event) => {
const { x, y } = JSON.parse(event.data);
xBuf.push(x);
yBuf.push(y);
});
// Flush to chart at 20fps — avoids per-point overhead
setInterval(() => {
if (xBuf.length === 0) return;
chart.appendData(
Float64Array.from(xBuf),
Float64Array.from(yBuf),
);
xBuf = [];
yBuf = [];
}, 50);REST Polling
Best for low-frequency updates (<1/sec) or when push isn't available.
import { createChart } from '@vishinvents/aerostat-charts';
const chart = createChart({
container: '#chart',
line: { color: '#f59e0b', width: 2 },
});
let lastTimestamp = 0;
async function poll() {
const res = await fetch(`/api/metrics?since=${lastTimestamp}`);
const { x, y } = await res.json();
if (x.length > 0) {
chart.appendData(Float64Array.from(x), Float64Array.from(y));
lastTimestamp = x[x.length - 1];
}
}
const interval = setInterval(poll, 1000);
// Cleanup
// clearInterval(interval);
// chart.destroy();REST Batch (Full Replace)
For dashboards that reload a full dataset periodically.
async function loadChart(timeRange: string) {
const res = await fetch(`/api/metrics?range=${timeRange}`);
const { x, y } = await res.json();
// setData replaces everything with a spring-animated transition
chart.setData([{
x: Float64Array.from(x),
y: Float64Array.from(y),
}]);
}
loadChart('24h');Fetch Streaming (NDJSON)
For streaming responses from LLM APIs, log tails, etc.
const res = await fetch('/api/stream');
const reader = res.body!.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop()!; // keep incomplete line
const xs: number[] = [];
const ys: number[] = [];
for (const line of lines) {
if (!line) continue;
const { x, y } = JSON.parse(line);
xs.push(x);
ys.push(y);
}
if (xs.length > 0) {
chart.appendData(Float64Array.from(xs), Float64Array.from(ys));
}
}Performance Notes
- Downsampling: All data is reduced to ≤4,000 points via LTTB before GPU upload. You can feed millions of points — GPU cost stays constant.
- Zero idle CPU: The render loop self-terminates when nothing is animating. No
requestAnimationFrameticking when the chart is static. - Float64 precision: X/Y data is stored as Float64 internally. Only downsampled points are converted to Float32 for the GPU, with an offset applied to preserve precision for large values (e.g., Unix timestamps).
- Streaming:
appendDatais O(batch_size) for the append + O(visible_points) for the viewport scan. At 100K points/sec it uses <1ms per batch. - Memory: Each series stores raw Float64 arrays + ≤4K Float32 downsampled points. 1M points ≈ 16MB per series.
Architecture
src/
├── index.ts # Barrel re-export (tree-shakeable)
├── types.ts # All public TypeScript interfaces
├── charts/
│ ├── line.ts # createLineChart() — line + area fill + thick AA line
│ ├── bar.ts # createBarChart() — gradient-filled bar chart
│ ├── lollipop.ts # createLollipopChart() — SDF dot heads + stems
│ ├── step.ts # createStepChart() — step-line + ghost hatch fill + double-stroke
│ ├── heatmap.ts # createHeatmapChart() — halftone SDF grid, single draw call
│ ├── ray.ts # createRayChart() — radial lines + ghost rings + double-stroke
│ └── flow.ts # createFlowChart() — monochromatic Sankey with animated marching ants
├── core/
│ ├── constants.ts # Shared defaults (padding, spring presets, downsample limit)
│ ├── renderer.ts # WebGL context, program creation, buffer helpers
│ ├── theme.ts # Theme resolver (dark/light/spatial)
│ ├── shaders.ts # GLSL shaders (line, fill, thick, grid, hatch, outline, SDF dot)
│ ├── buffers.ts # Buffer builders (interleave, fill, thick, bar, step, lollipop)
│ └── downsample.ts # LTTB downsampling with viewport clipping
├── scales/
│ └── linear.ts # Tick generation, extent padding
├── interaction/
│ ├── viewport.ts # Zoom, pan, viewport clamping
│ ├── hover.ts # Nearest-point search
│ └── tooltip.ts # Spring-animated tooltip DOM
└── overlay/
└── axes.ts # Canvas 2D axis labels + grid linesLicense
MIT
