// The `aura chart` viewer: turns the injected window.AURA_TRACES (xs + per-series // Option points, already timestamp-union-aligned by the CLI) into uPlot charts. // // buildCharts is PURE (no DOM, no uPlot) and is the unit the headless guard drives. // mount() is browser-only — it instantiates the vendored global `uPlot` per config. (function () { var PALETTE = ["#89b4fa", "#f38ba8", "#a6e3a1", "#f9e2af", "#cba6f7", "#94e2d5"]; // wheelZoomPlugin: a pure factory (safe to construct headless — its hooks run only in // a browser). On init it wires wheel-zoom (x, cursor-anchored), drag-pan, and // dblclick-reset onto u.over. uPlot's default drag-box-zoom is disabled in buildCharts // (cursor.drag off) so drag pans instead. function wheelZoomPlugin() { var ZOOM = 0.9; // one wheel notch shrinks/grows the visible x-range by this factor return { hooks: { init: function (u) { var over = u.over; over.addEventListener("wheel", function (e) { e.preventDefault(); var rect = over.getBoundingClientRect(); var xVal = u.posToVal(e.clientX - rect.left, "x"); var lo = u.scales.x.min, hi = u.scales.x.max; var f = e.deltaY < 0 ? ZOOM : 1 / ZOOM; // wheel up = zoom in u.setScale("x", { min: xVal - (xVal - lo) * f, max: xVal + (hi - xVal) * f }); }, { passive: false }); over.addEventListener("mousedown", function (d) { if (d.button !== 0) return; var x0 = d.clientX, lo = u.scales.x.min, hi = u.scales.x.max; var unitsPerPx = (hi - lo) / over.clientWidth; function move(m) { var dx = (m.clientX - x0) * unitsPerPx; u.setScale("x", { min: lo - dx, max: hi - dx }); } function up() { document.removeEventListener("mousemove", move); document.removeEventListener("mouseup", up); } document.addEventListener("mousemove", move); document.addEventListener("mouseup", up); }); over.addEventListener("dblclick", function () { u.setScale("x", { min: u.data[0][0], max: u.data[0][u.data[0].length - 1] }); }); } } }; } // {xs, series:[{name, y_scale_id, points}]} + mode + xMode -> array of {opts, data}. // xMode in {"ordinal","continuous"}, default "ordinal". ordinal: data[0] is the index // spine [0..n-1] (gaps collapse) and the x-axis/legend map an index back to the real // xs[i]; continuous: data[0] is xs. series points (data[1..]) are untouched either way // (null gaps preserved verbatim). overlay -> one chart; panels -> one chart per series, // sharing a cursor.sync key so the crosshair lines up. function buildCharts(traces, mode, xMode) { xMode = xMode || "ordinal"; var xs = traces.xs; var n = xs.length; var idx = []; for (var k = 0; k < n; k++) idx.push(k); var ordinal = xMode !== "continuous"; var xData = ordinal ? idx : xs; // map an x-position to its real-timestamp label (total; no epoch assumption): // ordinal -> position is an index -> xs[round(pos)]; continuous -> position IS the ts. function xLabel(pos) { if (!ordinal) return String(pos); var j = Math.max(0, Math.min(n - 1, Math.round(pos))); return String(xs[j]); } function xAxisCfg() { return ordinal ? { values: function (u, splits) { return splits.map(xLabel); } } : {}; } function xSeriesCfg() { return { value: function (u, v) { return v == null ? "--" : xLabel(v); } }; } if (mode === "panels") { var SYNC = "aura-x"; // shared key -> crosshair sync across panels return traces.series.map(function (s, i) { return { opts: { title: s.name, scales: { x: { time: false }, [s.y_scale_id]: {} }, series: [xSeriesCfg(), { label: s.name, scale: s.y_scale_id, stroke: PALETTE[i % PALETTE.length] }], axes: [xAxisCfg(), { scale: s.y_scale_id }], cursor: { sync: { key: SYNC }, drag: { x: false, y: false } }, plugins: [wheelZoomPlugin()], }, data: [xData, s.points], }; }); } // overlay (default) var scales = { x: { time: false } }; var uSeries = [xSeriesCfg()]; var axes = [xAxisCfg()]; traces.series.forEach(function (s, i) { scales[s.y_scale_id] = {}; uSeries.push({ label: s.name, scale: s.y_scale_id, stroke: PALETTE[i % PALETTE.length] }); // the first two series carry a drawn axis (left, then right); the rest keep their // own auto-scale, read via the legend. if (i < 2) axes.push({ scale: s.y_scale_id, side: i === 0 ? 3 : 1 }); }); return [{ opts: { scales: scales, series: uSeries, axes: axes, cursor: { drag: { x: false, y: false } }, plugins: [wheelZoomPlugin()] }, data: [xData].concat(traces.series.map(function (s) { return s.points; })), }]; } // Browser-only: instantiate the vendored global uPlot per config into #stage, and wire // the header #xmode-toggle to flip the x spine (ordinal <-> continuous) and re-render. function mount() { var traces = window.AURA_TRACES; var stage = document.getElementById("stage"); var toggle = document.getElementById("xmode-toggle"); var xMode = "ordinal"; var insts = []; // The ordinal ("collapsed") label is authored once, server-side, as the button's // initial text (render.rs CHART_HEAD) — read it back rather than re-hardcoding the // same string here, so the served page carries that label exactly once. var labelOrdinal = toggle ? toggle.textContent : ""; var labelContinuous = "x: continuous (real time)"; function render() { insts.forEach(function (u) { u.destroy(); }); insts = []; stage.innerHTML = ""; var configs = buildCharts(traces, traces.mode, xMode); var w = stage.clientWidth || 900; var h = configs.length > 1 ? Math.max(160, Math.floor(440 / configs.length)) : 460; configs.forEach(function (cfg) { // eslint-disable-next-line no-undef insts.push(new uPlot(Object.assign({ width: w, height: h }, cfg.opts), cfg.data, stage)); }); if (toggle) toggle.textContent = xMode === "ordinal" ? labelOrdinal : labelContinuous; } if (toggle) toggle.addEventListener("click", function () { xMode = xMode === "ordinal" ? "continuous" : "ordinal"; render(); }); render(); } if (typeof window !== "undefined" && typeof document !== "undefined" && window.AURA_TRACES) { if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", mount); else mount(); } if (typeof module !== "undefined" && module.exports) module.exports = { buildCharts: buildCharts }; })();