Skip to content

Commit 2e2537c

Browse files
authored
fix(dashboard): widget audit bugs — titles, IconCard, Image, resize hooks, time axis, axis labels (#62)
* fix(quick-260423-q3v-01): add title rendering to 5 chart widgets - BarChart/Histogram/Scatter/Heatmap/Image widgets now render Title on axes - Uses canonical title block from RawAxesWidget (fg color + WidgetTitleFontSize) - ImageWidget keeps axes invisible but forces title Visible='on' * fix(quick-260423-q3v-02): IconCardWidget Tag branch falls back to Tag.Y(end) - valueAt(now) returns [] when Tag.X is seconds (not serial date) - Added Tag.Y(end) fallback mirroring the Sensor branch - Preserves Units inheritance block unchanged * fix(quick-260423-q3v-03): ImageWidget uses imagesc with explicit colormap for 2D data - Branch on ndims(imgData)==2: imagesc+parula for matrices, image() for RGB - image() was clipping scalar-field matrices to colormap 1..64, rendering a dark block - Title kept visible in refresh() too (idempotent with render() setting) - Scaling property untouched (API compat) * fix(quick-260423-q3v-04): add SizeChangedFcn+relayout_ to 7 pixel-dependent widgets - ChipBar/IconCard/MultiStatus/Number/Sparkline/Status/Text now rescale on resize - relayout_ tears down uicontrols+axes children and re-renders (idempotent) - try/catch guards SizeChangedFcn for Octave versions that reject it - New private methods block added to SparklineCardWidget and TextWidget * fix(quick-260423-q3v-05): human-readable time-axis labels via formatTimeAxis_ - FastSenseWidget: call formatTimeAxis_ after fp.render() in render/rebuildForTag_ and after updateData() in refresh/update paths - EventTimelineWidget: call formatTimeAxis_ at end of refresh() - Helper converts numeric seconds to HH:MM:SS (range >= 1h) or MM:SS (< 1h) via datestr(xt/86400, fmt); no-op for ranges <= 300s - Cosmetic-only: underlying numeric X data untouched, zoom may regenerate numeric ticks (known limitation) * fix(quick-260423-q3v-06): ScatterWidget auto-derives xlabel/ylabel from SensorX/SensorY - refresh() now calls xlabel/ylabel after plot when SensorX/SensorY set - axisLabelForSensor_ helper builds 'Name (Units)' with graceful Key/empty fallback - No-op when sensors unset (preserves current behavior) * fix(widgets): title re-apply in refresh + thermometer aspect ratio BarChartWidget.refresh and HistogramWidget.refresh now re-apply the title after bar()/plot() calls. Those plot commands internally call newplot which clears the axes title, so the render-time title was being wiped the moment data rendered. GaugeWidget.renderThermometer drops the fixed DataAspectRatio [1 2 1] and widens its axes Position ([0.15 0.10 0.7 0.80]) so the thermometer fills a narrow 4-column panel instead of cramping the bulb, value, and min/max labels on top of each other. Follow-up to quick-task 260423-q3v audit bugs. * fix(IconCardWidget): stop self-erasing Tag via Sensor alias The constructor's "if Tag is set, clear Sensor" block was destructive: after Phase 1011 migrated Sensor to a Dependent alias for Tag (see DashboardWidget.set.Sensor), `obj.Sensor = []` resolves to `obj.Tag = []` and wipes the Tag we just assigned. The refresh loop then hits the empty-Tag branch and the widget shows "--" forever, even with a fully populated SensorTag bound. This was the real root cause of bug #2 in the widget audit — the valueAt(now)/Y(end) fallback I added in quick-260423-q3v-02 never ran because Tag was already empty by the time refresh fired. Kept the Threshold clear (Threshold is a real independent property).
1 parent c289374 commit 2e2537c

15 files changed

Lines changed: 227 additions & 5 deletions

libs/Dashboard/BarChartWidget.m

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ function render(obj, parentPanel)
2727
'Color', theme.WidgetBackground, ...
2828
'XColor', theme.AxisColor, ...
2929
'YColor', theme.AxisColor);
30+
if ~isempty(obj.Title)
31+
title(obj.hAxes, obj.Title, ...
32+
'Color', theme.ForegroundColor, ...
33+
'FontSize', theme.WidgetTitleFontSize);
34+
end
3035
obj.refresh();
3136
end
3237

@@ -83,6 +88,13 @@ function refresh(obj)
8388
set(obj.hAxes, 'XTick', 1:numel(cats), 'XTickLabel', cats);
8489
end
8590
end
91+
% Re-apply title after plot commands (bar/barh may clear via newplot)
92+
if ~isempty(obj.Title)
93+
theme = obj.getTheme();
94+
title(obj.hAxes, obj.Title, ...
95+
'Color', theme.ForegroundColor, ...
96+
'FontSize', theme.WidgetTitleFontSize);
97+
end
8698
end
8799

