Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 33 additions & 6 deletions libs/Dashboard/FastSenseWidget.m
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,8 @@ function render(obj, parentPanel)
% 'auto' — no-op (default)
% numeric scalar/vector — one upper threshold per value
% cell of structs — {struct('Value',..,'Direction',..,'Label',..), ...}
% entries may instead carry X/Y vectors for a
% time-varying limit: struct('X',..,'Y',..,'Direction',..)
applyThresholds_(fp, obj.Thresholds);

% Set title and axis labels.
Expand Down Expand Up @@ -894,9 +896,14 @@ function autoScaleY_(obj, y)
if iscell(obj.Thresholds)
for i = 1:numel(obj.Thresholds)
e = obj.Thresholds{i};
if isstruct(e) && isfield(e, 'Value') && isfinite(e.Value)
if isstruct(e) && isfield(e, 'Value') && ...
~isempty(e.Value) && isfinite(e.Value)
yMin = min(yMin, e.Value);
yMax = max(yMax, e.Value);
elseif isstruct(e) && isfield(e, 'Y') && ...
~isempty(e.Y) && any(isfinite(e.Y(:)))
yMin = min(yMin, min(e.Y(:), [], 'omitnan'));
yMax = max(yMax, max(e.Y(:), [], 'omitnan'));
end
end
elseif isnumeric(obj.Thresholds) && ~isempty(obj.Thresholds)
Expand Down Expand Up @@ -2023,7 +2030,9 @@ function rebuildForTag_(obj)
function applyThresholds_(fp, spec)
%APPLYTHRESHOLDS_ Push a Thresholds spec into a FastSense instance.
% Accepts 'auto' / [] (no-op), numeric scalar/vector (upper lines),
% or a cell of structs with fields Value / Direction / Label.
% or a cell of structs: Value (scalar limit) or X / Y vectors
% (time-varying step limit; NaN Y = no limit), plus optional
% Direction / Label / Color / LineStyle (severity styling).
if isempty(spec)
return;
end
Expand All @@ -2040,7 +2049,12 @@ function applyThresholds_(fp, spec)
if iscell(spec)
for i = 1:numel(spec)
e = spec{i};
if ~isstruct(e) || ~isfield(e, 'Value')
if ~isstruct(e)
continue;
end
isTimeVarying = isfield(e, 'X') && isfield(e, 'Y') && ...
~isempty(e.X) && ~isempty(e.Y);
if ~isTimeVarying && ~isfield(e, 'Value')
continue;
end
dir = 'upper';
Expand All @@ -2051,10 +2065,23 @@ function applyThresholds_(fp, spec)
if isfield(e, 'Label') && ~isempty(e.Label)
lbl = e.Label;
end
if isempty(lbl)
fp.addThreshold(e.Value, 'Direction', dir);
args = {'Direction', dir};
if ~isempty(lbl)
args = [args, {'Label', lbl}]; %#ok<AGROW>
end
if isfield(e, 'Color') && ~isempty(e.Color)
args = [args, {'Color', e.Color}]; %#ok<AGROW>
end
if isfield(e, 'LineStyle') && ~isempty(e.LineStyle)
args = [args, {'LineStyle', e.LineStyle}]; %#ok<AGROW>
end
if isTimeVarying
% Time-varying entry — forward to the core step-function
% form addThreshold(thX, thY, ...). NaN samples in Y break
% the line where no limit applies (state-dependent limits).
fp.addThreshold(e.X, e.Y, args{:});
else
fp.addThreshold(e.Value, 'Direction', dir, 'Label', lbl);
fp.addThreshold(e.Value, args{:});
end
end
end
Expand Down
89 changes: 89 additions & 0 deletions tests/suite/TestFastSenseWidget.m
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,95 @@ function testRenderCreatesAxes(testCase)
testCase.verifyTrue(isa(w.FastSenseObj, 'FastSense'));
end

