Matplotlib automatic legend outside plot [duplicate] - python

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

Related

How can I fix the space between a plot and legend so that new text doesn't change the spacing?

To be more specific, How can I fix the space between a plot and legend such that if more text is included the legend won't overlap the plot?
For example, if you look at the plots below, when I add more text from "back now"(Plot 1) to "let's break hu"(Plot 2) in the first legend entry -- the legend extends to the left where it begins to cover the plot.
Is there a way to keep the space between the plot and the legend fixed? So that when there is more text the figure extends to the right rather than onto the plot itself.
Plot 1, Plot 2
Code used for the legend:
lgd = ax.legend( patches, lgnd, loc="center right", bbox_to_anchor=(1.25, 0.5), prop=font )
Part of the answer you're looking for depends on how you made the legend and placed them in the first place. That's why people emphasize "Minimal, Complete, and Verifiable example".
To give you a simple introduction, you can control legend location using bbox_to_anchor, which accepts tuple of floats. Since you want to put the legend on the right, I would suggest using bbox_to_anchor with loc=2. This setting comes from bbox_transform. A simplified way to understand it is: bbox_to_anchor defines the relative location of the corner of the legend box and which corner of the 4 is defined by loc.
In the following example, it puts the "upper left" corner of the legend box at (1, 1) which is the upper right corner of the plot ((0,0) is the "lower left" corner of the plot). And to make it clear, I set borderaxespad to 0.
import matplotlib.pyplot as plt
plt.plot([1,2,3], label="test1")
plt.legend(bbox_to_anchor=(1, 1), loc=2, borderaxespad=0.)
plt.show()

Python - Legend overlaps with the pie chart

