Two colorbars on two subplots, same figure - python

I am trying to make a matplotlib plot with two subplots, and one colorbar to the right of each subplot. Here is my code currently:
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from mpl_toolkits.axes_grid1 import make_axes_locatable
X = tsne_out[:,0]
Y = tsne_out[:,1]
Z = tsne_out[:,2]
fig = plt.figure(figsize = (20,15))
ax1 = fig.add_subplot(221)
ax1.scatter(X, Y, c = material, s = df['Diameter (nm)']/4, cmap = plt.get_cmap('nipy_spectral', 11))
ax1.set_title("2D Representation", fontsize = 18)
ax1.set_xlabel("TSNE1", fontsize = 14)
ax1.set_ylabel("TSNE2", fontsize = 14)
ax1.set_xlim(-20,20)
ax1.set_ylim(-20,20)
ax1.set_xticks(list(range(-20,21,10)))
ax1.set_yticks(list(range(-20,21,10)))
cbar = fig.colorbar(cax, ticks=list(range(0,9)))
cbar.ax.tick_params(labelsize=15)
cbar.ax.set_yticklabels(custom_ticks) # horizontal colorbar
ax2 = fig.add_subplot(222, projection='3d')
ax2.scatter(X, Y, Z, c = material, s = df['Diameter (nm)']/4, cmap = plt.get_cmap('nipy_spectral', 11))
ax2.set_title("3D Representation", fontsize = 18)
ax2.set_xlabel("TSNE1", fontsize = 14)
ax2.set_ylabel("TSNE2", fontsize = 14)
ax2.set_zlabel("TSNE3", fontsize = 14)
ax2.set_xlim(-20,20)
ax2.set_ylim(-20,20)
ax2.set_zlim(-20,20)
ax2.set_xticks(list(range(-20,21,10)))
ax2.set_yticks(list(range(-20,21,10)))
ax2.set_zticks(list(range(-20,21,10)))
cbar = fig.colorbar(cax, ticks = list(range(0,9)))
cbar.ax.tick_params(labelsize=15)
cbar.ax.set_yticklabels(custom_ticks)
This provides the following figure:
My question is: why does the first colorbar not show my custom ticks and how do I fix this?

The issue seems to be that ScalarMappable objects seem to be able to have at most one colorbar associated with them. When you draw the second colorbar with the same ScalarMappable, the original colorbar is unlinked and the previous settings are lost for the first colorbar.
Your code is missing some details (in particular, the definition of cax), so you either have to create two separate mappables, or directly use what each scatter call gives you. Furthermore, I'd be explicit about where you want to get your colorbars to be inserted.
An example fix, assuming that cax was really meant to refer to your scatter plots:
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import numpy as np
X = np.random.rand(100) * 40 - 20
Y = np.random.rand(100) * 40 - 20
Z = np.random.rand(100) * 40 - 20
C = np.random.randint(1,8,100)
custom_ticks = list('ABCDEFGH')
fig = plt.figure(figsize = (20,15))
ax1 = fig.add_subplot(121)
sc1 = ax1.scatter(X, Y, c = C, cmap='viridis') # use this mappable
ax1.set_title("2D Representation", fontsize = 18)
ax1.set_xlabel("TSNE1", fontsize = 14)
ax1.set_ylabel("TSNE2", fontsize = 14)
ax1.set_xlim(-20,20)
ax1.set_ylim(-20,20)
ax1.set_xticks(list(range(-20,21,10)))
ax1.set_yticks(list(range(-20,21,10)))
cbar = fig.colorbar(sc1, ax=ax1, ticks=list(range(0,9))) # be explicit about ax1
cbar.ax.tick_params(labelsize=15)
cbar.ax.set_yticklabels(custom_ticks)
ax2 = fig.add_subplot(122, projection='3d')
sc2 = ax2.scatter(X, Y, Z, c=C, cmap='viridis') # next time use this one
ax2.set_title("3D Representation", fontsize = 18)
ax2.set_xlabel("TSNE1", fontsize = 14)
ax2.set_ylabel("TSNE2", fontsize = 14)
ax2.set_zlabel("TSNE3", fontsize = 14)
ax2.set_xlim(-20,20)
ax2.set_ylim(-20,20)
ax2.set_zlim(-20,20)
ax2.set_xticks(list(range(-20,21,10)))
ax2.set_yticks(list(range(-20,21,10)))
ax2.set_zticks(list(range(-20,21,10)))
cbar = fig.colorbar(sc2, ax=ax2, ticks=list(range(0,9))) # sc1 here is the bug
cbar.ax.tick_params(labelsize=15)
cbar.ax.set_yticklabels(custom_ticks)
plt.show()
This produces the following:
Note that I created an MCVE for you, and I simplified a few things, for instance the number of subplots. The point is that the colorbar settings stick now that they use separate mappables.
Another option is to create your colorbars first (using the same ScalarMappable if you want to), then customize both afterwards:
sc = ax1.scatter(X, Y, c = C, cmap='viridis')
cbar1 = fig.colorbar(sc, ax=ax1, ticks=np.arange(0,9))
ax2.scatter(X, Y, Z, c=C, cmap='viridis')
cbar2 = fig.colorbar(sc, ax=ax2, ticks=np.arange(0,9)) # sc here too
for cbar in cbar1,cbar2:
cbar.ax.tick_params(labelsize=15)
cbar.ax.set_yticklabels(custom_ticks)
The fact that the above works may suggest that the original behaviour is a bug.

