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