Using matplotlib in python. The legend overlaps with my pie chart. Tried various options for "loc" such as "best" ,1,2,3... but to no avail. Any Suggestions as to how to either exactly mention the legend position (such as giving padding from the pie chart boundaries) or at least make sure that it does not overlap?
The short answer is: You may use plt.legend's arguments loc, bbox_to_anchor and additionally bbox_transform and mode, to position the legend in an axes or figure.
The long version:
Step 1: Making sure a legend is needed.
In many cases no legend is needed at all and the information can be inferred by the context or the color directly:
If indeed the plot cannot live without a legend, proceed to step 2.
Step 2: Making sure, a pie chart is needed.
In many cases pie charts are not the best way to convey information.
If the need for a pie chart is unambiguously determined, let's proceed to place the legend.
Placing the legend
plt.legend() has two main arguments to determine the position of the legend. The most important and in itself sufficient is the loc argument.
E.g. plt.legend(loc="upper left") placed the legend such that it sits in the upper left corner of its bounding box. If no further argument is specified, this bounding box will be the entire axes.
However, we may specify our own bounding box using the bbox_to_anchor argument. If bbox_to_anchor is given a 2-tuple e.g. bbox_to_anchor=(1,1) it means that the bounding box is located at the upper right corner of the axes and has no extent. It then acts as a point relative to which the legend will be placed according to the loc argument. It will then expand out of the zero-size bounding box. E.g. if loc is "upper left", the upper left corner of the legend is at position (1,1) and the legend will expand to the right and downwards.
This concept is used for the above plot, which tells us the shocking truth about the bias in Miss Universe elections.
import matplotlib.pyplot as plt
import matplotlib.patches
total = [100]
labels = ["Earth", "Mercury", "Venus", "Mars", "Jupiter", "Saturn",
"Uranus", "Neptune", "Pluto *"]
plt.title('Origin of Miss Universe since 1952')
plt.gca().axis("equal")
pie = plt.pie(total, startangle=90, colors=[plt.cm.Set3(0)],
wedgeprops = { 'linewidth': 2, "edgecolor" :"k" })
handles = []
for i, l in enumerate(labels):
handles.append(matplotlib.patches.Patch(color=plt.cm.Set3((i)/8.), label=l))
plt.legend(handles,labels, bbox_to_anchor=(0.85,1.025), loc="upper left")
plt.gcf().text(0.93,0.04,"* out of competition since 2006", ha="right")
plt.subplots_adjust(left=0.1, bottom=0.1, right=0.75)
In order for the legend not to exceed the figure, we use plt.subplots_adjust to obtain more space between the figure edge and the axis, which can then be taken up by the legend.
There is also the option to use a 4-tuple to bbox_to_anchor. How to use or interprete this is detailed in this question: What does a 4-element tuple argument for 'bbox_to_anchor' mean in matplotlib?
and one may then use the mode="expand" argument to make the legend fit into the specified bounding box.
There are some useful alternatives to this approach:
Using figure coordinates
Instead of specifying the legend position in axes coordinates, one may use figure coordinates. The advantage is that this will allow to simply place the legend in one corner of the figure without adjusting much of the rest. To this end, one would use the bbox_transform argument and supply the figure transformation to it. The coordinates given to bbox_to_anchor are then interpreted as figure coordinates.
plt.legend(pie[0],labels, bbox_to_anchor=(1,0), loc="lower right",
bbox_transform=plt.gcf().transFigure)
Here (1,0) is the lower right corner of the figure. Because of the default spacings between axes and figure edge, this suffices to place the legend such that it does not overlap with the pie.
In other cases, one might still need to adapt those spacings such that no overlap is seen, e.g.
title = plt.title('What slows down my computer')
title.set_ha("left")
plt.gca().axis("equal")
pie = plt.pie(total, startangle=0)
labels=["Trojans", "Viruses", "Too many open tabs", "The anti-virus software"]
plt.legend(pie[0],labels, bbox_to_anchor=(1,0.5), loc="center right", fontsize=10,
bbox_transform=plt.gcf().transFigure)
plt.subplots_adjust(left=0.0, bottom=0.1, right=0.45)
Saving the file with bbox_inches="tight"
Now there may be cases where we are more interested in the saved figure than at what is shown on the screen. We may then simply position the legend at the edge of the figure, like so
but then save it using the bbox_inches="tight" to savefig,
plt.savefig("output.png", bbox_inches="tight")
This will create a larger figure, which sits tight around the contents of the canvas:
A sophisticated approach, which allows to place the legend tightly inside the figure, without changing the figure size is presented here:
Creating figure with exact size and no padding (and legend outside the axes)
Using Subplots
An alternative is to use subplots to reserve space for the legend. In this case one subplot could take the pie chart, another subplot would contain the legend. This is shown below.
fig = plt.figure(4, figsize=(3,3))
ax = fig.add_subplot(211)
total = [4,3,2,81]
labels = ["tough working conditions", "high risk of accident",
"harsh weather", "it's not allowed to watch DVDs"]
ax.set_title('What people know about oil rigs')
ax.axis("equal")
pie = ax.pie(total, startangle=0)
ax2 = fig.add_subplot(212)
ax2.axis("off")
ax2.legend(pie[0],labels, loc="center")

How can I get the actual axis limits when using ax.axis('equal')?

I am using ax.axes('equal') to make the axis spacing equal on X and Y, and also setting xlim and ylim. This over-constrains the problem and the actual limits are not what I set in ax.set_xlim() or ax.set_ylim(). Using ax.get_xlim() just returns what I provided. How can I get the actual visible limits of the plot?
f,ax=plt.subplots(1) #open a figure
ax.axis('equal') #make the axes have equal spacing
ax.plot([0,20],[0,20]) #test data set
#change the plot axis limits
ax.set_xlim([2,18])
ax.set_ylim([5,15])
#read the plot axis limits
xlim2=array(ax.get_xlim())
ylim2=array(ax.get_ylim())
#define indices for drawing a rectangle with xlim2, ylim2
sqx=array([0,1,1,0,0])
sqy=array([0,0,1,1,0])
#plot a thick rectangle marking the xlim2, ylim2
ax.plot(xlim2[sqx],ylim2[sqy],lw=3) #this does not go all the way around the edge
What commands will let me draw the green box around the actual edges of the figure?
Related: Force xlim, ylim, and axes('equal') at the same time by letting margins auto-adjust
The actual limits are not known until the figure is drawn. By adding a canvas draw after setting the xlim and ylim, but before obtaining the xlim and ylim, then one can get the desired limits.
f,ax=plt.subplots(1) #open a figure
ax.axis('equal') #make the axes have equal spacing
ax.plot([0,20],[0,20]) #test data set
#change the plot axis limits
ax.set_xlim([2,18])
ax.set_ylim([5,15])
#Drawing is crucial
f.canvas.draw() #<---------- I added this line
#read the plot axis limits
xlim2=array(ax.get_xlim())
ylim2=array(ax.get_ylim())
#define indices for drawing a rectangle with xlim2, ylim2
sqx=array([0,1,1,0,0])
sqy=array([0,0,1,1,0])
#plot a thick rectangle marking the xlim2, ylim2
ax.plot(xlim2[sqx],ylim2[sqy],lw=3)
Not to detract from the accepted answer, which does solve the problem of getting updated axis limits, but is this perhaps an example of the XY problem? If what you want to do is draw a box around the axes, then you don't actually need the xlim and ylim in data coordinates. Instead, you just need to use the ax.transAxes transform which causes both x and y data to be interpreted in normalized coordinates instead of data-centered coordinates:
ax.plot([0,0,1,1,0],[0,1,1,0,0], lw=3, transform=ax.transAxes)
The great thing about this is that your line will stay around the edges of the axes even if the xlim and ylim subsequently change.
You can also use transform=ax.xaxis.get_transform() or transform=ax.yaxis.get_transform() if you want only x or only y to be defined in normalized coordinates, with the other one in data coordinates.

