Common legend and axes using tiled layout

180 views (last 30 days)
I am using the following code to loop through a handful of directories to plot data. Each subplot has the same units, so I'd like to clean up the figure with one common legend, and common axes.
I am able to get a common legend to show up, but the axes labels are moved below the legend. I'd like to have the x-axis label situated between the legend and the plots.
Is this possible? I know tiledlayout can be a little tricky with a common legend, which I think is contributing to the issue.
%% plotting in log-space
h2 = figure
t = tiledlayout('flow')
for i = 1:length(SDsubFolderNames)
% folders
p = i;
cd(SDsubFolderNames{i})
load('DDMT_Results_workspace.mat')
% plotting done here
nexttile,semilogx(dt,c,'o--','MarkerEdgeColor','#0072BD','MarkerFaceColor','#FFFFFF','MarkerSize',5)
hold on
semilogx(dt,cim,'d--','MarkerEdgeColor','#D95319','MarkerFaceColor','#FFFFFF','MarkerSize',5)
semilogx(dt,concAvg,'.--','MarkerEdgeColor','#7E2F8E','MarkerFaceColor','#FFFFFF')
xlabel('Times [s]'), ylabel('Concentration [mol/m^3]')
hold off
title({['Inlet Pressure: ' num2str(inletP(p)) 'Pa']}')
% plotting done, switch back a directory for the next data
cd ..
end
% figure formatting
t.TileSpacing = 'compact';
t.Padding = 'compact';
title(t,'Simulated Data & 1D Analytic Solution')
lgd = legend('C Mobile, Analytic','C Immobile, Analytic',...
'C Mobile, Simmulated Data')
lgd.Layout.Tile = 'south';
xlabel(t,'Times [s]'), ylabel(t,'Concentration [mol/m^3]')
Attached is an image where I'd like the axis label to be.
  1 Comment
Matthew Elmer
Matthew Elmer on 14 Apr 2023
Edited: Matthew Elmer on 15 Apr 2023
I'm struggling with this same issue. MATLAB has proven to be the most terrible programming language I've had the displeasure of using. I can't believe my school pays for this. I'm just going to save my variables as a .mat file, open it in Python, and do all of the plotting with the wonderful Matplotlib.
Between the absolute unusablilty of the plotting interface, the terrible parallelization performance (and terrible performance in general), the constant harassment for license activation (it seems to forget where my license file is every other day), the lack of dark mode in the IDE, the unusual, unintuitive, and poor-habit-encouraging function syntax, the lack of object-oriented syntax for functions clearly suited to it, the lack of DPI-aware behavior on certain platforms, the excruciating difficulty in saving a figure as a pdf, the insistence of allowing undefined behavior that you have to manually disable with clear all;, the buggy string concatenation, the named parameter arguments requiring two arguments each that better not get mixed up, the bugginess of trying to put subscripts in a figure title, the dynamic typing, the ability to do completely nonsensical things without getting so much as a warning, and a million other things -- I have no idea for the life of me why everyone hasn't abandoned this obsolete language.

Sign in to comment.

Answers (3)

Matt J
Matt J on 14 May 2023
Edited: Matt J on 14 May 2023
One way you can do it is with nested tiled layouts:
TL=tiledlayout(5,1); %Outer layout
t=tiledlayout(TL,2,2); %Inner layout
t.Layout.Tile = 1;
t.Layout.TileSpan = [4 1];
t.TileSpacing = 'compact';
t.Padding = 'compact';
for i=1:4 %Populate the inner layout
nexttile(t)
plot(rand(5,2)); axis square
end
xlabel(t,'Times [s]'); ylabel(t,'Concentration [mol/m^3]');
nexttile(TL,5); %Make a fake plot in the bottom-most outer tile and add legend
h=plot(nan(5,2));axis off;
Hleg=legend('Location','north');
  2 Comments
Matthew Elmer
Matthew Elmer on 17 May 2023
Nested tiled layouts! The idea makes me want to vomit, but I admit that it's a clever workaround!
Matt J
Matt J on 28 Mar 2024
Edited: Matt J on 28 Mar 2024
Downloading nestedLayouts from the File Exchange, we can simplify this (somewhat),
[ax,t,T]=nestedLayouts([5,1],[2,2]);
for i=1:4
plot( ax(1,1,i) , rand(5,2)); axis square
end
xlabel(t(1),'Times [s]'); ylabel(t(1),'Concentration [mol/m^3]');
plot(ax(5,1,1),nan(5,2)); %Fake plot to set legend position
%figure formating
t(1).TileSpacing='compact'; t(1).Padding='compact';
t(1).Layout.TileSpan=[4;1];
ax(5,1,1).Layout.TileSpan=[2,2];
axis(ax(2:end,1,:,:),'off')
%Add legend last
legend(ax(5,1,1),'Location','North')

Sign in to comment.


Frantisek Gaspar
Frantisek Gaspar on 13 Apr 2022
Crude way to achive this is to shink the tiledlayout to make space under it and move the legend there. Both can be done using Position property of both tilelayout and legend.

Matthew Elmer
Matthew Elmer on 13 May 2023
Edited: Matthew Elmer on 13 May 2023
You can't do this in MATLAB -- it's just too incompetent.
You have to export the data you're going to plot as a .mat file (which it looks like you've already done) and then use matplotlib in python. I find it amusing that you can't set the syntax highlighting to accomodate other (better) languages, but here's a MATLAB script that attempts to do what you want, followed by a Python script that actually does what you want. Note that the Python script requires half the number of lines. Both are accompanied by their respective outputs:
legend_above_label.m
clear all; % Required for deterministic behavior in this godawful language
% Create example data
x = linspace(0, 1);
f = x;
g = x.^2;
h = exp(x);
l = log(x);
% Try plotting this in the godawful native functionality
% Default lines are nearly invisible, so we have to thicken them up
set(groot, 'DefaultLineLineWidth', 1.5);
% MATLAB is so terrible at outputting PDFs that we have to hand-tune the
% dimensions of the paper
dfh = 13; % Set default figure height
dfw = 20; % Set default figure width
dlm = 0.0; % Set default left margin
drm = 0.0; % Set default right margin
dtm = 0.0; % Set default top margin
dbm = 0.0; % Set default bottom margin
set(groot, "defaultFigureUnits", "centimeters");
set(groot, "defaultFigurePaperUnits", "centimeters");
set(groot, "defaultFigurePosition", [dlm, dbm, dfw, dfh]);
set(groot, "defaultFigurePaperPosition", ...
[0, 0, dlm + dfw + drm, dbm + dfh + dtm]);
set(groot, "defaultFigurePaperSize", [dlm + dfw + drm, dbm + dfh + dtm]);
% If we don't keep and increment figure number ourselves, we'll run into issues
% when trying to save more than one figure (doesn't matter for this example, but
% is worth noting)
fignum = 1;
fig = figure(fignum);
fignum = fignum + 1;
figTiles = tiledlayout(2, 2, "TileSpacing", "tight");
% Note that we have to tediously call the same named arguments over and over
% again.
title(figTiles, "Some Basic Functions", "FontSize", 14)
xlabel(figTiles, "Function Input x", "FontSize", 14)
ylabel(figTiles, "Function Output", "FontSize", 14)
nexttile;
plot(x, f, "DisplayName", "original");
hold on;
plot(x, f + 0.1, "DisplayName", "shifted up");
plot(x, f - 0.1, "DisplayName", "shifted down");
hold off;
title("Linear");
ylabel("f(x) = x")
grid();
% We can't set these as global defaults, so get used to typing them over and
% over again for each figure if you want something other than the default:
ax = gca;
ax.Title.FontSize = 14;
nexttile;
plot(x, g, "DisplayName", "original");
hold on;
plot(x, g + 0.1, "DisplayName", "shifted up");
plot(x, g - 0.1, "DisplayName", "shifted down");
hold off;
title("Quadratic");
ylabel("g(x) = x^2")
grid();
% We can't set these as global defaults, so get used to typing them over and
% over again for each figure if you want something other than the default:
ax = gca;
ax.Title.FontSize = 14;
nexttile;
plot(x, h, "DisplayName", "original");
hold on;
plot(x, h + 0.1, "DisplayName", "shifted up");
plot(x, h - 0.1, "DisplayName", "shifted down");
hold off;
title("Exponential");
ylabel("h(x) = exp(x)")
grid();
% We can't set these as global defaults, so get used to typing them over and
% over again for each figure if you want something other than the default:
ax = gca;
ax.Title.FontSize = 14;
nexttile;
plot(x, l, "DisplayName", "original");
hold on;
plot(x, l + 0.1, "DisplayName", "shifted up");
plot(x, l - 0.1, "DisplayName", "shifted down");
hold off;
title("Logarithmic");
ylabel("l(x) = log_e(x)")
grid();
% We can't set these as global defaults, so get used to typing them over and
% over again for each figure if you want something other than the default:
ax = gca;
ax.Title.FontSize = 14;
lgd = legend();
lgd.Layout.Tile = 'south';
% Despite our best efforts, the output looks terrible.
% 1) Notice the graphical glitches on subscripts/superscripts
% 2) Notice the lines spilling outside the axes
% 3) We can't save the figure unless we let it pop up first!
% 4) Most importantly, we can't get the legend under the xlabel!
print("matlab_various_functions", "-dpdf", "-fillpage");
close all;
% Instead, export our data for better, easier plots using matplotlib
save("results_data", "x", "f", "g", "h", "l");
% Not including import statements, conversion constants, comments, newline
% spacing, or the save command, this required 74 lines of code.
MATLAB output:
legend_below_label.py
import numpy as np
import scipy as sp
import matplotlib.pyplot as plt
IN2CM = 2.54
CM2IN = 1 / IN2CM
# Setting the defaults is incredibly easy (notice how the default linewidth is
# perfectly fine)
plt.rcParams.update({
"font.size": 14,
"axes.grid": True,
"axes.titleweight": "bold",
"grid.linestyle": "--",
"figure.figsize": (20 * CM2IN, 13 * CM2IN)
})
# Loading the data is incredibly easy
rd = sp.io.loadmat("results_data")
x, f, g, h, l = (rd["x"][0], rd["f"][0], rd["g"][0], rd["h"][0], rd["l"][0])
# Notice that we DON'T have to hand-tune the paper size; matplotlib is smart
# enough to figure it out automatically.
# Also notice that we DON'T have to manually track and increment figure number;
# matplotlib will save multiple figures without an issue.
# Note a couple of things:
# 1) key=value arguments are much easier to read
# 2) We can have strings for figure numbers!
fig, ax = plt.subplots(2, 2, layout="constrained", num="various_functions")
((ax1, ax2),
(ax3, ax4)) = ax
# Note that matplotlib automatically scaled the titles relative to the baseline
# fontsize so that we don't have to repetitively set each font size
fig.suptitle("Some Basic Functions")
fig.supylabel("Function Output")
fig.supxlabel("Function Input $x$")
# Note that the axes we're plotting to aren't state-dependent! i.e. you don't
# have to keep track of doing things in any particular order for it to work
# correctly (like how MATLAB requires you to do `nexttile`)
ax1.set_title("Linear")
ax1.set_ylabel("$f(x) = x$")
ax1.plot(x, f, label="original")
ax1.plot(x, f + 0.1, label="shifted up")
ax1.plot(x, f - 0.1, label="shifted down")
ax2.set_title("Quadratic")
ax2.set_ylabel("$f(x) = x^2$")
ax2.plot(x, g)
ax2.plot(x, g + 0.1)
ax2.plot(x, g - 0.1)
ax3.set_title("Exponential")
ax3.set_ylabel("$f(x) = \exp(x)$")
ax3.plot(x, h)
ax3.plot(x, h + 0.1)
ax3.plot(x, h - 0.1)
ax4.set_title("Logarithmic")
ax4.set_ylabel("$f(x) = \log_e(x)$")
ax4.plot(x, l)
ax4.plot(x, l + 0.1)
ax4.plot(x, l - 0.1)
# Notice how much better the output looks!
# 1) We don't have margins around the figure that we didn't ask for
# 2) Subscripts/superscripts come out great
# 3) Lines don't spill outside the axes
# 4) We can save the figures without having them pop up at all
# But most importantly, our legend is exactly where we want it!
leg = fig.legend(loc="center", ncols=3, bbox_to_anchor=(0.5, -0.05))
plt.savefig("various_functions.pdf", bbox_inches="tight")
# Not including import statements, conversion constants, comments or newline
# spacing, this required 37 lines of code. That's exactly half the number
# required in MATLAB!
Python script output:
  2 Comments
