How do you match the width of figures in a tiledlayout when using axis equal?

There are 4 figures combined in a tiledlayout. The respective ratio of their dimensions is 1:1, using axis equal. However, their widths do not match. How can this be adapted? See the MWE:
clear variables; close all; clc;
tiledlayout(4,1)
amplitudes = [200e-3,50e-3,10e-3,200e-6];
wavelenghts = [50,500e-3,50e-3,500e-6];
for i = 1:numel(amplitudes)
nexttile
axis equal
x = 0:(wavelenghts(i)/100):(wavelenghts(i)/2);
y = amplitudes(i)*(1-cospi(2*1/wavelenghts(i)*x));
hold on
area(-x,y)
area(x,y)
xlim([-1 1]*wavelenghts(i)/2)
ylim([0 max(y)])
clear x y;
end

 Accepted Answer

Now that I've gone through the exercise of doing that using tiledlayout, I think I would suggest using neither tiledlayout nor subplot. In @Mathieu NOE's code, he is using subplot, but then manually specifying positions using pixel positions, but at that point there is no point in using subplot. @Mathieu NOE's code is also complicated by trying to do all the layout in pixel coordinates, but normalized units make the calculations easier, and allows the axes to scale more easily with the figure.
Here is how I would do it (again, borrowing inspiration from @Mathieu NOE's code):
amplitudes = [200e-3,50e-3,10e-3,200e-6];
wavelenghts = [50,500e-3,50e-3,500e-6];
ratios = (2*amplitudes)./wavelenghts;
% Allow for ~0.05% space between axes, and above and below.
onespace = 0.03;
allspace = onespace*(numel(ratios)+1);
ratios = (1-allspace).*ratios./sum(ratios);
f = figure;
top = 1;
for i = 1:numel(amplitudes)
% Make each InnerPosition full-width. The actual plot box will shrink
% the width to honor the xlim, ylim, and DataAspectRatio.
left = 0;
width = 1;
% Adjust the heights based on the ratios.
height = ratios(i);
bottom = top - onespace - height;
top = bottom;
ax = axes(f,Units='normalized',Position=[left bottom width height]);
daspect(ax, [1 1 1]);
x = 0:(wavelenghts(i)/100):(wavelenghts(i)/2);
y = amplitudes(i)*(1-cospi(2*1/wavelenghts(i)*x));
hold on
area(-x,y)
area(x,y)
xlim([-1 1]*wavelenghts(i)/2)
ylim([0 max(y)])
clear x y;
end

4 Comments

@Mathieu NOE: Happy to help.
One thing I should point out: If you look closely, you will notice the top axes in the image above is not perfectly aligned with the other three axes. I have no idea why, and I ran out of time when working on my answer above to figure it out (although it was driving me nuts, so I will probably spend some more time looking at it today).
To elaborate a bit further: The axes has five properties (along with their related Mode properties) that can impact the size and shape of the "plot box" (the center or white part of the axes).
  • XLim, YLim, ZLim (along with XLimMode, YLimMode, ZLimMode): These are the familiar properties most users are familiar with. They specify the range of the rulers in X, Y, and Z.
  • DataAspectRatio (and DataAspectRatioMode): This property controls the size ratio (on the screen) of one unit of data in the X, Y, and Z direction. If you set the DataAspectRatio to [1 1 1], you are telling MATLAB that a rectangle that is 1 unit wide in X and 1 unit wide in Y should be square on the screen (and, because axes support three dimensions, a cube that is 1 unit wide in X, 1 unit wide in Y, and 1 unit wide in Z, should be cubical on the screen).
  • PlotBoxAspectRatio (and PlotBoxAspectRatioMode): This property controls the size ratio (on the screen) of the plot box. If you set the PlotBoxAspectRatio to [1 1 1], you are telling MATLAB that a 2D axes should have a square plot box and a 3D axes should have cubical plot box.
By default all five of those properties are automatically calculated (and the corresponding mode properties are set to "auto"). In this configuration:
  • The "plot box" will automatically fill the entire InnerPosition specified, so the PlotBoxAspectRatio will be calculated based on the InnerPosition.
  • The XLim, YLim, and ZLim will be calculated based on the data, and then fudged a little to make them nice round numbers.
  • Once the plot box size is determined, and the XLim, YLim, and ZLim have been calculated, the DataAspectRatio will be determined based on the other four properties.
One important thing to note about these properties is that if you set all five properties the system is overconstrainted.
Let me give an example:
ax = axes;
ax.XLim = [0 5];
ax.YLim = [0 3];
ax.DataAspectRatio = [1 1 1];
ax.PlotBoxAspectRatio = [1 1 1];
ax.Box = 'on';
rectangle(ax, Position=[1 1 1 1]) % Unit rectangle for reference
In the code above, I set the XLim, YLim, DataAspectRatio, and the PlotBoxAspectRatio. However, if you look closely, you will realize that it is impossible to satisfy all the values I specified. Let me go through each of the properties:
  • I set the x-limits and y-limits to [0 1] and [0 5] respectively. You can see those reflected in the picture. As requested, the x-ruler ranges from 0 to 5, and the y-ruler ranges from 0 to 3.
  • I set the DataAspectRatio to [1 1 1]. This tells MATLAB to make a rectangle that is one unit wide in both X and Y a square on the screen. As requested, the unit rectangle in the picture is square on the screen.
  • I also set the PlotBoxAspectRatio to [1 1 1]. This tells MATLAB that the center part of the axes should also be square, but as you can see above, the center part of the axes is definitely not square. MATLAB has ignored the requested PlotBoxAspectRatio in the code above!
Why did MATLAB ignore my instructions? Because it is impossible to make the plot box square while also satisifying the other criteria specified. For example, MATLAB could have made the axes taller to make it square, but that would have meant wither increasing the y-limits or changing the data aspect ratio. Similarly, MATLAb could have made the axes narrower, but that would have meant either decreasing the x-limits or changing the data aspect ratio. In this scenario, MATLAB actively chooses to ignore the PlotBoxAspectRatio specified.
The MATLAB documentation used to have a table that explained this behavior, but it was removed in R2018a (I'm not sure why). You can still find it using the Wayback Machine (look at the property description for the PlotBoxAspectRatioMode). Here is a screen-shot of the table:
Now back to the original question (plotting four axes on top of one another with specific ratios in size).
  • In this example, we are hard-coding the xlim and ylim.
  • The requirement is that the "respective ratio of their dimensions is 1:1", which means DataAspectRatio must be [1 1 1].
  • The width of all four axes should be the same.
  • The four axes should not overlap each other (they should be stacked).
Once you've specified the limits and the data aspect ratio, then the PlotBoxAspectRatio can be derived from the other two values (or from the data directly). This is captured by the ratio in your code (and in the code I borrowed from you). In a 2D axes, the last value of PlotBoxAspectRatio doesn't really impact anything, and the first two values are used to form a ratio between x and y, which would match the value of ratio in your code.
All that is left is to ensure that the widths of all four axes are the same, and then make sure the axes are not overlapping each other, and MATLAB will take care of making sure the limits and requested DataAspectRatio are respected.
This is where the next tricky bit arrises. When you set the InnerPosition (or Position) of an axes, you are setting a bounding box, and MATLAB will draw your axes inside that bounding box. It may not fill that bounding box. If you have specified PlotBoxAspectRatio or DataAspectRatio (or both), MATLAB may shrink your axes (in either height or width) to make it fit inside the allowed space.
Considering the example above, I'm going to add an annotation rectangle that reflects the InnerPosition of the axes.
ax = axes;
ax.XLim = [0 5];
ax.YLim = [0 3];
ax.DataAspectRatio = [1 1 1];
ax.PlotBoxAspectRatio = [1 1 1];
ax.Box = 'on';
rectangle(ax, Position=[1 1 1 1]) % Unit rectangle for reference
annotation('rectangle', Position=ax.InnerPosition, Color='red')
You can see that the InnerPosition of the axes is larger than the actual plot box. That is because MATLAB needed to shrink the axes to meet the criteria (the specific limits and data aspect ratio) specified.
In my version of the code that is plotting four stacked axes:
  • I'm specifying the limits.
  • I'm specifying the DataAspectRatio should be [1 1 1] (similar to calilng axis equal).
  • Those two things combined is sufficient to restrict the PlotBoxAspectRatio to a specific value, so there is no need to set the value (it would have been ignored anyway). The beneit of not setting the PlotBoxAspectRatio is that MATLAB will calculate the value, and you can query the value to make sure it matches your expectations.
  • I'm setting the height of each axes based on the ratio, which should ensure that each axes has the correct relative height.
  • I'm setting the bottom of each axes to avoid overlapping the axes.
  • Finally, I'm setting the left and width of each axes to "fill the container", but that only controls the maximum bounding box for the plot box. Because of the other contraints, MATLAB will shrink the width of the plot box to maintain the requested limits and data aspect ratio (and effective plot box aspect ratio).
For those reasons, I'm not 100% sure why the top axes is not the same width as the others. If I can figure it out, I will add a new comment to this post.
a very big thank you for all this information - quite amazing !
all the best

Sign in to comment.

More Answers (2)

hello
well using axis equal will give you different tile widths according to your data range, so for me, there is a conflict between your requirement of having plots with same width and using axis equal
you can have the required display if you don't use axis equal (for which purpose ? :
clear variables; close all; clc;
tiledlayout(4,1)
amplitudes = [200e-3,50e-3,10e-3,200e-6];
wavelenghts = [50,500e-3,50e-3,500e-6];
for i = 1:numel(amplitudes)
nexttile
% axis equal
x = 0:(wavelenghts(i)/100):(wavelenghts(i)/2);
y = amplitudes(i)*(1-cospi(2*1/wavelenghts(i)*x));
hold on
area(-x,y)
area(x,y)
xlim([-1 1]*wavelenghts(i)/2)
% ylim([0 max(y)]) % no need here
clear x y;
end

10 Comments

I use axis equal, so that the ratio of the amplitude and wavelength match for the individual figure, but not that the ratio of the wavelengths match for all figures. I want the figures to have the same tile width, resulting in different tile heights.
I understand but you may end up with such different tiles heights that some plots will be completely squeezed...
So it is not possible with MATLAB, if I understand correctly, right?
I don't say this is a limitation of Matlab (only) , but let's figure out how it should look like as you describe it
but to do so I will instead plot 4 figures with axis equal - now imagine the result of puting those 4 figures into one : do you really want the result to look like this ?
clear variables; close all; clc;
amplitudes = [200e-3,50e-3,10e-3,200e-6];
wavelenghts = [50,500e-3,50e-3,500e-6];
for i = 1:numel(amplitudes)
figure
x = 0:(wavelenghts(i)/100):(wavelenghts(i)/2);
y = amplitudes(i)*(1-cospi(2*1/wavelenghts(i)*x));
hold on
area(-x,y)
area(x,y)
xlim([-1 1]*wavelenghts(i)/2)
% ylim([0 max(y)]) % no need here
axis equal
clear x y;
end
If they were all cropped to ylim([0 max(y)], yes. Additionally, there should be no space between the figures. It is for an illustration.
maybe this ?
vs is the vertical separation , here = 0.05
do you want to put it to zero and remove the XTicks & XTickslabels for the 3 upper subplots ?
clear variables; close all; clc;
amplitudes = [200e-3,50e-3,10e-3,200e-6];
wavelenghts = [50,500e-3,50e-3,500e-6];
ratios = amplitudes./wavelenghts;
ratios = ratios/sum(ratios);
h = figure('Units','normalized','Position',[0.1 0.1 0.8 0.8]);
top = 0.95;
bottom = 0.05;
vs = 0.05; % vertical separation between the subplots
left = 0.1;
width = 0.7;
height = width*ratios;
for i = 1:numel(amplitudes)
x = 0:(wavelenghts(i)/100):(wavelenghts(i)/2);
y = amplitudes(i)*(1-cospi(2*1/wavelenghts(i)*x));
s = subplot(4,1,i);
hold on
area(-x,y)
area(x,y)
axis equal
pos1 = ([left (top-sum(height(1:i)) - vs*(i-1)) width height(i)]); % [left bottom width height]
set(s,'Position',pos1);
xlim([-1 1]*wavelenghts(i)/2)
ylim([0 max(y)])
clear x y;
end
I still have to test it but that should do what I want it to do. Though I wouldn't have thought someone suggesting me to go back to subplot instead of tiledlayout. Concerning your questions, I will have to see, and potentially get back to you. Thanks for now already!
Just tested it. Why would the first subplot not be of equal width and how could this be corrected? This behavior can also be seen in your figure, just checked it.
hello again
just figured out there was a few bugs in my code :
so, hopefully, now a better code
clear variables; close all; clc;
amplitudes = [200e-3,50e-3,10e-3,200e-6];
wavelenghts = [50,500e-3,50e-3,500e-6];
ratios = (2*amplitudes)./wavelenghts;
ratios = ratios/sum(ratios);
N = numel(amplitudes);
%%%Matlab convention [left bottom width height]%%%
set(0,'Units','pixels');
scrsz = get(0,'ScreenSize');
scr_width = scrsz(3);
scr_heigth = scrsz(4);
h = figure('Units','pixels','Position',[0.1*scr_width 0.1*scr_heigth 0.8*scr_width 0.8*scr_heigth]);
top = 0.75*scr_heigth;
bottom = 0.05*scr_heigth;
vs = 0.03*scr_heigth; % vertical separation between the subplots
width = 0.1*scr_width; % initial value
height = width*ratios*scr_width/scr_heigth;
height_sum = sum(height);
available_height = top - bottom - (N-1)*vs;
correctio_factor = available_height/height_sum;
width = width*correctio_factor;
height = width*ratios*scr_width/scr_heigth;
left = (scr_width)/2 - width;
for i = 1:N
x = 0:(wavelenghts(i)/100):(wavelenghts(i)/2);
y = amplitudes(i)*(1-cospi(2*1/wavelenghts(i)*x));
s(i) = subplot(4,1,i);
hold on
area(-x,y)
area(x,y)
xlim([-1 1]*wavelenghts(i)/2)
ylim([0 2*amplitudes(i)])
pos1 = ([left (top-sum(height(1:i)) - vs*(i-1)) width height(i)]); % [left bottom width height]
set(s(i),'Units','pixels','Position',pos1);
pbaspect([1 1 1]) % Make the x-axis, y-axis equal lengths
daspect([1 1 1]); % equal lengths in all directions
clear x y;
end
and regarding tiledlayout maybe there is also a solution, but I have to admit I am still using the "old" way with subplot most of the time
I am not sure though that it's so obvious (after some searches)

Sign in to comment.

You can do this with tiledlayout, but it is not really what tiledlayout was designed to do. Part of the core of the layout algorithm for tiledlayout is that each tile is given the same height/width within which to draw, but in your situation you don't want each tile to have the same height.
To get around this, you need to use tile spanning. You can tell a single axes that it should occupy multiple tiles, and that can serve as a sort of proxy for the axes height/width. However, this doesn't work perfectly for your specific scenario because tiledlayout is allocating space based on the OuterPosition (including the tick labels) not the InnerPosition (which is just the white part of the axes). This means that (for example) when you tell one axes to be 2 tiles tall, the actual white part of the axes won't necessarily be twice as tall as the one below it, but rather it is drawing using 2 tiles worth of space. For example:
tcl = tiledlayout('vertical');
ax(1) = nexttile(1, [2 1]); % Occupy 2 tiles tall and 1 tile wide.
ax(2) = nexttile(2, [1 1]); % Occupy 1 tile tall and 1 tile wide.
set(ax, Units='pixels');
vertcat(ax.InnerPosition)
ans = 2×4
73.8000 174.8556 434.0000 214.6444 73.8000 47.2000 434.0000 86.9889
<mw-icon class=""></mw-icon>
<mw-icon class=""></mw-icon>
In the output, notice that the first axes is a bit more than twice the height of the second axes, rather than being exactly twice the height. That is what I mean when I say that tiledlayout is not really designed to do this.
With all that lead-in, here is a version of your script that does something similar using tiledlayout (borrowing some code from @Mathieu NOE)
amplitudes = [200e-3,50e-3,10e-3,200e-6];
wavelenghts = [50,500e-3,50e-3,500e-6];
ratios = (2*amplitudes)./wavelenghts;
ratios = round(ratios/min(ratios));
% The first axes wasn't visible at all, so I'm skewing the data a bit here
% to make it visible.
ratios(1) = 5;
f = figure;
tcl = tiledlayout(f, 'vertical', TileSpacing='tight',Padding='compact');
for i = 1:numel(amplitudes)
ax = nexttile(tcl, i, [ratios(i) 1]);
x = 0:(wavelenghts(i)/100):(wavelenghts(i)/2);
y = amplitudes(i)*(1-cospi(2*1/wavelenghts(i)*x));
hold on
area(-x,y)
area(x,y)
xlim([-1 1]*wavelenghts(i)/2)
ylim([0 max(y)])
clear x y;
end

Categories

Find more on Graphics Object Properties in Help Center and File Exchange

Products

Release

R2022a

Community Treasure Hunt

Find the treasures in MATLAB Central and discover how the community can help you!

Start Hunting!