<bmx-timeseries>
A time-series explorer for millions of points: sensor and machine data, energy meters, application metrics, prices, lab instruments.
21 properties · 4 events · 14 methods · 5 parts
Example
A year of readings from one compressor, about 1.2 million points. Drag to move, Shift and drag to zoom to a range, Ctrl and the wheel to zoom; or use the overview underneath.
Show markup
<bmx-timeseries id="ex-ts" label="Compressor C-3, Riverside plant" zone="Europe/London" presets="1h,1d,7d,30d,all"></bmx-timeseries>
<p class="note" id="ex-ts-out" role="status">A year of readings from one compressor, about 1.2 million points. Drag to move, Shift and drag to zoom to a range, Ctrl and the wheel to zoom; or use the overview underneath.</p>
<div class="row" style="gap: 0.75rem; flex-wrap: wrap; align-items: center; margin-block: 0.5rem 1.5rem">
<button type="button" id="ex-ts-stream">Start streaming</button>
<button type="button" id="ex-ts-bearing">Show the bearing replacement</button>
</div>
<script type="module">
await customElements.whenDefined('bmx-timeseries');
const el = document.getElementById('ex-ts');
const out = document.getElementById('ex-ts-out');
// A repeatable random source, so the page looks the same every time.
let seed = 20261014;
const rnd = () => {
seed = (seed + 0x6d2b79f5) | 0;
let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
const noise = () => (rnd() + rnd() + rnd() - 1.5) / 1.5;
const MIN = 60_000;
const DAY = 86_400_000;
const end = Date.UTC(2026, 9, 13, 23, 59);
const start = end - 365 * DAY;
const replaced = Date.UTC(2026, 2, 12, 8);
const shutdown = [Date.UTC(2025, 11, 24, 18), Date.UTC(2026, 0, 2, 6)];
const outage = [Date.UTC(2026, 5, 3, 2), Date.UTC(2026, 5, 3, 9, 30)];
// Running: weekdays 06:00-22:00, Saturday mornings; never in the shutdown.
const running = t => {
if (t >= shutdown[0] && t < shutdown[1]) return false;
const d = new Date(t);
const day = d.getUTCDay();
const hour = d.getUTCHours();
return (day >= 1 && day <= 5 && hour >= 6 && hour < 22) || (day === 6 && hour >= 6 && hour < 13);
};
const ambientAt = t => 11 - 7 * Math.cos(((t - Date.UTC(2026, 0, 15)) / (365 * DAY)) * 2 * Math.PI) + 4 * Math.sin(((t % DAY) / DAY) * 2 * Math.PI - 2);
const column = (step, f) => {
const n = Math.floor((end - start) / step) + 1;
const times = new Float64Array(n);
const values = new Float64Array(n);
let w = 0;
for (let k = 0; k < n; k++) {
const t = start + k * step;
if (t >= outage[0] && t < outage[1]) continue;
times[w] = t;
values[w] = f(t);
w += 1;
}
return { times: times.subarray(0, w), values: values.subarray(0, w) };
};
// Vibration creeps up as the bearing wears, then drops when it is replaced; now and then a knock.
const vibration = column(MIN, t => {
if (!running(t)) return 0.4 + 0.1 * noise();
const wear = t < replaced ? 1.4 + 3.2 * Math.pow((t - start) / (replaced - start), 3) : 1.5 + 0.6 * ((t - replaced) / (end - replaced));
const knock = rnd() < 0.0004 ? 3 + 4 * rnd() : 0;
return Math.max(0.2, wear + 0.25 * noise() + knock);
});
const power = column(MIN, t => (running(t) ? 182 + 14 * Math.sin((t % DAY) / 3_600_000) + 6 * noise() : 4 + noise()));
const temperature = column(5 * MIN, t => {
const base = ambientAt(t) + (running(t) ? 38 : 4);
const worn = t < replaced ? 9 * Math.pow((t - start) / (replaced - start), 4) : 0;
return base + worn + 0.6 * noise();
});
const ambient = column(15 * MIN, t => ambientAt(t) + 0.4 * noise());
el.panes = [
{ id: 'vibration', title: 'Vibration', weight: 1.3, min: 0 },
{ id: 'thermal', title: 'Temperature' },
{ id: 'power', title: 'Power', min: 0 },
];
el.series = [
{ id: 'vib', name: 'Vibration (RMS)', unit: 'mm/s', pane: 'vibration', decimals: 2, ...vibration },
{ id: 'bearing', name: 'Bearing temperature', unit: '°C', pane: 'thermal', decimals: 1, ...temperature },
{ id: 'ambient', name: 'Ambient', unit: '°C', pane: 'thermal', decimals: 1, ...ambient },
{ id: 'kw', name: 'Motor power', unit: 'kW', pane: 'power', kind: 'area', decimals: 0, ...power },
];
el.thresholds = [
{ pane: 'vibration', value: 4.5, label: 'Alert 4.5 mm/s', tone: 'warning', breach: 'above' },
{ pane: 'vibration', value: 7.1, label: 'Trip 7.1 mm/s', tone: 'danger', breach: 'above' },
{ pane: 'thermal', value: 70, to: 90, label: 'Bearing hot', tone: 'danger' },
];
el.events = [
{ time: shutdown[0], end: shutdown[1], label: 'Christmas shutdown', tone: 'info' },
{ time: replaced, end: replaced + 6 * 3_600_000, label: 'Bearing replaced', tone: 'success', description: 'Drive-end bearing changed after vibration passed the alert level.' },
{ time: outage[0], end: outage[1], label: 'Sensor gateway outage', tone: 'warning', description: 'No readings for seven and a half hours.' },
{ time: Date.UTC(2026, 7, 18, 10), label: 'Controller firmware 4.2', tone: 'info' },
];
el.addEventListener('bmxRangeChange', e => {
const days = (e.detail.end - e.detail.start) / DAY;
out.textContent = `Window: ${days >= 2 ? `${Math.round(days)} days` : `${Math.round(days * 24 * 10) / 10} hour${Math.round(days * 240) === 10 ? '' : 's'}`}${e.detail.follow ? ', following new data' : ''}.`;
});
el.addEventListener('bmxEventSelect', e => {
if (e.detail.event) out.textContent = `${e.detail.event.label}${e.detail.event.description ? `: ${e.detail.event.description}` : ''}`;
});
document.getElementById('ex-ts-bearing').addEventListener('click', () => el.setRange(replaced - 21 * DAY, replaced + 7 * DAY));
// Streaming: a reading a second, as if from the plant's gateway.
let timer = 0;
let clock = end;
const button = document.getElementById('ex-ts-stream');
button.addEventListener('click', async () => {
if (timer) {
clearInterval(timer);
timer = 0;
el.follow = false;
button.textContent = 'Start streaming';
return;
}
await el.setRange(clock - 3_600_000, clock);
el.follow = true;
button.textContent = 'Stop streaming';
timer = setInterval(() => {
clock += 10_000;
el.appendPoints('vib', [clock], [2 + 0.3 * noise() + (rnd() < 0.02 ? 3 : 0)]);
el.appendPoints('kw', [clock], [190 + 6 * noise()]);
}, 250);
});
</script>
WHAT IT DOES
- Series in stacked panes, each measured on a left or right axis, drawn as lines, areas, steps or points. Points too close together to tell apart are thinned to the first, least, greatest and last in each pixel column, found through a summary pyramid, so a million points redraw in a few milliseconds and every spike still shows.
- Move and zoom with the mouse, touch (pinch), the keyboard or the overview under the chart; presets for the last hour, day, week, month.
- Live: new points are appended and the window follows the latest, with
older points dropped past
max-points. - Thresholds as lines or bands, events as flags and shaded spans, anomalies (spikes far from the points around them) marked, and the same window a period, day, week or year earlier drawn behind for comparison.
- A cursor that reads every series at a time; figures for the window (least, greatest, mean, latest, change) as a table; PNG and CSV out.
Data stays in the page. Nothing is sent anywhere.
Properties
| Property | Attribute | Type | Default | Description |
|---|---|---|---|---|
anomalies |
anomalies |
boolean |
false |
Mark anomalies: points far from the points around them. |
anomalyThreshold |
anomaly-threshold |
number |
4 |
How many standard deviations from that mean is an anomaly. |
anomalyWindow |
anomaly-window |
number |
60 |
How many neighbouring points (half each side) an anomaly is judged against. |
compare |
compare |
BmxTsCompare |
'off' |
Draw the same window from earlier behind each series. |
decimation |
decimation |
'auto' | 'lttb' | 'none' |
'auto' |
How lines are thinned: auto (per pixel column, exact extremes), lttb (smoother) or none (every point). |
end |
end |
number | string |
— | The end of the window shown. Unset: the end of the data. |
events |
events |
Partial<BmxTsEvent>[] | string |
— | Moments and spans to mark: { time, end?, label, tone?, description? }. |
fileName |
file-name |
string |
'timeseries' |
The name exported files are saved under, without the extension. |
follow |
follow |
boolean |
false |
Keep the window's end at the newest point as data is appended. |
label |
label |
string |
— | The element's accessible name. |
locale |
locale |
string |
— | The language for numbers and dates (the page's when unset). |
maxPoints |
max-points |
number |
0 |
The most points each series keeps (oldest dropped first); 0 keeps everything. |
panes |
panes |
Partial<BmxTsPane>[] | string |
— | The panes, top to bottom (any a series names are added). |
presets |
presets |
string |
'1h,1d,7d,30d,all' |
The quick ranges offered, comma-separated: 15m, 1h, 1d, 7d, 3mo, 1y, all. |
series |
series |
BmxTsSeriesInput[] | string |
— | The series: definitions with their points (columns, typed arrays, pairs or objects), or JSON. |
showOverview |
show-overview |
boolean |
true |
Show the overview under the chart. |
showStats |
show-stats |
boolean |
true |
Show the figures table under the chart. |
start |
start |
number | string |
— | The start of the window shown (epoch ms or ISO). Unset: the start of the data. |
strings |
strings |
Partial<BmxTimeseriesStrings> | string |
— | Replacements for any of the wording (an object, or its JSON). |
thresholds |
thresholds |
Partial<BmxTsThreshold>[] | string |
— | Lines and bands at values: { value, to?, pane?, label?, tone?, axis? }. |
zone |
zone |
string |
— | The time zone times are shown in (an IANA name; the reader's own when unset). |
Events
| Event | Detail | Description |
|---|---|---|
bmxCursorMove |
BmxTsCursorDetail |
Fired when the cursor moves, with every series' reading. |
bmxEventSelect |
BmxTsEventDetail |
Fired when an event's flag is chosen (or the choice cleared). |
bmxRangeChange |
BmxTsRangeDetail |
Fired when the window moves or zooms. |
bmxSeriesToggle |
BmxTsToggleDetail |
Fired when a series is shown or hidden. |
Methods
| Method | Signature | Description |
|---|---|---|
appendPoints |
appendPoints(id: string, times: ArrayLike<number | string> | ArrayLike<unknown>, values?: ArrayLike<number | string | null>) => Promise<number> |
Adds points to a series: two columns, or [time, value] pairs. Returns how many were added. |
download |
download(kind?: "png" | "csv" | "csv-averaged") => Promise<void> |
Saves a picture or CSV. |
exportCsv |
exportCsv(options?: { start?: number; end?: number; interval?: number; aggregate?: BmxTsAggregate; all?: boolean; }) => Promise<string> |
The visible series as CSV: every point in the window, or averaged (or otherwise summed up) every interval ms. |
exportPng |
exportPng() => Promise<Blob | null> |
The chart as a PNG picture, with its title and legend. |
getAnomalies |
getAnomalies(id: string) => Promise<BmxTsSpan[]> |
A series' anomalies, with the element's settings. |
getPointCount |
getPointCount() => Promise<{ total: number; series: Record<string, number>; }> |
How many points each series holds, and in all. |
getRange |
getRange() => Promise<{ start: number; end: number; }> |
The window shown. |
getReadings |
getReadings(time: number | string) => Promise<BmxTsReading[]> |
Every visible series' reading nearest a time. |
getStats |
getStats(id: string, start?: number | string, end?: number | string) => Promise<BmxTsStats | null> |
Figures for a series over a range (the window when unset). |
resetZoom |
resetZoom() => Promise<void> |
Shows all the data. |
setCursor |
setCursor(time: number | string | null) => Promise<void> |
Puts the cursor at a time, or takes it away with null. |
setRange |
setRange(start: number | string, end: number | string) => Promise<void> |
Shows a window (epoch ms or ISO). |
setSeriesData |
setSeriesData(id: string, times: ArrayLike<number | string>, values: ArrayLike<number | string | null>) => Promise<void> |
Replaces a series' points. |
zoom |
zoom(factor: number, around?: number) => Promise<void> |
Zooms by a factor (2 shows half as much) around a time (the middle when unset). |
CSS shadow parts
| Part | Description |
|---|---|
legend |
The series list. |
overview |
The overview and its window. |
plot |
The chart. |
stats |
The figures table. |
toolbar |
The tools along the top. |
CSS custom properties
| Property | Description |
|---|---|
--bmx-timeseries-color-0 |
The first series, unless it names its own colour. Colours 0 to 9 are used in turn. |
--bmx-timeseries-color-1 |
The second series. |
--bmx-timeseries-color-2 |
The third series. |
--bmx-timeseries-color-3 |
The fourth series. |
--bmx-timeseries-color-4 |
The fifth series. |
--bmx-timeseries-color-5 |
The sixth series. |
--bmx-timeseries-color-6 |
The seventh series. |
--bmx-timeseries-color-7 |
The eighth series. |
--bmx-timeseries-color-8 |
The ninth series. |
--bmx-timeseries-color-9 |
The tenth series. |
--bmx-timeseries-cursor |
The cursor line. |
--bmx-timeseries-grid |
Grid lines and frames. |
--bmx-timeseries-height |
The chart's height (all panes and the time axis). |
--bmx-timeseries-overview-height |
The overview's height. |
--bmx-timeseries-radius |
Corner radius of the frame. |