Related

Contourf not showing full range of values

I have two datasets that when compared result in a basically random distribution of values between -1 and 1. When I plot this using contourf, however, the figure shows almost all values > 0.5. When I plot every 10th point (thin the data), I get a graph that is more reasonable. But it is not clear why the contourf function is doing this.
I replicated this using a random number list of the same size as my data. The result is the same.
import numpy as np
import matplotlib.pyplot as plt
from netCDF4 import Dataset
from matplotlib.cm import get_cmap
import numpy as np
random = np.random.random((360,1600))*2.-1.
f, ax = plt.subplots(1,2,figsize=(15,5))
#heights = ax.contour(to_np(hgt),3,colors='k')
#ax.clabel(heights, fmt='%2.0f', colors='k', fontsize=8)
#cbar = f.colorbar(heights)
#heights.levels=[0,100,3000]
#plt.clabel(heights, heights.levels)
clevs = [-0.5,-0.1,0.1,0.5]
diffplot = ax[0].contourf(random[::10,::10],clevs,extend='both')
cbar = f.colorbar(diffplot,ax=ax[0])
clevs = [-0.5,-0.1,0.1,0.5]
diffplot2 = ax[1].contourf(random[:,:],clevs,extend='both')
cbar = f.colorbar(diffplot2,ax=ax[1])
Result of code
The full range is shown, but the issue is in the large number of points vs. resolution.
That means points drawn 'later' (aka the ones at the end of the color levels range = higher values) have a certain minimum size (you still need to see them) that then overlaps previous (aka smaller values) points, resulting in the image colors looking skewed.
Solution is to increase the dpi of the figure (matplotlib default is 100dpi):
Note how the reduced points from your example plotted on the left hand side looks the same but the right hand side only looks similar >=300 dpi.
Code:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.cm import get_cmap
import numpy as np
random = np.random.random((360,1600))*2.-1.
clevs = [-0.5,-0.1,0.1,0.5]
f1, ax = plt.subplots(1,2,figsize=(15,5)) # ,dpi=100
diffplot = ax[0].contourf(random[::10,::10],clevs,extend='both')
cbar = f1.colorbar(diffplot,ax=ax[0])
diffplot2 = ax[1].contourf(random[:,:],clevs,extend='both')
cbar = f1.colorbar(diffplot2,ax=ax[1])
f1.set_dpi(100)
f1.suptitle('dpi = 100', fontweight = 'bold', fontsize = 20)
f2, ax = plt.subplots(1,2,figsize=(15,5)) # ,dpi=150
diffplot = ax[0].contourf(random[::10,::10],clevs,extend='both')
cbar = f2.colorbar(diffplot,ax=ax[0])
diffplot2 = ax[1].contourf(random[:,:],clevs,extend='both')
cbar = f2.colorbar(diffplot2,ax=ax[1])
f2.set_dpi(150)
f2.suptitle('dpi = 150', fontweight = 'bold', fontsize = 20)
f3, ax = plt.subplots(1,2,figsize=(15,5)) # ,dpi=300
diffplot = ax[0].contourf(random[::10,::10],clevs,extend='both')
cbar = f3.colorbar(diffplot,ax=ax[0])
diffplot2 = ax[1].contourf(random[:,:],clevs,extend='both')
cbar = f3.colorbar(diffplot2,ax=ax[1])
f3.set_dpi(300)
f3.suptitle('dpi = 300', fontweight = 'bold', fontsize = 20)
f4, ax = plt.subplots(1,2,figsize=(15,5)) # ,dpi=600
diffplot = ax[0].contourf(random[::10,::10],clevs,extend='both')
cbar = f4.colorbar(diffplot,ax=ax[0])
diffplot2 = ax[1].contourf(random[:,:],clevs,extend='both')
cbar = f4.colorbar(diffplot2,ax=ax[1])
f4.set_dpi(600)
f4.suptitle('dpi = 600', fontweight = 'bold', fontsize = 20)
f5, ax = plt.subplots(1,2,figsize=(15,5)) # ,dpi=900
diffplot = ax[0].contourf(random[::10,::10],clevs,extend='both')
cbar = f5.colorbar(diffplot,ax=ax[0])
diffplot2 = ax[1].contourf(random[:,:],clevs,extend='both')
cbar = f5.colorbar(diffplot2,ax=ax[1])
f5.set_dpi(900)
f5.suptitle('dpi = 900', fontweight = 'bold', fontsize = 20)
plt.show()
Note the two options for setting the dpi:
fig, ax = plt.subplots(1,2,figsize=(15,5), dpi=600)
# or
fig.set_dpi(600)
An explanation from another angle - bare with me for some plots:
Note: The following plots are arranged with gridspec to be shown in a single figure.
That way the 'resolution' is depicted in a comparable way.
1) The effect you recognized depends on the plot size
See the code below, all 3 plots contain the same data ..., the only difference is their size.
Notice how with the increased size the colors distribution looks more and more as expected.
from matplotlib import gridspec
import numpy as np
import matplotlib.pyplot as plt
random = np.random.random((360,1600))*2.-1.
#random = np.random.random((100,100))*2.-1.
clevs = [-0.5,-0.1,0.1,0.5]
fig = plt.figure(figsize=(18,20), facecolor=(1, 1, 1))
gs = gridspec.GridSpec(3, 4, height_ratios=[1,1,4])
cmap='viridis' # Note: 'viridis' is the default cmap
ax1 = fig.add_subplot(gs[0,:1])
ax1.set_title('ax1')
diffplot1 = ax1.contourf(random[:,:],clevs,extend='both', cmap=cmap)
fig.colorbar(diffplot1, ax=ax1)
ax2 = fig.add_subplot(gs[0:2,2:])
ax2.set_title('ax2')
diffplot2 = ax2.contourf(random[:,:],clevs,extend='both', cmap=cmap)
fig.colorbar(diffplot2, ax=ax2)
ax3 = fig.add_subplot(gs[2,:])
ax3.set_title('ax3')
diffplot3 = ax3.contourf(random[:,:],clevs,extend='both', cmap=cmap)
fig.colorbar(diffplot3, ax=ax3)
fig.tight_layout()
# plt.savefig("Contourf_Colorbar.png")
plt.show()
2) The effect you recognized depends on number of points 'cramped' into a plot
Basically the same that you've already noticed in your question with plotting only every 10th value.
Notice how the colors distribution looks as expected kinda the same for the 3 plot sizes.
Activate random = np.random.random((100,100))*2.-1. in the code block above to get this plot.
3) Reversed color cmap as another way of showing the effect
Notice how this is like the plot from 1) but just with reversed colors.
from matplotlib import gridspec
import numpy as np
import matplotlib.pyplot as plt
random = np.random.random((360,1600))*2.-1.
clevs = [-0.5,-0.1,0.1,0.5]
fig = plt.figure(figsize=(18,20), facecolor=(1, 1, 1))
gs = gridspec.GridSpec(3, 4, height_ratios=[1,1,4])
# reverse cmap
cmap='viridis' # Note: 'viridis' is the default cmap
cmap=plt.cm.get_cmap(cmap)
cmap = cmap.reversed()
ax1 = fig.add_subplot(gs[0,:1])
ax1.set_title('ax1')
diffplot1 = ax1.contourf(random[:,:],clevs,extend='both', cmap=cmap)
fig.colorbar(diffplot1, ax=ax1)
ax2 = fig.add_subplot(gs[0:2,2:])
ax2.set_title('ax2')
diffplot2 = ax2.contourf(random[:,:],clevs,extend='both', cmap=cmap)
fig.colorbar(diffplot2, ax=ax2)
ax3 = fig.add_subplot(gs[2,:])
ax3.set_title('ax3')
diffplot3 = ax3.contourf(random[:,:],clevs,extend='both', cmap=cmap)
fig.colorbar(diffplot3, ax=ax3)
fig.tight_layout()
# plt.savefig("Contourf_Colorbar_reverse.png")
plt.show()
Finally for reference from matplotlib contourf docu:
algorithm{'mpl2005', 'mpl2014', 'serial', 'threaded'}, optional
Which contouring algorithm to use to calculate the contour lines and polygons. The algorithms are implemented in ContourPy, consult the
ContourPy documentation for further information.
The default is taken from rcParams["contour.algorithm"] (default: 'mpl2014').
I've tried some options (wasn't able to get algorithm going, but checked antialiased ... actually more in a try&error method) without an improvement.
But you could look into the referenced ContourPy to maybe find a way to reduce the 'size of the dots' that are drawn, but that's out of my league.

