Arrows between matplotlib subplots - python

I decided to play around with this example code a bit. I was able to figure out how to draw a straight line between the two subplots, even when the line is outside the bounds of one of the subplots.
import matplotlib.pyplot as plt
import matplotlib as mpl
import numpy as np
fig = plt.figure(figsize=(10, 5))
ax1 = fig.add_subplot(121)
ax2 = fig.add_subplot(122)
axs = [ax1, ax2]
# Fixing random state for reproducibility
np.random.seed(19680801)
# generate some random test data
all_data = [np.random.normal(0, std, 100) for std in range(6, 10)]
# plot violin plot
axs[0].violinplot(all_data,
showmeans=False,
showmedians=True)
axs[0].set_title('Violin plot')
# plot box plot
axs[1].boxplot(all_data)
axs[1].set_title('Box plot')
# adding horizontal grid lines
for ax in axs:
ax.yaxis.grid(True)
ax.set_xticks([y + 1 for y in range(len(all_data))])
ax.set_xlabel('Four separate samples')
ax.set_ylabel('Observed values')
for tick in ax.xaxis.get_major_ticks():
tick.label.set_fontsize(20)
plt.setp(axs[0], xticklabels=['x1', 'x2', 'x3', 'x4'])
transFigure = fig.transFigure.inverted()
coord1 = transFigure.transform(ax1.transData.transform([5,10]))
coord2 = transFigure.transform(ax2.transData.transform([2,-10]))
line = mpl.lines.Line2D((coord1[0],coord2[0]),(coord1[1],coord2[1]),
c='k', lw=5, transform=fig.transFigure)
fig.lines.append(line)
Yes that added line is ugly but I just wanted to get it functional.
However, what I'd really like to do is make an arrow between the subplots, and I can't figure out how without jury-rigging my own arrow tails. Is there a way to do this that uses the matplotlib.pyplot.arrow class?

I also wanted to draw an arrow between two subplots but I didn't even know where to start! However, the line between subplots example in the original question gave me enough of a clue to get started...
First, I reduced the code in the original question to a minimal working example:
from matplotlib import lines, pyplot as plt
fig = plt.figure()
# First subplot
ax1 = fig.add_subplot(121)
plt.plot([0, 1], [0, 1])
# Second subplot
ax2 = fig.add_subplot(122)
plt.plot([0, 1], [0, 1])
# Add line from one subplot to the other
xyA = [0.5, 1.0]
ax1.plot(*xyA, "o")
xyB = [0.75, 0.25]
ax2.plot(*xyB, "o")
transFigure = fig.transFigure.inverted()
coord1 = transFigure.transform(ax1.transData.transform(xyA))
coord2 = transFigure.transform(ax2.transData.transform(xyB))
line = lines.Line2D(
(coord1[0], coord2[0]), # xdata
(coord1[1], coord2[1]), # ydata
transform=fig.transFigure,
color="black",
)
fig.lines.append(line)
# Show figure
plt.show()
This produces the following output:
Then, using this blog post, I thought the answer was to create a matplotlib.patches.FancyArrowPatch and append it to fig.patches (instead of creating a matplotlib.lines.Line2D and appending it to fig.lines). After consulting the matplotlib.patches.FancyArrowPatch documentation, plus some trial and error, I came up with something that works in matplotlib 3.1.2:
from matplotlib import patches, pyplot as plt
fig = plt.figure()
# First subplot
ax1 = fig.add_subplot(121)
plt.plot([0, 1], [0, 1])
# Second subplot
ax2 = fig.add_subplot(122)
plt.plot([0, 1], [0, 1])
# Add line from one subplot to the other
xyA = [0.5, 1.0]
ax1.plot(*xyA, "o")
xyB = [0.75, 0.25]
ax2.plot(*xyB, "o")
transFigure = fig.transFigure.inverted()
coord1 = transFigure.transform(ax1.transData.transform(xyA))
coord2 = transFigure.transform(ax2.transData.transform(xyB))
arrow = patches.FancyArrowPatch(
coord1, # posA
coord2, # posB
shrinkA=0, # so tail is exactly on posA (default shrink is 2)
shrinkB=0, # so head is exactly on posB (default shrink is 2)
transform=fig.transFigure,
color="black",
arrowstyle="-|>", # "normal" arrow
mutation_scale=30, # controls arrow head size
linewidth=3,
)
fig.patches.append(arrow)
# Show figure
plt.show()
However, as per the comments below, this does not work in matplotlib 3.4.2, where you get this:
Notice that the ends of the arrow do not line up with the target points (orange circles), which they should do.
This matplotlib version change also causes the original line example to fail in the same way.
However, there is a better patch! Use ConnectionPatch (docs), which is a subclass of FancyArrowPatch, instead of using FancyArrowPatch directly as the ConnectionPatch is designed specifically for this use case and deals with the transform more correctly, as shown in this matplotlib documentation example:
fig = plt.figure()
# First subplot
ax1 = fig.add_subplot(121)
plt.plot([0, 1], [0, 1])
# Second subplot
ax2 = fig.add_subplot(122)
plt.plot([0, 1], [0, 1])
# Add line from one subplot to the other
xyA = [0.5, 1.0]
ax1.plot(*xyA, "o")
xyB = [0.75, 0.25]
ax2.plot(*xyB, "o")
# ConnectionPatch handles the transform internally so no need to get fig.transFigure
arrow = patches.ConnectionPatch(
xyA,
xyB,
coordsA=ax1.transData,
coordsB=ax2.transData,
# Default shrink parameter is 0 so can be omitted
color="black",
arrowstyle="-|>", # "normal" arrow
mutation_scale=30, # controls arrow head size
linewidth=3,
)
fig.patches.append(arrow)
# Show figure
plt.show()
This produces the correct output in both matplotlib 3.1.2 and matplotlib 3.4.2, which looks like this:
To draw a correctly positioned line connecting across two subplots in matplotlib 3.4.2, use a ConnectionPatch as above but with arrowstyle="-" (i.e. no arrow heads, so just a line).
NB: You cannot use:
plt.arrow as it is automatically added to the current axes so only appears in one subplot
matplotlib.patches.Arrow as the axes-figure transform skews the arrow-head
matplotlib.patches.FancyArrow as this also results in a skewed arrow-head

