Skip to content

Commit dfbe0bc

Browse files
committed
Merge: multitag preview cards
2 parents 7b1d5d6 + 6436fc5 commit dfbe0bc

1 file changed

Lines changed: 228 additions & 49 deletions

File tree

libs/FastSenseCompanion/InspectorPane.m

Lines changed: 228 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@
3434
hPlayBtn_ = [] % Play button (dashboard state only)
3535
hPauseBtn_ = [] % Pause button (dashboard state only)
3636
hChipsGrid_ = [] % chip list inner grid (multitag state only)
37+
hMultiSparkPanels_ = {} % cell of uipanel (multitag — one per tag)
38+
hMultiSparkAxes_ = {} % cell of axes
39+
hMultiSparkLines_ = {} % cell of line handles (live updates)
40+
hMultiRangeLbls_ = {} % cell of range uilabel
41+
RenderedMultiKeys_ = {} % cellstr of keys captured at last full render
3742
hModeOverlay_ = [] % "Overlay" mode button (multitag state only)
3843
hModeLinked_ = [] % "Linked grid" mode button (multitag state only)
3944
hPlotBtn_ = [] % Plot CTA (multitag state only)
@@ -129,6 +134,19 @@ function refreshLive(obj)
129134
return;
130135
end
131136
obj.hDashTable_.Data = obj.buildDashTableData_(db);
137+
138+
case 'multitag'
139+
if ~isfield(obj.Payload_, 'tags'); return; end
140+
keys = {};
141+
if isfield(obj.Payload_, 'tagKeys')
142+
keys = obj.Payload_.tagKeys;
143+
end
144+
if numel(obj.Payload_.tags) ~= numel(obj.hMultiSparkLines_) ...
145+
|| ~isequal(keys, obj.RenderedMultiKeys_)
146+
obj.renderState_();
147+
return;
148+
end
149+
obj.refreshMultiInPlace_();
132150
end
133151
catch
134152
% Live ticks must never throw.
@@ -178,6 +196,9 @@ function renderState_(obj)
178196
obj.hChipsGrid_ = []; obj.hModeOverlay_ = []; obj.hModeLinked_ = [];
179197
obj.hPlotBtn_ = []; obj.hTagTable_ = []; obj.hDashTable_ = [];
180198
obj.RenderedTagKey_ = ''; obj.RenderedDashName_ = '';
199+
obj.hMultiSparkPanels_ = {}; obj.hMultiSparkAxes_ = {};
200+
obj.hMultiSparkLines_ = {}; obj.hMultiRangeLbls_ = {};
201+
obj.RenderedMultiKeys_ = {};
181202
switch obj.State_
182203
case 'welcome'; obj.renderWelcome_();
183204
case 'tag'; obj.renderTag_();
@@ -391,25 +412,27 @@ function renderSparkline_(obj, tag)
391412
end
392413
end
393414

394-
function updateSparkTicks_(obj, tv, y)
415+
function updateSparkTicks_(obj, tv, y, ax)
395416
%UPDATESPARKTICKS_ Set 2-point X (start/end time) and Y (min/max) ticks.
396-
if isempty(obj.hSparkAxes_) || ~isvalid(obj.hSparkAxes_); return; end
417+
% ax is optional; defaults to obj.hSparkAxes_.
418+
if nargin < 4 || isempty(ax); ax = obj.hSparkAxes_; end
419+
if isempty(ax) || ~isvalid(ax); return; end
397420
if isempty(tv) || isempty(y); return; end
398421
try
399422
if numel(tv) >= 2 && tv(1) ~= tv(end)
400-
obj.hSparkAxes_.XTick = [tv(1), tv(end)];
401-
obj.hSparkAxes_.XTickLabel = {obj.formatXTick_(tv(1)), obj.formatXTick_(tv(end))};
423+
ax.XTick = [tv(1), tv(end)];
424+
ax.XTickLabel = {obj.formatXTick_(tv(1)), obj.formatXTick_(tv(end))};
402425
else
403-
obj.hSparkAxes_.XTick = tv(1);
404-
obj.hSparkAxes_.XTickLabel = {obj.formatXTick_(tv(1))};
426+
ax.XTick = tv(1);
427+
ax.XTickLabel = {obj.formatXTick_(tv(1))};
405428
end
406429
yMin = min(y); yMax = max(y);
407430
if yMin < yMax
408-
obj.hSparkAxes_.YTick = [yMin, yMax];
409-
obj.hSparkAxes_.YTickLabel = {obj.formatYTick_(yMin), obj.formatYTick_(yMax)};
431+
ax.YTick = [yMin, yMax];
432+
ax.YTickLabel = {obj.formatYTick_(yMin), obj.formatYTick_(yMax)};
410433
else
411-
obj.hSparkAxes_.YTick = yMin;
412-
obj.hSparkAxes_.YTickLabel = {obj.formatYTick_(yMin)};
434+
ax.YTick = yMin;
435+
ax.YTickLabel = {obj.formatYTick_(yMin)};
413436
end
414437
catch
415438
end
@@ -518,16 +541,17 @@ function refreshSparklineInPlace_(obj, tag)
518541
end
519542
end
520543

