|
19 | 19 | YLabel = '' % Y-axis label (auto-set from Sensor if empty) |
20 | 20 | YLimits = [] % Fixed Y-axis range [min max]; empty = auto-scale |
21 | 21 | ShowThresholdLabels = false % show inline name labels on threshold lines |
| 22 | + % Forwarded to FastSense.LiveViewMode on render: |
| 23 | + % 'reset' — window covers the full X range every tick (default: |
| 24 | + % matches dashboard-demo expectation that users see |
| 25 | + % every sample since session start) |
| 26 | + % 'follow' — window of current width tracks the latest sample |
| 27 | + % (use for long-running deployments where the full |
| 28 | + % range would exhaust memory / downsampling budget) |
| 29 | + % 'preserve' — frozen at the initial X range (legacy behaviour) |
| 30 | + LiveViewMode = 'reset' |
22 | 31 | end |
23 | 32 | % (Tag property now lives on the DashboardWidget base class — Plan 1009-02.) |
24 | 33 |
|
25 | 34 | properties (SetAccess = private) |
26 | 35 | FastSenseObj = [] |
27 | 36 | IsSettingTime = false % guard to distinguish programmatic vs user xlim change |
| 37 | + IsSettingYLim = false % guard so autoScaleY_ does not flip UserZoomedY |
| 38 | + UserZoomedY = false % true after user mouse-zooms Y; suspends autoScaleY_ |
28 | 39 | CachedXMin = inf % cached minimum of X data for O(1) getTimeRange() |
29 | 40 | CachedXMax = -inf % cached maximum of X data for O(1) getTimeRange() |
30 | 41 | LastTagRef = [] % Tag handle snapshot for cache-invalidation |
@@ -71,14 +82,14 @@ function render(obj, parentPanel) |
71 | 82 | obj.FastSenseObj = fp; |
72 | 83 | fp.ShowThresholdLabels = obj.ShowThresholdLabels; |
73 | 84 |
|
74 | | - % Slide the X window as new samples arrive on updateData(). The |
75 | | - % default empty LiveViewMode leaves the window frozen at the |
76 | | - % initial render's data range, so later samples appear off the |
77 | | - % right edge of live widgets. 'reset' grows the window to cover |
78 | | - % the full current X range each tick — appropriate for |
79 | | - % dashboard use where users expect to see all data since the |
80 | | - % live pipeline started. |
81 | | - fp.LiveViewMode = 'reset'; |
| 85 | + % Slide the X window as new samples arrive on updateData(). |
| 86 | + % Forwarded from the widget-level LiveViewMode property so |
| 87 | + % callers can swap between 'reset' (default: window grows to |
| 88 | + % cover all samples — best for short demos), 'follow' (fixed- |
| 89 | + % width window tracking the latest sample — best for long- |
| 90 | + % running deployments), and 'preserve' (frozen at the initial |
| 91 | + % X range — legacy behaviour). |
| 92 | + fp.LiveViewMode = obj.LiveViewMode; |
82 | 93 |
|
83 | 94 | % Bind data — Tag-first dispatch (v2.0). |
84 | 95 | if ~isempty(obj.Tag) |
@@ -148,6 +159,12 @@ function render(obj, parentPanel) |
148 | 159 | addlistener(ax, 'XLim', 'PostSet', @(~,~) obj.onXLimChanged()); |
149 | 160 | catch |
150 | 161 | end |
| 162 | + % Listen for manual Y zoom so autoScaleY_ stops fighting the |
| 163 | + % user after a scroll / drag / programmatic ylim. |
| 164 | + try |
| 165 | + addlistener(ax, 'YLim', 'PostSet', @(~,~) obj.onYLimChanged()); |
| 166 | + catch |
| 167 | + end |
151 | 168 | end |
152 | 169 |
|
153 | 170 | function refresh(obj) |
@@ -205,10 +222,16 @@ function autoScaleY_(obj, y) |
205 | 222 | % samples outside the initial range would fall off the chart. |
206 | 223 | % This helper recomputes the Y extent every tick (including any |
207 | 224 | % threshold values so MonitorTag lines stay visible) and updates |
208 | | - % the axes. Skipped when the widget has a user-pinned YLimits. |
| 225 | + % the axes. Skipped when: |
| 226 | + % - the widget has a user-pinned YLimits NV-pair, or |
| 227 | + % - the user manually zoomed Y via mouse (UserZoomedY), |
| 228 | + % so we never fight an explicit human interaction. |
209 | 229 | if ~isempty(obj.YLimits) |
210 | 230 | return; |
211 | 231 | end |
| 232 | + if obj.UserZoomedY |
| 233 | + return; |
| 234 | + end |
212 | 235 | if isempty(obj.FastSenseObj) || ~obj.FastSenseObj.IsRendered |
213 | 236 | return; |
214 | 237 | end |
@@ -241,7 +264,25 @@ function autoScaleY_(obj, y) |
241 | 264 | else |
242 | 265 | pad = max(abs(yMax) * 0.1, 1); |
243 | 266 | end |
244 | | - set(ax, 'YLim', [yMin - pad, yMax + pad]); |
| 267 | + obj.IsSettingYLim = true; |
| 268 | + try |
| 269 | + set(ax, 'YLim', [yMin - pad, yMax + pad]); |
| 270 | + catch |
| 271 | + end |
| 272 | + obj.IsSettingYLim = false; |
| 273 | + end |
| 274 | + |
| 275 | + function onYLimChanged(obj) |
| 276 | + %ONYLIMCHANGED Detach widget from automatic Y rescale after user zoom. |
| 277 | + % Fired by the YLim PostSet listener. When the YLim change came |
| 278 | + % from inside autoScaleY_ (IsSettingYLim==true) we ignore it; any |
| 279 | + % other source — mouse scroll, drag, zoom toolbar, programmatic |
| 280 | + % ylim() from user code — counts as a manual override and |
| 281 | + % latches UserZoomedY so live ticks stop fighting the user. |
| 282 | + if obj.IsSettingYLim |
| 283 | + return; |
| 284 | + end |
| 285 | + obj.UserZoomedY = true; |
245 | 286 | end |
246 | 287 |
|
247 | 288 | function setTimeRange(obj, tStart, tEnd) |
|
0 commit comments