Related

Error when trying to Move Figure Legend in MatplotLib [duplicate]

I have a series of 20 plots (not subplots) to be made in a single figure. I want the legend to be outside of the box. At the same time, I do not want to change the axes, as the size of the figure gets reduced.
I want to keep the legend box outside the plot area (I want the legend to be outside at the right side of the plot area).
Is there a way to reduce the font size of the text inside the legend box, so that the size of the legend box will be small?
There are a number of ways to do what you want. To add to what Christian Alis and Navi already said, you can use the bbox_to_anchor keyword argument to place the legend partially outside the axes and/or decrease the font size.
Before you consider decreasing the font size (which can make things awfully hard to read), try playing around with placing the legend in different places:
So, let's start with a generic example:
import matplotlib.pyplot as plt
import numpy as np
x = np.arange(10)
fig = plt.figure()
ax = plt.subplot(111)
for i in xrange(5):
ax.plot(x, i * x, label='$y = %ix$' % i)
ax.legend()
plt.show()
If we do the same thing, but use the bbox_to_anchor keyword argument we can shift the legend slightly outside the axes boundaries:
import matplotlib.pyplot as plt
import numpy as np
x = np.arange(10)
fig = plt.figure()
ax = plt.subplot(111)
for i in xrange(5):
ax.plot(x, i * x, label='$y = %ix$' % i)
ax.legend(bbox_to_anchor=(1.1, 1.05))
plt.show()
Similarly, make the legend more horizontal and/or put it at the top of the figure (I'm also turning on rounded corners and a simple drop shadow):
import matplotlib.pyplot as plt
import numpy as np
x = np.arange(10)
fig = plt.figure()
ax = plt.subplot(111)
for i in xrange(5):
line, = ax.plot(x, i * x, label='$y = %ix$'%i)
ax.legend(loc='upper center', bbox_to_anchor=(0.5, 1.05),
ncol=3, fancybox=True, shadow=True)
plt.show()
Alternatively, shrink the current plot's width, and put the legend entirely outside the axis of the figure (note: if you use tight_layout(), then leave out ax.set_position():
import matplotlib.pyplot as plt
import numpy as np
x = np.arange(10)
fig = plt.figure()
ax = plt.subplot(111)
for i in xrange(5):
ax.plot(x, i * x, label='$y = %ix$'%i)
# Shrink current axis by 20%
box = ax.get_position()
ax.set_position([box.x0, box.y0, box.width * 0.8, box.height])
# Put a legend to the right of the current axis
ax.legend(loc='center left', bbox_to_anchor=(1, 0.5))
plt.show()
And in a similar manner, shrink the plot vertically, and put a horizontal legend at the bottom:
import matplotlib.pyplot as plt
import numpy as np
x = np.arange(10)
fig = plt.figure()
ax = plt.subplot(111)
for i in xrange(5):
line, = ax.plot(x, i * x, label='$y = %ix$'%i)
# Shrink current axis's height by 10% on the bottom
box = ax.get_position()
ax.set_position([box.x0, box.y0 + box.height * 0.1,
box.width, box.height * 0.9])
# Put a legend below current axis
ax.legend(loc='upper center', bbox_to_anchor=(0.5, -0.05),
fancybox=True, shadow=True, ncol=5)
plt.show()
Have a look at the matplotlib legend guide. You might also take a look at plt.figlegend().
Placing the legend (bbox_to_anchor)
A legend is positioned inside the bounding box of the axes using the loc argument to plt.legend.
E.g., loc="upper right" places the legend in the upper right corner of the bounding box, which by default extents from (0, 0) to (1, 1) in axes coordinates (or in bounding box notation (x0, y0, width, height) = (0, 0, 1, 1)).
To place the legend outside of the axes bounding box, one may specify a tuple (x0, y0) of axes coordinates of the lower left corner of the legend.
plt.legend(loc=(1.04, 0))
A more versatile approach is to manually specify the bounding box into which the legend should be placed, using the bbox_to_anchor argument. One can restrict oneself to supply only the (x0, y0) part of the bbox. This creates a zero span box, out of which the legend will expand in the direction given by the loc argument. E.g.,
plt.legend(bbox_to_anchor=(1.04, 1), loc="upper left")
places the legend outside the axes, such that the upper left corner of the legend is at position (1.04, 1) in axes coordinates.
Further examples are given below, where additionally the interplay between different arguments like mode and ncols are shown.
l1 = plt.legend(bbox_to_anchor=(1.04, 1), borderaxespad=0)
l2 = plt.legend(bbox_to_anchor=(1.04, 0), loc="lower left", borderaxespad=0)
l3 = plt.legend(bbox_to_anchor=(1.04, 0.5), loc="center left", borderaxespad=0)
l4 = plt.legend(bbox_to_anchor=(0, 1.02, 1, 0.2), loc="lower left",
mode="expand", borderaxespad=0, ncol=3)
l5 = plt.legend(bbox_to_anchor=(1, 0), loc="lower right",
bbox_transform=fig.transFigure, ncol=3)
l6 = plt.legend(bbox_to_anchor=(0.4, 0.8), loc="upper right")
Details about how to interpret the 4-tuple argument to bbox_to_anchor, as in l4, can be found in this question. The mode="expand" expands the legend horizontally inside the bounding box given by the 4-tuple. For a vertically expanded legend, see this question.
Sometimes it may be useful to specify the bounding box in figure coordinates instead of axes coordinates. This is shown in the example l5 from above, where the bbox_transform argument is used to put the legend in the lower left corner of the figure.
Postprocessing
Having placed the legend outside the axes often leads to the undesired situation that it is completely or partially outside the figure canvas.
Solutions to this problem are:
Adjust the subplot parameters
One can adjust the subplot parameters such, that the axes take less space inside the figure (and thereby leave more space to the legend) by using plt.subplots_adjust. E.g.,
plt.subplots_adjust(right=0.7)
leaves 30% space on the right-hand side of the figure, where one could place the legend.
Tight layout
Using plt.tight_layout Allows to automatically adjust the subplot parameters such that the elements in the figure sit tight against the figure edges. Unfortunately, the legend is not taken into account in this automatism, but we can supply a rectangle box that the whole subplots area (including labels) will fit into.
plt.tight_layout(rect=[0, 0, 0.75, 1])
Saving the figure with bbox_inches = "tight"
The argument bbox_inches = "tight" to plt.savefig can be used to save the figure such that all artist on the canvas (including the legend) are fit into the saved area. If needed, the figure size is automatically adjusted.
plt.savefig("output.png", bbox_inches="tight")
Automatically adjusting the subplot parameters
A way to automatically adjust the subplot position such that the legend fits inside the canvas without changing the figure size can be found in this answer: Creating figure with exact size and no padding (and legend outside the axes)
Comparison between the cases discussed above:
Alternatives
A figure legend
One may use a legend to the figure instead of the axes, matplotlib.figure.Figure.legend. This has become especially useful for Matplotlib version 2.1 or later, where no special arguments are needed
fig.legend(loc=7)
to create a legend for all artists in the different axes of the figure. The legend is placed using the loc argument, similar to how it is placed inside an axes, but in reference to the whole figure - hence it will be outside the axes somewhat automatically. What remains is to adjust the subplots such that there is no overlap between the legend and the axes. Here the point "Adjust the subplot parameters" from above will be helpful. An example:
import numpy as np
import matplotlib.pyplot as plt
x = np.linspace(0, 2*np.pi)
colors = ["#7aa0c4", "#ca82e1", "#8bcd50", "#e18882"]
fig, axes = plt.subplots(ncols=2)
for i in range(4):
axes[i//2].plot(x, np.sin(x+i), color=colors[i], label="y=sin(x + {})".format(i))
fig.legend(loc=7)
fig.tight_layout()
fig.subplots_adjust(right=0.75)
plt.show()
Legend inside dedicated subplot axes
An alternative to using bbox_to_anchor would be to place the legend in its dedicated subplot axes (lax).
Since the legend subplot should be smaller than the plot, we may use gridspec_kw={"width_ratios":[4, 1]} at axes creation.
We can hide the axes lax.axis("off"), but we still put a legend in. The legend handles and labels need to obtained from the real plot via h, l = ax.get_legend_handles_labels() and can then be supplied to the legend in the lax subplot, lax.legend(h, l). A complete example is below.
import matplotlib.pyplot as plt
plt.rcParams["figure.figsize"] = 6, 2
fig, (ax, lax) = plt.subplots(ncols=2, gridspec_kw={"width_ratios":[4, 1]})
ax.plot(x, y, label="y=sin(x)")
....
h, l = ax.get_legend_handles_labels()
lax.legend(h, l, borderaxespad=0)
lax.axis("off")
plt.tight_layout()
plt.show()
This produces a plot which is visually pretty similar to the plot from above:
We could also use the first axes to place the legend, but use the bbox_transform of the legend axes,
ax.legend(bbox_to_anchor=(0, 0, 1, 1), bbox_transform=lax.transAxes)
lax.axis("off")
In this approach, we do not need to obtain the legend handles externally, but we need to specify the bbox_to_anchor argument.
Further reading and notes:
Consider the Matplotlib legend guide with some examples of other stuff you want to do with legends.
Some example code for placing legends for pie charts may directly be found in answer to this question: Python - Legend overlaps with the pie chart
The loc argument can take numbers instead of strings, which make calls shorter, however, they are not very intuitively mapped to each other. Here is the mapping for reference:
Just call legend() after the plot() call like this:
# Matplotlib
plt.plot(...)
plt.legend(loc='center left', bbox_to_anchor=(1, 0.5))
# Pandas
df.myCol.plot().legend(loc='center left', bbox_to_anchor=(1, 0.5))
Results would look something like this:
You can make the legend text smaller by specifying set_size of FontProperties.
Resources:
Legend guide
matplotlib.legend
matplotlib.pyplot.legend
matplotlib.font_manager
set_size(self, size)
Valid font size are xx-small, x-small, small, medium, large, x-large, xx-large, larger, smaller, and None.
Real Python: Python Plotting With Matplotlib (Guide)
import matplotlib.pyplot as plt
from matplotlib.font_manager import FontProperties
fontP = FontProperties()
fontP.set_size('xx-small')
p1, = plt.plot([1, 2, 3], label='Line 1')
p2, = plt.plot([3, 2, 1], label='Line 2')
plt.legend(handles=[p1, p2], title='title', bbox_to_anchor=(1.05, 1), loc='upper left', prop=fontP)
fontsize='xx-small' also works, without importing FontProperties.
plt.legend(handles=[p1, p2], title='title', bbox_to_anchor=(1.05, 1), loc='upper left', fontsize='xx-small')
To place the legend outside the plot area, use loc and bbox_to_anchor keywords of legend(). For example, the following code will place the legend to the right of the plot area:
legend(loc="upper left", bbox_to_anchor=(1,1))
For more info, see the legend guide
Short answer: you can use bbox_to_anchor + bbox_extra_artists + bbox_inches='tight'.
Longer answer:
You can use bbox_to_anchor to manually specify the location of the legend box, as some other people have pointed out in the answers.
However, the usual issue is that the legend box is cropped, e.g.:
import matplotlib.pyplot as plt
# data
all_x = [10,20,30]
all_y = [[1,3], [1.5,2.9],[3,2]]
# Plot
fig = plt.figure(1)
ax = fig.add_subplot(111)
ax.plot(all_x, all_y)
# Add legend, title and axis labels
lgd = ax.legend( [ 'Lag ' + str(lag) for lag in all_x], loc='center right', bbox_to_anchor=(1.3, 0.5))
ax.set_title('Title')
ax.set_xlabel('x label')
ax.set_ylabel('y label')
fig.savefig('image_output.png', dpi=300, format='png')
In order to prevent the legend box from getting cropped, when you save the figure you can use the parameters bbox_extra_artists and bbox_inches to ask savefig to include cropped elements in the saved image:
fig.savefig('image_output.png', bbox_extra_artists=(lgd,), bbox_inches='tight')
Example (I only changed the last line to add 2 parameters to fig.savefig()):
import matplotlib.pyplot as plt
# data
all_x = [10,20,30]
all_y = [[1,3], [1.5,2.9],[3,2]]
# Plot
fig = plt.figure(1)
ax = fig.add_subplot(111)
ax.plot(all_x, all_y)
# Add legend, title and axis labels
lgd = ax.legend( [ 'Lag ' + str(lag) for lag in all_x], loc='center right', bbox_to_anchor=(1.3, 0.5))
ax.set_title('Title')
ax.set_xlabel('x label')
ax.set_ylabel('y label')
fig.savefig('image_output.png', dpi=300, format='png', bbox_extra_artists=(lgd,), bbox_inches='tight')
I wish that matplotlib would natively allow outside location for the legend box as Matlab does:
figure
x = 0:.2:12;
plot(x,besselj(1,x),x,besselj(2,x),x,besselj(3,x));
hleg = legend('First','Second','Third',...
'Location','NorthEastOutside')
% Make the text of the legend italic and color it brown
set(hleg,'FontAngle','italic','TextColor',[.3,.2,.1])
In addition to all the excellent answers here, newer versions of matplotlib and pylab can automatically determine where to put the legend without interfering with the plots, if possible.
pylab.legend(loc='best')
This will automatically place the legend away from the data if possible!
However, if there isn't any place to put the legend without overlapping the data, then you'll want to try one of the other answers; using loc="best" will never put the legend outside of the plot.
Short Answer: Invoke draggable on the legend and interactively move it wherever you want:
ax.legend().draggable()
Long Answer: If you rather prefer to place the legend interactively/manually rather than programmatically, you can toggle the draggable mode of the legend so that you can drag it to wherever you want. Check the example below:
import matplotlib.pylab as plt
import numpy as np
#define the figure and get an axes instance
fig = plt.figure()
ax = fig.add_subplot(111)
#plot the data
x = np.arange(-5, 6)
ax.plot(x, x*x, label='y = x^2')
ax.plot(x, x*x*x, label='y = x^3')
ax.legend().draggable()
plt.show()
Newer versions of Matplotlib have made it much easier to position the legend outside the plot. I produced this example with Matplotlib version 3.1.1.
Users can pass a 2-tuple of coordinates to the loc parameter to position the legend anywhere in the bounding box. The only gotcha is you need to run plt.tight_layout() to get matplotlib to recompute the plot dimensions so the legend is visible:
import matplotlib.pyplot as plt
plt.plot([0, 1], [0, 1], label="Label 1")
plt.plot([0, 1], [0, 2], label='Label 2')
plt.legend(loc=(1.05, 0.5))
plt.tight_layout()
This leads to the following plot:
References:
matplotlib.pyplot.legend
It is not exactly what you asked for, but I found it's an alternative for the same problem.
Make the legend semitransparent, like so:
Do this with:
fig = pylab.figure()
ax = fig.add_subplot(111)
ax.plot(x, y, label=label, color=color)
# Make the legend transparent:
ax.legend(loc=2, fontsize=10, fancybox=True).get_frame().set_alpha(0.5)
# Make a transparent text box
ax.text(0.02, 0.02, yourstring, verticalalignment='bottom',
horizontalalignment='left',
fontsize=10,
bbox={'facecolor':'white', 'alpha':0.6, 'pad':10},
transform=self.ax.transAxes)
As noted, you could also place the legend in the plot, or slightly off it to the edge as well. Here is an example using the Plotly Python API, made with an IPython Notebook. I'm on the team.
To begin, you'll want to install the necessary packages:
import plotly
import math
import random
import numpy as np
Then, install Plotly:
un='IPython.Demo'
k='1fw3zw2o13'
py = plotly.plotly(username=un, key=k)
def sin(x,n):
sine = 0
for i in range(n):
sign = (-1)**i
sine = sine + ((x**(2.0*i+1))/math.factorial(2*i+1))*sign
return sine
x = np.arange(-12,12,0.1)
anno = {
'text': '$\\sum_{k=0}^{\\infty} \\frac {(-1)^k x^{1+2k}}{(1 + 2k)!}$',
'x': 0.3, 'y': 0.6,'xref': "paper", 'yref': "paper",'showarrow': False,
'font':{'size':24}
}
l = {
'annotations': [anno],
'title': 'Taylor series of sine',
'xaxis':{'ticks':'','linecolor':'white','showgrid':False,'zeroline':False},
'yaxis':{'ticks':'','linecolor':'white','showgrid':False,'zeroline':False},
'legend':{'font':{'size':16},'bordercolor':'white','bgcolor':'#fcfcfc'}
}
py.iplot([{'x':x, 'y':sin(x,1), 'line':{'color':'#e377c2'}, 'name':'$x\\\\$'},\
{'x':x, 'y':sin(x,2), 'line':{'color':'#7f7f7f'},'name':'$ x-\\frac{x^3}{6}$'},\
{'x':x, 'y':sin(x,3), 'line':{'color':'#bcbd22'},'name':'$ x-\\frac{x^3}{6}+\\frac{x^5}{120}$'},\
{'x':x, 'y':sin(x,4), 'line':{'color':'#17becf'},'name':'$ x-\\frac{x^5}{120}$'}], layout=l)
This creates your graph, and allows you a chance to keep the legend within the plot itself. The default for the legend if it is not set is to place it in the plot, as shown here.
For an alternative placement, you can closely align the edge of the graph and border of the legend, and remove border lines for a closer fit.
You can move and re-style the legend and graph with code, or with the GUI. To shift the legend, you have the following options to position the legend inside the graph by assigning x and y values of <= 1. E.g :
{"x" : 0,"y" : 0} -- Bottom Left
{"x" : 1, "y" : 0} -- Bottom Right
{"x" : 1, "y" : 1} -- Top Right
{"x" : 0, "y" : 1} -- Top Left
{"x" :.5, "y" : 0} -- Bottom Center
{"x": .5, "y" : 1} -- Top Center
In this case, we choose the upper right, legendstyle = {"x" : 1, "y" : 1}, also described in the documentation:
I simply used the string 'center left' for the location, like in MATLAB.
I imported pylab from Matplotlib.
See the code as follows:
from matplotlib as plt
from matplotlib.font_manager import FontProperties
t = A[:, 0]
sensors = A[:, index_lst]
for i in range(sensors.shape[1]):
plt.plot(t, sensors[:, i])
plt.xlabel('s')
plt.ylabel('°C')
lgd = plt.legend(loc='center left', bbox_to_anchor=(1, 0.5), fancybox = True, shadow = True)
You can also try figlegend. It is possible to create a legend independent of any Axes object. However, you may need to create some "dummy" Paths to make sure the formatting for the objects gets passed on correctly.
Something along these lines worked for me. Starting with a bit of code taken from Joe, this method modifies the window width to automatically fit a legend to the right of the figure.
import matplotlib.pyplot as plt
import numpy as np
plt.ion()
x = np.arange(10)
fig = plt.figure()
ax = plt.subplot(111)
for i in xrange(5):
ax.plot(x, i * x, label='$y = %ix$'%i)
# Put a legend to the right of the current axis
leg = ax.legend(loc='center left', bbox_to_anchor=(1, 0.5))
plt.draw()
# Get the ax dimensions.
box = ax.get_position()
xlocs = (box.x0,box.x1)
ylocs = (box.y0,box.y1)
# Get the figure size in inches and the dpi.
w, h = fig.get_size_inches()
dpi = fig.get_dpi()
# Get the legend size, calculate new window width and change the figure size.
legWidth = leg.get_window_extent().width
winWidthNew = w*dpi+legWidth
fig.set_size_inches(winWidthNew/dpi,h)
# Adjust the window size to fit the figure.
mgr = plt.get_current_fig_manager()
mgr.window.wm_geometry("%ix%i"%(winWidthNew,mgr.window.winfo_height()))
# Rescale the ax to keep its original size.
factor = w*dpi/winWidthNew
x0 = xlocs[0]*factor
x1 = xlocs[1]*factor
width = box.width*factor
ax.set_position([x0,ylocs[0],x1-x0,ylocs[1]-ylocs[0]])
plt.draw()
New in matplotlib 3.7
Legends now accept "outside" locations directly, e.g., loc='outside right upper'.
Just make sure the layout is constrained and then prepend "outside" to the loc string:
import matplotlib.pyplot as plt
import numpy as np
fig, ax = plt.subplots(layout='constrained')
# --------------------
x = np.linspace(-np.pi, np.pi)
ax.plot(x, x, label='$f(x) = x$')
ax.plot(x, np.sin(x), label='$f(x) = sin(x)$')
ax.plot(x, np.cos(x), label='$f(x) = cos(x)$')
fig.legend(loc='outside right upper')
# -------
plt.show()
Multiple subplots also work fine with the new "outside" locations:
fig, (ax1, ax2) = plt.subplots(1, 2, layout='constrained')
# --------------------
x = np.linspace(-np.pi, np.pi)
ax1.plot(x, x, '-', label='$f(x) = x$')
ax1.plot(x, np.sin(x), '--', label='$f(x) = sin(x)$')
ax2.plot(x, np.cos(x), ':', label='$f(x) = cos(x)$')
fig.legend(loc='outside right center')
# -------
Of course the available "outside" locations are preset, so use the older answers if you need finer positioning. However the standard locations should fit most use cases:
locs = [
'outside upper left', 'outside upper center', 'outside upper right',
'outside center right', 'upper center left',
'outside lower right', 'outside lower center', 'outside lower left',
]
for loc in locs:
fig.legend(loc=loc, title=loc)
locs = [
'outside right upper', 'outside right lower',
'outside left lower', 'outside left upper',
]
for loc in locs:
fig.legend(loc=loc, title=loc)
The solution that worked for me when I had a huge legend was to use an extra empty image layout.
In the following example, I made four rows and at the bottom I plotted the image with an offset for the legend (bbox_to_anchor). At the top it does not get cut.
f = plt.figure()
ax = f.add_subplot(414)
lgd = ax.legend(loc='upper left', bbox_to_anchor=(0, 4), mode="expand", borderaxespad=0.3)
ax.autoscale_view()
plt.savefig(fig_name, format='svg', dpi=1200, bbox_extra_artists=(lgd,), bbox_inches='tight')
Here's another solution, similar to adding bbox_extra_artists and bbox_inches, where you don't have to have your extra artists in the scope of your savefig call. I came up with this since I generate most of my plot inside functions.
Instead of adding all your additions to the bounding box when you want to write it out, you can add them ahead of time to the Figure's artists. Using something similar to Franck Dernoncourt's answer:
import matplotlib.pyplot as plt
# Data
all_x = [10, 20, 30]
all_y = [[1, 3], [1.5, 2.9], [3, 2]]
# Plotting function
def gen_plot(x, y):
fig = plt.figure(1)
ax = fig.add_subplot(111)
ax.plot(all_x, all_y)
lgd = ax.legend(["Lag " + str(lag) for lag in all_x], loc="center right", bbox_to_anchor=(1.3, 0.5))
fig.artists.append(lgd) # Here's the change
ax.set_title("Title")
ax.set_xlabel("x label")
ax.set_ylabel("y label")
return fig
# Plotting
fig = gen_plot(all_x, all_y)
# No need for `bbox_extra_artists`
fig.savefig("image_output.png", dpi=300, format="png", bbox_inches="tight")
.
Here is an example from the matplotlib tutorial found here. This is one of the more simpler examples but I added transparency to the legend and added plt.show() so you can paste this into the interactive shell and get a result:
import matplotlib.pyplot as plt
p1, = plt.plot([1, 2, 3])
p2, = plt.plot([3, 2, 1])
p3, = plt.plot([2, 3, 1])
plt.legend([p2, p1, p3], ["line 1", "line 2", "line 3"]).get_frame().set_alpha(0.5)
plt.show()

Draw a separator or lines between subplots

I have plot four subplots in one figure and they shares xaxis with each other.
However, there is no separator between those subplots.
I want to draw a line between each of them. or is there any separator could be adopted in those subplots?
At least there should be separator between the subplots' axis. I think it should be looked as below figure.
\------------------------------------
subplot1
\------------------------------------
subplot2
\------------------------------------
...
\------------------------------------
If the axes/subplots have decorators like x labels or tick labels, it's not straight forward to find the correct position of the lines that should separate the subplots, such that they do not overlap with the texts.
One solution to this can be to get the extent of the axes including decorators and take the mean in between the bottom of the upper and the top of the lower extent.
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.transforms as mtrans
fig, axes = plt.subplots(3,2, squeeze=False)
for i, ax in enumerate(axes.flat):
ax.plot([1,2])
ax.set_title('Title ' + str(i+1))
ax.set_xlabel('xaxis')
ax.set_ylabel('yaxis')
# rearange the axes for no overlap
fig.tight_layout()
# Get the bounding boxes of the axes including text decorations
r = fig.canvas.get_renderer()
get_bbox = lambda ax: ax.get_tightbbox(r).transformed(fig.transFigure.inverted())
bboxes = np.array(list(map(get_bbox, axes.flat)), mtrans.Bbox).reshape(axes.shape)
#Get the minimum and maximum extent, get the coordinate half-way between those
ymax = np.array(list(map(lambda b: b.y1, bboxes.flat))).reshape(axes.shape).max(axis=1)
ymin = np.array(list(map(lambda b: b.y0, bboxes.flat))).reshape(axes.shape).min(axis=1)
ys = np.c_[ymax[1:], ymin[:-1]].mean(axis=1)
# Draw a horizontal lines at those coordinates
for y in ys:
line = plt.Line2D([0,1],[y,y], transform=fig.transFigure, color="black")
fig.add_artist(line)
plt.show()
I find a solution but not a perfect one, but works for me.
Apply below code to each object of subplot.
Where [-1, 1.5] are values suppose to cover all areas of X axis in figure. not all the same.
axes.plot([-1, 1.5], [0, 0], color='black', lw=1, transform=axes.transAxes, clip_on=False)
axes.plot([-1, 1.5], [1, 1], color='black', lw=1, transform=axes.transAxes, clip_on=False)
I tried another way which I think is the most perfect way. As the code show below.
trans = blended_transform_factory(self.figure.transFigure, axes.transAxes)
line = Line2D([0, 1], [0, 0], color='w', transform=trans)
self.figure.lines.append(line)
In above code the line would begin at the start of every figure edge, and it will change when the figure size changed.

matplotlib: Unexpected gridspec behavior

I try to plot two plots vertically within one figure using gridspec. The upper one is supposed to be twice as high as the lower one (i.e. the ratio is 2:1 with 3 equal vertical parts) and they have to share the x-axis scale. I have the following minimal example, which produces the correct 3-part plot, but it does not span the upper subplot twice as high as the lower one. The first plot is at the top third, the second is in the bottom third, and the middle is emptyWhere is the error?
import matplotlib.gridspec as gridspec
x = y = [1,2,3]
gs = gridspec.GridSpec(3, 1)
ax1 = plt.subplot(gs[0:1, 0])
ax1.plot(x, y)
ax2 = plt.subplot(gs[2, 0], sharex=ax1)
ax2.plot(x,y)
plt.show()
When using gridspec the indexing acts similarly to numpy arrays. As such you need gs[0:2, 0] to get the output that you want.
import matplotlib.pyplot as plt
from matplotlib import gridspec
x = y = [1,2,3]
gs = gridspec.GridSpec(3, 1)
ax1 = plt.subplot(gs[0:2, 0])
ax1.plot(x, y)
ax2 = plt.subplot(gs[2, 0], sharex=ax1)
ax2.plot(x,y)
plt.show()
Note that you could use plt.tight_layout() to remove the overlap of axis

My matplotlib.pyplot legend is being cut off

I'm attempting to create a plot with a legend to the side of it using matplotlib. I can see that the plot is being created, but the image bounds do not allow the entire legend to be displayed.
lines = []
ax = plt.subplot(111)
for filename in args:
lines.append(plt.plot(y_axis, x_axis, colors[colorcycle], linestyle='steps-pre', label=filename))
ax.legend(bbox_to_anchor=(1.05, 1), loc=2, borderaxespad=0.)
This produces:
Eventhough that it is late, I want to refer to a nice recently introduced alternative:
New matplotlib feature: The tight bounding box
If you are interested in the output file of plt.savefig: in this case the flag bbox_inches='tight' is your friend!
import matplotlib.pyplot as plt
fig = plt.figure(1)
plt.plot([1, 2, 3], [1, 0, 1], label='A')
plt.plot([1, 2, 3], [1, 2, 2], label='B')
plt.legend(loc='center left', bbox_to_anchor=(1, 0))
fig.savefig('samplefigure', bbox_inches='tight')
I want to refer also to a more detailed answer: Moving matplotlib legend outside of the axis makes it cutoff by the figure box
Advantages
There is no need to adjust the actual data/picture.
It is compatible with plt.subplots as-well where as the others are not!
It applies at least to the mostly used output files, e.g. png, pdf.
As pointed by Adam, you need to make space on the side of your graph.
If you want to fine tune the needed space, you may want to look at the add_axes method of matplotlib.pyplot.artist.
Below is a rapid example:
import matplotlib.pyplot as plt
import numpy as np
# some data
x = np.arange(0, 10, 0.1)
y1 = np.sin(x)
y2 = np.cos(x)
# plot of the data
fig = plt.figure()
ax = fig.add_axes([0.1, 0.1, 0.6, 0.75])
ax.plot(x, y1,'-k', lw=2, label='black sin(x)')
ax.plot(x, y2,'-r', lw=2, label='red cos(x)')
ax.set_xlabel('x', size=22)
ax.set_ylabel('y', size=22)
ax.legend(bbox_to_anchor=(1.05, 1), loc=2, borderaxespad=0.)
plt.show()
and the resulting image:
Just use plt.tight_layout()
import matplotlib.pyplot as plt
fig = plt.figure(1)
plt.plot([1, 2, 3], [1, 0, 1], label='A')
plt.plot([1, 2, 3], [1, 2, 2], label='B')
plt.legend(loc='center left', bbox_to_anchor=(1, 0))
plt.tight_layout()
This is probably introduced in the newer matplotlib version and neatly does the job.
Here is another way of making space (shrinking an axis):
# get the current axis
ax = plt.gca()
# Shink current axis by 20%
box = ax.get_position()
ax.set_position([box.x0, box.y0, box.width * 0.8, box.height])
where 0.8 scales the width of the axis by 20%. On my win7 64 machine, using a factor greater than 1 will make room for the legend if it's outside the plot.
This code was referenced from: How to put the legend out of the plot
Edit: #gcalmettes posted a better answer.
His solution should probably be used instead of the method shown below.
Nonetheless I'll leave this since it sometimes helps to see different ways of doing things.
As shown in the legend plotting guide, you can make room for another subplot and place the legend there.
import matplotlib.pyplot as plt
ax = plt.subplot(121) # <- with 2 we tell mpl to make room for an extra subplot
ax.plot([1,2,3], color='red', label='thin red line')
ax.plot([1.5,2.5,3.5], color='blue', label='thin blue line')
ax.legend(bbox_to_anchor=(1.05, 1), loc=2, borderaxespad=0.)
plt.show()
Produces:
Store your legend call instance to a variable. e.g:
rr = sine_curve_plot.legend(loc=(0.0,1.1))
Then, include the bbox_extra_artists, bbox_inches keyword argument to plt.savefig. i.e:
plt.savefig('output.pdf', bbox_inches='tight', bbox_extra_artists=(rr))
bbox_extra_artists accepts an iterable, so you can include as many legend instances into it. The bbox_extra_artists automatically tells plt to cover every extra info passed into bbox_extra_artists.
DISCLAIMER: The loc variable simply defines the position of the legend, you can tweak the values for better flexibility in positioning. Of course, strings like upper left, upper right, etc. are also valid.

twinx kills tick label color

I am plotting a double plot with two y-axes. The second axis ax2 is created by twinx. The problem is that the coloring of the second y-axis via yticks is not working anymore. Instead I have to set_color the labels individually. Here is the relevant code:
fig = plt.figure()
fill_between(data[:,0], 0, (data[:,2]), color='yellow')
yticks(arange(0.2,1.2,0.2), ['.2', '.4', '.6', '.8', ' 1'], color='yellow')
ax2 = twinx()
ax2.plot(data[:,0], (data[:,1]), 'green')
yticks(arange(0.1,0.6,0.1), ['.1 ', '.2', '.3', '.4', '.5'], color='green')
# color='green' has no effect here ?!
# instead this is needed:
for t in ax2.yaxis.get_ticklabels(): t.set_color('green')
show()
Resulting in:
This issue only occurs if I set the tick strings.
yticks(arange(0.1,0.6,0.1), ['.1 ', '.2', '.3', '.4', '.5'], color='green')
Omit it, like here
yticks(arange(0.1,0.6,0.1), color='green')
and the coloring works fine.
Is that a bug (could not find any reports to this), a feature (?!) or
am I missing something here? I am using python 2.6.5 with matplotlib 0.99.1.1 on ubuntu.
For whatever it's worth, you code works fine on my system even without the for loop to set the label colors. Just as a reference, here's a stand-alone example trying to follow essentially exactly what you posted:
import matplotlib.pyplot as plt
import numpy as np
# Generate some data
num = 200
x = np.linspace(501, 1200, num)
yellow_data, green_data = np.random.random((2,num))
green_data -= np.linspace(0, 3, yellow_data.size)
# Plot the yellow data
plt.fill_between(x, yellow_data, 0, color='yellow')
plt.yticks([0.0, 0.5, 1.0], color='yellow')
# Plot the green data
ax2 = plt.twinx()
ax2.plot(x, green_data, 'g-')
plt.yticks([-4, -3, -2, -1, 0, 1], color='green')
plt.show()
My guess is that your problem is mostly coming from mixing up references to different objects. I'm guessing that your code is a bit more complex, and that when you call plt.yticks, ax2 is not the current axis. You can test that idea by explicitly calling sca(ax2) (set the current axis to ax2) before calling yticks and see if that changes things.
Generally speaking, it's best to stick to either entirely the matlab-ish state machine interface or the OO interface, and don't mix them too much. (Personally, I prefer just sticking to the OO interface. Use pyplot to set up figure objects and for show, and use the axes methods otherwise. To each his own, though.)
At any rate, with matplotlib >= 1.0, the tick_params function makes this a bit more convenient. (I'm also using plt.subplots here, which is only in >= 1.0, as well.)
import matplotlib.pyplot as plt
import numpy as np
# Generate some data
yellow_data, green_data = np.random.random((2,2000))
yellow_data += np.linspace(0, 3, yellow_data.size)
green_data -= np.linspace(0, 3, yellow_data.size)
# Plot the data
fig, ax1 = plt.subplots()
ax2 = ax1.twinx()
ax1.plot(yellow_data, 'y-')
ax2.plot(green_data, 'g-')
# Change the axis colors...
ax1.tick_params(axis='y', labelcolor='yellow')
ax2.tick_params(axis='y', labelcolor='green')
plt.show()
The equivalent code for older versions of matplotlib would look more like this:
import matplotlib.pyplot as plt
import numpy as np
# Generate some data
yellow_data, green_data = np.random.random((2,2000))
yellow_data += np.linspace(0, 3, yellow_data.size)
green_data -= np.linspace(0, 3, yellow_data.size)
# Plot the data
fig = plt.figure()
ax1 = fig.add_subplot(1,1,1)
ax2 = ax1.twinx()
ax1.plot(yellow_data, 'y-')
ax2.plot(green_data, 'g-')
# Change the axis colors...
for ax, color in zip([ax1, ax2], ['yellow', 'green']):
for label in ax.yaxis.get_ticklabels():
label.set_color(color)
plt.show()

Categories

Resources