1+ classdef (Abstract ) Canvas < handle
2+ % The :mat:class:`Canvas` class is an abstract base class used to manage
3+ % figure creation and plotting layout configuration in the :mat:pkg:`+display` module.
4+ %
5+ % The :mat:class:`Canvas` class standardizes figure and axis formatting,
6+ % provides tiled layout management, and offers utilities for axis selection and cycling.
7+ %
8+ % A :mat:class:`Canvas` subclass can be initialized as follows: ::
9+ %
10+ % config = combustiontoolbox.utils.display.PlotConfig();
11+ % fig = combustiontoolbox.utils.display.PlotFigure(config);
12+ %
13+ % See also: :mat:class:`PlotConfig`, :mat:class:`PlotFigure`, :mat:class:`PlotComposition`
14+
15+ properties
16+ ax matlab.graphics.axis.Axes % Axis handle
17+ tiled matlab.graphics.layout.TiledChartLayout % Tiled layout for multiple axes
18+ fig matlab.ui.Figure % Figure handle
19+ config (1 , 1 ) combustiontoolbox.utils.display.PlotConfig % Plot configuration object
20+ end
21+
22+ properties (Access = private )
23+ currentIndex = 1 % Current index for sequential filling of axes
24+ end
25+
26+ properties (Dependent )
27+ size % Size of the axis array
28+ numAxis % Number of axes in the figure
29+ end
30+
31+ methods
32+
33+ function value = get .size(obj )
34+ % Get the size axis array
35+ value = size(obj .ax );
36+ end
37+
38+ function value = get .numAxis(obj )
39+ % Get the number of axes in the figure
40+ value = numel(obj .ax );
41+ end
42+
43+ end
44+
45+ methods (Access = public )
46+
47+ function obj = Canvas(config )
48+ % Constructor
49+ if nargin > 0
50+ obj.config = config ;
51+ return
52+ end
53+
54+ % Default configuration
55+ obj.config = combustiontoolbox .utils .display .PlotConfig();
56+ end
57+
58+ function obj = add(obj )
59+ % Add a new axis to the figure
60+ % This method is intended to be overridden in subclasses
61+ % to add specific axes or plots.
62+
63+ % Definitions
64+ config = obj .config ;
65+
66+ % Check if fig is already created
67+ if isempty(obj .fig )
68+ obj.fig = figure ;
69+ set(obj .fig , ' units' , ' normalized' , ...
70+ ' innerposition' , config .innerpositionLayout , ...
71+ ' outerposition' , config .outerpositionLayout )
72+ end
73+
74+ % Check if tiled layout is already created
75+ if isempty(obj .tiled )
76+ obj.tiled = tiledlayout(obj .fig , ' flow' , ...
77+ ' Padding' , obj .config .padding , ...
78+ ' TileSpacing' , obj .config .tilespacing );
79+ end
80+
81+ % Create a new axis in the tiled layout
82+ ax = nexttile(obj .tiled );
83+
84+ % Set the axes properties
85+ setFormat(obj , ax );
86+
87+ % Append new axis
88+ obj .ax(end + 1 ) = ax ;
89+
90+ % Add a listener so when the user closes the figure, we reset
91+ addlistener(obj .fig , ' ObjectBeingDestroyed' , @(~,~) obj .onFigureClose());
92+ end
93+
94+ function [ax , config , fig ] = new(obj )
95+ % Create and configure a new figure
96+
97+ % Definitions
98+ config = obj .config ;
99+
100+ % Create figure
101+ fig = figure ;
102+ set(fig , ' units' , ' normalized' , ...
103+ ' innerposition' , config .innerposition , ...
104+ ' outerposition' , config .outerposition )
105+
106+ % Create axes
107+ ax = axes(fig );
108+
109+ % Set axes properties
110+ setFormat(obj , ax );
111+
112+ % Store the axes and figure in the object
113+ obj.ax = ax ;
114+ obj.fig = fig ;
115+
116+ % Add a listener so when the user closes the figure, we reset
117+ addlistener(fig , ' ObjectBeingDestroyed' , @(~,~) obj .onFigureClose());
118+ end
119+
120+ function [axArray , config , fig ] = newArray(obj , varargin )
121+ % Create a figure with an nrows x ncols tiled layout
122+ % and return an array of PlotFigure subclass instances
123+
124+ % Definitions
125+ config = obj .config ;
126+
127+ % Initialization
128+ nrows = [];
129+ ncols = [];
130+
131+ % Parse input arguments
132+ if nargin > 1
133+ nrows = varargin{1 };
134+ ncols = varargin{2 };
135+ end
136+
137+ % Create parent figure and tiled layout
138+ fig = figure ;
139+ set(fig , ' units' , ' normalized' , ...
140+ ' innerposition' , config .innerpositionLayout , ...
141+ ' outerposition' , config .outerpositionLayout )
142+
143+ % Check if nrows and ncols are provided
144+ if isempty(nrows ) || isempty(ncols )
145+ nrows = 1 ; ncols = 1 ;
146+ tiled = tiledlayout(fig , ' flow' , ...
147+ ' Padding' , config .padding , ...
148+ ' TileSpacing' , config .tilespacing );
149+ else
150+ tiled = tiledlayout(fig , nrows , ncols , ...
151+ ' Padding' , config .padding , ...
152+ ' TileSpacing' , config .tilespacing );
153+ end
154+
155+ % Initialize array of objects
156+ axArray = gobjects(ncols , nrows ); % Preallocate array of axes
157+
158+ % Loop through rows and columns to create axes
159+ for i = 1 : ncols
160+
161+ for j = 1 : nrows
162+ % Create new axes in the tiled layout
163+ ax = nexttile(tiled );
164+
165+ % Set the axes properties
166+ setFormat(obj , ax );
167+
168+ % Save the axis in the array
169+ axArray(i , j ) = ax ;
170+ end
171+
172+ end
173+
174+ % Store the axes and figure in the object
175+ obj.ax = axArray ;
176+ obj.tiled = tiled ;
177+ obj.fig = fig ;
178+
179+ % Add a listener so when the user closes the figure, we reset
180+ addlistener(fig , ' ObjectBeingDestroyed' , @(~,~) obj .onFigureClose());
181+ end
182+
183+ function next(obj )
184+ % Cycle to the next index
185+ obj.currentIndex = mod(obj .currentIndex , obj .numAxis ) + 1 ;
186+ end
187+
188+ function ax = getAxis(obj , varargin )
189+ % Return axis to use for plotting
190+ %
191+ % Args:
192+ % obj (PlotFigure): Instance of the PlotFigure superclass
193+ %
194+ % Optional Args:
195+ % index (vector): Index of the axis to use [row, col]
196+ % FLAG_SEQUENTIAL (bool): If true, use sequential filling of axes
197+ %
198+ % Returns:
199+ % ax (Axes): Axis to use for plotting
200+ %
201+ % Examples:
202+ % * ax = obj.getAxis();
203+ % * ax = obj.getAxis('index', [1, 2]);
204+ % * ax = obj.getAxis('FLAG_SEQUENTIAL', true);
205+ % * ax = obj.getAxis('index', [1, 2], 'FLAG_SEQUENTIAL', true); % Start sequence at (1, 2)
206+
207+ % Check if obj.ax exists and is valid
208+ if isempty(obj .ax )
209+ ax = new(obj );
210+ return
211+ end
212+
213+ % If varargin is empty, return current axis
214+ if nargin < 1
215+ ax = obj .ax(1 , 1 );
216+ return
217+ end
218+
219+ % Parse input
220+ p = inputParser ;
221+ addParameter(p , ' index' , [], @(x ) isvector(x ) && length(x ) == 2 );
222+ addParameter(p , ' FLAG_SEQUENTIAL' , false , @(x ) islogical(x ) && isscalar(x ));
223+ parse(p , varargin{: });
224+
225+ % Set properties
226+ index = p .Results .index ;
227+ FLAG_SEQUENTIAL = p .Results .FLAG_SEQUENTIAL ;
228+
229+ % Get axis based on index
230+ if ~FLAG_SEQUENTIAL
231+ if isempty(index )
232+ index1 = ceil(obj .currentIndex / obj .size(2 ));
233+ index2 = mod(obj .currentIndex - 1 , obj .size(2 )) + 1 ;
234+ end
235+
236+ ax = obj .ax(index1 , index2 );
237+ return
238+ end
239+
240+ % Use sequential filling if multiple axes exist
241+ if ~isempty(index )
242+ obj.currentIndex = sub2ind(size(obj .ax ), index(1 ), index(2 ));
243+ end
244+
245+ % Get axis index
246+ k = obj .currentIndex ;
247+
248+ % Get axis
249+ ax = obj .ax(k );
250+
251+ % Increment the current index for sequential filling
252+ next(obj )
253+ end
254+
255+ function ax = setFormat(obj , ax )
256+ % Set the format of the axes based on the configuration
257+ %
258+ % Args:
259+ % obj (PlotFigure): Instance of the PlotFigure superclass
260+ % ax (Axes): Axes to format
261+ %
262+ % Returns:
263+ % ax (Axes): Formatted axes
264+ %
265+ % Example:
266+ % ax = obj.setFormat(gca);
267+
268+ % Definitions
269+ config = obj .config ;
270+
271+ % Set axes properties
272+ set(ax , ' LineWidth' , config .linewidth , ' FontSize' , config .fontsize - 2 );
273+ set(ax , ' BoxStyle' , ' full' , ' TickLabelInterpreter' , ' latex' );
274+ set(ax , ' Layer' , ' Top' );
275+ set(ax , ' XScale' , config .xscale , ' YScale' , config .yscale );
276+ set(ax , ' XDir' , config .xdir , ' YDir' , config .ydir );
277+ xlim(ax , config .axis_x );
278+ ylim(ax , config .axis_y );
279+ box(ax , config .box );
280+ grid(ax , config .grid );
281+ hold(ax , config .hold );
282+ xlabel(ax , config .labelx , ' FontSize' , config .fontsize , ' Interpreter' , ' latex' );
283+ ylabel(ax , config .labely , ' FontSize' , config .fontsize , ' Interpreter' , ' latex' );
284+ end
285+
286+ function setTitle(obj , titleText )
287+ % Set figure title with LaTeX interpreter
288+ if nargin < 2 || isempty(titleText )
289+ titleText = obj .config .title ;
290+ end
291+
292+ if isempty(titleText )
293+ return
294+ end
295+
296+ title(obj .ax , titleText , ' Interpreter' , ' latex' , ' FontSize' , obj .config .fontsize + 4 );
297+ end
298+
299+ function setLegend(obj , legendLabels )
300+ % Set legend on the axis
301+ legend(obj .ax , legendLabels , ' FontSize' , obj .config .fontsize - 2 , ...
302+ ' Location' , obj .config .legend_location , ' Interpreter' , ' latex' );
303+ end
304+
305+ end
306+
307+ methods (Abstract )
308+ plot(obj , varargin )
309+ end
310+
311+ methods (Access = private )
312+
313+ function onFigureClose(obj )
314+ % Reset the axes and figure when the figure is closed
315+ obj.ax = matlab .graphics .axis .Axes .empty(1 ,0 );
316+ obj.tiled = matlab .graphics .layout .TiledChartLayout .empty(1 ,0 );
317+ obj.fig = matlab .ui .Figure .empty(1 ,0 );
318+ obj.currentIndex = 1 ;
319+ end
320+
321+ end
322+
323+ end
0 commit comments