Files
Aura/crates/aura-cli/tests/chart_viewer_build.mjs
T
Brummel 15ee6d5f4f feat(trace-charts): serve persisted traces as a web chart (iter 2)
Iteration 2 (serve half) of the visual face — spec 0056 / plan 0056:
read a persisted run's traces and serve them as a self-contained uPlot
HTML chart. Completes the goal "tap data from a graph, serve it as a
chart in the browser".

- chart-viewer.js (first-party): a pure buildCharts(traces, mode) that
  maps the injected window.AURA_TRACES to uPlot configs (no DOM) — overlay
  = one plot, every series on its own y-scale over the shared xs; panels =
  one plot per series, all on the same timestamp x. A browser-only mount()
  instantiates the vendored global uPlot. Guarded by a node .mjs DOM test
  (+ a sparse/gaps variant) shelling out exactly like viewer_dot.rs.
- render_chart_html + ChartMode/ChartData/Series (aura-cli/render.rs):
  mirrors render_html — self-contained page, uPlot (1.6.32, vendored
  d534f10) + chart-viewer.js inlined via include_str!, data injected as
  window.AURA_TRACES.
- emit_chart + build_chart_data (aura-cli/main.rs): the spec-§6 3-step
  union-spine alignment — xs = sorted-dedup union of every tap's ts; a
  synthetic empty-payload spine + ALL taps as symmetric join_on_ts sides
  (no tap privileged, no point dropped); flatten each (tap,column) to a
  Series of Option<f64>. New arms `aura chart <name> [--panels]`; a
  missing run is stderr + exit 2 (TraceStoreError::NotFound), never a
  panic. USAGE advertises the verb in lockstep.

Verified by the orchestrator: `cargo test --workspace` green (incl. the
node guards, render_chart_html, and the chart e2e); `cargo clippy
--workspace --all-targets -D warnings` exit 0; and a real end-to-end —
`aura run --trace demo && aura chart demo` emits a 57KB self-contained
page (doctype + inlined uPlot + AURA_TRACES carrying the equity/exposure
series), `--panels` reflects the panels mode, `aura chart nope` exits 2.
Implementer deviation folded in (compiler-prescribed): serde added to
aura-cli/Cargo.toml for the Series derive (Cargo.lock updated).

refs #101
2026-06-19 00:52:45 +02:00

50 lines
2.5 KiB
JavaScript

// Headless guard for the trace-chart viewer (chart-viewer.js). Property protected:
// buildCharts maps an AURA_TRACES payload to uPlot configs WITHOUT a DOM — overlay
// is one chart with every series on its own y-scale over the shared xs; panels is
// one chart per series, each on the SAME shared xs (timestamp-aligned). It loads the
// real viewer module (a fix/regression there is observed here) and drives the
// exported pure `buildCharts`; uPlot itself (which needs `document`) is never
// instantiated — mount() is browser-only and not called here.
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", "sample-traces.json"), "utf8"));
const fail = (msg) => { console.error("FAIL: " + msg); process.exit(1); };
const eq = (a, b, msg) => { if (a !== b) fail(`${msg} (got ${a}, want ${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 -> chart-viewer.js's browser mount is skipped; require gives
// us only the pure export (mirrors viewer_dot_ids.mjs stubbing window before require).
global.window = { AURA_TRACES: traces };
const { buildCharts } = require(join(here, "..", "assets", "chart-viewer.js"));
const n = traces.series.length;
// overlay: ONE chart, all series superimposed on the shared xs, each on its own scale.
const overlay = buildCharts(traces, "overlay");
eq(overlay.length, 1, "overlay is one chart");
eq(overlay[0].data.length, 1 + n, "overlay data = xs + N series");
eq(overlay[0].opts.series.length, 1 + n, "overlay uPlot series = x + N");
sameArr(overlay[0].data[0], traces.xs, "overlay x-axis is the shared xs");
const scaleIds = overlay[0].opts.series.slice(1).map((s) => s.scale);
eq(new Set(scaleIds).size, n, "each overlay series has a distinct y-scale");
// panels: ONE chart per series, all timestamp-aligned on the SAME shared xs.
const panels = buildCharts(traces, "panels");
eq(panels.length, n, "panels = one chart per series");
for (const p of panels) {
eq(p.data.length, 2, "each panel = xs + its one series");
sameArr(p.data[0], traces.xs, "panel shares the same xs (timestamp-aligned)");
}
console.log(`OK — overlay 1 chart / ${n} series, panels ${n} charts, shared xs.`);
process.exit(0);