Python produces an aspect ratio that is suitable for its content e.g., respects the structure of the font of each label, axis title, etc. This is the basic code using Jupyter Notebook:
fig, ax = plt.subplots()
ax.boxplot(dataLipid)
ax.set_title("Lipid contact analysis")
plt.xticks([1,2,3,4,5],["x4 Monomers","x2 Monomers\nDimer","x2 Dimers","Monomer\nTrimer", "x4mer"])
plt.show()
However, I want to save the image as a tiff, with a dpi of 600, and a width of 8.3cm (maximum height is an A4 page, but the nature of my question will make that irrelevant).
I'm using the code:
fig.savefig("bar.tiff", dpi=600, format="tiff", pil_kwards = {"compression":"tiff_lzm"})
This produces the following:
All good so far. Next, the Royal Soc. of Chemistry expect a single column image to be 8.3 cm in width (height, no more than the page).
My question:
Is there any way for Python to calculate the height of the figure given only the wdith, whilst maintaining the correct aspect ratio for the fonts, titles and ticks etc.? If I specify width=height, the image looks terrible:
fig.set_size_inches(3.26,3.26)
fig.savefig("bar.tiff", dpi=600, format="tiff", pil_kwards = {"compression":"tiff_lzm"})
Or is this a case where I define the size of the figure first, then adjust the font sizes as a separate step? I'm looking more for a one-fix solution as I have multiple figures of different size requirements (all being dpi=600 though) to produce.
Here you go:
dataLipid = np.random.uniform(0,1,(100,5)) * 90000
fig, ax = plt.subplots()
ax.boxplot(dataLipid)
ax.set_title("Lipid contact analysis")
plt.xticks([1,2,3,4,5],["x4 Monomers","x2 Monomers\nDimer","x2 Dimers","Monomer\nTrimer", "x4mer"])
fig.set_size_inches(3.26,3.26)
# rotate ticks
plt.xticks(rotation=45)
# set bottom margin
plt.subplots_adjust(left=0.2, bottom=0.3)
fig.savefig("bar.tiff", dpi=600, format="tiff", pil_kwards = {"compression":"tiff_lzm"})
There is no general solution as far as I know. So setting the correct margin depends on your content and your data. Rotating the ticks is always a good option to make them readable in case of close spacing.
You can use the Axes.set_aspect method.
# square plot
ax.set_aspect(1)
Also have a look at the tight_layout method to ensure everything is redrawn to fit in the figure.
Related
When using matplotlib to prepare publication-ready figures that include text, one wants to avoid re-scaling or stretching the image when including it into the (e.g. LaTeX) document. To achieve this, the standard procedure is to choose a figure width which matches the document in preparation (in LaTeX this is often the \textwidth or \columnwidth). On the other hand, it is usually less important to fix the figure height. If the figure content has a well-defined aspect ratio (for example, when plotting a grid of square images), there is an optimal figure height that maximally fills the figure, avoiding undue white space.
Ideally then, it should be possible to fix the figure width, tell matplotlib to use a constrained-layout, and let it figure out a good figure height.
Consider the following scenario:
import matplotlib.pyplot as plt
import numpy as np
ims = [np.random.random((20, 20)) for i in range(3)]
# Based on the \textwidth in our LaTeX document
width_inch = 4
# We plot the images in a 1x3 grid
fig, axes = plt.subplots(
nrows=1,
ncols=3,
layout="constrained",
linewidth=5,
edgecolor="black") # Uses the default figsize
for ax, im in zip(axes.flatten(), ims):
ax.imshow(im)
# Fix the figure width
fig.set_figwidth(width_inch)
This produces:
Clearly the figure height is much too large. On the other hand, making it too small leaves too much whitespace:
fig, axes = plt.subplots(
nrows=1,
ncols=3,
layout="constrained",
linewidth=5,
edgecolor="black",
figsize=(width_inch, 1))
Is it possible to get matplotlib to calculate and apply an optimal figure height, or does this need to be manually adjusted?
I want to display several figures with different sizes, making sure that the text has always the same size when the figures are printed. How can I achieve that?
As an example. Let's say I have two figures:
import matplotlib.pylab as plt
import matplotlib as mpl
mpl.rc('font', size=10)
fig1 = plt.figure(figsize = (3,1))
plt.title('This is fig1')
plt.plot(range(0,10),range(0,10))
plt.show()
mpl.rc('font', size=?)
fig2 = plt.figure(figsize = (20,10))
plt.title('This is fig2')
plt.plot(range(0,10),range(0,10))
plt.show()
How can I set the fontsize in such way that when printed the title and axis ticklabels in fig1 will have the same size as those in fig2?
In this case, the font size would be the same (i.e. also 10 points).
However, in Jupyter Notebook the figures may be displayed at a different size if they are too wide, see below:
Note that font size in points has a linear scale, so if you would want the size of the letters to be exactly twice as big, you would need to enter exactly twice the size in points (e.g. 20pt). That way, if you expect to print the second figure at 50% of the original size (length and width, not area), the fonts would be the same size.
But if the only purpose of this script is to make figures to then print, you would do best to set the size as desired (on paper or on screen), and then make the font size equal. You could then paste them in a document at that exact size or ratio and the fonts would indeed be the same size.
As noted by tcaswell, bbox_inches='tight' effectively changes the size of the saved figure, so that the size is different from what you set as figsize. As this might crop more whitespaces from some figures than others, the relative sizes of objects and fonts could end up being different for a given aspect ratio.
What I would like to achive are plots with equal scale aspect ratio, and fixed width, but a dynamically chosen height.
To make this more concrete, consider the following plotting example:
import matplotlib as mpl
import matplotlib.pyplot as plt
def example_figure(slope):
# Create a new figure
fig = plt.figure()
ax = fig.add_subplot(111)
# Set axes to equal aspect ratio
ax.set_aspect('equal')
# Plot a line with a given slope,
# starting from the origin
ax.plot([x * slope for x in range(5)])
# Output the result
return fig
This example code will result in figures of different widths, depending on the data:
example_figure(1).show()
example_figure(2).show()
Matplotlib seems to fit the plots into a certain height, and then chooses the width to accomodate the aspect ratio. The ideal outcome for me would be the opposite -- the two plots above would have the same width, but the second plot would be twice as tall as the first.
Bonus — Difficulty level: Gridspec
In the long run, I would like to create a grid in which one of the plots has a fixed aspect ratio, and I would again like to align the graphs exactly.
# Create a 2x1 grid
import matplotlib.gridspec as gridspec
gs = gridspec.GridSpec(2, 1)
# Create the overall graphic, containing
# the top and bottom figures
fig = plt.figure()
ax1 = fig.add_subplot(gs[0, :], aspect='equal')
ax2 = fig.add_subplot(gs[1, :])
# Plot the lines as before
ax1.plot(range(5))
ax2.plot(range(5))
# Show the figure
fig.show()
The result is this:
So again, my question is: How does one create graphs that vary flexibly in height depending on the data, while having a fixed width?
Two points to avoid potential misunderstandings:
In the above example, both graphs have the same x-axis. This cannot be
taken for granted.
I am aware of the height_ratios option in the gridspec. I can compute
the dimensions of the data, and set the ratios, but this unfortunately
does not control the graphs directly, but rather their bounding boxes,
so (depending on the axis labels), graphs of different widths still occur.
Ideally, the plots' canvas would be aligned exactly.
Another unsolved question is similar, but slightly more convoluted.
Any ideas and suggestions are very welcome, and I'm happy to specify the question further, if required. Thank you very much for considering this!
Have you tried to fix the width with fig.set_figwidth()?
I have a figure with some fairly delicate features that are sensitive to linewidth. I want to save this figure as a PDF that can be easily printed (i.e. no scaling on the receiver's side, just Command+P and go). Unfortunately, when I set figsize=(8.5,11) in order to correctly size the PDF for printing, matplotlib picks a very thick default linewidth and text size that mess up the plot (the legend is too large and the lines in the bar chart overlap). If I set figsize=(17,22) I get a very workable default linewidth and textsize after scaling the PDF to 50% for printing. This is what I have been using, but that solution has become unworkable due to politics and I really don't want to scale the PDF in illustrator every time I make a change.
If I could work with bitmaps I could achieve the desired result by setting figsize=(17,22) and setting dpi to half of the target dpi, but this does not work for PDFs since the dpi parameter seems to be ignored. I would like a PDF that
looks like boxes_good.png (the size-tricked bitmap with thin lines, small text)
has dimensions 8.5x11in (or prints like it does)
can be edited in illustrator (is not a bitmap wrapped in a pdf)
I can't help but suspect that there is an easy way to pull the "double size, half dpi" trick when saving as a PDF, but I gave up on getting that to work and started trying to directly manipulate the linewidths and textsizes. I succeeded in modifying textsize but not linewidth. Here is a record of the things I tried:
# Tried:
# fig.set_size_inches(17,22)
# fig.savefig('boxes.pdf', dpi=100,150,300)
# dpi parameter has no effect on linewidth, text size, or the PDF's dimensions
# 'markerscale=.5' on plt.legend and pax.legend
# no effect
# mp.rcParams['font.size']=8
# worked! made text smaller, now for linewidth...
# mp.rcParams['lines.linewidth']=5
# no effect
# fig.set_linewidth(5)
# no effect
# pax.axhline(linewidth=5)
# only changes x axis not box surrounding subplot
# fig.set_size_inches(8.5,11) immediately before plt.savefig('boxes.pdf')
# identical results to calling plt.figure(figsize=(8.5,11)) in the first place
# I tried passing plt.figure(figsize=(17,22)) and swapping it to 8.5x11 using
# fig.set_size_inches right before saving, but the lines were thick and the text
# was large in the PDF, exactly as if I had set figsize=(8.5,11) to begin with
Here is the sourcefile (I have reduced the plot to essentials, so obvious styling workarounds probably aren't workable solutions)
import numpy as np
import matplotlib as mp
import matplotlib.pyplot as plt
x = np.arange(200)
bottom_red_bar = -np.random.random(200)
bottom_black_bar = np.random.random(200) * bottom_red_bar
fig = plt.figure()
for subplotnum in [1,2,3]:
pax = plt.subplot(310+subplotnum)
pax.set_ylim([-1,1])
bot_rb = pax.bar(x, bottom_red_bar,1,color='r')
bot_bb = pax.bar(x+(1-.3)/2,bottom_black_bar,.3,color='k')
pax.legend([bot_rb,bot_bb],['Filler Text 1','Filler Text 2'],loc=4)
fig.set_size_inches(8.5,11)
fig.savefig('boxes_bad.png',dpi=300) # Lines are too thick
fig.set_size_inches(17,22)
fig.savefig('boxes_good.png',dpi=150) # Lines are just right
fig.set_size_inches(8.5,11)
plt.savefig('boxes.pdf') # Lines are too thick
fig.set_size_inches(17,22) # Lines are just right
plt.savefig('boxes.pdf') # but the PDF needs scaling before printing
So I'm after a way to either adjust the linewidth of an entire figure or a way to have matplotlib save a pdf with dimension metadata different from figsize. Any suggestions?
Thanks Marius, I'll upvote as soon as I get 15 reputation required to do so. While your rcParams didn't quite match what I wanted to do, rcParams itself was the correct place to look so I listed rcParams containing 'linewidth' via rcParams.keys():
>>> [s for s in mp.rcParams.keys() if 'linewidth' in s]
['axes.linewidth', 'grid.linewidth', 'lines.linewidth', 'patch.linewidth']
After some experimentation, I matched up what each param controlled:
mp.rcParams['axes.linewidth']: the square surrounding the entire plot (not ticks, not the y=0 line)
mp.rcParams['grid.linewidth']: didn't test, presumably grid width
mp.rcParams['lines.linewidth']: the width of line plots made using pyplot.plot
mp.rcParams['patch.linewidth']: the width of rectangle strokes including the bars of a pyplot.bar plot, legends, and legend labels of bar plots
mp.rcParams['xtick.minor.width']: the linewidth of small xticks (yticks similar)
mp.rcParams['xtick.major.width']: the linewidth of large xticks (yticks similar)
The specific solution I wound up using was
mp.rcParams['axes.linewidth'] = .5
mp.rcParams['lines.linewidth'] = .5
mp.rcParams['patch.linewidth'] = .5
I would suggest to adjust parameters like linewidth via the rcParams (or your matplotlibrc file):
# mp.rcParams['figure.figsize'] = fig_size # set figure size
mp.rcParams['font.size'] = font_size
mp.rcParams['axes.labelsize'] = font_size
mp.rcParams['axes.linewidth'] = font_size / 12.
mp.rcParams['axes.titlesize'] = font_size
mp.rcParams['legend.fontsize'] = font_size
mp.rcParams['xtick.labelsize'] = font_size
mp.rcParams['ytick.labelsize'] = font_size
I normally use the standard figure.figsize which is (8,6) and a linewidth in the axes object that is 1/12 of the font size (eg. font_size = 16 when I include the plots in twocolumn papers).
Remember that vector graphics doesn't mean, that all lines and letters always have the same size when scaling. It means that you can scale without loosing quality or sharpness (roughly speaking).
I have a problem with plotting multiple subplots. I would like to set the PHYSICAL aspect ratio of the subplots to a fixed value.
In my example I have 12 subplots (4 rows and 3 columns) on a landscape A4 figure. There all subplots are nicely placed on the whole figure, and for all subplots the height is nearly equal to the width.
But if I change the layout of my figure to portrait, the subplots are stretched vertically.
And this is exactly what should not happen. I would like to have the same height and width of the subplots as on the landscape figure. Is there a possibility that the aspect ratio of the subplots stay the same?
Thanks in advance,
Peter
EDIT:
I have found a workaround. But this just works for non-logarithmic axes...
aspectratio=1.0
ratio_default=(ax.get_xlim()[1]-ax.get_xlim()[0])/(ax.get_ylim()[1]-ax.get_ylim()[0])
ax.set_aspect(ratio_default*aspectratio)
Actually, what you're wanting is quite simple... You just need to make sure that adjustable is set to 'box' on your axes, and you have a set aspect ratio for the axes (anything other than 'auto').
You can either do this with the adjustable kwarg when you create the subplots. Alternatively, you can do this after their creation by calling ax.set_adjustable('box'), or by calling ax.set_aspect(aspect, adjustable='box') (where aspect is either 'equal' or a number).
Now, regardless of how the figure is resized, the subplots will maintain the same aspect ratio.
For example:
import matplotlib.pyplot as plt
fig = plt.figure()
ax1 = fig.add_subplot(2,1,1, adjustable='box', aspect=0.3)
ax2 = fig.add_subplot(2,1,2)
ax1.plot(range(10))
ax2.plot(range(10))
plt.show()
Now, compare how the top subplot responds to resizing, vs. how the bottom subplot responds:
The initial plot
Resized to a vertical layout:
Resized to a horizontal layout:
Your workaround works for me. After plotting the data, I call the following function:
def fixed_aspect_ratio(ratio):
'''
Set a fixed aspect ratio on matplotlib plots
regardless of axis units
'''
xvals,yvals = gca().axes.get_xlim(),gca().axes.get_ylim()
xrange = xvals[1]-xvals[0]
yrange = yvals[1]-yvals[0]
gca().set_aspect(ratio*(xrange/yrange), adjustable='box')
In reply to the remark about the solution not working for logarithmic plots in the edit to the original question - you need to adapt as follows:
def fixed_aspect_ratio_loglog(ratio):
'''
Set a fixed aspect ratio on matplotlib loglog plots
regardless of axis units
'''
xvals,yvals = gca().axes.get_xlim(),gca().axes.get_ylim()
xrange = log(xvals[1])-log(xvals[0])
yrange = log(yvals[1])-log(yvals[0])
gca().set_aspect(ratio*(xrange/yrange), adjustable='box')
(Adaptation for semilog plots should now be obvious)