88100
function t = getType(~)

libs/Dashboard/ChipBarWidget.m

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@
5454
function render(obj, parentPanel)
5555
%RENDER Draw all chips in a single shared axes inside parentPanel.
5656
obj.hPanel = parentPanel;
57+
% Re-layout on resize so pixel-scaled fonts/geometry stay correct.
58+
try obj.hPanel.SizeChangedFcn = @(~,~) obj.relayout_(); catch, end
5759
theme = obj.getTheme();
5860

5961
nChips = numel(obj.Chips);
@@ -224,6 +226,14 @@ function refresh(obj)
224226
end
225227

226228
methods (Access = private)
229+
function relayout_(obj)
230+
%RELAYOUT_ Rebuild pixel-scaled elements on panel resize.
231+
if isempty(obj.hPanel) || ~ishandle(obj.hPanel), return; end
232+
try delete(findobj(obj.hPanel, '-depth', 1, 'Type', 'uicontrol')); catch, end
233+
try delete(findobj(obj.hPanel, '-depth', 1, 'Type', 'axes')); catch, end
234+
obj.render(obj.hPanel);
235+
end
236+
227237
function chipColor = resolveChipColor(~, chip, theme)
228238
%RESOLVECHIPCOLOR Map chip struct to an [r g b] color triple.
229239
%

libs/Dashboard/EventTimelineWidget.m

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,9 @@ function refresh(obj)
156156

157157
set(obj.hAxes, 'YTick', 1:numel(labels), 'YTickLabel', labels);
158158
set(obj.hAxes, 'YLim', [0.3, numel(labels) + 0.7]);
159+
160+
% Reformat time-axis ticks to HH:MM:SS / MM:SS for readability.
161+
obj.formatTimeAxis_(obj.hAxes);
159162
end
160163

161164
function t = getType(~)
@@ -357,5 +360,28 @@ function onXLimChanged(obj)
357360
end
358361
end
359362

363+
function formatTimeAxis_(~, ax)
364+
%FORMATTIMEAXIS_ Replace numeric-seconds x-ticks with HH:MM:SS labels.
365+
% No-op when range <= 300s (raw seconds readable) or ax invalid.
366+
if isempty(ax) || ~ishandle(ax), return; end
367+
xl = get(ax, 'XLim');
368+
rangeSec = xl(2) - xl(1);
369+
if rangeSec <= 300, return; end
370+
xt = get(ax, 'XTick');
371+
if isempty(xt), return; end
372+
if rangeSec >= 3600
373+
fmt = 'HH:MM:SS';
374+
else
375+
fmt = 'MM:SS';
376+
end
377+
lbl = cell(1, numel(xt));
378+
for i = 1:numel(xt)
379+
% xt(i) is seconds; serial-date day = seconds / 86400
380+
lbl{i} = datestr(xt(i) / 86400, fmt);
381+
end
382+
set(ax, 'XTickMode', 'manual', 'XTickLabelMode', 'manual', ...
383+
'XTickLabel', lbl);
384+
end
385+
360386
end
361387
end

libs/Dashboard/FastSenseWidget.m

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,9 @@ function render(obj, parentPanel)
9898

9999
fp.render();
100100

101+
% Reformat time-axis ticks to HH:MM:SS / MM:SS for readability.
102+
obj.formatTimeAxis_(ax);
103+
101104
% Apply fixed Y-axis limits if configured
102105
if ~isempty(obj.YLimits) && numel(obj.YLimits) == 2
103106
ylim(ax, obj.YLimits);
@@ -132,6 +135,7 @@ function refresh(obj)
132135
[x, y] = obj.Tag.getXY();
133136
obj.FastSenseObj.updateData(1, x, y);
134137
obj.updateTimeRangeCache();
138+
obj.formatTimeAxis_(obj.FastSenseObj.hAxes);
135139
return;
136140
catch
137141
% fall through to full teardown/rebuild
@@ -153,6 +157,7 @@ function update(obj)
153157
[x, y] = obj.Tag.getXY();
154158
obj.FastSenseObj.updateData(1, x, y);
155159
obj.updateTimeRangeCache();
160+
obj.formatTimeAxis_(obj.FastSenseObj.hAxes);
156161
return;
157162
catch
158163
% fall through to refresh()
@@ -267,6 +272,29 @@ function onXLimChanged(obj)
267272
end
268273

