Skip to content

viewer: Lit chart spike + CSS tokens + charts.css extraction#913

Merged
thinkingfish merged 6 commits into
mainfrom
claude/embed-dashboards-webcomponents-3rwYx
May 12, 2026
Merged

viewer: Lit chart spike + CSS tokens + charts.css extraction#913
thinkingfish merged 6 commits into
mainfrom
claude/embed-dashboards-webcomponents-3rwYx

Conversation

@thinkingfish
Copy link
Copy Markdown
Member

@thinkingfish thinkingfish commented May 12, 2026

Summary

Four commits, each a contained step toward embed-friendly charts:

  1. docs/TODOs.md — sketches the path to embed-friendly Rezolus charts (web components consuming Plot/View JSON descriptors from crates/dashboard/, with pluggable WASM and Datastar/SSE data adapters).

  2. <rezolus-chart> spike — vendors lit@3.2.1 (16 KB self-contained ESM) at src/viewer/assets/lib/lit/ and adds a minimal Lit custom element at src/viewer/assets/lib/embed/rezolus-chart.js accepting the existing Plot descriptor shape with inline series. Demo at /lib/embed/demo.html. No build step.

  3. Promote chart palette to CSS tokens — moves three palettes from colormap.js JS constants into style.css :root:

    • --chart-blue--chart-purple (10 series accent colors)
    • --chart-1--chart-10 (d3 schemeCategory10 cycling palette)
    • --cgroup-1--cgroup-16 (hash-indexed cgroup palette)

    colormap.js reads them via getComputedStyle (with hex fallbacks for robustness). Also fixes a latent bug where style.css:993 referenced var(--chart-cyan) but the token was never declared. Embedders can now override any palette slot via CSS without touching JS.

  4. Extract chart CSS into charts.css — moves 1136 lines from style.css into a new lib/charts.css, loaded immediately after it from both viewer entry points (src/viewer/assets/index.html and site/viewer/index.html). Symlinks added under site/viewer/lib/ for charts.css, lit/, and embed/. Sections moved: Chart Groups, Chart Cards, Single Chart View, Cgroups Section, CPU Topology Diagram, Compare mode (3 sub-sections). A handful of intentionally borderline rules (selection-card chart-wrapper overrides, compare-badge :has() rules in TopNav, mobile @media overrides) stay in style.css where extracting them would fragment a coherent block.

Why this shape

The work deliberately stops at "Lit component + inline data + Shadow DOM + tokens for theming." It validates the descriptor → component pipeline, gets the design tokens (including chart palette) onto a clean CSS contract embedders can override, and separates chart styling into a focused stylesheet that future embed scenarios can adopt without inheriting the full viewer chrome.

What remains for a real embed (deliberately out of scope here): JS-side wiring so the component reads CSS tokens into echarts setOption (axis/grid/series colors), a MutationObserver for live theme switches, and the HTTP data adapter + <rezolus-section> for the systemslab use case.

Test plan

  • Build the viewer (cargo build --features developer-mode) and load the regular dashboard — visual output should be byte-identical to before the CSS extraction.
  • Load /lib/embed/demo.html:
    • First chart renders synthetic CPU-usage line series.
    • Second chart shows "no data" placeholder.
    • Host page CSS does not leak into the chart.
    • Resizing the browser window resizes the chart canvas.
  • Toggle light/dark theme in the regular viewer — chart palette stays correct (theme-invariant series colors still work).
  • Static-site WASM viewer (./crates/viewer/build.sh + open site/viewer/index.html) loads with the new charts.css link working through the symlink.
  • (Not done in CI sandbox — author has no browser; flagging for manual check.)

Sketch a path to web-component-wrapped charts/sections that consume the
existing crates/dashboard/ JSON descriptors via a pluggable data
adapter, enabling both an in-browser WASM backend and a Datastar/SSE
backend without forking the frontend.
@thinkingfish thinkingfish marked this pull request as ready for review May 12, 2026 03:05
Vendor lit@3.2.1 (16 KB self-contained ESM bundle) into
src/viewer/assets/lib/lit/ and add a minimal <rezolus-chart> custom
element that consumes the Plot descriptor shape produced by
crates/dashboard/ with inline series data ([[timestamps_ms], [values]]).