Continuous color scale and nice range with pyplor LogLocator

I am trying to get a continuous color scale in matplotlib for a log plot. But I also want to preserve the nice tick structure and upper and lower limits in the colorbar.
I can only figure out how to do one or the other.
Here the code that generates the two versions
import matplotlib.ticker as ticker
import numpy as np
x = np.linspace(1,200, 50)
y = np.linspace(1,300, 50)
z = np.outer(y, x)
bounds = [np.amin(z), np.amax(z)]
bounds = np.log10(bounds)
bounds[0] = np.floor(bounds[0])
bounds[1] = np.ceil(bounds[1])
bounds = np.power(10, bounds)
fig, ax = plt.subplots()
tickLocator = ticker.LogLocator()
CS = ax.contourf(x, y, z, locator=tickLocator)
ax.set_title("Not enough color bar levels")
cbar = plt.colorbar(CS)
fig, ax = plt.subplots()
tickLocator = ticker.LogLocator(subs=range(1, 10))
CS = ax.contourf(x, y, z, locator=tickLocator)
ax.set_title("Labels missing and not enough range in color bar")
cbar = plt.colorbar(CS)
print("Boundary values")
print(bounds)
print("Tick values")
print(cbar.get_ticks())
plt.show()
With the first version I get nice end points for the ticks, but the levels are very coarse.
With the second version most of the tick labels are missing and the highest tick is smaller than the biggest value in the array.
I found something that works for me by using pcolormesh instead of contourf.
Here the code and output for anyone with a similar problem
import matplotlib.pyplot as plt
import matplotlib.colors as colors
import numpy as np
x = np.linspace(1,200, 200)
y = np.linspace(1,300, 200)
z = np.outer(y, x)
bounds = [np.amin(z), np.amax(z)]
bounds = np.log10(bounds)
bounds[0] = np.floor(bounds[0])
bounds[1] = np.ceil(bounds[1])
bounds = np.power(10, bounds)
fig, ax = plt.subplots()
CS = ax.pcolormesh(x, y, z, norm=colors.LogNorm(*bounds), shading="auto")
cbar = plt.colorbar(CS, ax=ax)
print("Boundary values")
print(bounds)
print("Tick values")
print(cbar.get_ticks())
plt.show()