521-
function fitSparkAxes_(obj)
522-
%FITSPARKAXES_ Tight axis with small padding.
523-
if isempty(obj.hSparkAxes_) || ~isvalid(obj.hSparkAxes_); return; end
544+
function fitSparkAxes_(obj, ax)
545+
%FITSPARKAXES_ Tight axis with small padding. Defaults to obj.hSparkAxes_.
546+
if nargin < 2 || isempty(ax); ax = obj.hSparkAxes_; end
547+
if isempty(ax) || ~isvalid(ax); return; end
524548
try
525-
axis(obj.hSparkAxes_, 'tight');
526-
xl = obj.hSparkAxes_.XLim; yl = obj.hSparkAxes_.YLim;
549+
axis(ax, 'tight');
550+
xl = ax.XLim; yl = ax.YLim;
527551
px = (xl(2) - xl(1)) * 0.02;
528-
if px > 0; obj.hSparkAxes_.XLim = xl + [-px, px]; end
552+
if px > 0; ax.XLim = xl + [-px, px]; end
529553
py = (yl(2) - yl(1)) * 0.05;
530-
if py > 0; obj.hSparkAxes_.YLim = yl + [-py, py]; end
554+
if py > 0; ax.YLim = yl + [-py, py]; end
531555
catch
532556
end
533557
end
@@ -836,46 +860,101 @@ function onPause_(obj, dashboard)
836860
end
837861

