diff --git a/libs/Dashboard/BarChartWidget.m b/libs/Dashboard/BarChartWidget.m index ac6dcb90..6a476861 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 @@ -83,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/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/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 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/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..1dc0f173 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 @@ -70,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 diff --git a/libs/Dashboard/IconCardWidget.m b/libs/Dashboard/IconCardWidget.m index d2698fa2..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) @@ -86,6 +89,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; @@ -161,6 +166,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; @@ -333,6 +340,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/ImageWidget.m b/libs/Dashboard/ImageWidget.m index e093267a..d67d8cd6 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', ... @@ -62,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(~) 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/ScatterWidget.m b/libs/Dashboard/ScatterWidget.m index 566d945f..920549c1 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 @@ -61,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(~) @@ -107,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(); 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