Google Maps for time-series data.
A framework-agnostic time-series charting library with mipmap-style level of detail: like map tiles, it only ever fetches and draws the points the current viewport can actually show. Pan and zoom across millions of samples — years of high-frequency data — and the chart stays smooth because the data is streamed and decimated to the screen, not loaded all at once.
Built on uPlot. No framework dependency — hosts integrate through plain-JS callback hooks, so it drops into React, Vue, Svelte, Blazor, or no framework at all.
A mipmap is a precomputed pyramid of
progressively lower-detail versions of an image — the GPU samples whichever
level matches the screen. mip-chart does the same for time series: it keeps
per-density tiers and serves the one that matches your zoom, so the number of
points drawn stays roughly constant no matter how far you zoom out.
- Virtual level of detail — per-density tiers with on-demand fetch and eviction; only missing sub-ranges are requested, covered pans cost nothing.
- Streaming + decimation — designed around a fragment contract: the host returns viewport-sized, downsampled fragments (LTTB / OHLC-aggregate / transition-preserving). Out-of-order fragments commit atomically.
- Prioritised request lanes — the viewport and its two directional prefetch pads each ride a sticky, independently-conflated lane, so the visible range always paints first and a fast pan never blocks behind buffering (details).
- Synced multi-pane — price + oscillators + state lanes share one time axis; pan/zoom in any pane mirrors the rest with no tearing.
- Per-pane Y-autofit — tracks the visible window, log-aware, OHLC-aware.
- Gestures — drag-pan, wheel-zoom about the cursor, double-click reset, shift-drag box zoom, velocity-projected prefetch.
- Rendering — constant-width candles, shape-preserving (PCHIP) line curves, density-adaptive line width (thins toward a device-pixel hairline when zoomed out), causal hover values, UTC gridlines.
- Minimap — full-extent overview scrubber driven purely by the time axis.
- Offline snapshot export — serialize the current viewport to a self-contained HTML file that renders with no backend.
npm install mip-chartimport { ChartNavigator } from 'mip-chart';
import 'mip-chart/style.css';
const chart = new ChartNavigator(document.getElementById('chart'), {
recipe, // panes + series description (see ViewRecipe)
extent, // full time range of the data
barMs, // native sample cadence
minimap: true,
hooks: {
// Called when the navigator needs data for a (range, density). Return
// viewport-sized, downsampled fragments — this is where your server (or a
// local decimator) does level-of-detail reduction.
requestFrame: (req) => myProvider.load(req),
},
});See examples/ for a complete, runnable host: a synthetic OHLC
provider (deterministic random walk, latency jitter, out-of-order delivery)
wired to the navigator exactly the way a real backend would be.
The navigator never fires one bulk request. Each frame it splits the work across
three sticky lanes — the visible viewport plus two prefetch pads (ahead /
behind) — and calls requestFrame once per lane that's missing data. Each
NavigationRequest carries:
laneId— a stable id per lane (the same three ids for the chart's life). Echo it back on everySeriesFragmentso the navigator routes the response to the lane that asked.priority—0for the viewport (always first); the prefetch pads are1/2, assigned dynamically by travel direction (the pad you're panning toward outranks the trailing one). A backend draining a conflated priority queue paints the viewport first and buffers the pads behind it.phaseId— monotonic within a lane. A newer phase supersedes an in-flight one on the same lane; other lanes are untouched.sampleIntervalMs— the density the request was issued at; echo it back so fragments cache into the matching level-of-detail tier — even abandoned ones.
The contract is deliberately forgiving. A backend may conflate (keep only the
newest phase per lane, dropping superseded ones) and load-shed (stop
computing a superseded phase mid-flight and ship only what it finished, tagged
cacheOnly). The navigator caches every delivered fragment — committed, stale,
or cache-only — widening its tiers as you browse, and renders a phase only once
all of its fragments arrive (atomically, regardless of arrival order). Zero-row
fragments are left uncached so the range stays re-fetchable once data exists.
See NavigationRequest and SeriesFragment in src/types.ts
for the full field-by-field contract.
npm install
npm run dev # live example at http://localhost:8123
npm test # vitest unit tests
npm run typecheck # tsc --noEmit
npm run build # bundle + type declarations into dist/MIT © Mark Hammond