feat: trace-chart viewer comfort (Tier 1) — collapse-gaps x-axis (optional continuous), zoom/pan/cursor-sync #103

Closed
opened 2026-06-19 18:09:02 +02:00 by Brummel · 1 comment
Owner

Summary

Follow-on to #101 (trace charts, first cut). The shipped viewer is the deliberately-naive
cut: static line / overlay / panels on uPlot defaults. This is the "Tier 1" comfort pass
named in the web-viewer effort estimate — the cheap, high-leverage jump that makes the
served page feel like a chart rather than a screenshot — plus the one explicitly-requested
data-model change: the x-axis should not be continuous by default.

The whole feature lives in crates/aura-cli/assets/chart-viewer.js (pure buildCharts +
browser-only mount) and a little page chrome in crates/aura-cli/src/render.rs
(CHART_HEAD / CSS). The Rust serve contract (build_chart_data, render_chart_html,
ColumnarTrace / TraceStore) is unchanged: window.AURA_TRACES already carries xs
(real timestamps) + per-series points; the new behaviour is purely how the viewer maps them.

X-axis: collapse gaps by default, continuous optional

Today the x-axis plots against the real union timestamps (scales.x.time=false,
data[0]=xs), so non-trading time (nights, weekends) shows as proportional horizontal
blanks. Requested: the x-axis should not be continuous (or only optionally).