Scope is deliberately narrow — validates that a Lit component can wrap
the existing echarts global, render inside a Shadow DOM root, and react
to property updates. Future adapters (HTTP, WASM, SSE) will feed the
same .plot property without changing the component contract.

Demo at /lib/embed/demo.html mounts the component twice (synthetic
single-series and an empty-data placeholder) against the existing
lib/charts/echarts.min.js global. No build step introduced; everything
loads as raw modules from /lib like the rest of the viewer assets.

Not browser-verified in this sandbox (rezolus binary not built).
@thinkingfish thinkingfish changed the title docs: TODO for embed-friendly charts with swappable data backend viewer: &lt;rezolus-chart&gt; Lit spike + TODO for embed-friendly charts May 12, 2026
claude added 2 commits May 12, 2026 03:46
Move the three palettes that were hardcoded in colormap.js into the
:root token block in style.css:

  --chart-blue … --chart-purple   (10 series accent colors)
  --chart-1   … --chart-10        (default cycling palette,
                                   d3 schemeCategory10)
  --cgroup-1  … --cgroup-16       (hash-indexed cgroup palette,
                                   d3 schemeCategory20 minus the
                                   muted brown + gray pairs)

colormap.js reads these via getComputedStyle at access time (already
the pattern for theme-sensitive tokens) with the previous hex values
retained as fallbacks. The COLORS Proxy now maps every key to a CSS
variable; no more split between theme-sensitive and series-constant
branches.

Two side-effects worth noting:
  * style.css:993 referenced var(--chart-cyan) but the token was never
    declared — the linear-gradient fell back to nothing. Now resolved
    by the new --chart-cyan declaration.
  * Embedders (incl. the upcoming <rezolus-chart>) can override any
    palette slot via host-page CSS without touching JS.

No light-theme overrides for these tokens — series colors stay
theme-invariant, matching previous behavior. JS-side colormap ramps
(viridis, inferno, RdYlGn, diverging blue/green) deliberately remain
as JS constants since they're perceptual ramps, not theme decisions.
Move 8 chart-related sections (1136 lines) out of style.css into a new
lib/charts.css, loaded immediately after style.css from both
src/viewer/assets/index.html and site/viewer/index.html so the cascade
is preserved:

  - Chart Groups
  - Chart Cards
  - Single Chart View
  - Cgroups Section
  - CPU Topology Diagram
  - Compare mode (3 sub-sections: side-by-side/diff, header badge, landing)

style.css keeps design tokens, light-theme overrides, app chrome (nav,
sidebar, modals, toasts), Selection Section, Query Explorer, System
Info Section, and the responsive @media blocks. A few intentionally
borderline rules remain in style.css where extracting them would
fragment a coherent block: chart-wrapper-unavailable styling lives
inside Selection Section, compare-badge :has() rules live inside the
TopNav block, mobile @media overrides for chart classes stay inside
the responsive section. Worth a follow-up if we ever want a fully
self-contained charts.css.

Site-viewer (WASM/static) gets the same wiring. New symlinks under
site/viewer/lib/ for charts.css, lit/, and embed/ mirror the in-place
files from src/viewer/assets/lib/.

No selector renames, no rule changes — pure relocation. Visual output
should be byte-identical (not browser-verified in this sandbox).
@thinkingfish thinkingfish changed the title viewer: &lt;rezolus-chart&gt; Lit spike + TODO for embed-friendly charts viewer: Lit chart spike + CSS tokens + charts.css extraction May 12, 2026
claude added 2 commits May 12, 2026 04:10
Replace the synthetic single-series demo with the Rezolus → Resource
Usage → CPU % plot — the first plot of the rezolus self-report section
defined in crates/dashboard/src/dashboard/rezolus.rs.

Behavior:
  - On load, fetches /data/rezolus.json from same origin (works when
    the page is served by a viewer process).
  - If the response has series data, renders it. Status banner
    confirms the data-point count.
  - If the descriptor is there but data is empty (no recording
    loaded), synthesizes a series on top of the real descriptor.
  - If the fetch fails entirely (page opened standalone, no viewer),
    falls back to a hand-authored descriptor matching what rezolus.rs
    would emit, plus synthetic data, so the demo still renders.