function testTimeVaryingThresholdSpecReachesFastSense(testCase)
% A 'Thresholds' entry with X/Y vectors must forward to the
% core's time-varying addThreshold(thX, thY, ...) so dashboards
% can draw state-dependent (dynamic) limits. NaN Y samples mean
% "no limit here" and break the drawn line.
hFig = figure('Visible', 'off');
testCase.addTeardown(@() close(hFig));
hp = uipanel('Parent', hFig, 'Units', 'normalized', ...
'Position', [0 0 1 1]);

thX = 1:100;
thY = 5 * ones(1, 100);
thY(40:60) = NaN; % limit inactive mid-range
w = FastSenseWidget('Thresholds', ...
{struct('X', thX, 'Y', thY, ...
'Direction', 'upper', 'Label', 'UWL')});
w.XData = 1:100;
w.YData = rand(1, 100);
w.render(hp);

th = w.FastSenseObj.Thresholds;
testCase.assertNumElements(th, 1, ...
'X/Y spec entry must create exactly one threshold');
testCase.verifyNumElements(th(1).Y, 100, ...
'threshold must keep its per-sample Y series');
testCase.verifyEmpty(th(1).Value, ...
'time-varying threshold must not carry a scalar Value');
testCase.verifyEqual(th(1).Direction, 'upper');
testCase.verifyEqual(th(1).Label, 'UWL');
end

function testMixedScalarAndTimeVaryingThresholds(testCase)
% Scalar Value entries and X/Y entries must coexist in one spec.
hFig = figure('Visible', 'off');
testCase.addTeardown(@() close(hFig));
hp = uipanel('Parent', hFig, 'Units', 'normalized', ...
'Position', [0 0 1 1]);

thX = 1:50;
thY = [2 * ones(1, 25), NaN(1, 25)];
w = FastSenseWidget('Thresholds', { ...
struct('Value', 3, 'Direction', 'upper', 'Label', 'UAL'), ...
struct('X', thX, 'Y', thY, 'Direction', 'lower')});
w.XData = 1:50;
w.YData = rand(1, 50);
w.render(hp);

th = w.FastSenseObj.Thresholds;
testCase.assertNumElements(th, 2, ...
'both spec entries must create thresholds');
testCase.verifyEqual(th(1).Value, 3);
testCase.verifyNumElements(th(2).Y, 50);
testCase.verifyEqual(th(2).Direction, 'lower');
end

function testThresholdSpecStyleReachesFastSense(testCase)
% Color / LineStyle on a Thresholds spec entry must reach the
% core threshold (severity styling: e.g. warn yellow solid,
% alarm red solid) for both scalar and time-varying entries.
hFig = figure('Visible', 'off');
testCase.addTeardown(@() close(hFig));
hp = uipanel('Parent', hFig, 'Units', 'normalized', ...
'Position', [0 0 1 1]);

warnColor = [0.93 0.69 0.13];
alarmColor = [0.7 0 0];
thX = 1:50;
thY = 4 * ones(1, 50);
w = FastSenseWidget('Thresholds', { ...
struct('Value', 5, 'Direction', 'upper', 'Label', 'UAL', ...
'Color', alarmColor, 'LineStyle', '-'), ...
struct('X', thX, 'Y', thY, 'Direction', 'upper', ...
'Label', 'UWL', 'Color', warnColor, 'LineStyle', '-')});
w.XData = 1:50;
w.YData = rand(1, 50);
w.render(hp);

th = w.FastSenseObj.Thresholds;
testCase.assertNumElements(th, 2);
testCase.verifyEqual(th(1).Color, alarmColor, ...
'scalar entry Color must pass through');
testCase.verifyEqual(th(1).LineStyle, '-', ...
'scalar entry LineStyle must pass through');
testCase.verifyEqual(th(2).Color, warnColor, ...
'time-varying entry Color must pass through');
testCase.verifyEqual(th(2).LineStyle, '-', ...
'time-varying entry LineStyle must pass through');
end

function testToStructRoundTrip(testCase)
w = FastSenseWidget('Title', 'My Plot', 'Position', [5 2 16 3]);
w.XData = 1:10;
Expand Down
Loading