Skip to content

Commit 93eb18a

Browse files
committed
Merge: ad-hoc plot via DashboardEngine
2 parents 90175b9 + b901d22 commit 93eb18a

1 file changed

Lines changed: 194 additions & 0 deletions

File tree

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
function [hFig, skippedNames] = openAdHocPlot(tags, mode, themePreset)
2+
%OPENADHOCPLOT Spawn an ad-hoc multi-tag plot as a live DashboardEngine.
3+
% [hFig, skippedNames] = openAdHocPlot(tags, mode, themePreset)
4+
%
5+
% Replaces the classical-figure path: every "Plot" click in the
6+
% companion now spawns a fresh DashboardEngine with live refresh
7+
% running. Mode controls layout:
8+
% - 'Overlay' — ONE RawAxesWidget; every tag drawn as an overlaid
9+
% line in a single axes. Live tick re-runs the
10+
% PlotFcn so lines update in place.
11+
% - 'LinkedGrid' — ONE FastSenseWidget per tag tiled in a 2-column
12+
% grid. Each widget has ShowEventMarkers=true and,
13+
% when the tag has a matching MonitorTag in the
14+
% registry, the monitor's EventStore is forwarded
15+
% so threshold events overlay on the plot.
16+
%
17+
% Inputs:
18+
% tags - 1xN cell of Tag handles (already resolved by caller).
19+
% Must contain >= 2 entries.
20+
% mode - char: 'Overlay' or 'LinkedGrid'.
21+
% themePreset - char: 'dark' or 'light'.
22+
%
23+
% Outputs:
24+
% hFig - DashboardEngine.hFigure (figure window the engine owns)
25+
% skippedNames - 1xM cellstr of skipped tag names (may be empty).
26+
%
27+
% Errors:
28+
% FastSenseCompanion:invalidPlotMode - mode unknown or numel(tags)<2
29+
% FastSenseCompanion:plotSpawnFailed - all tags failed; no figure spawned
30+
%
31+
% Lifecycle: the engine starts its own live timer. The figure's
32+
% CloseRequestFcn stops live + deletes the figure so closing the
33+
% window cleans up.
34+
%
35+
% See also DashboardEngine, FastSenseWidget, RawAxesWidget,
36+
% FastSenseCompanion, TagRegistry.
37+
38+
validModes = {'Overlay', 'LinkedGrid'};
39+
if ~ischar(mode) || ~any(strcmp(mode, validModes))
40+
error('FastSenseCompanion:invalidPlotMode', ...
41+
'openAdHocPlot: mode must be one of: %s. Got: ''%s''.', ...
42+
strjoin(validModes, ', '), char(mode));
43+
end
44+
if ~iscell(tags) || numel(tags) < 2
45+
error('FastSenseCompanion:invalidPlotMode', ...
46+
'openAdHocPlot: requires a cell of >= 2 tags. Got %d.', numel(tags));
47+
end
48+
49+
% Filter tags that have data.
50+
validTags = {};
51+
validNames = {};
52+
skippedNames = {};
53+
for k = 1:numel(tags)
54+
tg = tags{k};
55+
try
56+
nm = tg.Name;
57+
catch
58+
nm = sprintf('<tag %d>', k);
59+
end
60+
try
61+
[t, ~] = tg.getXY();
62+
if isempty(t)
63+
skippedNames{end+1} = sprintf('%s (no data)', nm); %#ok<AGROW>
64+
continue;
65+
end
66+
validTags{end+1} = tg; %#ok<AGROW>
67+
validNames{end+1} = nm; %#ok<AGROW>
68+
catch ME
69+
skippedNames{end+1} = sprintf('%s (%s)', nm, ME.message); %#ok<AGROW>
70+
end
71+
end
72+
if isempty(validTags)
73+
error('FastSenseCompanion:plotSpawnFailed', ...
74+
'openAdHocPlot: no tags produced data. Skipped: %s', ...
75+
strjoin(skippedNames, '; '));
76+
end
77+
78+
figName = buildFigureName_(validNames);
79+
80+
engine = DashboardEngine(figName, ...
81+
'Theme', themePreset, 'LiveInterval', 1.0);
82+
83+
switch mode
84+
case 'Overlay'
85+
% One single widget: a RawAxesWidget that overlays every tag.
86+
% cla() runs in the widget's refresh, then PlotFcn redraws.
87+
engine.addWidget('rawaxes', ...
88+
'Title', figName, ...
89+
'PlotFcn', @(ax) plotOverlay_(ax, validTags, validNames), ...
90+
'Position', [1 1 24 12]);
91+
92+
case 'LinkedGrid'
93+
N = numel(validTags);
94+
cols = min(N, 2);
95+
rows = ceil(N / cols);
96+
unitW = max(1, floor(24 / cols));
97+
unitH = max(1, floor(12 / rows));
98+
for k = 1:N
99+
r = ceil(k / cols);
100+
c = mod(k - 1, cols) + 1;
101+
args = { ...
102+
'Title', char(validNames{k}), ...
103+
'Tag', validTags{k}, ...
104+
'ShowEventMarkers', true, ...
105+
'Position', [(c-1)*unitW + 1, (r-1)*unitH + 1, unitW, unitH]};
106+
es = findEventStoreFor_(validTags{k});
107+
if ~isempty(es)
108+
args = [args, {'EventStore', es}]; %#ok<AGROW>
109+
end
110+
engine.addWidget('fastsense', args{:});
111+
end
112+
end
113+
114+
engine.render();
115+
engine.startLive();
116+
117+
hFig = engine.hFigure;
118+
if ~isempty(hFig) && ishandle(hFig)
119+
set(hFig, 'CloseRequestFcn', @(s, ~) closeFcn_(s, engine));
120+
end
121+
end
122+
123+
% --------------------------- helpers --------------------------------
124+
125+
function plotOverlay_(ax, tags, names)
126+
%PLOTOVERLAY_ Draw every tag as a line in the same axes; called on every refresh.
127+
hold(ax, 'on');
128+
for k = 1:numel(tags)
129+
try
130+
[tv, y] = tags{k}.getXY();
131+
if isempty(tv); continue; end
132+
plot(ax, tv, y, 'DisplayName', char(names{k}), 'LineWidth', 1.2);
133+
catch
134+
end
135+
end
136+
hold(ax, 'off');
137+
try; legend(ax, 'show', 'Location', 'best'); catch; end
138+
grid(ax, 'on');
139+
xlabel(ax, 'Time');
140+
end
141+
142+
function es = findEventStoreFor_(tag)
143+
%FINDEVENTSTOREFOR_ Locate an EventStore via a MonitorTag whose Parent.Key matches.
144+
% Returns [] when no matching monitor or no EventStore is registered.
145+
es = [];
146+
try
147+
if ~isobject(tag) || ~isvalid(tag) || ~isprop(tag, 'Key'); return; end
148+
monitors = TagRegistry.find(@(tt) isa(tt, 'MonitorTag') ...
149+
&& ~isempty(tt.Parent) && isprop(tt.Parent, 'Key') ...
150+
&& strcmp(tt.Parent.Key, tag.Key));
151+
for k = 1:numel(monitors)
152+
m = monitors{k};
153+
if isprop(m, 'EventStore') && ~isempty(m.EventStore) && isvalid(m.EventStore)
154+
es = m.EventStore;
155+
return;
156+
end
157+
end
158+
catch
159+
end
160+
end
161+
162+
function closeFcn_(fig, engine)
163+
%CLOSEFCN_ Stop live + delete figure on close.
164+
try
165+
if ~isempty(engine) && isvalid(engine) && ismethod(engine, 'stopLive')
166+
engine.stopLive();
167+
end
168+
catch
169+
end
170+
try; delete(fig); catch; end
171+
end
172+
173+
function name = buildFigureName_(tagNames)
174+
%BUILDFIGURENAME_ Compose figure title with 80-char total truncation.
175+
prefix = 'FastSense Companion — ';
176+
maxTotal = 80;
177+
joined = strjoin(tagNames, ', ');
178+
full = [prefix, joined];
179+
if numel(full) <= maxTotal
180+
name = full;
181+
return;
182+
end
183+
budget = maxTotal - numel(prefix) - 1;
184+
if budget < 1
185+
name = [prefix, char(8230)];
186+
return;
187+
end
188+
cut = joined(1:min(budget, numel(joined)));
189+
lastSep = max(strfind(cut, ', '));
190+
if ~isempty(lastSep) && lastSep > 1
191+
cut = cut(1:lastSep-1);
192+
end
193+
name = [prefix, cut, char(8230)];
194+
end

0 commit comments

Comments
 (0)