838862
function renderMultitag_(obj)
839-
%RENDERMULTITAG_ Render composer shell: chips + mode toggle + Plot CTA.
863+
%RENDERMULTITAG_ Render multi-tag preview cards + mode toggle + Plot CTA.
864+
% Each tag gets its own card: name + remove-X + sparkline + range
865+
% label. Live mode updates each sparkline's XData/YData in place
866+
% via refreshLive's 'multitag' branch.
840867
t = obj.Theme_;
841868
if ~isfield(obj.Payload_, 'tags') || ~isfield(obj.Payload_, 'tagKeys'); return; end
842869
tags = obj.Payload_.tags;
843870
obj.CurrentTagKeys_ = obj.Payload_.tagKeys;
844-
g = uigridlayout(obj.hContent_, [8 1]);
845-
g.RowHeight = {16, 'fit', 8, 16, 28, 8, 32, '1x'}; % trailing 1x pins content to top
846-
g.ColumnWidth = {'1x'}; g.Padding = [16 16 16 16];
847-
g.RowSpacing = 0; g.BackgroundColor = t.WidgetBackground;
871+
obj.RenderedMultiKeys_ = obj.CurrentTagKeys_;
872+
nT = numel(tags);
873+
874+
% Per-card height: 16 (name row) + 60 (sparkline) + 14 (range) +
875+
% 4 spacing = ~94. Use 94 to give the sparkline some breathing.
876+
cardH = 94;
877+
nRows = nT + 4; % header + N cards + mode + plot + spacer
878+
rowH = cell(1, nRows);
879+
rowH{1} = 24;
880+
for k = 1:nT; rowH{1 + k} = cardH; end
881+
rowH{nT + 2} = 28; % mode toggle row
882+
rowH{nT + 3} = 32; % plot button
883+
rowH{nT + 4} = '1x'; % bottom spacer
884+
885+
g = uigridlayout(obj.hContent_, [nRows 1]);
886+
g.RowHeight = rowH;
887+
g.ColumnWidth = {'1x'};
888+
g.Padding = [16 16 16 16]; g.RowSpacing = 6;
889+
g.BackgroundColor = t.WidgetBackground;
890+
848891
hHdr = uilabel(g); hHdr.Layout.Row = 1; hHdr.Layout.Column = 1;
849-
hHdr.Text = 'Tags'; hHdr.FontSize = 11; hHdr.FontWeight = 'bold';
892+
hHdr.Text = sprintf('%d tags selected', nT);
893+
hHdr.FontSize = 14; hHdr.FontWeight = 'bold';
850894
hHdr.FontColor = t.ForegroundColor;
851-
hCS = uipanel(g); hCS.Layout.Row = 2; hCS.Layout.Column = 1;
852-
hCS.BorderType = 'none'; hCS.BackgroundColor = t.WidgetBackground;
853-
nT = numel(tags);
854-
obj.hChipsGrid_ = uigridlayout(hCS, [nT 1]);
855-
obj.hChipsGrid_.RowHeight = repmat({24}, 1, nT); obj.hChipsGrid_.ColumnWidth = {'1x'};
856-
obj.hChipsGrid_.Padding = [0 0 0 0]; obj.hChipsGrid_.RowSpacing = 4;
857-
obj.hChipsGrid_.BackgroundColor = t.WidgetBackground;
858-
for i = 1:nT
859-
tg = tags{i};
860-
cr = uigridlayout(obj.hChipsGrid_, [1 2]);
861-
cr.Layout.Row = i; cr.Layout.Column = 1;
862-
cr.ColumnWidth = {'1x', 20}; cr.RowHeight = {'1x'};
863-
cr.Padding = [4 0 4 0]; cr.ColumnSpacing = 4; cr.BackgroundColor = t.WidgetBackground;
864-
ln = uilabel(cr); ln.Layout.Row = 1; ln.Layout.Column = 1;
865-
ln.Text = tg.Name; ln.FontSize = 11; ln.FontColor = t.ForegroundColor;
866-
ln.HorizontalAlignment = 'left'; ln.VerticalAlignment = 'center';
867-
ln.Tooltip = tg.Key;
868-
bx = uibutton(cr, 'push'); bx.Layout.Row = 1; bx.Layout.Column = 2;
869-
bx.Text = char(215); bx.FontSize = 11; bx.FontColor = t.ToolbarFontColor;
895+
hHdr.HorizontalAlignment = 'left'; hHdr.VerticalAlignment = 'center';
896+
897+
obj.hMultiSparkPanels_ = cell(1, nT);
898+
obj.hMultiSparkAxes_ = cell(1, nT);
899+
obj.hMultiSparkLines_ = cell(1, nT);
900+
obj.hMultiRangeLbls_ = cell(1, nT);
901+
902+
for k = 1:nT
903+
tg = tags{k};
904+
cardRow = 1 + k;
905+
906+
cg = uigridlayout(g, [3 1]);
907+
cg.Layout.Row = cardRow; cg.Layout.Column = 1;
908+
cg.RowHeight = {16, '1x', 14};
909+
cg.ColumnWidth = {'1x'};
910+
cg.Padding = [0 0 0 0]; cg.RowSpacing = 2;
911+
cg.BackgroundColor = t.WidgetBackground;
912+
913+
nameRow = uigridlayout(cg, [1 2]);
914+
nameRow.Layout.Row = 1; nameRow.Layout.Column = 1;
915+
nameRow.ColumnWidth = {'1x', 24};
916+
nameRow.RowHeight = {'1x'};
917+
nameRow.Padding = [0 0 0 0]; nameRow.ColumnSpacing = 4;
918+
nameRow.BackgroundColor = t.WidgetBackground;
919+
ln = uilabel(nameRow);
920+
ln.Layout.Row = 1; ln.Layout.Column = 1;
921+
nm = char(tg.Name); ky = char(tg.Key);
922+
if ~strcmp(nm, ky)
923+
ln.Text = sprintf('%s %s %s', nm, char(183), ky);
924+
else
925+
ln.Text = nm;
926+
end
927+
ln.FontSize = 11; ln.FontWeight = 'bold';
928+
ln.FontColor = t.ForegroundColor;
929+
ln.Tooltip = ky;
930+
bx = uibutton(nameRow, 'push');
931+
bx.Layout.Row = 1; bx.Layout.Column = 2;
932+
bx.Text = char(215); bx.FontSize = 11;
870933
bx.BackgroundColor = t.WidgetBackground;
871-
bx.Tooltip = sprintf('Remove "%s" from selection', tg.Name);
872-
bx.ButtonPushedFcn = @(~,~) obj.onChipDeselect_(tg.Key);
934+
bx.Tooltip = sprintf('Remove "%s" from selection', nm);
935+
bx.ButtonPushedFcn = @(~,~) obj.onChipDeselect_(ky);
936+
937+
sp = uipanel(cg);
938+
sp.Layout.Row = 2; sp.Layout.Column = 1;
939+
sp.BackgroundColor = t.WidgetBackground;
940+
sp.BorderColor = t.WidgetBorderColor; sp.BorderType = 'line';
941+
obj.hMultiSparkPanels_{k} = sp;
942+
943+
rl = uilabel(cg);
944+
rl.Layout.Row = 3; rl.Layout.Column = 1;
945+
rl.Text = sprintf('Range: %s (max. %.0f min)', char(8212), obj.SparkWindowSec_/60);
946+
rl.FontSize = 10; rl.FontColor = t.PlaceholderTextColor;
947+
rl.HorizontalAlignment = 'left'; rl.VerticalAlignment = 'center';
948+
obj.hMultiRangeLbls_{k} = rl;
949+
950+
obj.renderMultiSparkline_(k, tg);
873951
end
874-
mh = uilabel(g); mh.Layout.Row = 4; mh.Layout.Column = 1;
875-
mh.Text = 'Mode'; mh.FontSize = 11; mh.FontWeight = 'bold'; mh.FontColor = t.ForegroundColor;
876-
mg = uigridlayout(g, [1 2]); mg.Layout.Row = 5; mg.Layout.Column = 1;
952+
953+
mg = uigridlayout(g, [1 2]);
954+
mg.Layout.Row = nT + 2; mg.Layout.Column = 1;
877955
mg.ColumnWidth = {'1x', '1x'}; mg.RowHeight = {'1x'};
878-
mg.Padding = [0 0 0 0]; mg.ColumnSpacing = 4; mg.BackgroundColor = t.WidgetBackground;
956+
mg.Padding = [0 0 0 0]; mg.ColumnSpacing = 4;
957+
mg.BackgroundColor = t.WidgetBackground;
879958
obj.hModeOverlay_ = uibutton(mg, 'push');
880959
obj.hModeOverlay_.Layout.Row = 1; obj.hModeOverlay_.Layout.Column = 1;
881960
obj.hModeOverlay_.Text = 'Overlay'; obj.hModeOverlay_.FontSize = 11;
@@ -885,14 +964,114 @@ function renderMultitag_(obj)
885964
obj.hModeLinked_.Text = 'Linked grid'; obj.hModeLinked_.FontSize = 11;
886965
obj.hModeLinked_.ButtonPushedFcn = @(~,~) obj.onModeToggle_('LinkedGrid');
887966
obj.applyModeToggleStyles_();
967+
888968
obj.hPlotBtn_ = uibutton(g, 'push');
889-
obj.hPlotBtn_.Layout.Row = 7; obj.hPlotBtn_.Layout.Column = 1;
969+
obj.hPlotBtn_.Layout.Row = nT + 3; obj.hPlotBtn_.Layout.Column = 1;
890970
obj.hPlotBtn_.Text = 'Plot'; obj.hPlotBtn_.FontSize = 11; obj.hPlotBtn_.FontWeight = 'bold';
891971
obj.hPlotBtn_.FontColor = t.DashboardBackground; obj.hPlotBtn_.BackgroundColor = t.Accent;
892972
obj.hPlotBtn_.Tooltip = 'Open an ad-hoc plot with the selected tags';
893973
obj.hPlotBtn_.ButtonPushedFcn = @(~,~) obj.onPlot_();
894974
end
895975

