matplotlib linewidth when saving a PDF - python

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).

Related

How to define the aspect ratio of a matplotlib figure?

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.

Adjust margin around matplotlib figure with wrapped xticks or yticks labels

I would like to create a figure in matplotlib that contains some rather long tick-labels. To fit the labels in reasonable space, I've tried using the wrapping provided in matplotlib:
plt.yticks([1],[a],wrap=True)
This works(output), however the tick-label text then extends to the edge of the figure. How can a margin be placed between the figure edge and the tick-label text while the text is set to wrap?
Failed Attempt:
I tried to create the desired margin by expanding the bounding box(output):
f.savefig("stillnomargin.pdf",bbox_inches=f.bbox_inches.expanded(2,2))
However, the wrapping changed to redistribute the text to the new figure edge.
Full Example:
import matplotlib.pyplot as plt
labeltext = "This is a really long string that I'd rather have wrapped so that it"\
" doesn't go outside of the figure, but if it's long enough it will go"\
" off the top or bottom!"
a4_width = 8.27
a4_height = 11.69
f = plt.figure()
plt.ylim([0,2])
plt.xlim([0,2])
plt.yticks([1],[labeltext],wrap=True)
f.set_size_inches(a4_width, a4_height)
plt.subplots_adjust(left=0.3) #leave space on left for large yticklabel
# outputs full page, yticklabel text to edge
f.savefig("nomargin.pdf")
# tried to expand bounding box to create margin,
# but yticklabel then rewraps to fills space to edge
f.savefig("stillnomargin.pdf",bbox_inches=f.bbox_inches.expanded(2,2))

How to align rows in matplotlib legend with 2 columns

I have an issue where some mathtext formatting is making some labels take up more vertical space than others, which causes them to not line up when placed in two columns in the legend. This is particularly important because the rows are also used to indicate related data.
Here is an example:
import matplotlib as mpl
import matplotlib.pyplot as plt
import matplotlib.mathtext as mathtext
mpl.rc("font", family="Times New Roman",weight='normal')
plt.rcParams.update({'mathtext.default': 'regular' })
plt.plot(1,1, label='A')
plt.plot(2,2, label='B')
plt.plot(3,3, label='C')
plt.plot(4,4,label='$A_{x}^{y}$')
plt.plot(5,5,label='$B_{x}^{y}$')
plt.plot(6,6,label='$C_{x}^{y}$')
plt.legend(fontsize='xx-large', ncol=2)
plt.show()
This generates a figure like so:
For a while, I was able to "fake it" a bit by adding some empty subscripts and superscripts, however this only works when the plot is exported to pdf. It does not appear to work when exporting to png. How can I spread out the first column of labels so that they line up with the second column?
You could set the handleheight keyword argument to a number which is just large enough that the height of the handle is larger than the space taken by the font. This makes the text appear aligned. Doing so may require to set the labelspacing to a small number, in order not to make the legend appear too big.
plt.legend(fontsize='xx-large', ncol=2,handleheight=2.4, labelspacing=0.05)
The drawback of this method, as can be seen in the picture, is that the lines shift upwards compared to the text's baseline. It would probably depent on the usage case if this is acceptable or not.
In case it is not, one needs to dig a little deeper. The following subclasses HandlerLine2D (which is the Handler for Lines) in order to set a slightly different position to the lines. Depending on the total legend size, font size etc. one would need to adapt the number xx in the SymHandler class.
from matplotlib.legend_handler import HandlerLine2D
import matplotlib.lines
class SymHandler(HandlerLine2D):
def create_artists(self, legend, orig_handle,xdescent, ydescent, width, height, fontsize, trans):
xx= 0.6*height
return super(SymHandler, self).create_artists(legend, orig_handle,xdescent, xx, width, height, fontsize, trans)
leg = plt.legend(handler_map={matplotlib.lines.Line2D: SymHandler()},
fontsize='xx-large', ncol=2,handleheight=2.4, labelspacing=0.05)

matplotlib - change figsize but keep fontsize constant

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.

matplotlib text only in plot area

I use the matplotlib library for plotting data in python. In my figure I also have some text to distinguish the data. The problem is that the text goes over the border in the figure window. Is it possible to make the border of the plot cut off the text at the corresponding position and only when I pan inside the plot the the rest of the text gets visible (but only when inside plot area). I use the text() function to display the text
[EDIT:]
The code looks like this:
fig = plt.figure()
ax = fig.add_subplot(111)
# ...
txt = ax.text(x, y, n, fontsize=10)
txt.set_clip_on(False) # I added this due to the answer from tcaswell
I think that your text goes over the border because you didn't set the limits of your plot.
Why don't you try this?
fig=figure()
ax=fig.add_subplot(1,1,1)
text(0.1, 0.85,'dummy text',horizontalalignment='left',verticalalignment='center',transform = ax.transAxes)
This way your text will always be inside the plot and its left corner will be at point (0.1,0.85) in units of your plot.
You just need to tell the text artists to not clip:
txt = ax.text(...)
txt.set_clip_on(False) # this will turn clipping off (always visible)
# txt.set_clip_on(True) # this will turn clipping on (only visible when text in data range)
However, there is a bug matplotlib (https://github.com/matplotlib/matplotlib/pull/1885 now fixed) which makes this not work. The other way to do this (as mentioned in the comments) is
to use
txt = ax.text(..., clip_on=True)

Categories

Resources