rezolus-chart.js: multiply timestamps by 1000 before passing to
echarts. PromQL emits seconds-since-epoch; echarts time axis expects
ms. line.js already does the same conversion (line 72) — moving it
into the component means the .plot property contract is now the
verbatim Plot JSON shape that crates/dashboard/ produces.
Two narrow-viewport bugs:

1. The mobile @media block hides .query-explorer-link but leaves the
   .sidebar-separator that precedes it in the DOM, producing a
   visible horizontal divider that separates nothing. Extend the same
   rule with :has(+ .query-explorer-link) so the orphan separator is
   hidden alongside its link.

2. #sidebar uses `height: calc(100vh - var(--header-height))`. On iOS
   Safari (and other mobile browsers with retracting chrome) 100vh
   includes the area beneath the address bar, so the sidebar's bottom
   edge sits below the visible viewport and the last items (System
   Info, Metadata) become unreachable. Add a 100dvh declaration as
   a second value, so browsers that support dvh use the visible
   viewport while older ones fall back to vh. Also pad the bottom by
   env(safe-area-inset-bottom) for iOS home-indicator clearance.

:has() and dvh are already used in the codebase (charts.css and
elsewhere), so no support-matrix change.
@thinkingfish thinkingfish merged commit 4022bbb into main May 12, 2026
28 checks passed
@thinkingfish thinkingfish deleted the claude/embed-dashboards-webcomponents-3rwYx branch May 13, 2026 19:27
thinkingfish added a commit that referenced this pull request May 16, 2026
* feat(dashboard): add chart_id field to Event

* feat(report-save): persist events to KEY_EVENTS on save

Adds `events: Vec<Event>` to `ReportPayload` (defaults empty, so existing
callers are wire-compatible) and writes a KEY_EVENTS footer entry whenever
the payload carries events. Both trim and embed paths handle it;
save_combined_ab_tarball passes None for now (Task 3 will wire it up).

* feat(report-save): write KEY_EVENTS to both sides of A/B tarball

* feat(viewer): add events_store with chart-scope filter

* feat(viewer): add event_markers buildMarkLine helper

* feat(viewer): seed eventsStore from fileMetadata on load

* feat(viewer): render event markers on every chart via markLine

* feat(viewer): show + Add Event link in frozen tooltip footer

* feat(viewer): add event_form popover component

* feat(viewer): open event form from frozen-tooltip Add Event link

* feat(viewer): include events in Save as Report payload

* fix(viewer): seedFromMetadata accepts wrapped events footer shape

* fix(wasm-viewer): symlink event_markers.js into site/viewer/lib/charts

* fix(viewer): preserve chart-style markLines when overlaying event markers

* fix(viewer): make Add Event link clickable + stack below FROZEN row

- Set tooltip enterable:true while frozen so link clicks land on the
  link instead of passing through to the canvas (which previously
  toggled freeze and unfroze the tooltip).
- Restructure footer to render the link as a block element under the
  state row instead of inline-left.

* fix(viewer): force tooltip pointer-events on freeze + add separator

ECharts caches the tooltip wrapper's pointer-events:none and doesn't
reapply when enterable toggles mid-display, so the link clicks were
still passing through. Walk up to the wrapper and patch directly.

Add a horizontal rule above the link via border-top + padding so it
visually separates from the FROZEN state row.

* fix(viewer): dark-teal markers w/ hover label + drop redundant timestamp field

