Files
Aura/crates/aura-cli/assets/chart-viewer.js
T
Brummel 08a272c73d fix(trace-charts): drop glitchy custom wheel/pan, restore uPlot default mouse control
The hand-rolled wheel-zoom + drag-pan (wheelZoomPlugin) was hakelig/glitchy —
uPlot is not built to be driven that way. Remove the plugin and the cursor.drag
override so uPlot's own defaults govern the mouse: drag = box-zoom on x,
double-click = reset to full range, with the legend value-readout and crosshair
unchanged.

Everything else from the Tier-1 pass stays: the ordinal (gap-collapsed) x-axis
default + continuous toggle, the ordinal-label honesty (ticks/legend report the
real xs[i]), and panel cursor-sync (cursor.sync key retained; only the drag
override and the plugin are dropped). The x-mode guard is flipped to pin the
revert — overlay carries no custom plugin and no cursor.drag override, panels
carry no plugin — and stays green alongside the other three chart guards.

refs #103
2026-06-19 23:15:39 +02:00

108 lines
5.0 KiB
JavaScript

// The `aura chart` viewer: turns the injected window.AURA_TRACES (xs + per-series
// Option<f64> 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"];
// {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. Mouse interaction is uPlot's
// default (drag = box-zoom on x, double-click = reset); we add no custom wheel/pan
// (the hand-rolled version was glitchy — uPlot is not built for it).
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 } },
},
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 },
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 };
})();