viewer: Lit chart spike + CSS tokens + charts.css extraction#913
Merged
Conversation
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.
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).
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).
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.
4 tasks
5 tasks
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Four commits, each a contained step toward embed-friendly charts:
docs/TODOs.md— sketches the path to embed-friendly Rezolus charts (web components consumingPlot/ViewJSON descriptors fromcrates/dashboard/, with pluggable WASM and Datastar/SSE data adapters).<rezolus-chart>spike — vendorslit@3.2.1(16 KB self-contained ESM) atsrc/viewer/assets/lib/lit/and adds a minimal Lit custom element atsrc/viewer/assets/lib/embed/rezolus-chart.jsaccepting the existingPlotdescriptor shape with inline series. Demo at/lib/embed/demo.html. No build step.Promote chart palette to CSS tokens — moves three palettes from
colormap.jsJS constants intostyle.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.jsreads them viagetComputedStyle(with hex fallbacks for robustness). Also fixes a latent bug wherestyle.css:993referencedvar(--chart-cyan)but the token was never declared. Embedders can now override any palette slot via CSS without touching JS.Extract chart CSS into
charts.css— moves 1136 lines fromstyle.cssinto a newlib/charts.css, loaded immediately after it from both viewer entry points (src/viewer/assets/index.htmlandsite/viewer/index.html). Symlinks added undersite/viewer/lib/forcharts.css,lit/, andembed/. 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@mediaoverrides) stay instyle.csswhere 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), aMutationObserverfor live theme switches, and the HTTP data adapter +<rezolus-section>for the systemslab use case.Test plan
cargo build --features developer-mode) and load the regular dashboard — visual output should be byte-identical to before the CSS extraction./lib/embed/demo.html:./crates/viewer/build.sh+ opensite/viewer/index.html) loads with the newcharts.csslink working through the symlink.