Tobias Kristensen
Tobias Kristensen on 28 Mar 2024
Edited: Tobias Kristensen on 28 Mar 2024
Could you try to relax, and not be so angry? Just because you dont know how to use Matlab doesnt mean that it cant do those things that you say it cant. I know for a fact that it can be done, just in altenative ways.
Both programs have its advantages and disadvantages. Matlab is much easier to use, and are more supported, have a faster debugger, and in many cases better algorithms.
Python is free! Python got many options, and many open source libraries which can be build upon, and is in general faster then matlab.
The speed is nearly never a fault of the program, only for very specific tasks, but more how the user implements this in the two programs.
dear a user of both python and matlab
Matthew Elmer
Matthew Elmer on 1 Apr 2024
> Could you try to relax, and not be so angry?
No, never!
> Just because you dont know how to use Matlab...
Not about to get a PhD in an obsolete programming language just to put four subplots on a figure.
> Matlab is much easier to use, ...
Right. Which is why it took twice the number of lines of code to even come close to the desired solution.
> are more supported
??? What does this mean?
> have a faster debugger
Okay now you're messing with me. When you're stepping through lines of code you shouldn't even notice a difference, unless something is wrong.
> and in many cases better algorithms
More expensive doesn't always mean more better! You must not have searched very hard for the veritable bevy of options for users of modern, open-source languages.
> Python is free! Python got many options, and many open source libraries which can be build upon, and is in general faster then matlab.
And Julia is even faster!
These days I just do everything in PGFPlots -- I find that the ultimate control I have over every detail is so much less frustrating than relying on crufty plotting APIs. Between MATLAB, Julia, and Python, I don't particularly care for any of their plotting APIs! There's always some little annoying detail that leaks through the abstraction.

Sign in to comment.

Community Treasure Hunt

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

Start Hunting!