@shlomo-dana/interactive-chart
v0.1.1
Published
Generic React time-series chart. The host owns data, colors, labels, and units.
Readme
What it is
InteractiveChart is a reusable plot for numeric X/Y series (typically time on X). It is not tied to a product, a sensor list, or a backend.
| The library does | The host does |
| --- | --- |
| Draw lines and points at full resolution | Fetch / store samples |
| Wheel zoom, pan, auto-scale, reset | Pick stroke colors |
| Legend, toolbar, tooltip, grid, theme tokens | Pick labels and units (Time (s), Height (m), …) |
| Keep deep zoom detail (no downsample) | Sort points by x |
Built on uPlot. Designed to ship as an npm package.
Install
Peer dependencies: React 18+. The package depends on uplot.
npm install interactive-chartWhen the package is published under a scope, use that name instead, for example:
npm install @your-org/interactive-chartimport { InteractiveChart } from 'interactive-chart'Until it is on the npm registry, this repo is a workspace package inside the host app (packages/interactive-chart).
Quick start
import { InteractiveChart } from 'interactive-chart'
export function AltitudePlot() {
return (
<div style={{ height: 420 }}>
<InteractiveChart
series={[
{
id: 'alt',
label: 'Altitude',
points: [
{ x: 0, y: 10 },
{ x: 12, y: 48 },
{ x: 30, y: 120 },
],
style: { stroke: '#22c55e', width: 1.5 },
mode: 'line',
},
]}
theme={{
textColor: '#e6edf3',
mutedTextColor: '#8b949e',
gridColor: 'rgba(255,255,255,0.06)',
}}
xAxis={{ label: 'Time', unit: 's', hardMin: 0, constrainToData: true }}
yAxis={{ label: 'Height', unit: 'm' }}
legend={{ position: 'top-left', interactive: true }}
toolbar={{ showAutoX: true, showAutoY: true, showReset: true }}
interaction={{
wheelZoom: { sensitivity: 'slow' },
cursor: { crosshair: 'xy' },
}}
/>
</div>
)
}The chart fills its parent. Give the parent a real height (height: 100% inside a flex layout, or a fixed pixel height).
See it working
The library has no backend. The fastest way to check fill, stacking, zoom, and legend is a query-param demo on the host app (this repo’s MapApp). No CSV and no telemetry.
1. Start the host
From the MapApp repo root (the folder that contains packages/interactive-chart):
npm run dev -- --port 5178 --strictPortVite prints the URL (here: http://localhost:5178/). Keep the InteractiveChart window open.
2. Open a demo URL
| URL | What you should see | What to click |
| --- | --- | --- |
| /?chartFill=1 | Two overlapping fills (cyan on top of red) plus a pink line on top of both. Stroke stays solid; only the area is transparent. | Legend: hide Fill on top — the red band remains. Hide Fill behind — cyan stays. Wheel-zoom X; drag to pan. |
| /?chartStress=1 | One dense line (~100k points, 0–600 s). A tall spike around 248.2 s is almost invisible until you zoom in. | Zoom X around 248 s until the spike is obvious. Reset / X restores the full range. |
| / | Live Safety MFC series (Limit is filled). Empty until mission CSVs / telemetry exist. | Use this after the demos. |
Fill demo series are listed in the legend as Fill on top, Fill behind, Line only. Legend order is the array order; paint order is zIndex (cyan is first in the legend, but drawn above red).
Leave a demo: go back to / (no query).
3. How that switch is wired (so you can add another)
Host-only — not inside this package:
- Build synthetic
ChartSeries[](seecreateFillDemoSeries/createStressTimeSeriesin the host). - Gate it on a query flag, e.g.
?chartFill=1. - Return that array before live data.
if (new URLSearchParams(location.search).get('chartFill') === '1') {
return createFillDemoSeries()
}To add a new check (log Y, points-only, zIndex: 'front', …): copy that pattern with a new flag (?chartFoo=1) and a new series factory. Reload the URL. No rebuild of telemetry is required.
4. Automated tests (no browser)
From the MapApp root:
npm test -- packages/interactive-chart/testsThat covers fill color/opacity, draw order (zIndex / 'front'), zoom, pan, and alignment. After API changes, rebuild types:
npm run build --prefix packages/interactive-chart
npx tsc -b5. Paste-in playground (any React host)
Drop this in a page with a real height. Same idea as ?chartFill=1, without MapApp.
import { InteractiveChart } from 'interactive-chart'
const n = 80
const xs = Array.from({ length: n }, (_, i) => (i / (n - 1)) * 60)
const band = (amp: number, base: number) =>
xs.map((x) => ({ x, y: base + amp * Math.sin(x / 5) }))
export function ChartPlayground() {
return (
<div style={{ height: 420 }}>
<InteractiveChart
series={[
{
id: 'top',
label: 'Fill on top',
points: band(10, 28),
mode: 'area',
zIndex: 1,
style: { stroke: '#22d3ee', fill: { opacity: 0.4, to: 0 } },
},
{
id: 'behind',
label: 'Fill behind',
points: band(16, 48),
mode: 'area',
zIndex: 0,
style: { stroke: '#f87171', fill: { opacity: 0.28, to: 0 } },
},
{
id: 'line',
label: 'Line only',
points: band(5, 10),
style: { stroke: '#f472b6' },
},
]}
xAxis={{ label: 'Time', unit: 's', hardMin: 0 }}
yAxis={{ label: 'Height', unit: 'm' }}
legend={{ interactive: true }}
/>
</div>
)
}Fill under the line
Turn a line into a shaded region from the curve down to a baseline (usually 0). The stroke stays solid; only the fill is transparent.
{
id: 'limit',
label: 'Limit',
points: [{ x: 0, y: 80 }, { x: 30, y: 40 }],
mode: 'area', // or keep 'line' and set fill
style: {
stroke: '#f87171',
width: 1.5,
fill: { opacity: 0.22, to: 0 },
},
}Shortcuts:
style: { stroke: '#22c55e', fill: true } // stroke color, 25% opacity, down to 0
style: { stroke: '#22c55e', fill: { opacity: 0.4 } }
style: { stroke: '#22c55e', fill: { color: '#22c55e', opacity: 0.15, to: 'scale' } }to: 0 (default) fills down to height 0. to: 'scale' fills to the bottom of the current Y view.
Live check: See it working → /?chartFill=1.
Who paints on top
Filled areas overlap. The library stacks them so you do not have to reorder the series array (legend order stays as you passed it).
Default (seriesLayering: 'auto'):
back → fills → lines → points → frontTwo fills with no zIndex keep array order: the later fill covers the earlier one.
Pin one series above or below the others with zIndex:
series={[
{
id: 'cover',
label: 'Fill on top',
points: bandA,
mode: 'area',
zIndex: 1, // or 'front' to sit above lines and points too
style: { stroke: '#38bdf8', fill: { opacity: 0.4, to: 0 } },
},
{
id: 'base',
label: 'Fill behind',
points: bandB,
mode: 'area',
zIndex: 0,
style: { stroke: '#f87171', fill: { opacity: 0.28, to: 0 } },
},
{
id: 'trace',
label: 'Line only',
points: trace,
// no zIndex → line layer, above both fills
style: { stroke: '#e879f9' },
},
]}| zIndex | Result |
| --- | --- |
| omitted | auto layer (fill / line / points) |
| 0, 1, 2, … | among the same kind, higher covers lower |
| 'front' | on top of everyone |
| 'back' | behind everyone |
seriesLayering: 'input' turns off the fill/line/points default and uses array order only. Per-series zIndex still wins.
Live check: same /?chartFill=1 URL — cyan (zIndex: 1) covers red (zIndex: 0); the line stays above both.
Two color systems
Do not mix these up.
theme → chrome (background, axes, grid, tooltip, toolbar, legend text)
series.style.stroke → the line / points themselves<InteractiveChart
theme={{
textColor: '#e6edf3',
gridColor: 'rgba(255,255,255,0.06)',
tooltip: { background: 'rgba(22,24,28,0.94)', text: '#e6edf3' },
}}
series={[
{ id: 'a', label: 'A', points, style: { stroke: '#22c55e' } },
{ id: 'b', label: 'B', points, style: { stroke: '#f472b6' } },
]}
/>If a series has no style.stroke, the library uses the theme text color — never a built-in neon palette.
Helpers: defaultDarkTheme, defaultLightTheme, resolveChartTheme.
Interactions
| Action | Result | | --- | --- | | Wheel over the plot | Zoom X, centered on the cursor | | Wheel over the Y axis | Zoom Y only | | Drag left–right | Pan X | | Double-click | Reset both axes to auto | | Toolbar X / Y | Auto-scale that axis | | Toolbar reset | Auto-scale both | | Click a legend item | Hide / show that series |
Wheel zoom defaults to 'slow' (deliberate, not jumpy). Use 'normal', 'fast', or a number if you want it snappier.
X and Y auto/manual are independent. Incoming data does not steal a manual zoom; press X or Y when you want the full range again.
hardMin / hardMax and constrainToData keep the viewport from sliding into empty space (for example time never below 0).
Series
{
id: 'main',
label: 'Main / INS', // legend + tooltip
points: [{ x: 1.2, y: 340 }], // already sorted by x
mode: 'line' | 'points' | 'area',
zIndex: 1, // higher covers lower; 'front' / 'back'
visible: true,
legend: true, // false = plot, skip legend
spanGaps: true,
style: {
stroke: '#22c55e',
width: 1.5,
dash: [4, 3],
opacity: 1,
point: { show: true, size: 6, fill: 'transparent' },
},
}- Points must be sorted by
x. The chart does not re-sort or downsample. - Non-finite
x/yare ignored. legend: falsestill draws the series and includes it in auto-scale.
Props (overview)
| Prop | Role |
| --- | --- |
| series | Data to plot |
| seriesLayering | 'auto' (fills behind lines) or 'input' (array order) |
| theme | Chrome colors (CSS variables under the hood) |
| xAxis / yAxis | label, unit, formatters, hardMin, padding, log Y |
| legend | Position, interactive, optional “All” |
| toolbar | Auto X, Auto Y, Reset |
| grid | { x, y } dashed grid |
| appearance | density: 'compact' \| 'comfortable', corner radius |
| interaction | Zoom, pan, wheel, crosshair |
| title | Optional heading inside the chart (hosts usually own the window title) |
Imperative handle:
ref.resetView()
ref.autoScale()
ref.autoScaleX()
ref.autoScaleY()
ref.getVisibleRange() // { xMin, xMax, yMin, yMax }Axis titles render as Label (unit) — e.g. Height (m).
Layout notes for hosts
- The component is a column flex child:
width/height: 100%,min-width/min-height: 0. - Put it in a panel that already has a height.
- ResizeObserver keeps the canvas in sync when the window is docked or stretched.
Repository layout
packages/interactive-chart/
├── README.md ← you are here
├── package.json
├── docs/ ← screenshots
├── tests/ ← jest (run from the host: npm test -- packages/interactive-chart/tests)
└── src/
├── index.ts ← public API
├── InteractiveChart.tsx
├── InteractiveChart.types.ts
├── InteractiveChart.css
├── core/ ← range, zoom, pan, alignment
├── interactions/ ← wheel + drag
├── theme/
├── ui/ ← legend, toolbar, tooltip, uPlot options
└── hooks/Nothing in src/ imports an application store, telemetry hub, or product palette.
License
MIT