Seaborn plot with second y axis

i wanted to know how to make a plot with two y-axis so that my plot that looks like this :
to something more like this by adding another y-axis :
i'm only using this line of code from my plot in order to get the top 10 EngineVersions from my data frame :
sns.countplot(x='EngineVersion', data=train, order=train.EngineVersion.value_counts().iloc[:10].index);
I think you are looking for something like:
import matplotlib.pyplot as plt
x = [1,2,3,4,5]
y = [1000,2000,500,8000,3000]
y1 = [1050,3000,2000,4000,6000]
fig, ax1 = plt.subplots()
ax2 = ax1.twinx()
ax1.bar(x, y)
ax2.plot(x, y1, 'o-', color="red" )
ax1.set_xlabel('X data')
ax1.set_ylabel('Counts', color='g')
ax2.set_ylabel('Detection Rates', color='b')
plt.show()
Output:
#gdubs If you want to do this with Seaborn's library, this code set up worked for me. Instead of setting the ax assignment "outside" of the plot function in matplotlib, you do it "inside" of the plot function in Seaborn, where ax is the variable that stores the plot.
import seaborn as sns # Calls in seaborn
# These lines generate the data to be plotted
x = [1,2,3,4,5]
y = [1000,2000,500,8000,3000]
y1 = [1050,3000,2000,4000,6000]
fig, ax1 = plt.subplots() # initializes figure and plots
ax2 = ax1.twinx() # applies twinx to ax2, which is the second y axis.
sns.barplot(x = x, y = y, ax = ax1, color = 'blue') # plots the first set of data, and sets it to ax1.
sns.lineplot(x = x, y = y1, marker = 'o', color = 'red', ax = ax2) # plots the second set, and sets to ax2.
# these lines add the annotations for the plot.
ax1.set_xlabel('X data')
ax1.set_ylabel('Counts', color='g')
ax2.set_ylabel('Detection Rates', color='b')
plt.show(); # shows the plot.
Output:
Seaborn output example
You could try this code to obtain a very similar image to what you originally wanted.
import seaborn as sb
from matplotlib.lines import Line2D
from matplotlib.patches import Rectangle
x = ['1.1','1.2','1.2.1','2.0','2.1(beta)']
y = [1000,2000,500,8000,3000]
y1 = [3,4,1,8,5]
g = sb.barplot(x=x, y=y, color='blue')
g2 = sb.lineplot(x=range(len(x)), y=y1, color='orange', marker='o', ax=g.axes.twinx())
g.set_xticklabels(g.get_xticklabels(), rotation=-30)
g.set_xlabel('EngineVersion')
g.set_ylabel('Counts')
g2.set_ylabel('Detections rate')
g.legend(handles=[Rectangle((0,0), 0, 0, color='blue', label='Nontouch device counts'), Line2D([], [], marker='o', color='orange', label='Detections rate for nontouch devices')], loc=(1.1,0.8))

