From 08a272c73df81a1256b8d0258d21ad3eed9622d9 Mon Sep 17 00:00:00 2001 From: Brummel Date: Fri, 19 Jun 2026 23:15:39 +0200 Subject: [PATCH] fix(trace-charts): drop glitchy custom wheel/pan, restore uPlot default mouse control MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- crates/aura-cli/assets/chart-viewer.js | 46 +++----------------- crates/aura-cli/tests/chart_viewer_xmode.mjs | 15 ++++--- 2 files changed, 14 insertions(+), 47 deletions(-) diff --git a/crates/aura-cli/assets/chart-viewer.js b/crates/aura-cli/assets/chart-viewer.js index f619745..b949466 100644 --- a/crates/aura-cli/assets/chart-viewer.js +++ b/crates/aura-cli/assets/chart-viewer.js @@ -6,49 +6,14 @@ (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. + // 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; @@ -77,8 +42,7 @@ 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()], + cursor: { sync: { key: SYNC } }, }, data: [xData, s.points], }; @@ -97,7 +61,7 @@ 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()] }, + opts: { scales: scales, series: uSeries, axes: axes }, data: [xData].concat(traces.series.map(function (s) { return s.points; })), }]; } diff --git a/crates/aura-cli/tests/chart_viewer_xmode.mjs b/crates/aura-cli/tests/chart_viewer_xmode.mjs index a220f4b..0ffdc6f 100644 --- a/crates/aura-cli/tests/chart_viewer_xmode.mjs +++ b/crates/aura-cli/tests/chart_viewer_xmode.mjs @@ -2,8 +2,9 @@ // Properties: (1) the DEFAULT x spine is ORDINAL — data[0] is [0..n-1], not the raw // timestamps, so non-trading gaps collapse; the x-axis carries an index->label values // formatter. (2) "continuous" mode restores data[0] === xs verbatim. (3) panels carry a -// shared cursor.sync.key so the crosshair lines up. (4) every config attaches an -// interaction plugin with an init hook. Drives the pure buildCharts; no DOM/uPlot. +// shared cursor.sync.key so the crosshair lines up. (4) no custom interaction plugin and +// no cursor.drag override — uPlot's default drag-box-zoom / dblclick-reset governs the +// mouse (the hand-rolled wheel/pan was glitchy). Drives the pure buildCharts; no DOM/uPlot. import { createRequire } from "node:module"; import { readFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; @@ -29,9 +30,10 @@ const ov = buildCharts(traces, "overlay"); sameArr(ov[0].data[0], idx, "default overlay x is the ordinal index spine [0..n-1]"); if (typeof ov[0].opts.axes[0].values !== "function") fail("ordinal overlay x-axis lacks a values formatter"); -// (4) interaction wired: a plugin with an init hook. -if (!Array.isArray(ov[0].opts.plugins) || ov[0].opts.plugins.length < 1) fail("no interaction plugin attached"); -if (typeof ov[0].opts.plugins[0].hooks.init !== "function") fail("interaction plugin has no init hook"); +// (4) interaction is uPlot's default: overlay carries no custom plugin and no +// cursor.drag override (so drag = box-zoom, dblclick = reset, both uPlot built-ins). +if (ov[0].opts.plugins && ov[0].opts.plugins.length) fail("overlay should carry no custom interaction plugin (uPlot defaults)"); +if (ov[0].opts.cursor && ov[0].opts.cursor.drag) fail("overlay should not override cursor.drag (uPlot default box-zoom)"); // (2) continuous mode: data[0] is xs verbatim. const ovc = buildCharts(traces, "overlay", "continuous"); @@ -42,7 +44,8 @@ const panels = buildCharts(traces, "panels"); const keys = panels.map((p) => p.opts.cursor && p.opts.cursor.sync && p.opts.cursor.sync.key); if (keys.some((k) => !k)) fail("a panel lacks cursor.sync.key"); if (new Set(keys).size !== 1) fail("panels do not share one cursor.sync key"); +if (panels.some((p) => p.opts.plugins && p.opts.plugins.length)) fail("panels should carry no custom interaction plugin (uPlot defaults)"); sameArr(panels[0].data[0], idx, "default panel x is the ordinal index spine"); -console.log("OK — ordinal default + continuous toggle + panel cursor-sync + interaction plugin."); +console.log("OK — ordinal default + continuous toggle + panel cursor-sync + uPlot-default controls."); process.exit(0);