Skip to content

Phase 1039: per-graph current-view boxes in the dashboard time slider#172

Open
HanSur94 wants to merge 13 commits into
mainfrom
claude/tender-lovelace-93986c-pr
Open

Phase 1039: per-graph current-view boxes in the dashboard time slider#172
HanSur94 wants to merge 13 commits into
mainfrom
claude/tender-lovelace-93986c-pr

Conversation

@HanSur94
Copy link
Copy Markdown
Owner

Summary

Adds a current-view indicator to the dashboard's lower preview slider (TimeRangeSelector). When a plot's x-limits are not synchronized with the slider selection (i.e. you've zoomed/panned that plot directly), a small box appears inside the slider marking that plot's current view — one box per out-of-sync graph, each drawn in the same colour as that graph's slider preview line. Boxes scope to the active page and update on tab switch. The slider's selection rectangle and its drag behaviour are unchanged.

What's included

  • TimeRangeSelectorsetCurrentViews(ranges, colorIdxs) / hideCurrentView() draw a reused pool of per-graph boxes (patch + dashed edges) in absolute data-time space. The preview-line palette is now a shared previewPalette_() used by both preview lines and boxes, so a box matches its line's colour by index. setCurrentView(a,b) kept as a single-box back-compat wrapper.
  • FastSenseWidget.getCurrentXLim() — returns the live axes XLim (not the data-extent cache), plus an engine-owned XLim-listener slot.
  • DashboardEngine.updateCurrentViewIndicator_ — collects each out-of-sync, visible widget's view + preview-index, shows a box only when it differs from the selection (epsilon 0.005·span), hides when synced. Wired to the per-widget XLim listener, live tick, post-broadcast, and page switch. A 0.15 s debounce (CurrentViewDebounceTimer_) coalesces FastSense's zoom re-resolve so the box reflects the settled view. Listeners are (re)attached for widgets realized later (other tabs / scroll).
  • DashboardTheme.CurrentViewBoxColor token (light + dark).
  • Tests (21): TestTimeRangeSelectorCurrentView (9), TestFastSenseWidgetCurrentXLim (5), TestDashboardCurrentViewIndicator (7, incl. multi-tab). MISS_HIT clean.
  • Demos: examples/demo_current_view_box.m, examples/demo_current_view_box_multitab.m.

Notes

  • Pure MATLAB / Octave-safe (axes/patch/line + PostSet listeners, all try/catch guarded; Octave skips the listener and refreshes via the tick/switch/broadcast hooks).
  • Backward compatible: no box appears unless a plot is out of sync; the slider selection and serialization are unchanged.
  • Branch-management note: an earlier exploratory approach (a corner-inset minimap on the FastSense axes) was implemented then fully reverted; this PR branch contains only the final lower-slider feature (no .planning/ artifacts, FastSense.m untouched).

🤖 Generated with Claude Code

HanSur94 and others added 13 commits May 29, 2026 21:16
- dark: amber [0.95 0.62 0.20], contrasts bluish-gray Selection
- light: dark amber [0.85 0.45 0.05], contrasts dark-blue Selection
- per-preset (mirrors MarkerPlantLog) so light/dark can differ
…im listener slot

- getCurrentXLim() returns the LIVE wrapped-FastSense axes XLim ([min max])
  read via get(ax,'XLim'), or [] when not rendered — distinct from the
  getTimeRange() data-extent cache
- CurrentViewXLimListener_ engine-owned listener slot + Hidden
  setCurrentViewXLimListenerForEngine_ setter (mirrors PlantLogXLimListener_)
- delete() releases the listener before FastSenseObj teardown (try/catch)
- hCurrentViewBox patch + hCurrentViewLeft/Right dashed edge lines
- distinct style: FaceAlpha 0.12, LineStyle '--' LineWidth 1, theme
  CurrentViewBoxColor with amber fallback; PickableParts/HitTest off
- setCurrentView(tStart,tEnd): validate finite, reorder, clamp to
  DataRange, store CurrentView, redraw, make visible (no-throw guarded)
- hideCurrentView(): clear CurrentView, NaN data, Visible off
- redraw_ refreshes box geometry when CurrentView is non-empty
- header doc updated (Properties/Methods/CurrentView)
- testEmptyBeforeRender: [] before render
- testReturnsLiveXLimAfterRender: 1x2 == live axes XLim
- testReflectsZoom: tracks programmatic xlim([2 5]) (reads LIVE axes)
- testNotEqualToDataExtentWhenZoomed: live view differs from getTimeRange cache
- testListenerSlotSetterAndDeleteNoThrow: engine slot setter + delete() are safe
- 7 class-based tests: visible-box, geometry, hide, clamp, reorder,
  distinct-style/non-interactive, no-throw-after-delete
- off-screen figure factory mirrors TestTimeRangeSelectorEventMarkers
- geometry asserts are orientation-agnostic (MATLAB/Octave parity)
testNoThrowAfterDelete asserted that calling setCurrentView/hideCurrentView
after delete(selector) does not throw — impossible in MATLAB, which throws
"Invalid or deleted object" at method dispatch on a deleted handle before any
in-method guard runs.

Replaced with testNoThrowAfterGraphicsDestroyed: destroy the underlying
figure (killing all graphics handles) while the selector object stays alive,
then call the API — exercises the real ishandle guards (redraw_ + each box
handle). Wave 1 now: TimeRangeSelector 7/7, FastSenseWidget 5/5.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- updateCurrentViewIndicator_ (private): collects out-of-sync widget
  XLims via getCurrentXLim, unions them, shows/hides the slider
  current-view box per the epsilon-vs-Selection rule (0.005*span)
- attachCurrentViewXLimListener_ (Hidden): engine-owned XLim PostSet
  listener mirroring attachPlantLogXLimListener_ (idempotent, Octave-skip,
  try/catch, namespaced DashboardEngine:currentViewIndicatorFailed)
- updateCurrentViewIndicatorForTest_ (Hidden seam): routes to the private
  method so Wave 3 integration tests drive the decision on Octave too

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- render() tail: attach a current-view XLim listener to every
  FastSenseWidget across all pages (SITE 1; Octave-skipped in the helper)
- broadcastTimeRange tail: recompute indicator so a re-sync hides the box
  (SITE 2)
- onLiveTick tail: recompute each tick so drift in/out of sync updates
  without user interaction (SITE 3)
- switchPage tail: recompute for the newly active page (SITE 4)

All calls appended + try/catch wrapped; sync/Selection semantics of
broadcastTimeRange, onLiveTick, and switchPage are unchanged (additive
only). mh_lint/style/metric clean; Octave render+seam smoke passes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- tests/suite/TestDashboardCurrentViewIndicator.m (6 tests): all-synced
  hidden, one-zoomed visible at window, two-zoomed union, re-sync hides,
  sub-epsilon stays hidden, no-selector guard no-throw. Drives the engine
  via updateCurrentViewIndicatorForTest_.
- examples/demo_current_view_box.m: 3-widget dashboard, top plot pre-zoomed
  so the amber current-view box shows on load; interaction instructions.
- FastSenseWidget: add CurrentXLimOverrideForTest_ Hidden seam — getCurrentXLim
  returns it verbatim when set. FastSense rebuilds its axes during zoom
  re-resolve, so a raw programmatic xlim() poke is not durable under the
  unittest runner's event flushing; the override makes the integration test
  deterministic. The real axes<->getCurrentXLim path stays covered by
  TestFastSenseWidgetCurrentXLim. Empty default => zero production impact.

Verified (matlab-MCP authoritative): TimeRangeSelector 7/7,
FastSenseWidget 5/5, Dashboard integration 6/6 = 18/18. Real interactive
zoom confirmed live: getCurrentXLim tracks the zoomed window, box shows it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Change from a single union box to ONE box per visible, out-of-sync graph,
each coloured to match that graph's slider preview line (per user request).

- TimeRangeSelector: replace single hCurrentViewBox with a reused handle POOL
  (hCurrentViewBoxes/EdgesL/EdgesR). New setCurrentViews(ranges, colorIdxs)
  draws N boxes; setCurrentView is a back-compat single-box wrapper;
  hideCurrentView clears the pool. Palette extracted to shared previewPalette_
  used by BOTH preview lines and current-view boxes, so box k matches preview
  line k exactly. redraw_ refreshes all boxes.
- DashboardEngine.updateCurrentViewIndicator_: collect per-out-of-sync-widget
  (range, preview-line index) instead of a union; show each box only when that
  graph's view differs from the Selection (epsilon 0.005*span); colour index =
  the graph's position among valid-preview widgets (mirrors linesList order).
- Tests: TimeRangeSelector 9/9 (added multi-box + pool-shrink + colour tests),
  Dashboard integration 6/6 (union test -> two-separate-boxes-with-distinct-
  colours). FastSenseWidget 5/5 unchanged. 20 tests total, MISS_HIT clean.
- Demo: pre-zooms two plots to different windows -> two differently-coloured
  boxes on load; instructions updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Update CONTEXT decision (union -> one coloured box per out-of-sync graph,
matching each graph's preview-line colour), HUMAN-UAT items, and the
integration test header/coverage doc to the per-graph design.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- TestDashboardCurrentViewIndicator: add testPageSwitchScopesBoxesToActivePage
  — builds a 2-page dashboard, zooms a graph on each page, and asserts boxes
  follow the ACTIVE page across REAL switchPage calls (P1 box clears on switch
  to P2, P2 box shows, P1 box restored on switch back). Verifies the indicator
  scopes to activePageWidgets() and the switchPage wiring (Plan 03 SITE 4)
  auto-refreshes — no manual seam call after the switches. 7/7 integration.
- examples/demo_current_view_box_multitab.m: live 2-page ("Process"/"Utilities")
  demo, one plot pre-zoomed per page; boxes track the active tab.

Phase 1039 totals: TimeRangeSelector 9 + FastSenseWidget 5 + Dashboard 7 = 21.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two bugs made the boxes never appear on a second tab:

1. Missing listener on later-realized pages. The current-view XLim listener
   was attached only during render(), but widgets on pages other than page 1
   aren't realized then (no axes) so the attach silently skipped them, and
   nothing re-attached when the tab opened. Fix: re-attach the active page's
   (now-realized) FastSenseWidgets in switchPage, and also attach in
   onScrollRealize for below-the-fold widgets. attachCurrentViewXLimListener_
   is idempotent + skips unrealized widgets, so repeats are safe.

2. Listener fired mid-rebuild. FastSense re-resolves/rebuilds its axes on a
   zoom, firing the XLim listener multiple times; the box could be left hidden
   on a transient read even though the final view is zoomed. Fix: debounce —
   the listener now restarts a 0.15s one-shot timer (CurrentViewDebounceTimer_,
   mirrors SliderDebounceTimer) so the box refreshes ONCE after FastSense
   settles, reading the final getCurrentXLim. Timer cleaned up alongside the
   other debounce timers; updateCurrentViewIndicator_ guards isObjValid_ for
   the post-delete fire.

Verified: 2-page dashboard, live-zoom (listener+debounce, no manual seam) on
BOTH tabs surfaces the correct box. TimeRangeSelector 9/9, Dashboard 7/7
(incl. multi-tab), FastSenseWidget 5/5. MISS_HIT clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@codecov
Copy link
Copy Markdown

codecov Bot commented May 29, 2026

Codecov Report

❌ Patch coverage is 86.36364% with 27 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
libs/Dashboard/DashboardEngine.m 82.29% 17 Missing ⚠️
libs/Dashboard/TimeRangeSelector.m 91.02% 7 Missing ⚠️
libs/Dashboard/FastSenseWidget.m 86.36% 3 Missing ⚠️

📢 Thoughts on this report? Let us know!

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.

1 participant