976+
function renderMultiSparkline_(obj, idx, tag)
977+
%RENDERMULTISPARKLINE_ Build the axes + line for the idx-th tag card.
978+
t = obj.Theme_;
979+
sp = obj.hMultiSparkPanels_{idx};
980+
if isempty(sp) || ~isvalid(sp); return; end
981+
try
982+
if ~ismethod(tag, 'getXY')
983+
obj.renderMultiNoData_(idx, 'No data'); return;
984+
end
985+
[tv, y] = tag.getXY();
986+
if isempty(tv) || isempty(y)
987+
obj.renderMultiNoData_(idx, 'No data'); return;
988+
end
989+
[tv, y] = obj.windowSparkData_(tv, y);
990+
ax = axes('Parent', sp, ...
991+
'Units', 'normalized', 'Position', [0.22 0.32 0.75 0.62], ...
992+
'Color', t.WidgetBackground, ...
993+
'XColor', t.PlaceholderTextColor, ...
994+
'YColor', t.PlaceholderTextColor, ...
995+
'Box', 'off', 'FontSize', 7, ...
996+
'TickLength', [0.005 0.005], 'TickDir', 'out');
997+
ln = plot(ax, tv, y, '-', 'Color', t.LineColors{1}, 'LineWidth', 1);
998+
obj.hMultiSparkAxes_{idx} = ax;
999+
obj.hMultiSparkLines_{idx} = ln;
1000+
obj.fitSparkAxes_(ax);
1001+
obj.updateSparkTicks_(tv, y, ax);
1002+
try; ax.Toolbar.Visible = 'off'; catch; end
1003+
try; ax.Interactions = []; catch; end
1004+
obj.updateMultiRangeLabel_(idx, tv);
1005+
catch
1006+
obj.renderMultiNoData_(idx, 'Sparkline unavailable');
1007+
end
1008+
end
1009+
1010+
function renderMultiNoData_(obj, idx, msgText)
1011+
%RENDERMULTINODATA_ Show a placeholder label when a tag has no data.
1012+
sp = obj.hMultiSparkPanels_{idx};
1013+
if isempty(sp) || ~isvalid(sp); return; end
1014+
t = obj.Theme_;
1015+
lb = uilabel(sp); lb.Text = msgText; lb.FontSize = 10;
1016+
lb.FontColor = t.PlaceholderTextColor;
1017+
lb.HorizontalAlignment = 'center';
1018+
lb.VerticalAlignment = 'center';
1019+
end
1020+
1021+
function updateMultiRangeLabel_(obj, idx, tv)
1022+
%UPDATEMULTIRANGELABEL_ Refresh the per-card "Range: …" label.
1023+
if idx > numel(obj.hMultiRangeLbls_); return; end
1024+
rl = obj.hMultiRangeLbls_{idx};
1025+
if isempty(rl) || ~isvalid(rl); return; end
1026+
maxMin = obj.SparkWindowSec_ / 60;
1027+
if isempty(tv) || numel(tv) < 1
1028+
rl.Text = sprintf('Range: %s (max. %.0f min)', char(8212), maxMin);
1029+
return;
1030+
end
1031+
spanSec = tv(end) - tv(1);
1032+
if spanSec > 0 && spanSec < 1 && tv(end) > 7e5
1033+
spanSec = spanSec * 86400;
1034+
end
1035+
if spanSec < 60
1036+
rl.Text = sprintf('Range: last %.0f s (max. %.0f min)', spanSec, maxMin);
1037+
else
1038+
rl.Text = sprintf('Range: last %.1f min (max. %.0f min)', spanSec/60, maxMin);
1039+
end
1040+
end
1041+
1042+
function refreshMultiInPlace_(obj)
1043+
%REFRESHMULTIINPLACE_ Per-card live update: XData/YData/ticks/range.
1044+
if ~isfield(obj.Payload_, 'tags'); return; end
1045+
tags = obj.Payload_.tags;
1046+
for k = 1:numel(tags)
1047+
if k > numel(obj.hMultiSparkLines_); break; end
1048+
try
1049+
tg = tags{k};
1050+
if ~isobject(tg) || ~isvalid(tg); continue; end
1051+
if ~ismethod(tg, 'getXY'); continue; end
1052+
[tv, y] = tg.getXY();
1053+
if isempty(tv); continue; end
1054+
[tv, y] = obj.windowSparkData_(tv, y);
1055+
ln = obj.hMultiSparkLines_{k};
1056+
ax = obj.hMultiSparkAxes_{k};
1057+
if isempty(ln) || ~isvalid(ln) || isempty(ax) || ~isvalid(ax)
1058+
% Stale; try to rebuild this card's sparkline only.
1059+
sp = obj.hMultiSparkPanels_{k};
1060+
if ~isempty(sp) && isvalid(sp)
1061+
delete(sp.Children);
1062+
obj.renderMultiSparkline_(k, tg);
1063+
end
1064+
continue;
1065+
end
1066+
ln.XData = tv; ln.YData = y;
1067+
obj.fitSparkAxes_(ax);
1068+
obj.updateSparkTicks_(tv, y, ax);
1069+
obj.updateMultiRangeLabel_(k, tv);
1070+
catch
1071+
end
1072+
end
1073+
end
1074+
8961075
function applyModeToggleStyles_(obj)
8971076
%APPLYMODETOGGLESTYLES_ Highlight active mode button; style inactive as idle.
8981077
t = obj.Theme_;

0 commit comments

Comments
 (0)