v2.6.0

<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

PropertyAttributeTypeDefaultDescription
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

EventDetailDescription
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

MethodSignatureDescription
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

PartDescription
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

PropertyDescription
--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.