269274
methods (Access = private)
275+
function formatTimeAxis_(~, ax)
276+
%FORMATTIMEAXIS_ Replace numeric-seconds x-ticks with HH:MM:SS labels.
277+
% No-op when range <= 300s (raw seconds readable) or ax invalid.
278+
if isempty(ax) || ~ishandle(ax), return; end
279+
xl = get(ax, 'XLim');
280+
rangeSec = xl(2) - xl(1);
281+
if rangeSec <= 300, return; end
282+
xt = get(ax, 'XTick');
283+
if isempty(xt), return; end
284+
if rangeSec >= 3600
285+
fmt = 'HH:MM:SS';
286+
else
287+
fmt = 'MM:SS';
288+
end
289+
lbl = cell(1, numel(xt));
290+
for i = 1:numel(xt)
291+
% xt(i) is seconds; serial-date day = seconds / 86400
292+
lbl{i} = datestr(xt(i) / 86400, fmt);
293+
end
294+
set(ax, 'XTickMode', 'manual', 'XTickLabelMode', 'manual', ...
295+
'XTickLabel', lbl);
296+
end
297+
270298
function updateTimeRangeCache(obj)
271299
%UPDATETIMERANGECACHE Maintain CachedXMin/CachedXMax incrementally.
272300
% For sorted time arrays (the common case) the last element is the
@@ -339,6 +367,9 @@ function rebuildForTag_(obj)
339367

340368
fp.render();
341369

370+
% Reformat time-axis ticks to HH:MM:SS / MM:SS for readability.
371+
obj.formatTimeAxis_(ax);
372+
342373
if ~isempty(obj.YLimits) && numel(obj.YLimits) == 2
343374
ylim(ax, obj.YLimits);
344375
end

libs/Dashboard/GaugeWidget.m

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -501,10 +501,9 @@ function renderThermometer(obj, parentPanel)
501501

502502
obj.hAxes = axes('Parent', parentPanel, ...
503503
'Units', 'normalized', ...
504-
'Position', [0.3 0.15 0.4 0.7], ...
504+
'Position', [0.15 0.10 0.7 0.80], ...
505505
'Visible', 'off', ...
506-
'XLim', [-0.5 1.5], 'YLim', [-0.3 1.3], ...
507-
'DataAspectRatio', [1 2 1], ...
506+
'XLim', [-0.5 1.5], 'YLim', [-0.3 1.4], ...
508507
'HitTest', 'off');
509508
try set(obj.hAxes, 'PickableParts', 'none'); catch , end
510509
try disableDefaultInteractivity(obj.hAxes); catch , end

libs/Dashboard/HeatmapWidget.m

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@ function render(obj, parentPanel)
3333
'XColor', theme.AxisColor, ...
3434
'YColor', theme.AxisColor);
3535

36+
if ~isempty(obj.Title)
37+
title(obj.hAxes, obj.Title, ...
38+
'Color', theme.ForegroundColor, ...
39+
'FontSize', theme.WidgetTitleFontSize);
40+
end
41+
3642
obj.refresh();
3743
end
3844

libs/Dashboard/HistogramWidget.m

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ function render(obj, parentPanel)
2727
'Color', theme.WidgetBackground, ...
2828
'XColor', theme.AxisColor, ...
2929
'YColor', theme.AxisColor);
30+
if ~isempty(obj.Title)
31+
title(obj.hAxes, obj.Title, ...
32+
'Color', theme.ForegroundColor, ...
33+
'FontSize', theme.WidgetTitleFontSize);
34+
end
3035
obj.refresh();
3136
end
3237

@@ -70,6 +75,13 @@ function refresh(obj)
7075
plot(obj.hAxes, xFit, yFit, 'r-', 'LineWidth', 1.5);
7176
hold(obj.hAxes, 'off');
7277
end
78+
% Re-apply title after plot commands (bar/plot may clear via newplot)
79+
if ~isempty(obj.Title)
80+
theme = obj.getTheme();
81+
title(obj.hAxes, obj.Title, ...
82+
'Color', theme.ForegroundColor, ...
83+
'FontSize', theme.WidgetTitleFontSize);
84+
end
7385
obj.Dirty = false;
7486
end
7587

libs/Dashboard/IconCardWidget.m

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,10 @@
7575
end
7676
if ~isempty(obj.Tag)
7777
obj.Threshold = [];
78-
obj.Sensor = [];
78+
% NOTE: do NOT clear obj.Sensor here. Sensor is a Dependent
79+
% alias for Tag (see DashboardWidget.set.Sensor) — setting
80+
% it to [] wipes the Tag we just stored, causing the widget
81+
% to render "--" forever.
7982
end
8083
% Mutual exclusivity: Threshold wins (per D-08)
8184
if ~isempty(obj.Threshold) && ~isempty(obj.Sensor)
@@ -86,6 +89,8 @@
8689
function render(obj, parentPanel)
8790
%RENDER Create icon, value text, and label inside parentPanel.
8891
obj.hPanel = parentPanel;
92+
% Re-layout on resize so pixel-scaled fonts/geometry stay correct.
93+
try obj.hPanel.SizeChangedFcn = @(~,~) obj.relayout_(); catch, end
8994
theme = obj.getTheme();
9095