Residual plot not aligned with main graph

What is wrong with my residual plot that is causing to not be aligned with my main graph? My code is below.
import matplotlib.pyplot as plt
from scipy import stats
import numpy as np
x = np.array([0.030956,0.032956,0.034956,0.036956,0.038956,0.040956])
y = np.array([10.57821088,11.90701212,12.55570876,13.97542486,16.05403248,16.36634177])
yerr = [0.101614114,0.363255259,0.057234211,0.09289917,0.093288198,0.420165796]
xerr = [0.00021]*len(x)
fig1 = plt.figure(1)
frame1=fig1.add_axes((.1,.3,.8,.6))
m, b = np.polyfit(x, y, 1)
print 'gradient',m,'intercept',b
plt.plot(x, m*x + b, '-', color='grey', alpha=0.5)
plt.plot(x,y,'.',color='black',markersize=6)
plt.errorbar(x,y,xerr=0,yerr=yerr,linestyle="None",color='black')
plt.ylabel('$1/\sqrt{F}$ $(N)$',fontsize=20)
plt.autoscale(enable=True, axis=u'both', tight=True)
plt.grid(False)
frame2=fig1.add_axes((.1,.1,.8,.2))
s = m*x+b #(np.sqrt(4*np.pi*8.85E-12)/2.23E-8)*x
difference = y-s
plt.plot(x, difference, 'ro')
frame2.set_ylabel('$Residual$',fontsize=20)
plt.xlabel('$2s+d_0$ $(m)$',fontsize=20)
you can specify the axis limits. the problem is that autoscale is moving your two plots differently. if you insert 2 lines of code, each specifying the axis limits, it will fix it.
plt.axis([.030,.0415, 10, 17]) #line 17
plt.axis([.030,.0415, -.6, .8]) #line 26
i believe this is what you're looking for.
Try using GridSpec.
from matplotlib import gridspec
fig = plt.figure()
gs = gridspec.GridSpec(2, 1, height_ratios=[3, 1])
ax0 = plt.subplot(gs[0])
ax1 = plt.subplot(gs[1])
ax0.plot(x, m*x + b, '-', color='grey', alpha=0.5)
ax0.plot(x,y,'.',color='black',markersize=6)
ax1.plot(x, difference, 'ro')
And use set_ylabel instead of ylabel (which you use for plt for example) for axes.

