I have a plot with two y-axes, using twinx()
. I also give labels to the lines, and want to show them with legend()
, but I only succeed to get the labels of one axis in the legend:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import rc
rc('mathtext', default='regular')
fig = plt.figure()
ax = fig.add_subplot(111)
ax.plot(time, Swdown, '-', label = 'Swdown')
ax.plot(time, Rn, '-', label = 'Rn')
ax2 = ax.twinx()
ax2.plot(time, temp, '-r', label = 'temp')
ax.legend(loc=0)
ax.grid()
ax.set_xlabel("Time (h)")
ax.set_ylabel(r"Radiation ($MJ\,m^{-2}\,d^{-1}$)")
ax2.set_ylabel(r"Temperature ($^\circ$C)")
ax2.set_ylim(0, 35)
ax.set_ylim(-20,100)
plt.show()
So I only get the labels of the first axis in the legend, and not the label 'temp' of the second axis. How could I add this third label to the legend?
https://i.stack.imgur.com/MdCYW.png
ax
with the style I use on ax2
: in your case, ax.plot([], [], '-r', label = 'temp')
. It's much faster and simpler than doing it properly...
ax.legend(loc=0)
. A simple and natural alternative that preserves the default merged legend without having to tweak is to replace that line with fig.legend(loc=0)
instead. As explained in the answer by @ImportanceOfBeingErnest below, the legend with multiple axes belong to the figure fig
, rather than to the left axis ax
. In retrospect, it should be obvious that ax.legend()
will mess things up. (I don't have your data to check your particular case, but this is what I've observed on other data)
You can easily add a second legend by adding the line:
ax2.legend(loc=0)
You'll get this:
https://i.stack.imgur.com/DLZkF.png
But if you want all labels on one legend then you should do something like this:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import rc
rc('mathtext', default='regular')
time = np.arange(10)
temp = np.random.random(10)*30
Swdown = np.random.random(10)*100-10
Rn = np.random.random(10)*100-10
fig = plt.figure()
ax = fig.add_subplot(111)
lns1 = ax.plot(time, Swdown, '-', label = 'Swdown')
lns2 = ax.plot(time, Rn, '-', label = 'Rn')
ax2 = ax.twinx()
lns3 = ax2.plot(time, temp, '-r', label = 'temp')
# added these three lines
lns = lns1+lns2+lns3
labs = [l.get_label() for l in lns]
ax.legend(lns, labs, loc=0)
ax.grid()
ax.set_xlabel("Time (h)")
ax.set_ylabel(r"Radiation ($MJ\,m^{-2}\,d^{-1}$)")
ax2.set_ylabel(r"Temperature ($^\circ$C)")
ax2.set_ylim(0, 35)
ax.set_ylim(-20,100)
plt.show()
Which will give you this:
https://i.stack.imgur.com/Z8pg4.png
I'm not sure if this functionality is new, but you can also use the get_legend_handles_labels() method rather than keeping track of lines and labels yourself:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import rc
rc('mathtext', default='regular')
pi = np.pi
# fake data
time = np.linspace (0, 25, 50)
temp = 50 / np.sqrt (2 * pi * 3**2) \
* np.exp (-((time - 13)**2 / (3**2))**2) + 15
Swdown = 400 / np.sqrt (2 * pi * 3**2) * np.exp (-((time - 13)**2 / (3**2))**2)
Rn = Swdown - 10
fig = plt.figure()
ax = fig.add_subplot(111)
ax.plot(time, Swdown, '-', label = 'Swdown')
ax.plot(time, Rn, '-', label = 'Rn')
ax2 = ax.twinx()
ax2.plot(time, temp, '-r', label = 'temp')
# ask matplotlib for the plotted objects and their labels
lines, labels = ax.get_legend_handles_labels()
lines2, labels2 = ax2.get_legend_handles_labels()
ax2.legend(lines + lines2, labels + labels2, loc=0)
ax.grid()
ax.set_xlabel("Time (h)")
ax.set_ylabel(r"Radiation ($MJ\,m^{-2}\,d^{-1}$)")
ax2.set_ylabel(r"Temperature ($^\circ$C)")
ax2.set_ylim(0, 35)
ax.set_ylim(-20,100)
plt.show()
errorbar
plots, while the accepted one fails (showing a line and its errorbars separately, and none of them with the right label). Plus, it's simpler.
ax2
and it doesn't have one set from the start
From matplotlib version 2.1 onwards, you may use a figure legend. Instead of ax.legend()
, which produces a legend with the handles from the axes ax
, one can create a figure legend
fig.legend(loc="upper right")
which will gather all handles from all subplots in the figure. Since it is a figure legend, it will be placed at the corner of the figure, and the loc
argument is relative to the figure.
import numpy as np
import matplotlib.pyplot as plt
x = np.linspace(0,10)
y = np.linspace(0,10)
z = np.sin(x/3)**2*98
fig = plt.figure()
ax = fig.add_subplot(111)
ax.plot(x,y, '-', label = 'Quantity 1')
ax2 = ax.twinx()
ax2.plot(x,z, '-r', label = 'Quantity 2')
fig.legend(loc="upper right")
ax.set_xlabel("x [units]")
ax.set_ylabel(r"Quantity 1")
ax2.set_ylabel(r"Quantity 2")
plt.show()
https://i.stack.imgur.com/tDdKp.png
In order to place the legend back into the axes, one would supply a bbox_to_anchor
and a bbox_transform
. The latter would be the axes transform of the axes the legend should reside in. The former may be the coordinates of the edge defined by loc
given in axes coordinates.
fig.legend(loc="upper right", bbox_to_anchor=(1,1), bbox_transform=ax.transAxes)
https://i.stack.imgur.com/7HJes.png
conda upgrade matplotlib
no newer versions found, I'm still using v.2.0.2
ax.legend_.remove();ax2.legend_.remove()
.
You can easily get what you want by adding the line in ax:
ax.plot([], [], '-r', label = 'temp')
or
ax.plot(np.nan, '-r', label = 'temp')
This would plot nothing but add a label to legend of ax.
I think this is a much easier way. It's not necessary to track lines automatically when you have only a few lines in the second axes, as fixing by hand like above would be quite easy. Anyway, it depends on what you need.
The whole code is as below:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import rc
rc('mathtext', default='regular')
time = np.arange(22.)
temp = 20*np.random.rand(22)
Swdown = 10*np.random.randn(22)+40
Rn = 40*np.random.rand(22)
fig = plt.figure()
ax = fig.add_subplot(111)
ax2 = ax.twinx()
#---------- look at below -----------
ax.plot(time, Swdown, '-', label = 'Swdown')
ax.plot(time, Rn, '-', label = 'Rn')
ax2.plot(time, temp, '-r') # The true line in ax2
ax.plot(np.nan, '-r', label = 'temp') # Make an agent in ax
ax.legend(loc=0)
#---------------done-----------------
ax.grid()
ax.set_xlabel("Time (h)")
ax.set_ylabel(r"Radiation ($MJ\,m^{-2}\,d^{-1}$)")
ax2.set_ylabel(r"Temperature ($^\circ$C)")
ax2.set_ylim(0, 35)
ax.set_ylim(-20,100)
plt.show()
The plot is as below:
https://i.stack.imgur.com/5ZIUv.png
Update: add a better version:
ax.plot(np.nan, '-r', label = 'temp')
This will do nothing while plot(0, 0)
may change the axis range.
One extra example for scatter
ax.scatter([], [], s=100, label = 'temp') # Make an agent in ax
ax2.scatter(time, temp, s=10) # The true scatter in ax2
ax.legend(loc=1, framealpha=1)
A quick hack that may suit your needs..
Take off the frame of the box and manually position the two legends next to each other. Something like this..
ax1.legend(loc = (.75,.1), frameon = False)
ax2.legend( loc = (.75, .05), frameon = False)
Where the loc tuple is left-to-right and bottom-to-top percentages that represent the location in the chart.
Preparation
import numpy as np
from matplotlib import pyplot as plt
fig, ax1 = plt.subplots( figsize=(15,6) )
Y1, Y2 = np.random.random((2,100))
ax2 = ax1.twinx()
Content
I'm surprised it did not show up so far but the simplest way is to either collect them manually into one of the axes objs (that lie on top of each other)
l1 = ax1.plot( range(len(Y1)), Y1, label='Label 1' )
l2 = ax2.plot( range(len(Y2)), Y2, label='Label 2', color='orange' )
ax1.legend( handles=l1+l2 )
https://i.stack.imgur.com/uvQpt.png
or have them collected automatically into the surrounding figure by fig.legend()
and fiddle around with the the bbox_to_anchor
parameter:
ax1.plot( range(len(Y1)), Y1, label='Label 1' )
ax2.plot( range(len(Y2)), Y2, label='Label 2', color='orange' )
fig.legend( bbox_to_anchor=(.97, .97) )
https://i.stack.imgur.com/j4YXS.png
Finalization
fig.tight_layout()
fig.savefig('stackoverflow.png', bbox_inches='tight')
scatter
rather than plot
, you need to do handles=[l1+l2]
in the legend call due to them being PathCollection
objects rather than simple lists.
I found an following official matplotlib example that uses host_subplot to display multiple y-axes and all the different labels in one legend. No workaround necessary. Best solution I found so far. http://matplotlib.org/examples/axes_grid/demo_parasite_axes2.html
from mpl_toolkits.axes_grid1 import host_subplot
import mpl_toolkits.axisartist as AA
import matplotlib.pyplot as plt
host = host_subplot(111, axes_class=AA.Axes)
plt.subplots_adjust(right=0.75)
par1 = host.twinx()
par2 = host.twinx()
offset = 60
new_fixed_axis = par2.get_grid_helper().new_fixed_axis
par2.axis["right"] = new_fixed_axis(loc="right",
axes=par2,
offset=(offset, 0))
par2.axis["right"].toggle(all=True)
host.set_xlim(0, 2)
host.set_ylim(0, 2)
host.set_xlabel("Distance")
host.set_ylabel("Density")
par1.set_ylabel("Temperature")
par2.set_ylabel("Velocity")
p1, = host.plot([0, 1, 2], [0, 1, 2], label="Density")
p2, = par1.plot([0, 1, 2], [0, 3, 2], label="Temperature")
p3, = par2.plot([0, 1, 2], [50, 30, 15], label="Velocity")
par1.set_ylim(0, 4)
par2.set_ylim(1, 65)
host.legend()
plt.draw()
plt.show()
As provided in the example from matplotlib.org, a clean way to implement a single legend from multiple axes is with plot handles:
import matplotlib.pyplot as plt
fig, ax = plt.subplots()
fig.subplots_adjust(right=0.75)
twin1 = ax.twinx()
twin2 = ax.twinx()
# Offset the right spine of twin2. The ticks and label have already been
# placed on the right by twinx above.
twin2.spines.right.set_position(("axes", 1.2))
p1, = ax.plot([0, 1, 2], [0, 1, 2], "b-", label="Density")
p2, = twin1.plot([0, 1, 2], [0, 3, 2], "r-", label="Temperature")
p3, = twin2.plot([0, 1, 2], [50, 30, 15], "g-", label="Velocity")
ax.set_xlim(0, 2)
ax.set_ylim(0, 2)
twin1.set_ylim(0, 4)
twin2.set_ylim(1, 65)
ax.set_xlabel("Distance")
ax.set_ylabel("Density")
twin1.set_ylabel("Temperature")
twin2.set_ylabel("Velocity")
ax.yaxis.label.set_color(p1.get_color())
twin1.yaxis.label.set_color(p2.get_color())
twin2.yaxis.label.set_color(p3.get_color())
tkw = dict(size=4, width=1.5)
ax.tick_params(axis='y', colors=p1.get_color(), **tkw)
twin1.tick_params(axis='y', colors=p2.get_color(), **tkw)
twin2.tick_params(axis='y', colors=p3.get_color(), **tkw)
ax.tick_params(axis='x', **tkw)
ax.legend(handles=[p1, p2, p3])
plt.show()
Here is another way to do this:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import rc
rc('mathtext', default='regular')
fig = plt.figure()
ax = fig.add_subplot(111)
pl_1, = ax.plot(time, Swdown, '-')
label_1 = 'Swdown'
pl_2, = ax.plot(time, Rn, '-')
label_2 = 'Rn'
ax2 = ax.twinx()
pl_3, = ax2.plot(time, temp, '-r')
label_3 = 'temp'
ax.legend([pl[enter image description here][1]_1, pl_2, pl_3], [label_1, label_2, label_3], loc=0)
ax.grid()
ax.set_xlabel("Time (h)")
ax.set_ylabel(r"Radiation ($MJ\,m^{-2}\,d^{-1}$)")
ax2.set_ylabel(r"Temperature ($^\circ$C)")
ax2.set_ylim(0, 35)
ax.set_ylim(-20,100)
plt.show()
If you are using Seaborn you can do this:
g = sns.barplot('arguments blah blah')
g2 = sns.lineplot('arguments blah blah')
h1,l1 = g.get_legend_handles_labels()
h2,l2 = g2.get_legend_handles_labels()
#Merging two legends
g.legend(h1+h2, l1+l2, title_fontsize='10')
#removes the second legend
g2.get_legend().remove()
Success story sharing
errorbar
plots. For a solution that correctly handles them, see below: stackoverflow.com/a/10129461/1319447ax1
. In this case uselns1=ax1.lines
and then appendlns2
to this list.loc
are explained here