9196
bgColor = theme.WidgetBackground;
@@ -161,6 +166,8 @@ function refresh(obj)
161166
v = obj.Tag.valueAt(now);
162167
if ~isempty(v) && ~any(isnan(v))
163168
obj.CurrentValue = v;
169+
elseif isprop(obj.Tag, 'Y') && ~isempty(obj.Tag.Y)
170+
obj.CurrentValue = obj.Tag.Y(end);
164171
end
165172
if isempty(obj.Units) && isprop(obj.Tag, 'Units') && ~isempty(obj.Tag.Units)
166173
obj.Units = obj.Tag.Units;
@@ -333,6 +340,14 @@ function refresh(obj)
333340
end
334341

335342
methods (Access = private)
343+
function relayout_(obj)
344+
%RELAYOUT_ Rebuild pixel-scaled elements on panel resize.
345+
if isempty(obj.hPanel) || ~ishandle(obj.hPanel), return; end
346+
try delete(findobj(obj.hPanel, '-depth', 1, 'Type', 'uicontrol')); catch, end
347+
try delete(findobj(obj.hPanel, '-depth', 1, 'Type', 'axes')); catch, end
348+
obj.render(obj.hPanel);
349+
end
350+
336351
function color = resolveIconColor(obj, theme)
337352
%RESOLVEICONCOLOR Map current state to a theme color.
338353
switch obj.CurrentState

libs/Dashboard/ImageWidget.m

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,13 @@ function render(obj, parentPanel)
3434
'Position', [0.02 captionH+0.02 0.96 0.96-captionH], ...
3535
'Visible', 'off');
3636

37+
if ~isempty(obj.Title)
38+
title(obj.hAxes, obj.Title, ...
39+
'Color', theme.ForegroundColor, ...
40+
'FontSize', theme.WidgetTitleFontSize);
41+
try set(get(obj.hAxes, 'Title'), 'Visible', 'on'); catch, end
42+
end
43+
3744
if ~isempty(obj.Caption)
3845
obj.hCaption = uicontrol(parentPanel, ...
3946
'Style', 'text', ...
@@ -62,9 +69,20 @@ function refresh(obj)
6269
end
6370
if isempty(imgData), return; end
6471

65-
obj.hImage = image(obj.hAxes, imgData);
72+
% For matrices (not RGB uint8), use imagesc so CData auto-scales to
73+
% the colormap range -- image() would clip to 1..64 and render a dark block.
74+
if ndims(imgData) == 2
75+
obj.hImage = imagesc(obj.hAxes, imgData);
76+
colormap(obj.hAxes, 'parula');
77+
else
78+
obj.hImage = image(obj.hAxes, imgData);
79+
end
6680
axis(obj.hAxes, 'image');
6781
set(obj.hAxes, 'Visible', 'off');
82+
% Keep title visible even though axes is invisible (set by render()).
83+
if ~isempty(obj.Title)
84+
try set(get(obj.hAxes, 'Title'), 'Visible', 'on'); catch, end
85+
end
6886
end
6987

7088
function t = getType(~)

libs/Dashboard/MultiStatusWidget.m

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020

2121
function render(obj, parentPanel)
2222
obj.hPanel = parentPanel;
23+
% Re-layout on resize so pixel-scaled fonts/geometry stay correct.
24+
try obj.hPanel.SizeChangedFcn = @(~,~) obj.relayout_(); catch, end
2325
theme = obj.getTheme();
2426
obj.hAxes = axes('Parent', parentPanel, ...
2527
'Units', 'normalized', ...
@@ -232,6 +234,14 @@ function refresh(obj)
232234
end
233235

234236
methods (Access = private)
237+
function relayout_(obj)
238+
%RELAYOUT_ Rebuild pixel-scaled elements on panel resize.
239+
if isempty(obj.hPanel) || ~ishandle(obj.hPanel), return; end
240+
try delete(findobj(obj.hPanel, '-depth', 1, 'Type', 'uicontrol')); catch, end
241+
try delete(findobj(obj.hPanel, '-depth', 1, 'Type', 'axes')); catch, end
242+
obj.render(obj.hPanel);
243+
end
244+
235245
function expandedItems = expandSensors_(obj)
236246
%EXPANDSENSORS_ Expand CompositeThreshold/CompositeTag items into children + summary.
237247
% Non-composite items pass through unchanged.

0 commit comments

Comments
 (0)