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)
Related
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 to plot some data and some vertical lines to delimit interesting intervals and then I would like to add some labels to that using text. I can not entirely avoid the labels overlapping with the data or the vertical lines, so I decided to put a bbox around the text to keep it readable. My problem is that I am not able to align it centrally within this box and this is clearly visible and quite annoying in my opinion.
I'm doing something like this:
import numpy
import matplotlib
import matplotlib.pyplot as plt
fig=plt.figure()
plot=fig.add_subplot(111)
x=numpy.linspace(1,10,50)
y=numpy.random.random(50)
plot.plot(x,y)
plot.text(4.5,.5,'TEST TEST',\
bbox={'facecolor':'white','alpha':1,'edgecolor':'none','pad':1})
plot.axvline(5,color='k',linestyle='solid')
plt.show()
Which creates the following plot:
It is quite apparent, that the text is not centered in its bbox. How can I change this? I've spent quite some time on Google but I could not find anything.
EDIT:
Thanks for the suggestions so far.
This suggests that what I see is actually desired behavior. Apparently the bbox in new versions of matplotlib is chosen taking into account the possible maximum descent of the text it contains (the descent of 'g').
When a 'g' appears in the text, this does indeed look good:
Unfortunately in my case there is no 'g' or anything with a similar descent. Does anyone have any further ideas?
Use the text properties ha and va:
plot.text(5.5,.5,'TEST TEST TEST TEST',
bbox={'facecolor':'white','alpha':1,'edgecolor':'none','pad':1},
ha='center', va='center')
To check, draw lines in the center of your plot:
plot.axvline(5.5,color='k',linestyle='solid')
plot.axhline(0.5,color='k',linestyle='solid')
It seems that there are now options to properly position the text in the coordinate system (in particular the new va = 'baseline'). However, as pointed out by user35915, this does not change the alignment of the box relative to the text. The misalignment is particularly obvious in single digit numbers, in particular number '1' (see also this bug). Until this is fixed, my workaround is to place the rectangle by hand, not via the bbox parameter:
import matplotlib.pyplot as plt
import matplotlib.patches as patches
# define the rectangle size and the offset correction
rect_w = 0.2
rect_h = 0.2
rect_x_offset = 0.004
rect_y_offset =0.006
# text coordinates and content
x_text = 0.5
y_text = 0.5
text = '1'
# create the canvas
fig,ax = plt.subplots(figsize=(1,1),dpi=120)
ax.set_xlim((0,1))
ax.set_ylim((0,1))
# place the text
ax.text(x_text, y_text, text, ha="center", va="center", zorder=10)
# compare: vertical alignment with bbox-command: box is too low.
ax.text(x_text+0.3, y_text, text, ha="center", va="center",
bbox=dict(facecolor='wheat',boxstyle='square',edgecolor='black',pad=0.1), zorder=10)
# compare: horizontal alignment with bbox-command: box is too much to the left.
ax.text(x_text, y_text+0.3, text, ha="center", va="center",
bbox=dict(facecolor='wheat',boxstyle='square',edgecolor='black',pad=0.2), zorder=10)
# create the rectangle (below the text, hence the smaller zorder)
rect = patches.Rectangle((x_text-rect_w/2+rect_x_offset, y_text-rect_h/2+rect_y_offset),
rect_w,rect_h,linewidth=1,edgecolor='black',facecolor='white',zorder=9)
# add rectangle to plot
ax.add_patch(rect)
# show figure
fig.show()
Created a legend and formatted the text as needed, but can't figure out how to remove the line "dashes" so that only text appears. Here's what I'm getting now (notice how the line is going through the text that is being right aligned):
#Add legend
leg = ax1.legend(bbox_to_anchor=(0.03, 1.05), prop={'size':8})
leg.get_frame().set_alpha(0)
legText = pylab.gca().get_legend().get_texts()
#Format legend text
legText[0].set_color('#5998ff')
legText[1].set_color('#ffbb82')
legText[2].set_color('#d689c4')
for text in legText:
text.set_ha('right')
As far as I know you can't remove the dashes (this is referred to as the legend handle I think) but you could replace it with something invisible. For example a common problem is to define the legend handle as a coloured rectangle.
The basic idea is to create the handle directly then pass all the items to be including in the legend as two lists. The first list being the handle and the second the text of the label.
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.patches import Rectangle
x = np.linspace(0, 1)
p1, = plt.plot(x, np.cos(x))
leg1 = Rectangle((0, 0), 0, 0, alpha=0.0)
plt.legend([leg1], ['label'], handlelength=0)
plt.show()
I suspect you will need to play with this a bit to get the exact look your looking for. If you don't need the frame I might suggest using the frameon=False argument when calling plt.legend() this way you don't need to worry about the alignment with respect to the box.
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).