From dd8ced854a340ed4595f05e075685356d841fcc4 Mon Sep 17 00:00:00 2001 From: Hannes Suhr Date: Thu, 23 Apr 2026 18:52:49 +0200 Subject: [PATCH 1/8] 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' --- libs/Dashboard/BarChartWidget.m | 5 +++++ libs/Dashboard/HeatmapWidget.m | 6 ++++++ libs/Dashboard/HistogramWidget.m | 5 +++++ libs/Dashboard/ImageWidget.m | 7 +++++++ libs/Dashboard/ScatterWidget.m | 5 +++++ 5 files changed, 28 insertions(+) diff --git a/libs/Dashboard/BarChartWidget.m b/libs/Dashboard/BarChartWidget.m index ac6dcb90..85035f3f 100644 --- a/libs/Dashboard/BarChartWidget.m +++ b/libs/Dashboard/BarChartWidget.m @@ -27,6 +27,11 @@ function render(obj, parentPanel) 'Color', theme.WidgetBackground, ... 'XColor', theme.AxisColor, ... 'YColor', theme.AxisColor); + if ~isempty(obj.Title) + title(obj.hAxes, obj.Title, ... + 'Color', theme.ForegroundColor, ... + 'FontSize', theme.WidgetTitleFontSize); + end obj.refresh(); end diff --git a/libs/Dashboard/HeatmapWidget.m b/libs/Dashboard/HeatmapWidget.m index 66384d1b..015f2972 100644 --- a/libs/Dashboard/HeatmapWidget.m +++ b/libs/Dashboard/HeatmapWidget.m @@ -33,6 +33,12 @@ function render(obj, parentPanel) 'XColor', theme.AxisColor, ... 'YColor', theme.AxisColor); + if ~isempty(obj.Title) + title(obj.hAxes, obj.Title, ... + 'Color', theme.ForegroundColor, ... + 'FontSize', theme.WidgetTitleFontSize); + end + obj.refresh(); end diff --git a/libs/Dashboard/HistogramWidget.m b/libs/Dashboard/HistogramWidget.m index ddd641a0..c458c6b0 100644 --- a/libs/Dashboard/HistogramWidget.m +++ b/libs/Dashboard/HistogramWidget.m @@ -27,6 +27,11 @@ function render(obj, parentPanel) 'Color', theme.WidgetBackground, ... 'XColor', theme.AxisColor, ... 'YColor', theme.AxisColor); + if ~isempty(obj.Title) + title(obj.hAxes, obj.Title, ... + 'Color', theme.ForegroundColor, ... + 'FontSize', theme.WidgetTitleFontSize); + end obj.refresh(); end diff --git a/libs/Dashboard/ImageWidget.m b/libs/Dashboard/ImageWidget.m index e093267a..e27349ac 100644 --- a/libs/Dashboard/ImageWidget.m +++ b/libs/Dashboard/ImageWidget.m @@ -34,6 +34,13 @@ function render(obj, parentPanel) 'Position', [0.02 captionH+0.02 0.96 0.96-captionH], ... 'Visible', 'off'); + if ~isempty(obj.Title) + title(obj.hAxes, obj.Title, ... + 'Color', theme.ForegroundColor, ... + 'FontSize', theme.WidgetTitleFontSize); + try set(get(obj.hAxes, 'Title'), 'Visible', 'on'); catch, end + end + if ~isempty(obj.Caption) obj.hCaption = uicontrol(parentPanel, ... 'Style', 'text', ... diff --git a/libs/Dashboard/ScatterWidget.m b/libs/Dashboard/ScatterWidget.m index 566d945f..29721187 100644 --- a/libs/Dashboard/ScatterWidget.m +++ b/libs/Dashboard/ScatterWidget.m @@ -29,6 +29,11 @@ function render(obj, parentPanel) 'Color', theme.WidgetBackground, ... 'XColor', theme.AxisColor, ... 'YColor', theme.AxisColor); + if ~isempty(obj.Title) + title(obj.hAxes, obj.Title, ... + 'Color', theme.ForegroundColor, ... + 'FontSize', theme.WidgetTitleFontSize); + end obj.refresh(); end From bc8d48715d1b6c854f18682958f9deb791099590 Mon Sep 17 00:00:00 2001 From: Hannes Suhr Date: Thu, 23 Apr 2026 18:53:01 +0200 Subject: [PATCH 2/8] 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 --- libs/Dashboard/IconCardWidget.m | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libs/Dashboard/IconCardWidget.m b/libs/Dashboard/IconCardWidget.m index d2698fa2..0bfc42a0 100644 --- a/libs/Dashboard/IconCardWidget.m +++ b/libs/Dashboard/IconCardWidget.m @@ -161,6 +161,8 @@ function refresh(obj) v = obj.Tag.valueAt(now); if ~isempty(v) && ~any(isnan(v)) obj.CurrentValue = v; + elseif isprop(obj.Tag, 'Y') && ~isempty(obj.Tag.Y) + obj.CurrentValue = obj.Tag.Y(end); end if isempty(obj.Units) && isprop(obj.Tag, 'Units') && ~isempty(obj.Tag.Units) obj.Units = obj.Tag.Units; From 927d71bdff9f4e4d7733e79c47b56dae8bb1f827 Mon Sep 17 00:00:00 2001 From: Hannes Suhr Date: Thu, 23 Apr 2026 18:53:22 +0200 Subject: [PATCH 3/8] 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) --- libs/Dashboard/ImageWidget.m | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/libs/Dashboard/ImageWidget.m b/libs/Dashboard/ImageWidget.m index e27349ac..d67d8cd6 100644 --- a/libs/Dashboard/ImageWidget.m +++ b/libs/Dashboard/ImageWidget.m @@ -69,9 +69,20 @@ function refresh(obj) end if isempty(imgData), return; end - obj.hImage = image(obj.hAxes, imgData); + % For matrices (not RGB uint8), use imagesc so CData auto-scales to + % the colormap range -- image() would clip to 1..64 and render a dark block. + if ndims(imgData) == 2 + obj.hImage = imagesc(obj.hAxes, imgData); + colormap(obj.hAxes, 'parula'); + else + obj.hImage = image(obj.hAxes, imgData); + end axis(obj.hAxes, 'image'); set(obj.hAxes, 'Visible', 'off'); + % Keep title visible even though axes is invisible (set by render()). + if ~isempty(obj.Title) + try set(get(obj.hAxes, 'Title'), 'Visible', 'on'); catch, end + end end function t = getType(~) From a6c0349ccc3581d93a3240f3b634e60737f5715e Mon Sep 17 00:00:00 2001 From: Hannes Suhr Date: Thu, 23 Apr 2026 18:55:43 +0200 Subject: [PATCH 4/8] 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 --- libs/Dashboard/ChipBarWidget.m | 10 ++++++++++ libs/Dashboard/IconCardWidget.m | 10 ++++++++++ libs/Dashboard/MultiStatusWidget.m | 10 ++++++++++ libs/Dashboard/NumberWidget.m | 10 ++++++++++ libs/Dashboard/SparklineCardWidget.m | 12 ++++++++++++ libs/Dashboard/StatusWidget.m | 10 ++++++++++ libs/Dashboard/TextWidget.m | 12 ++++++++++++ 7 files changed, 74 insertions(+) diff --git a/libs/Dashboard/ChipBarWidget.m b/libs/Dashboard/ChipBarWidget.m index 72e4ccb7..be35a62c 100644 --- a/libs/Dashboard/ChipBarWidget.m +++ b/libs/Dashboard/ChipBarWidget.m @@ -54,6 +54,8 @@ function render(obj, parentPanel) %RENDER Draw all chips in a single shared axes inside parentPanel. obj.hPanel = parentPanel; + % Re-layout on resize so pixel-scaled fonts/geometry stay correct. + try obj.hPanel.SizeChangedFcn = @(~,~) obj.relayout_(); catch, end theme = obj.getTheme(); nChips = numel(obj.Chips); @@ -224,6 +226,14 @@ function refresh(obj) end methods (Access = private) + function relayout_(obj) + %RELAYOUT_ Rebuild pixel-scaled elements on panel resize. + if isempty(obj.hPanel) || ~ishandle(obj.hPanel), return; end + try delete(findobj(obj.hPanel, '-depth', 1, 'Type', 'uicontrol')); catch, end + try delete(findobj(obj.hPanel, '-depth', 1, 'Type', 'axes')); catch, end + obj.render(obj.hPanel); + end + function chipColor = resolveChipColor(~, chip, theme) %RESOLVECHIPCOLOR Map chip struct to an [r g b] color triple. % diff --git a/libs/Dashboard/IconCardWidget.m b/libs/Dashboard/IconCardWidget.m index 0bfc42a0..c765f631 100644 --- a/libs/Dashboard/IconCardWidget.m +++ b/libs/Dashboard/IconCardWidget.m @@ -86,6 +86,8 @@ function render(obj, parentPanel) %RENDER Create icon, value text, and label inside parentPanel. obj.hPanel = parentPanel; + % Re-layout on resize so pixel-scaled fonts/geometry stay correct. + try obj.hPanel.SizeChangedFcn = @(~,~) obj.relayout_(); catch, end theme = obj.getTheme(); bgColor = theme.WidgetBackground; @@ -335,6 +337,14 @@ function refresh(obj) end methods (Access = private) + function relayout_(obj) + %RELAYOUT_ Rebuild pixel-scaled elements on panel resize. + if isempty(obj.hPanel) || ~ishandle(obj.hPanel), return; end + try delete(findobj(obj.hPanel, '-depth', 1, 'Type', 'uicontrol')); catch, end + try delete(findobj(obj.hPanel, '-depth', 1, 'Type', 'axes')); catch, end + obj.render(obj.hPanel); + end + function color = resolveIconColor(obj, theme) %RESOLVEICONCOLOR Map current state to a theme color. switch obj.CurrentState diff --git a/libs/Dashboard/MultiStatusWidget.m b/libs/Dashboard/MultiStatusWidget.m index 70ada2da..247f0846 100644 --- a/libs/Dashboard/MultiStatusWidget.m +++ b/libs/Dashboard/MultiStatusWidget.m @@ -20,6 +20,8 @@ function render(obj, parentPanel) obj.hPanel = parentPanel; + % Re-layout on resize so pixel-scaled fonts/geometry stay correct. + try obj.hPanel.SizeChangedFcn = @(~,~) obj.relayout_(); catch, end theme = obj.getTheme(); obj.hAxes = axes('Parent', parentPanel, ... 'Units', 'normalized', ... @@ -232,6 +234,14 @@ function refresh(obj) end methods (Access = private) + function relayout_(obj) + %RELAYOUT_ Rebuild pixel-scaled elements on panel resize. + if isempty(obj.hPanel) || ~ishandle(obj.hPanel), return; end + try delete(findobj(obj.hPanel, '-depth', 1, 'Type', 'uicontrol')); catch, end + try delete(findobj(obj.hPanel, '-depth', 1, 'Type', 'axes')); catch, end + obj.render(obj.hPanel); + end + function expandedItems = expandSensors_(obj) %EXPANDSENSORS_ Expand CompositeThreshold/CompositeTag items into children + summary. % Non-composite items pass through unchanged. diff --git a/libs/Dashboard/NumberWidget.m b/libs/Dashboard/NumberWidget.m index 5387855e..8858a407 100644 --- a/libs/Dashboard/NumberWidget.m +++ b/libs/Dashboard/NumberWidget.m @@ -38,6 +38,8 @@ function render(obj, parentPanel) obj.hPanel = parentPanel; + % Re-layout on resize so pixel-scaled fonts/geometry stay correct. + try obj.hPanel.SizeChangedFcn = @(~,~) obj.relayout_(); catch, end theme = obj.getTheme(); bgColor = theme.WidgetBackground; @@ -198,6 +200,14 @@ function refresh(obj) end methods (Access = private) + function relayout_(obj) + %RELAYOUT_ Rebuild pixel-scaled elements on panel resize. + if isempty(obj.hPanel) || ~ishandle(obj.hPanel), return; end + try delete(findobj(obj.hPanel, '-depth', 1, 'Type', 'uicontrol')); catch, end + try delete(findobj(obj.hPanel, '-depth', 1, 'Type', 'axes')); catch, end + obj.render(obj.hPanel); + end + function trend = computeTrend(obj) trend = ''; if isempty(obj.Sensor) || numel(obj.Sensor.Y) < 3 diff --git a/libs/Dashboard/SparklineCardWidget.m b/libs/Dashboard/SparklineCardWidget.m index bae7caab..7f043ac9 100644 --- a/libs/Dashboard/SparklineCardWidget.m +++ b/libs/Dashboard/SparklineCardWidget.m @@ -64,6 +64,8 @@ function render(obj, parentPanel) %RENDER Create all graphics objects inside parentPanel. obj.hPanel = parentPanel; + % Re-layout on resize so pixel-scaled fonts/geometry stay correct. + try obj.hPanel.SizeChangedFcn = @(~,~) obj.relayout_(); catch, end theme = obj.getTheme(); bgColor = theme.WidgetBackground; fgColor = theme.ForegroundColor; @@ -282,4 +284,14 @@ function refresh(obj) end end end + + methods (Access = private) + function relayout_(obj) + %RELAYOUT_ Rebuild pixel-scaled elements on panel resize. + if isempty(obj.hPanel) || ~ishandle(obj.hPanel), return; end + try delete(findobj(obj.hPanel, '-depth', 1, 'Type', 'uicontrol')); catch, end + try delete(findobj(obj.hPanel, '-depth', 1, 'Type', 'axes')); catch, end + obj.render(obj.hPanel); + end + end end diff --git a/libs/Dashboard/StatusWidget.m b/libs/Dashboard/StatusWidget.m index 96162424..dbf94fa3 100644 --- a/libs/Dashboard/StatusWidget.m +++ b/libs/Dashboard/StatusWidget.m @@ -51,6 +51,8 @@ function render(obj, parentPanel) obj.hPanel = parentPanel; + % Re-layout on resize so pixel-scaled fonts/geometry stay correct. + try obj.hPanel.SizeChangedFcn = @(~,~) obj.relayout_(); catch, end theme = obj.getTheme(); bgColor = theme.WidgetBackground; @@ -269,6 +271,14 @@ function refresh(obj) end methods (Access = private) + function relayout_(obj) + %RELAYOUT_ Rebuild pixel-scaled elements on panel resize. + if isempty(obj.hPanel) || ~ishandle(obj.hPanel), return; end + try delete(findobj(obj.hPanel, '-depth', 1, 'Type', 'uicontrol')); catch, end + try delete(findobj(obj.hPanel, '-depth', 1, 'Type', 'axes')); catch, end + obj.render(obj.hPanel); + end + function val = resolveCurrentValue_(obj) %RESOLVECURRENTVALUE_ Return the current scalar value from ValueFcn or Value. val = []; diff --git a/libs/Dashboard/TextWidget.m b/libs/Dashboard/TextWidget.m index 0e00173e..f3fef277 100644 --- a/libs/Dashboard/TextWidget.m +++ b/libs/Dashboard/TextWidget.m @@ -24,6 +24,8 @@ function render(obj, parentPanel) obj.hPanel = parentPanel; + % Re-layout on resize so pixel-scaled fonts/geometry stay correct. + try obj.hPanel.SizeChangedFcn = @(~,~) obj.relayout_(); catch, end theme = obj.getTheme(); bgColor = theme.WidgetBackground; @@ -151,4 +153,14 @@ function refresh(~) end end + methods (Access = private) + function relayout_(obj) + %RELAYOUT_ Rebuild pixel-scaled elements on panel resize. + if isempty(obj.hPanel) || ~ishandle(obj.hPanel), return; end + try delete(findobj(obj.hPanel, '-depth', 1, 'Type', 'uicontrol')); catch, end + try delete(findobj(obj.hPanel, '-depth', 1, 'Type', 'axes')); catch, end + obj.render(obj.hPanel); + end + end + end From d2034786e217587cc6163ee549478678a76e266c Mon Sep 17 00:00:00 2001 From: Hannes Suhr Date: Thu, 23 Apr 2026 18:56:48 +0200 Subject: [PATCH 5/8] 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) --- libs/Dashboard/EventTimelineWidget.m | 26 +++++++++++++++++++++++ libs/Dashboard/FastSenseWidget.m | 31 ++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/libs/Dashboard/EventTimelineWidget.m b/libs/Dashboard/EventTimelineWidget.m index 06861ab0..4302d8bb 100644 --- a/libs/Dashboard/EventTimelineWidget.m +++ b/libs/Dashboard/EventTimelineWidget.m @@ -156,6 +156,9 @@ function refresh(obj) set(obj.hAxes, 'YTick', 1:numel(labels), 'YTickLabel', labels); set(obj.hAxes, 'YLim', [0.3, numel(labels) + 0.7]); + + % Reformat time-axis ticks to HH:MM:SS / MM:SS for readability. + obj.formatTimeAxis_(obj.hAxes); end function t = getType(~) @@ -357,5 +360,28 @@ function onXLimChanged(obj) end end + function formatTimeAxis_(~, ax) + %FORMATTIMEAXIS_ Replace numeric-seconds x-ticks with HH:MM:SS labels. + % No-op when range <= 300s (raw seconds readable) or ax invalid. + if isempty(ax) || ~ishandle(ax), return; end + xl = get(ax, 'XLim'); + rangeSec = xl(2) - xl(1); + if rangeSec <= 300, return; end + xt = get(ax, 'XTick'); + if isempty(xt), return; end + if rangeSec >= 3600 + fmt = 'HH:MM:SS'; + else + fmt = 'MM:SS'; + end + lbl = cell(1, numel(xt)); + for i = 1:numel(xt) + % xt(i) is seconds; serial-date day = seconds / 86400 + lbl{i} = datestr(xt(i) / 86400, fmt); + end + set(ax, 'XTickMode', 'manual', 'XTickLabelMode', 'manual', ... + 'XTickLabel', lbl); + end + end end diff --git a/libs/Dashboard/FastSenseWidget.m b/libs/Dashboard/FastSenseWidget.m index 1b134bb5..c194da41 100644 --- a/libs/Dashboard/FastSenseWidget.m +++ b/libs/Dashboard/FastSenseWidget.m @@ -98,6 +98,9 @@ function render(obj, parentPanel) fp.render(); + % Reformat time-axis ticks to HH:MM:SS / MM:SS for readability. + obj.formatTimeAxis_(ax); + % Apply fixed Y-axis limits if configured if ~isempty(obj.YLimits) && numel(obj.YLimits) == 2 ylim(ax, obj.YLimits); @@ -132,6 +135,7 @@ function refresh(obj) [x, y] = obj.Tag.getXY(); obj.FastSenseObj.updateData(1, x, y); obj.updateTimeRangeCache(); + obj.formatTimeAxis_(obj.FastSenseObj.hAxes); return; catch % fall through to full teardown/rebuild @@ -153,6 +157,7 @@ function update(obj) [x, y] = obj.Tag.getXY(); obj.FastSenseObj.updateData(1, x, y); obj.updateTimeRangeCache(); + obj.formatTimeAxis_(obj.FastSenseObj.hAxes); return; catch % fall through to refresh() @@ -267,6 +272,29 @@ function onXLimChanged(obj) end methods (Access = private) + function formatTimeAxis_(~, ax) + %FORMATTIMEAXIS_ Replace numeric-seconds x-ticks with HH:MM:SS labels. + % No-op when range <= 300s (raw seconds readable) or ax invalid. + if isempty(ax) || ~ishandle(ax), return; end + xl = get(ax, 'XLim'); + rangeSec = xl(2) - xl(1); + if rangeSec <= 300, return; end + xt = get(ax, 'XTick'); + if isempty(xt), return; end + if rangeSec >= 3600 + fmt = 'HH:MM:SS'; + else + fmt = 'MM:SS'; + end + lbl = cell(1, numel(xt)); + for i = 1:numel(xt) + % xt(i) is seconds; serial-date day = seconds / 86400 + lbl{i} = datestr(xt(i) / 86400, fmt); + end + set(ax, 'XTickMode', 'manual', 'XTickLabelMode', 'manual', ... + 'XTickLabel', lbl); + end + function updateTimeRangeCache(obj) %UPDATETIMERANGECACHE Maintain CachedXMin/CachedXMax incrementally. % For sorted time arrays (the common case) the last element is the @@ -339,6 +367,9 @@ function rebuildForTag_(obj) fp.render(); + % Reformat time-axis ticks to HH:MM:SS / MM:SS for readability. + obj.formatTimeAxis_(ax); + if ~isempty(obj.YLimits) && numel(obj.YLimits) == 2 ylim(ax, obj.YLimits); end From 52bd6b64135904b2d145f8b9009340c83b920035 Mon Sep 17 00:00:00 2001 From: Hannes Suhr Date: Thu, 23 Apr 2026 18:57:14 +0200 Subject: [PATCH 6/8] 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) --- libs/Dashboard/ScatterWidget.m | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/libs/Dashboard/ScatterWidget.m b/libs/Dashboard/ScatterWidget.m index 29721187..920549c1 100644 --- a/libs/Dashboard/ScatterWidget.m +++ b/libs/Dashboard/ScatterWidget.m @@ -66,6 +66,16 @@ function refresh(obj) 'Marker', '.', ... 'MarkerSize', obj.MarkerSize); end + + % Auto-derive axis labels from SensorX/SensorY if present. + if ~isempty(obj.SensorX) + xl = obj.axisLabelForSensor_(obj.SensorX); + if ~isempty(xl), xlabel(obj.hAxes, xl); end + end + if ~isempty(obj.SensorY) + yl = obj.axisLabelForSensor_(obj.SensorY); + if ~isempty(yl), ylabel(obj.hAxes, yl); end + end end function t = getType(~) @@ -112,6 +122,30 @@ function refresh(obj) end end + methods (Access = private) + function lbl = axisLabelForSensor_(~, s) + %AXISLABELFORSENSOR_ Build "Name (Units)" label with graceful fallbacks. + lbl = ''; + if isempty(s), return; end + name = ''; + if isprop(s, 'Name') && ~isempty(s.Name) + name = s.Name; + elseif isprop(s, 'Key') && ~isempty(s.Key) + name = s.Key; + end + if isempty(name), return; end + units = ''; + if isprop(s, 'Units') && ~isempty(s.Units) + units = s.Units; + end + if isempty(units) + lbl = name; + else + lbl = sprintf('%s (%s)', name, units); + end + end + end + methods (Static) function obj = fromStruct(s) obj = ScatterWidget(); From 4f813392975e2f8ab0e146efa094191fbe54877a Mon Sep 17 00:00:00 2001 From: Hannes Suhr Date: Thu, 23 Apr 2026 19:21:35 +0200 Subject: [PATCH 7/8] 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. --- libs/Dashboard/BarChartWidget.m | 7 +++++++ libs/Dashboard/GaugeWidget.m | 5 ++--- libs/Dashboard/HistogramWidget.m | 7 +++++++ 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/libs/Dashboard/BarChartWidget.m b/libs/Dashboard/BarChartWidget.m index 85035f3f..6a476861 100644 --- a/libs/Dashboard/BarChartWidget.m +++ b/libs/Dashboard/BarChartWidget.m @@ -88,6 +88,13 @@ function refresh(obj) set(obj.hAxes, 'XTick', 1:numel(cats), 'XTickLabel', cats); end end + % Re-apply title after plot commands (bar/barh may clear via newplot) + if ~isempty(obj.Title) + theme = obj.getTheme(); + title(obj.hAxes, obj.Title, ... + 'Color', theme.ForegroundColor, ... + 'FontSize', theme.WidgetTitleFontSize); + end end function t = getType(~) diff --git a/libs/Dashboard/GaugeWidget.m b/libs/Dashboard/GaugeWidget.m index 66970ede..7f4a495e 100644 --- a/libs/Dashboard/GaugeWidget.m +++ b/libs/Dashboard/GaugeWidget.m @@ -501,10 +501,9 @@ function renderThermometer(obj, parentPanel) obj.hAxes = axes('Parent', parentPanel, ... 'Units', 'normalized', ... - 'Position', [0.3 0.15 0.4 0.7], ... + 'Position', [0.15 0.10 0.7 0.80], ... 'Visible', 'off', ... - 'XLim', [-0.5 1.5], 'YLim', [-0.3 1.3], ... - 'DataAspectRatio', [1 2 1], ... + 'XLim', [-0.5 1.5], 'YLim', [-0.3 1.4], ... 'HitTest', 'off'); try set(obj.hAxes, 'PickableParts', 'none'); catch , end try disableDefaultInteractivity(obj.hAxes); catch , end diff --git a/libs/Dashboard/HistogramWidget.m b/libs/Dashboard/HistogramWidget.m index c458c6b0..1dc0f173 100644 --- a/libs/Dashboard/HistogramWidget.m +++ b/libs/Dashboard/HistogramWidget.m @@ -75,6 +75,13 @@ function refresh(obj) plot(obj.hAxes, xFit, yFit, 'r-', 'LineWidth', 1.5); hold(obj.hAxes, 'off'); end + % Re-apply title after plot commands (bar/plot may clear via newplot) + if ~isempty(obj.Title) + theme = obj.getTheme(); + title(obj.hAxes, obj.Title, ... + 'Color', theme.ForegroundColor, ... + 'FontSize', theme.WidgetTitleFontSize); + end obj.Dirty = false; end From 2fead09fa3fb97e58d100fccc6ab1d31e7f37cc6 Mon Sep 17 00:00:00 2001 From: Hannes Suhr Date: Thu, 23 Apr 2026 19:25:52 +0200 Subject: [PATCH 8/8] fix(IconCardWidget): stop self-erasing Tag via Sensor alias MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- libs/Dashboard/IconCardWidget.m | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/libs/Dashboard/IconCardWidget.m b/libs/Dashboard/IconCardWidget.m index c765f631..e076362a 100644 --- a/libs/Dashboard/IconCardWidget.m +++ b/libs/Dashboard/IconCardWidget.m @@ -75,7 +75,10 @@ end if ~isempty(obj.Tag) obj.Threshold = []; - obj.Sensor = []; + % NOTE: do NOT clear obj.Sensor here. Sensor is a Dependent + % alias for Tag (see DashboardWidget.set.Sensor) — setting + % it to [] wipes the Tag we just stored, causing the widget + % to render "--" forever. end % Mutual exclusivity: Threshold wins (per D-08) if ~isempty(obj.Threshold) && ~isempty(obj.Sensor)