Plotting a line in between subplots

I have created a plot in Python with Pyplot that have multiple subplots.
I would like to draw a line which is not on any of the plots. I know how to draw a line which is part of a plot, but I don't know how to do it on the white space between the plots.
Thank you.
Thank you for the link but I don't want vertical lines between the plots. It is in fact a horizontal line above one of the plots to denote a certain range. Is there not a way to draw an arbitrary line on top of a figure?
First off, a quick way to do this is jut to use axvspan with y-coordinates greater than 1 and clip_on=False. It draws a rectangle rather than a line, though.
As a simple example:
import matplotlib.pyplot as plt
fig, ax = plt.subplots()
ax.plot(range(10))
ax.axvspan(2, 4, 1.05, 1.1, clip_on=False)
plt.show()
For drawing lines, you just specify the transform that you'd like to use as a kwarg to plot (the same applies to most other plotting commands, actually).
To draw in "axes" coordinates (e.g. 0,0 is the bottom left of the axes, 1,1 is the top right), use transform=ax.transAxes, and to draw in figure coordinates (e.g. 0,0 is the bottom left of the figure window, while 1,1 is the top right) use transform=fig.transFigure.
As #tcaswell mentioned, annotate makes this a bit simpler for placing text, and can be very useful for annotations, arrows, labels, etc. You could do this with annotate (by drawing a line between a point and a blank string), but if you just want to draw a line, it's simpler not to.
For what it sounds like you're wanting to do, though, you might want to do things a bit differently.
It's easy to create a transform where the x-coordinates use one transformation and the y-coordinates use a different one. This is what axhspan and axvspan do behind the scenes. It's very handy for something like what you want, where the y-coordinates are fixed in axes coords, and the x-coordinates reflect a particular position in data coords.
The following example illustrates the difference between just drawing in axes coordinates and using a "blended" transform instead. Try panning/zooming both subplots, and notice what happens.
import matplotlib.pyplot as plt
from matplotlib.transforms import blended_transform_factory
fig, (ax1, ax2) = plt.subplots(nrows=2)
# Plot a line starting at 30% of the width of the axes and ending at
# 70% of the width, placed 10% above the top of the axes.
ax1.plot([0.3, 0.7], [1.1, 1.1], transform=ax1.transAxes, clip_on=False)
# Now, we'll plot a line where the x-coordinates are in "data" coords and the
# y-coordinates are in "axes" coords.
# Try panning/zooming this plot and compare to what happens to the first plot.
trans = blended_transform_factory(ax2.transData, ax2.transAxes)
ax2.plot([0.3, 0.7], [1.1, 1.1], transform=trans, clip_on=False)
# Reset the limits of the second plot for easier comparison
ax2.axis([0, 1, 0, 1])
plt.show()
Before panning
After panning
Notice that with the bottom plot (which uses a "blended" transform), the line is in data coordinates and moves with the new axes extents, while the top line is in axes coordinates and stays fixed.

Autoscale a matplotlib Axes to make room for legend

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

Categories