Secondary axis with twinx(): how to add to legend?

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?
You can easily add a second legend by adding the line:
ax2.legend(loc=0)
You'll get this:
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:
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()
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()
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)
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:
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)
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 )
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) )
Finalization
fig.tight_layout()
fig.savefig('stackoverflow.png', bbox_inches='tight')
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.
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()
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()
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()
enter image description here
The solutions proposed so far have one or two inconvenients:
Handles needs to be collected individually when plotting, e.g. lns1 = ax.plot(time, Swdown, '-', label = 'Swdown'). There is a risk of forgetting handles when updating the code.
Legend is drawn for the whole figure, not by subplot, which is likely a no-go if you have multiple subplots.
This new solution takes advantage of Axes.get_legend_handles_labels() to collect existing handles and labels for the main axis and for the twin axis.
Collecting handles and labels automatically
This numpy operation will scan all axes which share the same subplot area than ax, including ax and return merged handles and labels:
hl = np.hstack([axis.get_legend_handles_labels()
for axis in ax.figure.axes
if axis.bbox.bounds == ax.bbox.bounds])
It can be used to feed legend() arguments this way:
import numpy as np
import matplotlib.pyplot as plt
t = np.arange(1, 200)
signals = [np.exp(-t/20) * np.cos(t*k) for k in (1, 2)]
fig, axes = plt.subplots(nrows=2, figsize=(10, 3), layout='constrained')
axes = axes.flatten()
for i, (ax, signal) in enumerate(zip(axes, signals)):
# Plot as usual, no change to the code
ax.plot(t, signal, label=f'plotted on axes[{i}]', c='C0', lw=9, alpha=0.3)
ax2 = ax.twinx()
ax2.plot(t, signal, label=f'plotted on axes[{i}].twinx()', c='C1')
# The only specificity of the code is when plotting the legend
h, l = np.hstack([axis.get_legend_handles_labels()
for axis in ax.figure.axes
if axis.bbox.bounds == ax.bbox.bounds]).tolist()
ax2.legend(handles=h, labels=l, loc='upper right')

Categories

Resources