45cc1c44c9
The served trace chart's x-axis no longer plots against raw timestamps by default — non-trading gaps (nights, weekends) left horizontal blanks that dominated the view. The x spine is now ordinal by default: every recorded slot sits at an even position, so gaps collapse and the signal fills the width. A header toggle flips to continuous (real-time-proportional) spacing and back; default ordinal honours the requested "x-axis should not be continuous", with continuous as the one-click option. Comfort, all on the read/render side (the engine and the Rust serve contract are untouched — window.AURA_TRACES still carries the real xs, the ordinal indices are derived client-side): - ordinal mode keeps the axis honest: ticks and the legend value-readout map a collapsed index back to the real xs[i], never the index itself. - wheel-zoom (x, cursor-anchored), drag-pan, double-click-reset on the plot (uPlot's default drag-box-zoom is disabled so drag pans). - panels share a cursor.sync key, so the crosshair lines up across stacked panels. Confined to chart-viewer.js (buildCharts gains an xMode param + a wheelZoomPlugin factory; mount re-renders on the toggle) and render.rs (CHART_HEAD toggle button + a chart-only style). The two twin node guards (build/gaps) re-point to the ordinal default in lockstep; a new x-mode guard pins ordinal-default / continuous / panel-sync / plugin wiring, and a label-honesty guard (gappy-traces fixture, xs far from the index spine) pins that ordinal ticks report the real timestamp. Verified: cargo test --workspace green (incl. the four chart guards + render inline tests), clippy --all-targets -D warnings clean, e2e (aura run --trace + aura chart) emits a self-contained page defaulting to ordinal with the toggle. Spec docs/specs/0057, plan docs/plans/0057 (boss-signed; grounding-check PASS). Derived-fork decision log in the issue comments. closes #103
61 lines
3.5 KiB
JavaScript
61 lines
3.5 KiB
JavaScript
// Headless guard for the trace-chart viewer (chart-viewer.js) ordinal-label honesty.
|
|
//
|
|
// Property protected: the ORDINAL x spine collapses gaps to indices [0..n-1], but the
|
|
// axis tick labels and the legend value-readout must still report the REAL timestamp
|
|
// xs[i] for each collapsed index — not the index itself. The gap-collapse is only
|
|
// honest if the axis tells the truth about which timestamp a collapsed position is.
|
|
// A regression that returned the index (String(pos)) instead of xs[round(pos)] would
|
|
// still pass "the formatter is a function" but silently mislabel every tick after a
|
|
// gap. Continuous mode (no collapse) must carry NO index->ts formatter, so its tick is
|
|
// the raw timestamp, and its legend echoes the timestamp verbatim.
|
|
//
|
|
// The fixture's timestamps [100, 200, 9000] diverge sharply from the index spine
|
|
// [0, 1, 2] across a large non-trading gap, so an index-vs-timestamp confusion fails
|
|
// loudly. Drives the exported pure buildCharts; uPlot/DOM never instantiated.
|
|
|
|
import { createRequire } from "node:module";
|
|
import { readFileSync } from "node:fs";
|
|
import { fileURLToPath } from "node:url";
|
|
import { dirname, join } from "node:path";
|
|
|
|
const require = createRequire(import.meta.url);
|
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
const traces = JSON.parse(readFileSync(join(here, "fixtures", "gappy-traces.json"), "utf8"));
|
|
|
|
const fail = (msg) => { console.error("FAIL: " + msg); process.exit(1); };
|
|
const eq = (a, b, msg) => { if (a !== b) fail(`${msg} (got ${JSON.stringify(a)}, want ${JSON.stringify(b)})`); };
|
|
const sameArr = (a, b, msg) => {
|
|
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length || a.some((v, i) => v !== b[i])) fail(msg);
|
|
};
|
|
|
|
// No `document` global -> the browser mount is skipped; we get only the pure export.
|
|
global.window = { AURA_TRACES: traces };
|
|
const { buildCharts } = require(join(here, "..", "assets", "chart-viewer.js"));
|
|
|
|
const xs = traces.xs; // [100, 200, 9000]
|
|
const idx = xs.map((_, i) => i); // [0, 1, 2]
|
|
|
|
// ordinal (default): the x AXIS formatter maps each index split back to its real ts.
|
|
const ov = buildCharts(traces, "overlay")[0];
|
|
const axisValues = ov.opts.axes[0].values;
|
|
if (typeof axisValues !== "function") fail("ordinal x-axis lacks a values formatter");
|
|
const labels = axisValues(null, idx); // uPlot passes the tick positions (indices)
|
|
sameArr(labels, xs.map(String), "ordinal axis must label each index with its real xs[i], not the index");
|
|
|
|
// ordinal: the LEGEND x value-readout maps a cursor index back to its real ts; a null
|
|
// cursor reads as the gap placeholder "--", never as a coerced number.
|
|
const legend = ov.opts.series[0].value;
|
|
if (typeof legend !== "function") fail("ordinal x series lacks a value-readout formatter");
|
|
eq(legend(null, 0), String(xs[0]), "legend readout at index 0 must be the real ts xs[0]");
|
|
eq(legend(null, 2), String(xs[2]), "legend readout at index 2 must be the real ts xs[2] (across the gap)");
|
|
eq(legend(null, null), "--", "legend readout at a null cursor must be the gap placeholder --");
|
|
|
|
// continuous: NO index->ts formatter on the axis (uPlot's default tick = the raw ts),
|
|
// and the legend echoes the timestamp verbatim.
|
|
const ovc = buildCharts(traces, "overlay", "continuous")[0];
|
|
eq(typeof ovc.opts.axes[0].values, "undefined", "continuous axis must carry no index->ts formatter");
|
|
eq(ovc.opts.series[0].value(null, xs[2]), String(xs[2]), "continuous legend echoes the raw timestamp");
|
|
|
|
console.log("OK — ordinal labels report real timestamps (not indices); continuous echoes raw ts.");
|
|
process.exit(0);
|