- Marker color is now dark teal (#0d8b8b) per design ask; lines also
  emphasize-on-hover with width/opacity bump.
- Inline marker label is hidden until cursor enters the line, then
  shows the description next to it.
- Remove the Timestamp input from the Add Event form (the chart's
  x-axis already shows the frozen position); submit reads the
  captured timestamp directly from prefill instead.

* fix(viewer): dark-orange event markers, horizontal label above plot grid

- Color: dark orange (#cc6600).
- Label position 'end' + rotate:0 + align:center pins the description
  above the top of the vertical line (which sits at the plot grid's
  top edge), so the text stays horizontal and legible.
- Pill background uses the same dark-orange so the hover label reads
  as a label tag rather than free-floating text.

* fix(viewer): clamp event-form popover so Add button stays in viewport

The pre-mount POPOVER_H_EST is a guess; after mount, measure the real
height and reseat the top edge if needed.

* fix(viewer): muted-darker orange + auto-unfreeze on Add submit

- Marker color now #a85d23 (less saturated, slightly darker than the
  previous #cc6600).
- onSubmit now unfreezes the tooltip + clears the axis-pointer line on
  the originating chart, so the transient interaction state goes away
  once the event lands as a permanent marker. Cancel/ESC paths leave
  the freeze intact, in case the user wants to retry.

* feat(viewer): editable Timestamp field on event form (pre-filled from freeze)

Brought back the Timestamp input so a user can fine-tune the exact
moment after freezing. Pre-populated from the captured frozen-axis
position as RFC3339; submit re-parses and validates.

* feat(viewer): click event marker to delete it

- EventsStore.remove({timestamp, description}) drops the matching
  entry by value (ECharts' internal data clones lose object identity,
  so reference-equality wouldn't be reliable).
- New openEventDelete popover anchored at the click point with a
  destructive Delete button.
- Chart's echart.on('click') intercepts markLine clicks (xAxis-defined
  is the discriminator vs scatter's yAxis OOB markLine), opens the
  popover, and sets a one-shot _suppressFreezeClick flag so the
  zr-click handler doesn't ALSO toggle freeze on the same event.

* feat(viewer): marker click freezes tooltip with x delete; clear ghost axis-pointer on Add

- Click an event marker now freezes the data tooltip at that x and
  shows '<description>  x' in the footer, instead of opening a separate
  delete popover. The x is a delete affordance that pops the
  confirmation popover. Empty-grid freezes still get the original
  FROZEN/+Add Event footer.
- buildFreezeFooterContent (new in base.js) is the single source of
  truth for the three footer states (not frozen / frozen-empty /
  frozen-marker). chart.js's freeze toggle reuses it so the rebuild
  doesn't drift.
- Add submit + Delete confirm now share _teardownFreezeAndAxisPointer
  which fires both synchronously AND on the next frame to clear the
  axis-pointer hairline that previously lingered after the popover
  unmounted and exposed the chart again.

* fix(viewer): half-width charts collapse to single column on narrow screens again

Regression from #913 (chart-CSS extraction): the base
'.group .charts { grid-template-columns: repeat(2, 1fr); }' moved into
charts.css, but the '@media (max-width: 1199px)' single-column
override stayed in style.css. Since charts.css loads after style.css
and both selectors have equal specificity, the unconditional 2-col
rule wins at every viewport width — narrow-screen collapse stopped
working.

Move the override next to its base rule in charts.css. Leave the
sibling overrides for .sysinfo-grid / .cgroup-pair-charts in style.css
since those selectors only exist there.

* fix(viewer): marker-click freeze sticks; gate event editing to Notebook

Two fixes:

1. Marker-click freeze actually freezes now. Previously showTip ran
   BEFORE _toggleTooltipFreeze set triggerOn:'none', so the just-shown
   tooltip got auto-hidden by the next mousemove. The hardcoded y:60
   also fell outside the plot grid for tall charts, silently failing
   axis-trigger showTip. Reorder + compute pixelY from the chart's
   grid rect.

2. Add Event / x Delete affordances now only render in Notebook.
   isEventEditingAllowed() is the single gate (route.startsWith
   ('/notebook')); buildFreezeFooterContent reads it before emitting
   either link, and chart.js's open-form / open-delete handlers
   no-op outside Notebook for defense in depth. Click-to-freeze and
   marker-hover labels still work everywhere — only event mutation
   is gated.

* feat(viewer): standalone event bubble replaces marker-frozen axis tooltip

Marker click no longer freezes the regular axis-trigger tooltip with
all series values. Instead it suppresses the regular tooltip and pops
a small standalone bubble next to the click position showing just the
event description (+ x delete in Notebook).

- New openEventBubble in event_form.js: positioned-once on oncreate
  (measure + clamp), dismisses on outside-click / ESC / x click, fires
  onClose so the chart can restore the suppressed regular tooltip.
- chart.js: marker-click handler now calls _suppressRegularTooltip()
  (setOption triggerOn:'none' + hideTip + axisPointer leave), opens
  the bubble, restores the regular tooltip on bubble close. The
  delete x triggers the existing openEventDelete confirmation popover.
- Drop now-dead marker-frozen plumbing: _frozenEvent state,
  tooltip-delete-event delegated handler, _openDeleteEventConfirm
  method, eventCtx branch in buildFreezeFooterContent / footer color.
  Footer is back to two states: not frozen / frozen-with-Add-Event.

* fix(viewer): event bubble sits above the hairline, centered on the marker

Anchor the bubble at the top of the plot grid at the marker's x
(translated from canvas to viewport coords), and position the bubble
above that anchor by default. The description + x now appear right
where the inline hover label does, instead of next to the click point.

* feat(viewer): event UI realign — subtle hairline + persistent off-canvas bubble + read-only info popup

- Hairline: thin dashed, opacity 0.45, silent (non-interactive),
  no on-canvas label.
- Description shows by default as a persistent HTML tag in a layer
  above the plot grid (off the canvas data area, so it never overlaps
  the hover tooltip). One tag per visible event, x-aligned to its
  hairline; repositioned on render / zoom / resize. Tags whose event
  scrolls out of the zoom window are dropped.
- Click a tag → openEventInfo: read-only popup of every populated
  field (Timestamp/Description/Kind/Source/Node/Instance) with a
  Delete action behind an inline confirm, shown only in Notebook.
- Removed the transient bubble + separate confirm popover, the
  marker-click freeze/tooltip-suppression plumbing, and the
  _frozenEvent footer state. Freeze footer is back to two states.
- markLine silent:true, label hidden — tests updated accordingly.

* fix(viewer): hairline opacity 0.7, larger event-tag click target

* fix(viewer): seat event tag just above the hairline; content-sized hit box

- Tag top is now the plot-grid top (per-tag, in JS) with a CSS
  translateY(-100%) + 4px gap, so it sits right above the hairline's
  top end instead of floating at the chart-container top.
- width:max-content (capped at max-width) so the pointer/click zone
  matches the visible bubble rather than a full-width block — fixes
  the flaky cursor that lingered as a pointer across empty space.

* feat(viewer): unsaved events survive refresh via scoped localStorage

Events now persist to a file-scoped localStorage key (rezolus_events_
<suffix>) the same way Notebook/Report state does, so in-memory adds
and deletes that haven't been Saved-as-Report survive a page reload.

- events_store: new replaceAll() for the restore path.
- selection.js owns persistence (events_store stays pure/testable):
  EVENTS_STORAGE_KEY scoped in setStorageScope; persistEvents on every
  store change; restoreEvents on scope set. A restored working set is
  authoritative (eventsRestoredFromStorage) so seedEventsFromMetadata
  becomes a no-op — persisted edits win over the footer, mirroring how
  Notebook localStorage overrides embedded report state. Persistence is
  suspended during restore/seed so only genuine user edits get written.
- Both bootstraps call seedEventsFromMetadata AFTER setStorageScope so
  the restore-vs-seed precedence is order-independent.
- Clear-All on the Notebook store also purges the events key.
- Also: trim event-tag padding back to 3px 9px for a leaner look.

* chore: bump version to 5.13.1-alpha.8

* feat(viewer): only render events in Notebook/Report views

Add isEventDisplayAllowed() (route /notebook or /report). Gate both
_applyEventMarkers (no event hairlines; base chart-style markLine such
as scatter's OOB separator still preserved) and _renderEventBubbles
(tears down the bubble layer and bails) on it. Raw metric sections no
longer show event hairlines or bubbles. Editing stays Notebook-only
via the existing isEventEditingAllowed().

* fix(viewer): guard SectionContent against an unresolved section

Render nothing instead of throwing on attrs.section.route when the
route doesn't map to a loaded section (mid-load / failed fetch). The
throw was desyncing mithril's vdom and cascading into removeChild
NotFoundErrors / a white screen.

* fix(viewer): don't phantom-load /overview when re-loading a trimmed report

uploadParquet (in-app Load Parquet / re-load) always fetched the
overview section and routed to /overview, ignoring report mode. A
trimmed Save-as-Report parquet has no section data and an empty
section list, so this fired a doomed overview fetch and landed on a
broken page. Mirror the cold-boot path: check ViewerApi.getMode(),
and for a report go straight to /report without the overview fetch.

* chore: refresh mr2-report.parquet fixture

* style: rustfmt report-save

* chore: refresh mr2-report.parquet fixture
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants