feat(viewer): add event annotations from chart tooltips#929
Merged
thinkingfish merged 38 commits intoMay 16, 2026
Conversation
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).
- 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.
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.
…amp 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.
- 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.
The pre-mount POPOVER_H_EST is a guess; after mount, measure the real height and reseat the top edge if needed.
- 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.
… 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.
- 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.
… 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.
…reens again Regression from iopsystems#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.
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.
…ltip 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.
…rker 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.
…vas 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.
…t 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.
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.
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().
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.
…eport 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.
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
Lets a viewer user create one-off event annotations against the loaded recording from a chart, in Notebook/Report. Events render as subtle vertical hairlines with a persistent description bubble, persist to the parquet footer on Save as Report, reload on subsequent open, and survive a page refresh while unsaved.
chart_id: Option<String>added todashboard::events::Event;events: Vec<Event>added toreport-save::ReportPayload, written to theKEY_EVENTSfooter key (single parquet + both sides of an A/B tarball).localStoragekey the same way Notebook/Report state does, so unsaved adds/deletes survive a refresh; a persisted working set is authoritative over the footer seed.@mediaoverride was stranded instyle.cssafter the base rule moved tocharts.css).Server viewer and WASM static-site viewer both covered (shared symlinked assets).
Test plan
cargo test -p dashboard -p report-save— Event/ReportPayload round-trip with & withoutchart_id/events;KEY_EVENTSwritten when events present, skipped when empty, written to both A/B sidesnode --test tests/events_store.test.mjs tests/event_markers.test.mjs— store filter/seed/replace/remove + markLine buildercargo clippy --all-targets -- -D warningsbash tests/viewer_smoke.sh🤖 Generated with Claude Code