I have the following function that makes boxplots given a pandas dataframe:
def plot_boxplots(data, ylabel):
ax = data.plot.box()
ax.set_ylabel(ylabel)
for label in ax.get_xticklabels():
label.set_rotation(90)
plt.tight_layout()
The reason I rotate the labels is that the label names can be long, and otherwise overlap one another. The y-axis label can also be long.
In the end, I get figures where the actual boxes are compressed in one fourth of the height of the figure, the space being mainly occupied by labels.
For instance:
test_df = pd.DataFrame({
"very_very_very_very_long_name_of_first_series" : np.random.normal(loc=-2, scale=2, size=10),
"very_very_very_very_long_name_of_second_series" : np.random.normal(loc=3, scale=1.5, size=10)})
plot_boxplots(test_df, "vertical_axis_with_long_label")
How can I stretch the canvas (or however it is named: the part with the boxes) vertically?
Ideally, I would like to have something automatic, for instance based on a desired proportion between the canvas and the labels part, or some minimal height for the canvas.
Partial solution
Following comments by Andrey Sobolev, I now have something working with a fixed total aspect ratio:
def plot_boxplots(data, ylabel):
fig = plt.figure(figsize=(6, 12))
ax = fig.add_subplot(111)
data.plot.box(ax=ax)
ax.set_ylabel(ylabel)
for label in ax.get_xticklabels():
label.set_rotation(90)
plt.tight_layout()
I'm still interested in a solution to set the canvas size independently of the total figure size.
It is often sufficient to adjust figure size in the way it's shown in the comments, but if you want to change the size of the axes, here is one way to do it:
ax = plt.axes([x_bl, y_bl, x_tr, y_tr])
So, instead of getting the Axes instance from add_subplot, you can explicitly add it with plt.axes, where x_bl, y_bl are the coordinates of bottom left corner, and x_tr, y_tr are the coordinates of top right corner of the canvas in Figure coordinates, i.e. 0., 0. is the bottom left corner of the Figure, and 1., 1. is the top right corner of the Figure.
Related
My plot function creates horizontal bars per year for data with different size. I have to change the figure size for each set of subplots.
I need to place my two legends on lower center of each figure below the x axis label. The positions need to vary depending on the figure size and remain consistent. So for all produced figures, the legends would look like this figure.
Find a snippet of my dataframe here. I have tried to simplify the code as much as I could and I know the plot is missing some element, but I just want to get to my question's answer, not to create a perfect plot here. I understand probably I need to create a variable for my anchor bounding box but I don't know how. Here is my code:
def plot_bars(data,ax):
""" Plots a single chart of work plan for a specific routeid
data: dataframe with section length and year
Returns: None"""
ax.barh(df['year'], df['sec_len'] , left = df['sec_begin'])
ax.set_yticklabels('')
def plot_fig(df):
# Draw the plots
ax_set = df[['routeid','num_bars']].drop_duplicates('routeid')
route_set = ax_set['routeid'].values
h_ratios = ax_set['num_bars'].values
len_ratio = h_ratios.sum()/BARS_PER_PAGE # Global constant set to 40 based on experiencing
fig, axes = plt.subplots(len(route_set), 1, squeeze=False, sharex=True
, gridspec_kw={'height_ratios':h_ratios}
, figsize=(10.25,7.5*len_ratio))
for i, r in enumerate(route_set):
plot_bars(df[df['routeid']==r], axes[i,0])
plt.xlabel('Section length')
## legends
fig.legend(labels=['Legend2'], loc=8, bbox_to_anchor=(0.5, -0.45))
fig.legend( labels=['Legend1'], loc = 8, bbox_to_anchor=(0.5, -0.3))
## Title
fig.suptitle('title', fontsize=16, y=1)
fig.subplots_adjust(hspace=0, top = 1-0.03/len_ratio)
for df in df_list:
plot_fig(df)
The problem is when the figure size changes, the legends move as in these pictures:
here
here
I think the problem boils down to having the correct relative position with respect to the xlabel, so are right that you need to calculate the bbox_to_anchor using the position of the xlabel and the height/width of the axes. Something like this:
fig, (ax, ax1) = plt.subplots(nrows=2, figsize=(5, 4), gridspec_kw={'height_ratios':[4, 1]})
ax.plot(range(10), range(10), label="myLabel")
ax.set_xlabel("xlabel")
x, y = ax.xaxis.get_label().get_position() # position of xlabel
h, w = ax.bbox.height, ax.bbox.width # height and width of the Axes
leg_pos = [x + 0 / w, y - 55 / h] # this needs to be adjusted according to your needs
fig.legend(loc="lower center", bbox_to_anchor=leg_pos, bbox_transform=ax.transAxes)
plt.show()
I'm having some issues in using imshow() from matplotlib, in specific with creating a pdf from it.
I'm dealing with a 500x500 matrix which, for the sake of this question, will be just random values:
np.random.seed(1)
arr = np.array(np.random.random((500, 500)))
The rows and columns are all labelled with different names, but for the sake of this question, let's just make them simple:
labels = ["Big_Label" if x % 2 == 0 else "Bigger_Big_Label" for x in range(500)]
So, I have the following code to plot that matrix:
plt.rc('figure', figsize=(5,5), dpi=500)
fig = plt.figure()
ax = fig.add_subplot(111)
im = ax.imshow(arr)
# defining the same labels for rows and columns
ax.set_xticklabels([''] + labels)
ax.set_yticklabels([''] + labels)
# showing the labels for all the ticks
ax.xaxis.set_major_locator(ticker.MultipleLocator(1))
ax.yaxis.set_major_locator(ticker.MultipleLocator(1))
# personalising the ticks. In particular, labels on top
ax.tick_params(axis='both', which='both', labelsize=0.5, length=0)
ax.tick_params(axis='x',which='both', labelbottom='off', labeltop='on')
ax.tick_params(axis='both', pad=1)
# vertical labels
for label in im.axes.xaxis.get_ticklabels():
label.set_rotation(90)
plt.colorbar(im)
plt.title("Just a Big Title With Words")
# Removing outer lines because they hide part of the lines/columns
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.spines['bottom'].set_visible(False)
ax.spines['left'].set_visible(False)
plt.savefig("fig.pdf")
plt.show()
The first issue is with the title because is on top of the xlabels:
The second issue is, when zooming to see the ylabels and xlabels, they are not aligned. With regards the left labels, they have varying spaces between them and the plot, when I specifically coded ax.tick_params(axis='both', pad=1); if I execute this python code in an IDE or in terminal, this issue doesn't happen (they are all close to the plot). So I guess something is going on when putting this image into a pdf?. With regards with both labels, you can see they are not aligned with the actual rows and columns; for example, the second label on the top is in middle of the blue and orange square, when it should be aligned with the middle of the orange square:
Finally, I tried to call fig.autofmt_xdate() just before plt.savefig(), but the result is even worse, as the top labels are just completely not aligned:
Can you help me solving this issues? I know you have to do a big zoom to see labels, but for the real matrices that I have that is necessary. I also inform that I'm using matplotlib 1.5; I can't use 2.x because of a compatibility issue with another tool
There is a PR for automatically moving the title. Should be included in v2.2 (Feb timeframe) https://github.com/matplotlib/matplotlib/pull/9498, and another for the title padding https://github.com/matplotlib/matplotlib/pull/9816. So we are working on it.
As for your label misalignment in PDF, it looks like there is a bug in label alignment if labelsize is less than 1.0, so don't do that: https://github.com/matplotlib/matplotlib/issues/9963 Its a bug, but I would imagine a low-priority one.
I have created a series of simple greyscale images which I have plotted in a grid (unfortunately, can't upload an image because I don't have a high enough reputation :( ).
The pseudo-code is
# Define matplotlib PyPlot object
nrow = 8
ncol = 12
fig, axes = plt.subplots(nrow, ncol, subplot_kw={'xticks': [], 'yticks': []})
fig.subplots_adjust(hspace=0.05, wspace=0.05)
# Sample the fine scale model at random well locations
for ax in axes.flat:
plot_data = # some Python code here to create 2D grey scale array...
# ... create sub-plot
img = ax.imshow(plot_data, interpolation='none')
img.set_cmap('gray')
# Display the plot
plt.show()
I want to change the aspect ratio so that the plots are squashed vertically and stretched horizontally. I have tried using ax.set_aspect and passing 'aspect' as a subplot_kw argument but to no avail. I also switched 'autoscale' off but I can then only see a handful of pixels. All suggestions welcome!
Thanks in advance!!
#JoeKington - thank you! That was a great reply!! Still trying to get my head around it all. Thanks also to the other posters for their suggestions. So, the original plot looked like this: http://imgur.com/Wi6v4cs
When I set' aspect='auto'' the plot looks like this: http://imgur.com/eRBO6MZ
which is a big improvement. All I need to do now is adjust the subplot size so that sub-plots are plotted in a portrait aspect ratio of eg 2:1, but with the plot filling the entire sub-plot. I guess 'colspan' would do this?
The Short Answer
You're probably wanting to call:
ax.imshow(..., aspect='auto')
imshow will set the aspect ratio of the axes to 1 when it is called, by default. This will override any aspect you specify when you create the axes.
However, this is a common source of confusion in matplotlib. Let me back up and explain what's going on in detail.
Matplotlib's Layout Model
aspect in matplotlib refers to the ratio of the xscale and yscale in data coordinates. It doesn't directly control the ratio of the width and height of the axes.
There are three things that control the size and shape of the "outside box" of a matplotlib axes:
The size/shape of the Figure (shown in red in figures below)
The specified extent of the Axes in figure coordinates (e.g. the subplot location, shown in green in figures below)
The mechanism that the Axes uses to accommodate a fixed aspect ratio (the adjustable parameter).
Axes are always placed in figure coordinates in other words, their shape/size is always a ratio of the figure's shape/size. (Note: Some things such as axes_grid will change this at draw time to get around this limitation.)
However, the extent the axes is given (either from its subplot location or explicitly set extent) isn't necessarily the size it will take up. Depending on the aspect and adjustable parameters, the Axes will shrink inside of its given extent.
To understand how everything interacts, let's plot a circle in lots of different cases.
No Fixed Aspect
In the basic case (no fixed aspect ratio set for the axes), the axes will fill up the entire space allocated to it in figure coordinates (shown by the green box).
The x and y scales (as set by aspect) will be free to change independently, distorting the circle:
When we resize the figure (interactively or at figure creation), the axes will "squish" with it:
Fixed Aspect Ratio, adjustable='box'
However, if the aspect ratio of the plot is set (imshow will force the aspect ratio to 1, by default), the Axes will adjust the size of the outside of the axes to keep the x and y data ratios at the specified aspect.
A key point to understand here, though, is that the aspect of the plot is the aspect of the x and y data scales. It's not the aspect of the width and height of the plot. Therefore, if the aspect is 1, the circle will always be a circle.
As an example, let's say we had done something like:
fig, ax = plt.subplots()
# Plot circle, etc, then:
ax.set(xlim=[0, 10], ylim=[0, 20], aspect=1)
By default, adjustable will be "box". Let's see what happens:
The maximum space the Axes can take up is shown by the green box. However, it has to maintain the same x and y scales. There are two ways this could be accomplished: Change the x and y limits or change the shape/size of the Axes bounding box. Because the adjustable parameter of the Axes is set to the default "box", the Axes shrinks inside of its maximum space.
And as we resize the figure, it will keep shrinking, but maintain the x and y scales by making the Axes use up less of the maximum space allocated to the axes (green box):
Two quick side-notes:
If you're using shared axes, and want to have adjustable="box", use adjustable="box-forced" instead.
If you'd like to control where the axes is positioned inside of the "green box" set the anchor of the axes. E.g. ax.set_anchor('NE') to have it remain "pinned" to the upper right corner of the "green box" as it adjusts its size to maintain the aspect ratio.
Fixed Aspect, adjustable="datalim"
The other main option for adjustable is "datalim".
In this case, matplotlib will keep the x and y scales in data space by changing one of the axes limits. The Axes will fill up the entire space allocated to it. However, if you manually set the x or y limits, they may be overridden to allow the axes to both fill up the full space allocated to it and keep the x/y scale ratio to the specified aspect.
In this case, the x limits were set to 0-10 and the y-limits to 0-20, with aspect=1, adjustable='datalim'. Note that the y-limit was not honored:
And as we resize the figure, the aspect ratio says the same, but the data limits change (in this case, the x-limit is not honored).
On a side note, the code to generate all of the above figures is at: https://gist.github.com/joferkington/4fe0d9164b5e4fe1e247
What does this have to do with imshow?
When imshow is called, it calls ax.set_aspect(1.0), by default. Because adjustable="box" by default, any plot with imshow will behave like the 3rd/4th images above.
For example:
However, if we specify imshow(..., aspect='auto'), the aspect ratio of the plot won't be overridden, and the image will "squish" to take up the full space allocated to the Axes:
On the other hand, if you wanted the pixels to remain "square" (note: they may not be square depending on what's specified by the extent kwarg), you can leave out the aspect='auto' and set the adjustable parameter of the axes to "datalim" instead.
E.g.
ax.imshow(data, cmap='gist_earth', interpolation='none')
ax.set(adjustable="datalim")
Axes Shape is Controlled by Figure Shape
The final part to remember is that the axes shape/size is defined as a percentage of the figure's shape/size.
Therefore, if you want to preserve the aspect ratio of the axes and have a fixed spacing between adjacent subplots, you'll need to define the shape of the figure to match. plt.figaspect is extremely handy for this. It simply generates a tuple of width, height based on a specified aspect ratio or a 2D array (it will take the aspect ratio from the array's shape, not contents).
For your example of a grid of subplots, each with a constant 2x1 aspect ratio, you might consider something like the following (note that I'm not using aspect="auto" here, as we want the pixels in the images to remain square):
import numpy as np
import matplotlib.pyplot as plt
nrows, ncols = 8, 12
dx, dy = 1, 2
figsize = plt.figaspect(float(dy * nrows) / float(dx * ncols))
fig, axes = plt.subplots(nrows, ncols, figsize=figsize)
for ax in axes.flat:
data = np.random.random((10*dy, 10*dx))
ax.imshow(data, interpolation='none', cmap='gray')
ax.set(xticks=[], yticks=[])
pad = 0.05 # Padding around the edge of the figure
xpad, ypad = dx * pad, dy * pad
fig.subplots_adjust(left=xpad, right=1-xpad, top=1-ypad, bottom=ypad)
plt.show()
This question already has answers here:
How to put the legend outside the plot
(18 answers)
Closed 5 years ago.
I am trying to use the keyword bbox_to_anchor() in a matplotlib plot in Python.
Here is a very basic plot that I have produced based on this example. :
import matplotlib.pyplot as plt
x = [1,2,3]
plt.subplot(211)
plt.plot(x, label="test1")
plt.plot([3,2,1], label="test2")
plt.legend(bbox_to_anchor=(0, -0.15, 1, 0), loc=2, ncol=2, mode="expand", borderaxespad=0)
plt.show()
I am trying to automatically place the legend outside the plot using bbox_to_anchor(). In this example, bbox_to_anchor() has 4 arguments listed.
In this particular example (above), the legend is placed below the plot so the number -0.15 needs to be manually entered each time a plot is changed (font size, axis title removed, etc.).
Is it possible to automatically calculate these 4 numbers for the following scenarios?:
legend below plot
legend above plot
legend to right of plot
If not, is it possible to make good guesses about these numbers, in Python?
Also, in the example code above I have set the last 2 numbers in bbox_to_anchor() to 1 and 0 since I do not understand what they are or how they work. What do the last 2 numbers in bbox_to_anchor() mean?
EDIT:
I HIGHLY RECOMMEND USING THE ANSWER FROM ImportanceOfBeingErnest:
How to put the legend outside the plot
EDIT END
This one is easier to understand:
import matplotlib.pyplot as plt
x = [1,2,3]
plt.subplot(211)
plt.plot(x, label="test1")
plt.plot([3,2,1], label="test2")
plt.legend(bbox_to_anchor=(0, 1), loc='upper left', ncol=1)
plt.show()
now play with the to coordinates (x,y). For loc you can use:
valid locations are:
right
center left
upper right
lower right
best
center
lower left
center right
upper left
upper center
lower center
The argument to bbox_to_anchor is in Axes Coordinates. matplotlib uses different coordinate systems to ease placement of objects on the screen. When dealing with positioning legends, the critical coordinate systems to deal with are Axes coordinates, Figure coordinates, and Display coordinates (in pixels) as shown below:
matplotlib coordinate systems
As previously mentioned, bbox_to_anchor is in Axes coordinates and does not require all 4 tuple arguments for a rectangle. You can simply give it a two-argument tuple containing (xpos, ypos) in Axes coordinates. The loc argument in this case will define the anchor point for the legend. So to pin the legend to the outer right of the axes and aligned with the top edge, you would issue the following:
lgd = plt.legend(bbox_to_anchor=(1.01, 1), loc='upper left')
This however does not reposition the Axes with respect to the Figure and this will likely position the legend off of the Figure canvas. To automatically reposition the Figure canvas to align with the Axes and legend, I have used the following algorithm.
First, draw the legend on the canvas to assign it real pixel coordinates:
plt.gcf().canvas.draw()
Then define the transformation to go from pixel coordinates to Figure coordinates:
invFigure = plt.gcf().transFigure.inverted()
Next, get the legend extents in pixels and convert to Figure coordinates. Pull out the farthest extent in the x direction since that is the canvas direction we need to adjust:
lgd_pos = lgd.get_window_extent()
lgd_coord = invFigure.transform(lgd_pos)
lgd_xmax = lgd_coord[1, 0]
Do the same for the Axes:
ax_pos = plt.gca().get_window_extent()
ax_coord = invFigure.transform(ax_pos)
ax_xmax = ax_coord[1, 0]
Finally, adjust the Figure canvas using tight_layout for the proportion of the Axes that must move over to allow room for the legend to fit within the canvas:
shift = 1 - (lgd_xmax - ax_xmax)
plt.gcf().tight_layout(rect=(0, 0, shift, 1))
Note that the rect argument to tight_layout is in Figure coordinates and defines the lower left and upper right corners of a rectangle containing the tight_layout bounds of the Axes, which does not include the legend. So a simple tight_layout call is equivalent to setting rect bounds of (0, 0, 1, 1).
I am plotting a 2D view of a spacecraft orbit using matplotlib. On this orbit, I identify and mark certain events, and then list these events and the corresponding dates in a legend. Before saving the figure to a file, I autozoom on my orbit plot, which causes the legend to be printed directly on top of my plot. What I would like to do is, after autoscaling, somehow find out the width of my legend, and then expand my xaxis to "make room" for the legend on the right side of the plot. Conceptually, something like this;
# ... code that generates my plot up here, then:
ax.autoscale_view()
leg = ax.get_legend()
leg_width = # Somehow get the width of legend in units that I can use to modify my axes
xlims = ax.get_xlim()
ax.set_xlim( [xlims[0], xlims[1] + leg_width] )
fig.savefig('myplot.ps',format='ps')
The main problem I'm having is that ax.set_xlim() takes "data" specific values, whereas leg.get_window_extent reports in window pixels (I think), and even that only after the canvas has been drawn, so I'm not sure how I can get the legend "width" in a way that I can use similar to above.
You can save the figure once to get the real legend location, and then use transData.inverted() to transform screen coordinate to data coordinate.
import pylab as pl
ax = pl.subplot(111)
pl.plot(pl.randn(1000), pl.randn(1000), label="ok")
leg = pl.legend()
pl.savefig("test.png") # save once to get the legend location
x,y,w,h = leg.get_window_extent().bounds
# transform from screen coordinate to screen coordinate
tmp1, tmp2 = ax.transData.inverted().transform([0, w])
print abs(tmp1-tmp2) # this is the with of legend in data coordinate
pl.savefig("test.png")