@0k/time-chart
v0.2.0
Published
Framework-free linked time-series charts with local calendar guides.
Maintainers
Readme
@0k/time-chart
Linked, zoomable time-series charts for the browser, built on Apache ECharts. No framework required.
- Day, month and year guides that fade in with zoom, weekends hatched, and a "now" line; all in local time, correct across DST changes.
- Wheel zoom and drag pan on the time axis; charts sharing a
linkkey zoom together. - Stacked areas, min/max bands and gaps for missing values.
- Light and dark themes, following the OS by default.
Install
npm install @0k/time-chartUsage
import { createTimeChart } from '@0k/time-chart';
const now = Date.now();
const chart = createTimeChart(document.getElementById('chart')!, {
series: [{
id: 'sensor-a',
label: 'Sensor A',
points: [[now - 3_600_000, 2], [now, 3]],
}],
formatValue: value => value.toFixed(2),
onWindowChange: (start, end) => { /* fetch data for this window */ },
});
chart.setSeries(newSeries); // keeps the current window and hidden series
chart.setWindow(start, end);
chart.destroy();The element must have a nonzero size. Times are Unix milliseconds.
Series
| Field | Description |
|--------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| id | Unique identifier |
| label | Name shown in the legend and tooltip |
| points | [time, value] pairs sorted by time; null value is a gap |
| min, max | Optional band bounds, one per point |
| group | Series of a group get close colors |
| stack | Stack with the other stacked series |
| color | Explicit CSS color instead of the generated one |
| dash | 'solid' (default), 'dashed' or 'dotted' line style |
| width | Finite positive line width in pixels (default 1); highlighting doubles it, dimming halves it |
| fill | Opacity in (0,1]; fill to the bottom of the y axis, not stacked; cannot be combined with stack: true |
| band | {pattern, opacity} for min / max bounds; pattern is 'solid' (default) or diagonal 'hatch' in the series color; opacity is in (0,1] (default 0.18) |
| line | Draw the line (default true); false keeps its band/fill and legend entry but excludes it from the tooltip |
Options
| Option | Default | Description |
|-----------------------|---------------------|------------------------------------------------------------------------------------------------------------------------------------------|
| series | required | Series to draw |
| window | data extent | Initial {start, end} shown |
| formatValue | localized number | Formats y-axis labels and tooltip values |
| formatTime | localized date/time | Formats the tooltip time |
| yAxis | | {min, max, name, interval, minorInterval}; max: 'data' fixes it at 1.2 × data maximum (default) |
| yAxis.interval | automatic | Finite positive major tick and label step |
| yAxis.minorInterval | disabled | Finite positive minor tick step with dashed minor split lines; requires interval and must divide it into an integer count from 2 to 99 |
| tooltip | 'axis' | 'axis' (all series) or 'item' (one) |
| legend | | {show, collapsed, width, maxWidth}, scrolls, on right |
| legend.width | 340 | Column width in pixels, or 'auto' to fit the widest label |
| legend.maxWidth | 240 for 'auto' | Caps the column; when set, also caps a numeric width |
| link | | Charts with the same key zoom, pan and hover together by time; their 'auto' legends share the widest column |
| onWindowChange | | Called after the user zooms or pans |
| onLegendToggle | | Called when the user toggles a series |
| locale | browser locale | Locale for labels and default formatters |
| theme | 'auto' | 'auto', 'light' or 'dark' |
With legend: { width: 'auto' } the legend column is as wide as its
widest label: at least 80 pixels (less only if maxWidth is smaller),
at most maxWidth; longer labels are ellipsized and shown in full on
hover. Labels are measured again when setSeries changes them. Charts
sharing a link key and an 'auto' legend all take the widest column
of the group, each within its own maxWidth, so with the same
maxWidth their plots line up; the group adjusts when one of them
changes labels or is destroyed. A chart with a numeric width keeps it.
Charts sharing a link key are linked by time: after a user zoom or pan
they all show the same time window, and hovering one puts the others'
pointers at the same time, even when their data covers different ranges
or different timestamps. With tooltip: 'axis' each linked chart also
shows its tooltip, with the values of its own points nearest to that
time; with 'item' only the pointer is linked. A chart hides its
pointer when the time is outside the span of its points or outside its
window. onWindowChange fires on every affected chart after 150 ms
without another zoom or pan, with that chart's current window.
setWindow changes only the receiving chart and never fires the
callback; set it on each linked chart when choosing a range
programmatically.
Methods
| Method | Description |
|----------------------------|--------------------------------------------|
| setSeries(series) | Replace the data |
| setWindow(start, end) | Show this time range (no onWindowChange) |
| getWindow() | Current {start, end} |
| setHidden(ids) | Hide these series |
| highlight(ids) | Emphasize these series; null resets |
| setLegendCollapsed(bool) | Hide or show the legend |
| resize() | Redraw after a size change |
| destroy() | Release the chart; safe to call twice |
calendarGuides(start, end, widthPx, now, locale?) is also exported: it
returns the guide lines and weekend bands for a time range, without
drawing anything.
Development
npm install
npm run check # typecheck, tests, build
npm run demo # http://127.0.0.1:5187/demo/To use a working copy from another project without changing its committed files:
npm run watch # here
npm install --no-save /path/to/time-chart # in the other projectLicense
MIT
Changelog
0.2.0 (2026-10-11)
New
Fit the legend to its labels, keeping linked charts aligned. [Valentin Lab]
With
legend: { width: 'auto' }the legend column fits its widest label, measured in the legend's font, between 80 pixels andlegend.maxWidth(240 by default; an explicitmaxWidthbelow 80 wins); longer labels are ellipsized. Charts sharing alinkkey with auto legends all take the group's widest column, each within its ownmaxWidth, so with the samemaxWidththeir plots line up; they adjust when one of them changes labels or is destroyed.maxWidthalso caps a numericwidth; an invalid value for either now throws. The demo charts switch to auto legends.Choose the step between y-axis labels, with dashed minor lines. [Valentin Lab]
yAxis.intervalsets the step between y-axis labels, andyAxis.minorIntervalsubdivides it with dashed minor lines. Invalid values throw:minorIntervalrequiresintervaland must divide it into an integer count from 2 to 99 (ECharts ignores more). Minor ticks are drawn in the palette's axis color, so they follow the light and dark themes.Draw dashed, dotted, thick, filled, hatched and line-less series. [Valentin Lab]
Each series can now set its line style with
dash('solid','dashed'or'dotted') andwidthin pixels, which highlighting doubles and dimming halves.fillshades the area down to the bottom of the y axis at the given opacity, without stacking.band: {pattern, opacity}draws themin/maxband solid or hatched diagonally in the series color, at a chosen opacity.line: falsedraws only a series' fill or band: it keeps its legend entry but is left out of the tooltip.Invalid widths and opacities throw, as do
fillwithstack: trueandbandon a series withoutminandmax.
Changes
Depend on ECharts 6, which
npm auditno longer flags. [Valentin Lab]ECharts 5 has a security advisory (GHSA-fgmj-fm8m-jvvx, an XSS in the tooltip of
linesseries) that only 6.1.0 fixes, and 5.x gets no more fixes. The charts drawn here never used alinesseries, but every consumer'snpm auditreported it, with no way to resolve it.Two ECharts 6 defaults are pinned back so the charts look as before:
grid.outerBoundsMode: "none": ECharts 6 shrinks the plot to fit wide axis labels, which would move the plot away from the calendar guides, the legend and the alignment of linked charts.yAxis.nameMoveOverlap: false: ECharts 6 pushes the y-axis name past its labels, off the canvas; it stays 46 px from the axis line.
AGENTS.mdnow names~/dev/ts/rrd-graphas the consumer that uses this working copy.Contract:
echartsis^6.1.0; the public API and options are unchanged.- The plot fills exactly the grid insets, whatever the label widths.
- The y-axis name is drawn 46 px left of the axis line.
- Tests:
test/option.test.ts;demo/verify.pypasses in Chrome, and the demo renders as with ECharts 5.6 apart from ECharts' own time-axis labelling.
Fix
Link charts by time, so zoom, pan and hover line up when their data differs. [Valentin Lab]
echarts.connectrelays a zoom as a percentage of each chart's own time axis, and a hover as a data index of the hovered chart. Linked charts whose data covered different ranges therefore showed different windows, and hovering one put the other charts' pointers and tooltips at other times, sometimes 600 px away, or nowhere.The link registry now relays by time instead of using
connect:- a user zoom or pan sends its time window to every linked chart, which moves only its zoom (its axis grows if the window falls outside it);
- a hover sends the hovered time; each linked chart draws its pointer
there, with its axis tooltip showing its own nearest values, or nothing
when the time is outside the span of its points or its window. The
pointer does not snap to the nearest point (that would differ between
charts), and it also links with
tooltip: "item", without a tooltip.
Contract:
- After a user zoom or pan, every chart of a
linkgroup shows the same window and firesonWindowChangeonce, after 150 ms; no echo. setWindowandsetSeriesnever move another chart's window or pointer; only a change of the shared auto-legend width redraws them.- A hover points every linked chart exactly at the hovered time, or hides its pointer outside its data's span or window; leaving hides them all.
- Tests:
test/link.test.ts(real ECharts),test/chart.test.ts,demo/verify.pyin Chrome.
Let a long legend scroll, aligned with the top of the plot. [Valentin Lab]
The legend was the canvas legend of
echarts, which can only page with small arrow buttons when its entries do not fit: the mouse wheel over it zoomed the chart instead, and its first entry sat a few pixels below the plot's top edge.The legend is now an HTML list over the chart's right margin. It spans exactly the plot's height, scrolls with the wheel without zooming the chart or scrolling the page, and keeps its scroll position across redraws. Long labels are truncated clear of overlay scrollbars and show their full text on hover. Clicking an entry, or pressing Space or Enter on it, still hides or shows its series and calls
onLegendToggle; hidden series are greyed out. The options and API are unchanged.A host element with
position: staticis switched torelativeso the legend can be placed inside it.
0.1.2 (2026-10-08)
New
Draw linked, zoomable time-series charts with calendar guides. [Valentin Lab]
Framework-free TypeScript library over Apache ECharts: day, month and year guides fading with zoom density, hatched weekends, a "now" line, time-axis wheel zoom and drag pan past the data, a y-axis fixed from the full data, stacked areas, minimum and maximum bands, light and dark palettes following the OS, and charts sharing a
linkkey zooming together.
Fix
Draw the hatching of a weekend already in progress at the window start. [Valentin Lab]
A window starting on a Saturday or Sunday drew no hatching for that weekend, because bands were only emitted for Saturday midnights inside the window. The band of a weekend overlapping the window start is now drawn too.
Keep day guides on local midnight after a DST jump at midnight. [Valentin Lab]
In zones whose clocks jump at midnight (Chile, Azores, Cuba), the jump day starts at 01:00. Stepping one Date forward with
setDatecarried that 01:00 onto every following day guide and weekend band, until the end of the window. Each day is now built from its date parts, so only the jump day itself starts at 01:00.Draw legend icons as solid swatches readable in dark mode. [Valentin Lab]
ECharts' default legend icon is an empty circle filled white, a glaring white dot on a dark background.