Decision (derived, recorded here): add an ordinal x-mode and make it the default;
keep continuous as a one-click toggle in the page.

  • ordinal: data[0] = [0,1,…,n-1] (every slot evenly spaced → gaps collapse); the x-axis
    tick values and the legend x-readout map the index back to the real xs[i] value.
  • continuous: data[0] = xs (today's behaviour), reachable via an in-page toggle.

Rationale for an in-page toggle over a CLI flag: the broader goal is a more comfortable
viewer, and a live toggle lets you compare both spacings without regenerating the page; it
also keeps the injected payload and the whole Rust side untouched (the choice is client-side).
Default ordinal honours the primary clause ("should not be continuous"); continuous is the
"(or only optional)" fallback.

Pretty date/time axis labels (epoch→HH:MM, session-aware ticks) are deliberately out of
scope
here — they need the timestamp unit / instrument context (manifest / Aura.toml),
which is the #102 / Tier-3 "session-aware axis" strand. Ordinal mode here shows the raw
xs[i] on hover; no epoch assumption (the synthetic demos use small-integer timestamps).

Tier 1 interaction comfort

  • wheel-zoom + drag-pan on the x-axis (small uPlot over-wheel handler; not in uPlot core).
  • drag box-zoom + double-click reset (uPlot built-in — verify wired).
  • value readout at cursor (uPlot legend; x-readout shows the real xs[i]).
  • cursor sync across panels — a shared cursor.sync key so the vertical crosshair lines
    up across stacked panels (panels mode).

Acceptance

  • the default served page opens with an ordinal (gap-collapsed) x-axis; a visible toggle
    flips to continuous and back, both modes rendering the same series.
  • in ordinal mode the crosshair / legend still reports the real timestamp for the hovered slot.
  • wheel scrolls zoom the x-axis; drag pans; double-click restores the full range.
  • in panels mode the crosshair is synchronized across panels.
  • buildCharts(traces, mode, xMode) stays pure + headless-guardable: a node guard asserts
    ordinal data[0] is the index array with an x values formatter, continuous data[0] is
    xs, and null gaps are still preserved verbatim (the existing gap guard must stay green).
  • the self-contained-page invariant holds (no remote <script src>; uPlot + data inlined).

Lineage: #101 (first cut), C22 (playground = trace explorer), the Tier-1 row of the web-viewer
effort estimate. Not the deferred families/comparison (C21) or the aura serve backend.

## Summary Follow-on to #101 (trace charts, first cut). The shipped viewer is the deliberately-naive cut: static line / overlay / panels on uPlot defaults. This is the "Tier 1" comfort pass named in the web-viewer effort estimate — the cheap, high-leverage jump that makes the served page *feel* like a chart rather than a screenshot — plus the one explicitly-requested data-model change: **the x-axis should not be continuous by default**. The whole feature lives in `crates/aura-cli/assets/chart-viewer.js` (pure `buildCharts` + browser-only `mount`) and a little page chrome in `crates/aura-cli/src/render.rs` (`CHART_HEAD` / CSS). The Rust serve contract (`build_chart_data`, `render_chart_html`, `ColumnarTrace` / `TraceStore`) is **unchanged**: `window.AURA_TRACES` already carries `xs` (real timestamps) + per-series points; the new behaviour is purely how the viewer maps them. ## X-axis: collapse gaps by default, continuous optional Today the x-axis plots against the real union timestamps (`scales.x.time=false`, `data[0]=xs`), so non-trading time (nights, weekends) shows as proportional horizontal blanks. Requested: the x-axis should **not** be continuous (or only optionally). **Decision (derived, recorded here):** add an **ordinal** x-mode and make it the **default**; keep **continuous** as a one-click toggle in the page. - ordinal: `data[0] = [0,1,…,n-1]` (every slot evenly spaced → gaps collapse); the x-axis tick `values` and the legend x-readout map the index back to the real `xs[i]` value. - continuous: `data[0] = xs` (today's behaviour), reachable via an in-page toggle. Rationale for an **in-page toggle** over a CLI flag: the broader goal is a *more comfortable* viewer, and a live toggle lets you compare both spacings without regenerating the page; it also keeps the injected payload and the whole Rust side untouched (the choice is client-side). Default ordinal honours the primary clause ("should not be continuous"); continuous is the "(or only optional)" fallback. Pretty date/time axis labels (epoch→`HH:MM`, session-aware ticks) are deliberately **out of scope** here — they need the timestamp unit / instrument context (manifest / `Aura.toml`), which is the #102 / Tier-3 "session-aware axis" strand. Ordinal mode here shows the raw `xs[i]` on hover; no epoch assumption (the synthetic demos use small-integer timestamps). ## Tier 1 interaction comfort - **wheel-zoom + drag-pan** on the x-axis (small uPlot `over`-wheel handler; not in uPlot core). - **drag box-zoom** + **double-click reset** (uPlot built-in — verify wired). - **value readout at cursor** (uPlot legend; x-readout shows the real `xs[i]`). - **cursor sync across panels** — a shared `cursor.sync` key so the vertical crosshair lines up across stacked panels (panels mode). ## Acceptance - the default served page opens with an **ordinal** (gap-collapsed) x-axis; a visible toggle flips to continuous and back, both modes rendering the same series. - in ordinal mode the crosshair / legend still reports the real timestamp for the hovered slot. - wheel scrolls zoom the x-axis; drag pans; double-click restores the full range. - in panels mode the crosshair is synchronized across panels. - `buildCharts(traces, mode, xMode)` stays pure + headless-guardable: a node guard asserts ordinal `data[0]` is the index array with an x `values` formatter, continuous `data[0]` is `xs`, and null gaps are still preserved verbatim (the existing gap guard must stay green). - the self-contained-page invariant holds (no remote `<script src>`; uPlot + data inlined). Lineage: #101 (first cut), C22 (playground = trace explorer), the Tier-1 row of the web-viewer effort estimate. Not the deferred families/comparison (C21) or the `aura serve` backend.
Brummel added the feature label 2026-06-19 18:09:02 +02:00
Author
Owner

Design decisions (specify, spec 0057) — derived forks

Recording the load-bearing forks this spec decides, beyond the ordinal/continuous
default already in the issue body. All are derived calls (rationale below), not user
decisions — vetoable.

  • Fork: interaction gestures (drag vs box-zoom). The body listed both "drag box-zoom"
    and "drag-pan" — contradictory, since drag is one gesture. → Resolved: drag = pan,
    wheel = zoom (x, cursor-anchored), double-click = reset.
    uPlot's default drag-box-zoom
    is overridden in favour of drag-pan. Basis (derived): the acceptance already says "drag
    pans" / "double-click restores the full range", and drag-pan is the TradingView-comfort
    norm this Tier-1 pass targets; box-zoom is the less idiomatic gesture and is dropped
    rather than bound to a modifier (keep the naive viewer simple).

  • Fork: ordinal-mode axis/readout label.Resolved: show the raw xs[i] timestamp
    value, no epoch→date formatting.
    Basis (derived): the synthetic demos use small-integer
    timestamps, so new Date(x) would render 1970 nonsense; honest date formatting needs the
    timestamp unit / instrument context (manifest / Aura.toml), which is the #102 / Tier-3
    "session-aware axis" strand, explicitly out of this cut's scope.

  • Fork: toggle granularity.Resolved: one global x-mode toggle re-renders all charts
    (overlay's single chart, or every panel) in the same mode.
    Basis (derived): a per-chart
    toggle would let panels drift out of x-alignment, contradicting the timestamp-aligned-panels
    contract from #101; global keeps panels coherent and the control simple.

  • Fork: cursor-sync scope.Resolved: panels share a uPlot cursor.sync key so the
    crosshair lines up across stacked panels; overlay is a single chart and needs none.
    Basis
    (derived): sync is only meaningful across multiple charts (panels mode); overlay already
    superimposes on one chart.

## Design decisions (specify, spec 0057) — derived forks Recording the load-bearing forks this spec decides, beyond the ordinal/continuous default already in the issue body. All are **derived** calls (rationale below), not user decisions — vetoable. - **Fork: interaction gestures (drag vs box-zoom).** The body listed both "drag box-zoom" and "drag-pan" — contradictory, since drag is one gesture. → **Resolved: drag = pan, wheel = zoom (x, cursor-anchored), double-click = reset.** uPlot's default drag-box-zoom is overridden in favour of drag-pan. Basis (derived): the acceptance already says "drag pans" / "double-click restores the full range", and drag-pan is the TradingView-comfort norm this Tier-1 pass targets; box-zoom is the less idiomatic gesture and is dropped rather than bound to a modifier (keep the naive viewer simple). - **Fork: ordinal-mode axis/readout label.** → **Resolved: show the raw `xs[i]` timestamp value, no epoch→date formatting.** Basis (derived): the synthetic demos use small-integer timestamps, so `new Date(x)` would render 1970 nonsense; honest date formatting needs the timestamp unit / instrument context (manifest / `Aura.toml`), which is the #102 / Tier-3 "session-aware axis" strand, explicitly out of this cut's scope. - **Fork: toggle granularity.** → **Resolved: one global x-mode toggle re-renders all charts (overlay's single chart, or every panel) in the same mode.** Basis (derived): a per-chart toggle would let panels drift out of x-alignment, contradicting the timestamp-aligned-panels contract from #101; global keeps panels coherent and the control simple. - **Fork: cursor-sync scope.** → **Resolved: panels share a uPlot `cursor.sync` key so the crosshair lines up across stacked panels; overlay is a single chart and needs none.** Basis (derived): sync is only meaningful across multiple charts (panels mode); overlay already superimposes on one chart.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Brummel/Aura#103