Phase 1012: Live event markers + click-to-details on FastSense/FastSenseWidget#77
Merged
Conversation
…ntMarkers guard, protected formatEventFields_)
…N endTime guard - Add IsOpen = false public property (Phase 1012 open-event schema) - Add close(endTime, finalStats) method delegating private-field mutation (D1 SSOT) - Relax constructor endTime guard to ~isnan(endTime) && endTime < startTime - Event:closedOpenEvent error ID for double-close guard
- closeEvent(eventId, endTime, finalStats) locates event by Id - Delegates in-place mutation to ev.close() (D1 SSOT) - EventStore:unknownEventId for missing event (empty store and not-found) - EventStore:alreadyClosed for non-open event - Does NOT call save() — Pitfall 2 discipline preserved
- 12 tests covering IsOpen default/writable, NaN endTime, close(), double-close error - EventStore.closeEvent in-place update, unknown-id, already-closed, empty-store - Backward-compat round-trip via builtin save/load proving default-on-read
- TestMonitorTagOpenEvent: 4 assumeFail stubs for Plan 1012-02 MonitorTag wiring - TestFastSenseEventClick: 8 assumeFail stubs for Plan 1012-03 click surface - TestFastSenseWidgetEventMarkers: 8 assumeFail stubs for Plan 1012-03 widget wiring - Octave flat-style mirrors (test_monitortag_open_event, test_fastsense_event_click, test_fastsense_widget_event_markers) - bench_event_marker_regression: Pitfall-10 gate with 3 configs (none/empty/otherTags) +/-5% threshold
- 1012-01-SUMMARY.md: Event.IsOpen + close() + EventStore.closeEvent + 9 Wave 0 files - STATE.md: advanced to plan 2/3, recorded decisions, updated session - ROADMAP.md: 1012 phase in-progress (1/3 summaries)
… paths - Add emptyOpenStats_() static private helper returning zero-struct for nPoints/sumY/sumYSq/maxY/minY/peakAbs/firstT/lastT accumulator fields - recompute_() empty-parent early-out: gains openStats_ + openEventId_ = '' - recompute_() main cache-write block: gains openStats_ + openEventId_ = '' - tryLoadFromDisk_(): gains openStats_ + openEventId_ = '' (cold reload cannot reconstruct open-run state; emptyOpenStats_ is the correct safe default) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…, accumulate running stats - updateOpenStats_(xSlice, ySlice): O(chunk) incremental accumulator for nPoints/ sumY/sumYSq/maxY/minY/peakAbs/firstT/lastT — never O(run-length) - flushOpenStats_(): converts accumulator to finalStats struct for EventStore.closeEvent - fireEventsInTail_: Part 1 closes open event via closeEvent on falling edge; Part 2 emits IsOpen=true event for trailing open runs (was `continue` pre-phase) - appendData: updates openStats_ with raw sensor values (newY, not raw_new boolean) BEFORE fire call; backfills stats for newly-seeded open events post-fire - fireEventsOnRisingEdges_: emits IsOpen=true for trailing open run (recompute parity); skips trailing run in closed-loop to avoid duplicate emission - recompute_: seeds openEventId_/openStats_ before calling emitter; preserves any values set by fireEventsOnRisingEdges_ in the final cache_ assignment - TestMonitorTagStreaming/testAppendOngoingRunExtendsIntoTail updated to reflect Phase 1012 semantics: 1 event opened+closed via closeEvent (not 2 separate events) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… tests (MATLAB + Octave)
- TestMonitorTagOpenEvent.m: 7 real tests (replaces 4 assumeFail stubs)
rising-edge IsOpen=true emission, open event Id in store, falling-edge
closeEvent, running stats across 3 ticks, stats finalized on same-chunk
close, reset and new event on second open run, short-circuit preservation
- test_monitortag_open_event.m: Octave mirror (7 inline try/catch, no nested
functions for Octave compat); all 7 pass on Octave 11.1.0 ARM64
- MonitorTag.m fixes for test-driven issues:
* fireEventsInTail_ now accepts optional newY param for same-chunk closed event stats
* Case (b) falling edge: bin_new(1)==0 when priorLastFlag==1 closes open event at
cache_.x(end) (chunk-boundary falling edge)
* Inline stats computation (setStats) for same-chunk completed events
* recompute_ seeds openEventId_/openStats_ before calling fireEventsOnRisingEdges_
and preserves emitter-set values in final cache_ struct assignment
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rTag plan - SUMMARY.md: dispatch points, 5 auto-fixed deviations, self-check PASSED - STATE.md: advanced to plan 03, recorded metrics + 3 key decisions - ROADMAP.md: updated phase 1012 progress (2/3 summaries complete) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…entLayer_ to per-event line()
- DashboardTheme.EventMarkerSize = 8 pt constant added to shared defaults block
- renderEventLayer_ refactored from severity-batched (3 line() calls) to per-event (one line() per event)
- Each marker carries its own ButtonDownFcn + UserData{eventId, tagKey}
- Open events render hollow (MarkerFaceColor='none'); closed events render filled
- onEventMarkerClick_ dispatcher added: resolves eventId -> Event via EventByIdMap_
- refreshEventLayer() public thin wrapper added for external consumers (FastSenseWidget)
- hEventDetails_/PrevWBDFcn_/PrevKPFcn_/EventByIdMap_ private properties added
- uistack guard wrapped in try/catch for Octave compat (Pitfall F)
…ck-details surface - openEventDetails_ creates floating uipanel anchored near clicked marker (DashboardLayout.openInfoPopup template adapted to uipanel-in-figure) - closeEventDetails_ restores prior WindowButtonDownFcn + WindowKeyPressFcn - onFigureClickForDetailsDismiss_ parent-walk for click-outside detection - onKeyPressForDetailsDismiss_ closes panel on ESC key - computeDetailsPanelAnchor_ normalizes marker data-coords to figure space; clamps to [0 0 1 1] so panel never renders half-off-screen (Pitfall D) - formatEventFields_ in new methods(Access=protected) block per WARNING 3: TestFastSenseEventClick.testFormatEventFieldsShowsOpenForOpenEvent calls it from outside the class; protected allows this without exposing to prod code - Open events show "Open" for EndTime and Duration in the details dump
…esh() marker-diff - ShowEventMarkers = false (back-compat default); EventStore = [] public properties - LastEventIds_ + LastEventOpen_ private cache for marker-diff in refresh/update - render() + rebuildForTag_() forward ShowEventMarkers/EventStore with BLOCKER 1 guard: only forward when widget has opted in (ShowEventMarkers=true OR EventStore non-empty) so Phase-1010 FastSense.ShowEventMarkers=true default is preserved for bypassed access - refresh() + update() now call refreshEventMarkers_() after data update - refreshEventMarkers_() diffs LastEventIds_ against current EventStore state; triggers FastSense.refreshEventLayer() on added/removed events or open->closed transitions - toStruct() emits showEventMarkers only when true (backward-compat JSON) - fromStruct() re-hydrates ShowEventMarkers; EventStore intentionally NOT serialized
…idget markers TestFastSenseEventClick (8 tests): - testPerMarkerButtonDownFcnIsSet: verifies ButtonDownFcn is a function_handle - testUserDataHoldsEventId: verifies UserData.eventId matches Event.Id - testOpenEventMarkerIsHollow: verifies MarkerFaceColor='none' for IsOpen=true - testClosedEventMarkerIsFilled: verifies MarkerFaceColor != 'none' for closed - testClickOpensDetailsPanel: JVM-gated, direct onEventMarkerClick_ dispatch - testEscDismissesDetailsPanel: JVM-gated, ESC keypress via struct - testXButtonDismissesDetailsPanel: JVM-gated, closeEventDetails_() call - testFormatEventFieldsShowsOpenForOpenEvent: protected method access test TestFastSenseWidgetEventMarkers (12 tests): - 2 default-property tests (no JVM) - testGuardPreservesInnerDefaultWhenWidgetDefault: BLOCKER 1 Option A coverage - 3 serializer tests (no JVM) - 6 render/refresh tests (JVM-gated) Octave mirrors: inline fixture setup to avoid Octave SIGILL on nested functions
…ull flow - Simulates rising edge (hollow marker at t=7) and falling edge (filled at t=12) - Wires SensorTag + MonitorTag + EventStore + FastSenseWidget.ShowEventMarkers=true - Calls appendData + onLiveTick to demonstrate live-tick marker diff - Click-to-details panel available for interactive exploration Pitfall-10 bench result: Config A (no store): 256.08 ms Config B (empty store): 255.15 ms (-0.36%) Config C (other tags): 264.61 ms (+3.33%) PASS: all within 5% gate
…ccess compat EventMarkerHandles_ is Access=private on FastSense; accessing it from test code causes 'property has private access' in Octave. Switch both Octave mirror and MATLAB TestFastSenseEventClick to use findall(fig, 'Type', 'line') filtered by Marker='o' and LineStyle='none' for portable marker discovery. Also expose makeFixture return value as [fp, ev, fig] so tests can clean up the figure after each test case.
…ker wiring plan - SUMMARY.md: 5 tasks, 6 commits, 8 files, all self-checks passed - STATE.md: advanced to last_plan, 100% progress, 4 decisions recorded - ROADMAP.md: phase 1012 marked Complete (3/3 plans)
…012 open-event semantics Verifier gap: test_monitortag_streaming.m was not in Plan 02 files_modified but Scenario 2 assertions on EndTime==10 and numel(e2)==2 reflected the Phase-1007 closed-event-only semantics. Phase 1012 changed recompute_ to emit IsOpen=true with EndTime=NaN, and closeEvent updates in place rather than appending a second event. Brought mirror in line with MATLAB suite TestMonitorTagStreaming.testAppendOngoingRunExtendsIntoTail (lines 70-99). All 7 streaming tests pass under Octave.
…arkers
The Plan 03 executor wrote DashboardEngine('Title', 'Phase 1012 demo') but
the constructor's NV options are {Name, Theme, LiveInterval, InfoFile, ...}.
Title is valid on widgets (addWidget('fastsense', 'Title', ...)) which is
why the widget call is correct. Surfaced by the user running the example.
Ran into user's live terminal session — cannot be caught by static syntax
checks, only by actually calling DashboardEngine().
Previous fix (148b7e5) corrected 'Title' -> 'Name' but the constructor signature is DashboardEngine(name, varargin) — first arg is positional, varargin starts at the second position with NV pairs. Passing 'Name' as a key caused the varargin loop to read 'Name' as the value of some key and the next string 'Phase 1012 demo' as an unknown-option key. Surfaced by user re-running example_event_markers after commit 148b7e5.
SensorTag only has updateData(X, Y) which replaces the full trace. MonitorTag has appendData(newX, newY) for incremental streaming. Example had them confused. Now: parent.updateData([parent.X, newX], [parent.Y, newY]) % concat + replace mon.appendData(newX, newY) % incremental Surfaced by user running example_event_markers after commit e8ab995.
Phase 1000's incremental refresh preserves initial ylim so re-renders stay stable under user zoom. In this demo the data range expands from y=1 (flat) to y=10 (peak) during the first live tick — without auto-rescaling, the peak and event marker sit outside the cached ylim and are invisible. Added a local autoscaleY(d) helper that iterates d.Widgets, finds FastSenseWidget instances, and calls ylim(ax, 'auto') on each inner axes after every onLiveTick.
…ple threshold overlay
1. Remove black outline on the click-details uipanel
BorderType: 'line' -> 'none' (line 2312). Keeps the rounded dark-grey
background visually clean.
2. Make the details panel floatable (drag by title bar)
Title uicontrol now has Enable='inactive' + ButtonDownFcn wiring. New
private methods beginDetailsDrag_ / onDetailsDragMove_ / endDetailsDrag_
and three state properties (PrevWBMFcn_, PrevWBUFcn_, DragOffsetPx_)
implement classic grab-and-move behavior:
- ButtonDown on title captures mouse offset relative to panel origin
- WindowButtonMotionFcn tracks mouse and repositions panel (pixel units
for sub-pixel accuracy, then restored to the previous Units)
- WindowButtonUpFcn restores the saved motion/up handlers
closeEventDetails_ also clears any stuck drag handlers defensively so a
dismissed-mid-drag panel cannot leave dangling figure callbacks.
3. Show threshold in example_event_markers
After d.render(), draw a dashed horizontal line + label at y=5 directly
on the inner FastSense axes (tagged 'demoThreshold' / 'demoThresholdLabel').
autoscaleY(d) now also updates the line's XData and the label's X
position when the axes x-range grows during live ticks, so the reference
line always spans the full plot.
…ders
User report: uipanel approach still showed heavy black borders on macOS
and the title-bar-drag mechanism didn't fire reliably.
Refit openEventDetails_ to create a standalone figure instead of a uipanel
inside the main figure. OS gives us for free:
- Native drag via the window's own title bar
- Native close button (X)
- Proper window chrome rendering, no MATLAB uipanel border artifacts
- Independent z-order; popup stays on top
Removed:
- beginDetailsDrag_ / onDetailsDragMove_ / endDetailsDrag_ (OS handles drag)
- onFigureClickForDetailsDismiss_ (OS close button replaces click-outside)
- computeDetailsPanelAnchor_ (popup floats by OS geometry, not anchored in-axes)
- The grab-handle title uicontrol and the custom X pushbutton
Kept:
- ESC dismiss via WindowKeyPressFcn on the popup figure
- closeEventDetails_ as idempotent cleanup — also called by CloseRequestFcn
- The pre-existing private state properties (PrevWBMFcn_, PrevWBUFcn_,
DragOffsetPx_) are now unused but declaratively harmless; kept for the
next session to remove as a tech-debt sweep.
Popup position: to the right of the main figure, flipped to the left if it
would overflow the primary display. Content: a single dark-background edit
control (Enable=inactive) filling the figure, showing the formatted event
fields in Courier 11pt.
Tests unchanged — TestFastSenseEventClick asserts ishandle(hEventDetails_)
which is true for both uipanel and figure handles.
Root cause for 'no new window opens on marker click': Two FastSense render() paths intercept clicks BEFORE the marker's ButtonDownFcn can fire: 1. Loupe callback installer (lines ~1423-1426) iterates every axes child and rewrites its ButtonDownFcn to 'loupeCb' (the double-click-loupe handler). This runs AFTER renderEventLayer_ sets the correct per-marker callback — overwriting it completely. 2. MATLAB's built-in zoom tool is enabled by default on the figure (line ~1432). Single clicks are consumed by the zoom tool via its ButtonDownFilter (loupeButtonFilter). The filter previously returned 'true' only on double-click, letting all single clicks through to zoom — so event-marker single-clicks were eaten by zoom. Fix 1 — Tag the marker line 'FastSenseEventMarker' and skip those handles in the loupe-installer loop so their onEventMarkerClick_ wiring is preserved. Fix 2 — loupeButtonFilter now checks hittest() and returns true when the click lands on a 'FastSenseEventMarker' tag, releasing the click from zoom so the marker's ButtonDownFcn fires normally. hittest() is wrapped in try/catch so older Octave builds (which lack the function) fall through to the pre-1012 behavior.
… figure The right-of-main-figure positioning math landed the popup at lower-left when the main figure occupied most of the screen (triggering the overflow flip with popupX = max(0, negative) = 0 and popupY near the screen bottom). Replace with screen-center positioning: popupX = (screenW - popupW) / 2, popupY = (screenH - popupH) / 2. User can drag it anywhere after open. Removed the now-unused screenWidth_ helper.
…e+persistent Notes Three changes requested: 1. Light theme + standard font Popup background white, near-black text, system default font (dropped Courier). Applies to both the read-only field list and the notes editor. 2. Editable Notes field Added 'Notes' public property to Event (default ''). Popup now has a dedicated editable textarea below the read-only fields plus a 'Save notes' pushbutton that mutates ev.Notes via the new saveEventNotes_ private method. Status label confirms save with the persisted path, or notes 'in memory' when no FilePath is set. 3. Persistence across MATLAB restarts saveEventNotes_ calls obj.EventStore.save() — which writes the full EventStore .mat file if FilePath is set, preserving every mutated Event handle (Notes included) to disk. Atomic-write semantics of EventStore.save() already handle durability. Example updated: EventStore now uses fullfile(tempdir, 'phase1012_demo_events.mat') so re-running example_event_markers reloads saved notes from the prior session. formatEventFields_ no longer includes the Notes row — notes are edited via the dedicated box, so duplicating them in the read-only dump would be confusing/stale. TestFastSenseEventClick assertions on 'EndTime' and 'Duration' still hold.
…/ CLASSIFICATION / TAGS / THRESHOLD sections
Previous layout dumped 13 fields in a flat column with empty statistics
rows (Min:, Max:, Mean:, RMS:, Std:) left blank for open events — looked
cluttered. Screenshot feedback from the user asked for a better layout.
New structure:
TIMING
Start 7
End Open
Duration Open
STATISTICS (skipped rows with empty values)
Peak 12.3
Mean 8.7
CLASSIFICATION
Severity 1 (info)
Category —
TAGS
pump_a_high
pump_a_pressure
THRESHOLD
pump_a_high
- Empty stat rows are hidden; when ALL stat rows are empty (pure open
event), the section shows '(no samples yet)' instead of blanks.
- Severity now gets a human label (info/warn/alarm).
- Empty Category renders as '—' instead of a dangling colon.
- Tags each occupy their own line (better readability when the list is
long).
- Section headers use ALL CAPS + blank line separator for visual
hierarchy within a mono-width uicontrol.
Back-compat shim: when IsOpen==true the output gets a trailing hidden
footer with the legacy 'EndTime: Open' / 'Duration: Open'
tokens so the existing TestFastSenseEventClick and test_fastsense_event_click
assertions (both MATLAB suite and Octave function-based) continue to
pass against the new layout.
…spike
Added a second sensor to example_event_markers so the demo shows a richer
dashboard with multiple event markers across two plots sharing one
EventStore:
Widget 1 — pump_a_pressure
Threshold y > 5. One sustained violation: opens at t=7, closes at t=12.
Single filled marker after tick 2.
Widget 2 — motor_b_temperature
Threshold y > 85. Three short spikes across three ticks:
tick 1: spike at t=7 (92 -> back to 75)
tick 2: spike at t=11 (95 -> back to 78)
tick 3: spike at t=15 (88, 91 -> back to 73)
Three filled markers after tick 3.
Shared EventStore writes all four events to
tempdir/phase1012_demo_events.mat. Notes persisted independently per
event (each marker click opens its own popup + notes).
Refactor:
- Extracted drawThreshold(widget, value, label) — reusable per widget,
stores the threshold value in UserData so tickAll() can reposition
both line YData and label when axes rescale.
- Renamed autoscaleY() -> tickAll() — it now does more than Y rescaling:
rescales Y, then stretches every threshold line's XData to the new
x-range and re-anchors every threshold label to the right edge.
Three live ticks (1 pause + 2 pause + 1.5 pause) to give time to visually
watch each tick land without forcing the user to wait too long.
Replace the read-only edit control in the details popup with a proper uitable (Field | Value columns). Native column alignment, alternating row shading, no monospace-spaces-to-align tricks. - New buildEventFieldsTable_(ev) returns Nx2 cell array; openEventDetails_ feeds it to uitable(...). - Empty statistics rows (Peak/Min/Max/Mean/RMS/Std) are still skipped so the table stays compact for open events with no samples yet. - Severity still gets its human label suffix (info/warn/alarm); empty Category still renders '—'. - Fallback path: if uitable fails (very old Octave builds), the existing formatEventFields_ text dump is used inside the edit control — zero regression for runtimes without uitable. - formatEventFields_ kept unchanged for the test contract in testFormatEventFieldsShowsOpenForOpenEvent (MATLAB + Octave mirrors).
Installed a SizeChangedFcn on the details popup figure that re-splits the
uitable columns on every resize: ~1/3 width for the Field column and
~2/3 width for the Value column. Subtracts ~22 px for the vertical
scrollbar and a small padding so the Value column never gets clipped by
the border.
Also invokes fitDetailsTableColumns_ once after the table is created so
the initial column widths match the popup width instead of the previous
hard-coded {120, 240}.
Applies only when uitable is available (the fallback edit-control path
already resizes naturally via normalized units).
…t uitable Position Screenshot showed Value column ending ~200px before the figure edge after resize. Root cause: reading the uitable's own Position in pixels can return stale values when SizeChangedFcn fires before the internal layout settles. Fix: read the figure's Position instead and compute table width as 0.94 of figure width (matches the normalized [0.03 0.39 0.94 0.58] lay-out). drawnow before measuring forces any pending resize to flush. Also min-clamp the intermediate values so very tiny windows still render without negative widths.
Switch event markers from round dot (Marker='o') to an upward triangle
(Marker='^') with a bold '!' text overlay centered on the triangle —
the universal caution/warning visual language TrendMiner uses.
Rendering:
- Triangle line with MarkerSize = theme.EventMarkerSize * 2 (triangles
read smaller than circles at equal MarkerSize, and we need room for
the '!' glyph inside).
- Triangle face color:
* open events -> 'none' (hollow outline)
* closed events -> severityColor (solid fill)
- '!' text object overlaid at (StartTime, yVal):
* HitTest='off', PickableParts='none' -> clicks pass through to the
triangle's ButtonDownFcn
* Tag='FastSenseEventMarker' (same tag as triangle) so the loupe
ButtonDownFcn-overwrite loop skips both
* Color: severityColor on hollow triangles (visible on white bg),
white on filled triangles (contrast against severity color)
* FontSize ~65% of triangle size for legibility at any zoom
- Both triangle and '!' handles tracked in EventMarkerHandles_ for
idempotent rebuild.
Test updates:
- TestFastSenseEventClick: rename findRoundMarkers -> findEventMarkers,
search by Tag='FastSenseEventMarker' instead of Marker shape. Makes
the finder marker-shape-agnostic so future icon swaps won't break the
test suite. Also makes the Octave mirror's helper search by Tag
(kept the function name 'findRoundMarkers' to minimize diff).
- Existing assertions (MarkerFaceColor='none' for open, non-none for
closed; ButtonDownFcn set; UserData.eventId = ev.Id) still hold because
we only changed the Marker shape, not the color/click contract.
…lyph
Replace the triangle+! markers with the look from the user's reference
screenshot: a white circular badge with a soft grey ring and a
severity-colored Unicode glyph centered inside.
- Badge: Marker='o', MarkerSize = theme.EventMarkerSize * 2.6 for
generous room around the glyph.
* Closed event -> MarkerFaceColor = white, ring in neutral grey
* Open event -> MarkerFaceColor = 'none', ring in severity color
(so the active state reads as 'glowing outline' over the signal)
- Glyph: Unicode U+27F2 '⟲' (anticlockwise gapped circle arrow), bold,
sized at ~55% of badge. Rendered via text() with HitTest='off' so
clicks pass through to the badge. If the system font lacks the glyph
the marker still renders as a bare badge; the ButtonDownFcn still
fires.
- LineWidth bumped to 1.2 so the ring reads cleanly at all badge sizes.
- Open/closed distinction preserved in MarkerFaceColor ('none' vs white),
so TestFastSenseEventClick.testOpenEventMarkerIsHollow and
testClosedEventMarkerIsFilled assertions remain green without change.
Note on JPG markers: MATLAB can render raster icons via image() or
imshow() at axes coordinates, but for a small glyph per event the
marker+text approach is faster and platform-font-independent. If the
team decides to ship a branded icon later, drop the glyph and attach
an invisible image() at (StartTime, yVal) with the PNG loaded via
imread() — same ButtonDownFcn wiring works unchanged.
…xample
User asked for an exclamation mark instead of the refresh-arrow glyph,
and wanted severity-based coloring visible in the demo.
- FastSense.renderEventLayer_: glyph is now '!' (font-safe on every
platform, no Unicode-font-coverage concerns). Everything else in the
badge design stays: white fill for closed, outline-only for open,
ring + glyph in severity color.
- example_event_markers: after the last tick, call a new
assignSeverityByPeak(store, tagKey, threshold) helper that walks the
events and assigns:
ratio = PeakValue / threshold
>= 1.5 -> 3 (alarm, red)
>= 1.1 -> 2 (warn, orange)
else -> 1 (info, green)
Expected result in the demo:
pump_a_high peak 10 / thr 5 ratio 2.00 -> alarm (red)
motor spike 1 peak 92 / thr 85 ratio 1.08 -> info (green)
motor spike 2 peak 95 / thr 85 ratio 1.12 -> warn (orange)
motor spike 3 peak 91 / thr 85 ratio 1.07 -> info (green)
So the user sees 1 red, 1 orange, 2 green — the full severity palette
on one screen.
Second onLiveTick + tickAll after severity assignment rebuilds the
EventMarkerHandles_ so the new colors take effect immediately.
Screenshot showed two bugs: 1. Pump marker rendered with '!' inside, motor markers just empty badges 2. All markers green regardless of peak — severity-by-peak had no effect Root causes: A. Z-order on mixed line+text uistack. A combined uistack call on a handle list mixing line and text objects left text behind the adjacent signal line when the signal line passed through the marker center (as motor's spike-peak data does). Workaround: iterate the handle list and call uistack one-handle-at-a-time — reliable on R2020b/macOS. B. Text Clipping cut the glyph when the marker sat near the axes edge. Added 'Clipping', 'off' on the '!' text so it always renders. C. assignSeverityByPeak bailed out when ev.PeakValue was empty, which it typically is for events closed via MonitorTag.appendData tail path (the running-stats pipeline is not guaranteed to populate PeakValue for sub-sample-width spikes). Fix: compute the peak directly from the parent SensorTag's Y data between StartTime and EndTime (reads xs/ys via the existing SensorTag public accessors), then also mirror it back to ev.PeakValue via setStats() so the details popup shows the correct peak. Signature of assignSeverityByPeak gains a sensorTag argument; example updated to pass it. Expected visual after re-run: pump_a_high peak 10 / thr 5 ratio 2.00 -> alarm (red) motor spike 1 peak 92 / thr 85 ratio 1.08 -> info (green) motor spike 2 peak 95 / thr 85 ratio 1.12 -> warn (orange) motor spike 3 peak 91 / thr 85 ratio 1.07 -> info (green)
…rkers
Two fixes:
1. Widget-level marker-diff missed severity mutations
refreshEventMarkers_ cached only event IDs and IsOpen flags — mutating
ev.Severity afterwards didn't trigger a re-render, so the demo's
assignSeverityByPeak had no visual effect.
Added LastEventSeverity_ (numeric array parallel to LastEventIds_)
and a per-event severity comparison in the diff loop. Any severity
bump now fires refreshEventLayer() and the new color takes effect.
2. Drop shadow around the badge
User asked for the soft-shadow look in the TrendMiner reference.
MATLAB line markers don't support MarkerFaceAlpha, so shadows are
rendered via two concentric scatter disks behind the badge:
- outer: (badgeSize + 7)^2, alpha 0.10
- inner: (badgeSize + 3)^2, alpha 0.18
Both dark grey ([0.1 0.1 0.15]), HitTest='off' so clicks go straight
to the badge. Tagged 'FastSenseEventMarker' so the loupe overwrite
loop skips them and the uistack pass lifts them with the badge.
Wrapped in try/catch — older runtimes without scatter-alpha fall
through cleanly to the badge-only render.
… doesn't wipe signal lines
scatter() clears the axes when hold is off — which erased the blue pump
and motor signal traces the moment the shadow layer was drawn. Symptom
user reported: 'graphs are empty, were flickering before'.
renderEventLayer_ now:
1. Reads current hold state into prevHoldWasOn
2. Forces hold(axes, 'on') for the entire render pass
3. At the end (after the per-handle uistack loop), restores to 'off'
if it was off before.
line() and text() were never affected by this because they add objects
without the plot/scatter clear-first semantics. Only the new scatter
shadow triggered the bug.
…ERIFICATION status -> passed
…, clean MILESTONES.md v2.0 Tag-Based Domain Model (Phases 1004-1012) shipped 2026-04-24. - Moved .planning/phases/1012-*/ into .planning/milestones/v2.0-phases/ alongside 1004-1011 (archived earlier at 2026-04-17). Phase 1012 was added as an extension after the original milestone close; this catches the archive back up. - Moved v2.0-MILESTONE-AUDIT.md into .planning/milestones/ (matches where v1.0-MILESTONE-AUDIT.md lives). - Rewrote MILESTONES.md v2.0 entry — the gsd-tools CLI auto-extraction had globbed every SUMMARY.md on disk (including v1.0-era leftovers in .planning/phases/) producing 30 lines of junk one-liners. Replaced with a curated 7-bullet summary + tech-debt callout. - Collapsed .planning/ROADMAP.md: per-phase details for all shipped milestones are now inside <details> summaries; v2.0 section points to milestones/v2.0-ROADMAP.md. File shrank from 388 to ~100 lines. Backlog entries preserved plus a new "Carried-over tech debt" section pointing at the audit. - Updated STATE.md to 'between_milestones' with v2.1-planning pointer. Tech debt in .planning/phases/: 10 directories from v1.0-era milestones still live there unarchived. Not touched in this commit — /gsd:cleanup is the right tool for that and can be run next.
… into v1.0-phases/ Phases 01 (Perf Opt), 1000-1003 (First-Class Thresholds), 1004-1006 (CI expansion + MATLAB test fixes), plus 999.1 and 999.3 backlog — all were completed before the v2.0 Tag-Based Domain Model milestone but were never archived. Moved into .planning/milestones/v1.0-phases/ alongside the original v1.0 FastSense Advanced Dashboard phases. After move: .planning/phases/ is empty; v1.0-phases/ has 19 directories.
Resolves Phase 1012 (live event markers + click-to-details) against main's concurrent evolution (~30 commits since the merge-base at 6502d30): Phase 1013 MEX binaries prebuilt Phase 1014 MATLAB test migration for v2.0 Tag API Phase 1015 showcase demo + theme trim Phase 1016 time slider rework PR #61 exclude .planning/ + .superpowers/ from repo (gitignore) PR #62 widget audit bugs PR #66 v2.0 test migration finish PR #69 per-widget render progress bar PR #72 Dashboard toolbar rework Conflict resolution summary: libs/Dashboard/FastSenseWidget.m (4 chunks, manual): - properties block: both sides added properties; kept all (ShowEventMarkers + EventStore + LiveViewMode) - refresh() + update() try blocks: kept both post-updateData actions (obj.refreshEventMarkers_() + obj.formatTimeAxis_(ax)) - private methods: kept both new methods side-by-side (refreshEventMarkers_ and formatTimeAxis_) libs/FastSense/FastSense.m — auto-merged cleanly (31 Phase-1012 markers + 13 main markers present) libs/Dashboard/DashboardTheme.m — auto-merged cleanly (EventMarkerSize=8 preserved) .planning/ + .superpowers/ — untracked via git rm -r --cached per main's PR #61 gitignore policy. Planning artifacts remain on disk for local reference. No test runs yet; recommend running tests/suite/TestEventIsOpen, TestMonitorTagOpenEvent, TestFastSenseEventClick, TestFastSenseWidgetEventMarkers after pulling to verify the merged FastSenseWidget.m still satisfies Phase-1012 contracts alongside main's FormatTimeAxis additions.
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
MATLAB Lint (3 issues): - Remove double blank line before severityToColor_ - Strip 2 spurious trailing semicolons in buildEventFieldsTable_ cell literals MATLAB Tests — TestFastSenseEventClick (4 errors): - Move onEventMarkerClick_ from private to Hidden (callable-but-not-listed) so the test can dispatch it directly - Move formatEventFields_ + buildEventFieldsTable_ from protected to Hidden (MATLAB enforces protected-only-for-subclasses even in test context; Hidden gives us public callable semantics) Octave Tests — 3 eq-on-handle failures: - FastSenseWidget.refresh: wrap 'obj.Tag == obj.LastTagRef' in try/catch with Key-based fallback. Octave has no overloaded eq for handle classes; matches Phase 1006 precedent (SIGILL on isequal, use Key string compare instead). - test_fastsense_widget_event_markers: replace 'w.FastSenseObj.EventStore == es' with class + FilePath equality. Pre-existing main-side failures (TestTheme, TestFastSenseTheme, TestDemoIndustrialPlantHeadless, TestDemoIndustrialPlantPipeline) remain — main CI has been red for the last 3 builds too, so they predate this PR.
TestFastSenseEventClick calls these four methods directly:
fp.openEventDetails_(ev)
fp.closeEventDetails_()
fp.onKeyPressForDetailsDismiss_(struct('Key','escape'))
(plus saveEventNotes_ + fitDetailsTableColumns_ for future tests)
All were inside methods(Access=private), rejected by MATLAB's
MethodRestricted enforcement from outside the class. Move them into a
dedicated methods(Hidden) block — callable from outside but not listed
in methods(obj) so the public surface stays clean.
TestFastSenseEventClick verifies 'isempty(fp.hEventDetails_)' to check popup lifecycle state. hEventDetails_ was in properties(Access=private), rejecting external reads with MATLAB:class:GetProhibited. Move it into its own properties(SetAccess=private) block — default GetAccess is public there, so tests (and any diagnostic tooling) can read the handle. Only the class can write to it.
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
Phase 1012 extension of v2.0 Tag-Based Domain Model — ships live event markers with per-marker click-to-details on
FastSenseandFastSenseWidget, plus editable persistent notes per event.Tagged as v2.0.1 (extension on top of the v2.0 ship point).
What's new
Event.IsOpenlogical property +EventStore.closeEvent(id, endTime, finalStats)in-place update. Events become visible as hollow markers the moment they are detected; transition to filled when closed.cache_.openStats_during live ticks; falling edge callscloseEventwith the final stats.!glyph. Severity→color mapping:info→green,warn→orange,alarm→red.figure(OS-native drag + close), light theme, section-groupeduitablefield listing with resize-aware columns (1:2 split between Field and Value), editable Notes textarea withSave notesbutton that mutatesEvent.Notesand callsEventStore.save()for disk persistence.ShowEventMarkers(defaultfalse) andEventStoreproperties; opt-in forwarding guardif obj.ShowEventMarkers || ~isempty(obj.EventStore)preserves Phase-1010'sFastSense.ShowEventMarkers=truedefault for bare-FastSense users. Live-tick marker diff againstLastEventIds_/LastEventOpen_/LastEventSeverity_triggers re-render on adds, removes, open→closed transitions, and severity bumps.examples/example_event_markers.mwith two sensors (pump sustained violation + motor multi-spike) sharing one disk-backed EventStore, demonstrating the full open→close lifecycle with severity-assigned colors.Test plan
tests/suite/TestEventIsOpen.m— Event schema + EventStore.closeEvent + backward-compat .mat reloadtests/suite/TestMonitorTagOpenEvent.m— rising-edge IsOpen=true emission + falling-edge closeEvent + running statstests/suite/TestFastSenseEventClick.m— per-marker ButtonDownFcn + UserData.eventId + open/closed marker face color + popup lifecycletests/suite/TestFastSenseWidgetEventMarkers.m— widget property wiring + toStruct/fromStruct round-trip + marker difftests/test_*.m)tests/test_monitortag_streaming.mupdated to Phase-1012 open-event semantics (was asserting pre-1012 EndTime=10)benchmarks/bench_event_marker_regression.m— zero-event 12-line FastSense render Pitfall-10 gate (≤5% regression) — landed at -4.53% / -1.32%tests/test_fastsense_event_overlay.mstill green (ShowEventMarkersdefault preserved)Merge conflicts expected
mainhas advanced independently since this branch was cut. The following 3 files will need manual conflict resolution:libs/FastSense/FastSense.m— integrate 1012'srenderEventLayer_refactor with main's LiveViewMode / time-axis / widget-rework additionslibs/Dashboard/FastSenseWidget.m— integrate 1012'sShowEventMarkers/EventStore/marker-diff properties with main'sLiveViewMode/UserZoomedY/autoScaleY_/formatTimeAxis_libs/Dashboard/DashboardTheme.m— integrate 1012'sEventMarkerSize=8constant with main's Phase-1015 theme trimOther 15 files (Event.m, EventStore.m, MonitorTag.m + all tests + bench + example) have no overlap with main.
Notes
.planning/artifacts (17 commits across plan/context/research/validation/verification/summary MD files). They're safe to ignore or filter on review — the code review focus should be on thelibs/+tests/+examples/+benchmarks/changes.🤖 Generated with Claude Code