How to produce nested legends in Matplotlib - python

I have to plot in Matplotlib a quantity which is the sum of various contributions.
I would like to highlight this fact in the legend of the plot by listing the various contribution as sub-elements of the main legend entry.
A sketch of the result I would like to obtain can be found in the picture below. Note that I do not need to necessarily achieve exactly the legend that is depicted, but just something similar.

You can try creating two separate legends to your figure. Sure, it’s a trick rather than a direct feature of the legend object, as there seems to be no implementation of what you need in matplotlib. But playing with the numbers in bbox and the fontsize you can customize it pretty nicely.
import matplotlib.pyplot as plt
import numpy as np
x = np.arange(0.0, 1, 0.01)
x1 = np.sin(2*np.pi*x)
x2 = np.sin(2*np.pi*x+1)
x3 = np.sin(2*np.pi*x+2)
fig, ax = plt.subplots()
f1, = ax.plot(x1, 'r', lw=4)
f2, = ax.plot(x2, 'k', lw=2)
f3, = ax.plot(x3, 'b', lw=2)
legend1 = plt.legend([f1], ["Main legend"], fontsize=12, loc=3, bbox_to_anchor=(0,0.1,0,0), frameon=False)
legend2 = plt.legend((f2, f3), ('sublegend 1', 'sublegend 2'), fontsize=9,
loc=3, bbox_to_anchor=(0.05,0,0,0), frameon=False)
plt.gca().add_artist(legend1)
plt.show()
EDIT:
Well, if we insert 2 legends, why not just inserting a completely new figure as inset inside the bigger figure, dedicated for a legend, inside which you can draw and write whatever you like? Admittedly it’s a hard work, you have to design each and every line inside including the precise location coordinates. But that’s the way I could think of for doing what you wanted:
import matplotlib.pyplot as plt
import numpy as np
x = np.arange(0.0, 1, 0.01)
x1 = np.sin(2*np.pi*x)
x2 = np.sin(2*np.pi*x+1)
x3 = np.sin(2*np.pi*x+2)
fig, ax = plt.subplots()
f1, = ax.plot(x1, 'r', lw=4)
f2, = ax.plot(x2, 'k', lw=2)
f3, = ax.plot(x3, 'b', lw=2)
## set all lines for inner figure
yline1 = np.array([-0.15, -0.15])
line1 = np.array([2, 10])
yline2 = np.array([3, 0])
line2 = np.array([4, 4])
yline3 = np.array([1.5, 1.5])
line3 = np.array([4, 6])
yline4 = np.array([1.5, 1.5])
line4 = np.array([7, 10])
yline5 = np.array([3, 3])
line5 = np.array([4, 6])
yline6 = np.array([3, 3])
line6 = np.array([7, 10])
## inset figure
axin1 = ax.inset_axes([2.5, -1, 30, 0.5], transform=ax.transData) #
## plot all lines
axin1.plot(line1, yline1, linewidth=4, c='r')
axin1.plot(line2, yline2, 'k', lw=1)
axin1.plot(line3, yline3, 'k', lw=1)
axin1.plot(line4, yline4, 'b', lw=3)
axin1.plot(line5, yline5, 'k', lw=1)
axin1.plot(line6, yline6, 'k', lw=3)
## text
axin1.text(12, 0, 'MAIN', fontsize=12)
axin1.text(12, 1.7, 'Subtext 1', fontsize=10)
axin1.text(12, 3.2, 'Subtext 2', fontsize=10)
## adjust
axin1.set_ylim([4, -1])
axin1.set_xlim([0, 27])
axin1.set_xticklabels('')
axin1.set_yticklabels('')

I looked for a custom example in the legend and could not see any indication of lowering the level. You can just line up the objects in the legend. I've created a hierarchy of the presented images in the form of colors and markers. The official reference has been customized. This has the effect of eliminating the need to annotate only the legend in a special way.
import matplotlib.pyplot as plt
from matplotlib.legend_handler import HandlerLineCollection, HandlerTuple
fig, ax1 = plt.subplots(1, 1, constrained_layout=True)
params = {'legend.fontsize': 16,
'legend.handlelength': 3}
plt.rcParams.update(params)
x = np.linspace(0, np.pi, 25)
xx = np.linspace(0, 2*np.pi, 25)
xxx = np.linspace(0, 3*np.pi, 25)
p1, = ax1.plot(x, np.sin(x), lw=5, c='r')
p2, = ax1.plot(x, np.sin(xx), 'm-d', c='g')
p3, = ax1.plot(x, np.sin(xxx), 'm-s', c='b')
# Assign two of the handles to the same legend entry by putting them in a tuple
# and using a generic handler map (which would be used for any additional
# tuples of handles like (p1, p2)).
l = ax1.legend([p1, (p1, p2), (p1, p3)], ['Legend entry', 'Contribution 1', 'Contribution 2'], scatterpoints=1,
numpoints=1, markerscale=1.3, handler_map={tuple: HandlerTuple(ndivide=None, pad=1.0)})
plt.show()

Related

Putting one color bar for several subplots from different dataframes

I looked everywhere and nothing really helped.
Here is my code:
fig = plt.figure(figsize=(12, 6))
marker_colors = pca_data2['Frame']
fig.suptitle('PCA')
plt.subplot(1, 2, 1)
x = pca_data2.PC_1
y = pca_data2.PC_2
plt.scatter(x, y, c = marker_colors, cmap = "inferno")
plt.colorbar()
plt.subplot(1, 2, 2)
x1 = pca_data.PC_1
y1 = pca_data.PC_2
plt.scatter(x1, y1, c = marker_colors, cmap = "inferno")
plt.colorbar()
plt.show()
pca_data and pca_data2 are two completely different dataframes from to completele different things. But I need them side by side with the 1 color bar being on the right side for all.
Thats how the figure looks like
When I try to remove the first plt.colorbar() then the two subplots look uneven.
I would really appreciate the help.
... since none of the answers seems to mention the fact that you can tell the colorbar the axes on which it should be drawn... here's a simple example how I would do it:
The benefits of this are:
it's much clearer to read
you have complete control over the size of the colorbar
you can extend this easily to any grid of subplots and any position of the colorbar
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.gridspec import GridSpec
# generate some data
data, data1 = np.random.rand(10,10), np.random.rand(10,10)
x, y = np.meshgrid(np.linspace(0,1,10), np.linspace(0,1,10))
# initialize a plot-grid with 3 axes (2 plots and 1 colorbar)
gs = GridSpec(1, 3, width_ratios=[.48,.48,.04])
# set vmin and vmax explicitly to ensure that both colorbars have the same range!
vmin = np.min([np.min(data), np.min(data1)])
vmax = np.max([np.max(data), np.max(data1)])
plot_kwargs = dict(cmap = "inferno", vmin=vmin, vmax=vmax)
fig = plt.figure(figsize=(12, 6))
ax_0 = fig.add_subplot(gs[0], aspect='equal')
ax_1 = fig.add_subplot(gs[1], aspect='equal')
ax_cb = fig.add_subplot(gs[2])
s1 = ax_0.scatter(x, y, c = data, **plot_kwargs)
s2 = ax_1.scatter(x, y, c = data1, **plot_kwargs)
plt.colorbar(s1, cax=ax_cb)
You can use aspect to set a fixed aspect ratio on the subplots. Then append the colorbars to the right side of each axis and discard the first colorbar, to get an even layout:
import matplotlib.pyplot as plt
import numpy as np
from mpl_toolkits.axes_grid1 import make_axes_locatable
fig = plt.figure(figsize=(12, 6))
marker_colors = range(0,10)
x = x1 = np.random.randint(0,10,10)
y = y1 = np.random.randint(0,10,10)
ax1 = fig.add_subplot(1, 2, 1, aspect="equal") # or e.g. aspect=0.9
g1 = ax1.scatter(x, y, c = marker_colors, cmap = "inferno", )
ax2 = fig.add_subplot(1, 2, 2, aspect="equal") # or e.g. aspect=0.9
g2 = ax2.scatter(x1, y1, c = marker_colors, cmap = "inferno")
# put colorbars right next to axes
divider1 = make_axes_locatable(ax1)
cax1 = divider1.append_axes("right", size="5%", pad=0.05)
divider2 = make_axes_locatable(ax2)
cax2 = divider2.append_axes("right", size="5%", pad=0.05)
# reserve space for 1st colorbar, then remove
cbar1 = fig.colorbar(g1, cax=cax1)
fig.delaxes(fig.axes[2])
# 2nd colorbar
cbar2 = fig.colorbar(g2, cax=cax2)
plt.tight_layout()
plt.show()
If you want a different aspect ratio, you can modify aspect, e.g. to aspect=0.9. The result will have locked aspect ratios for the subplots, even if you resize the figure box:
use following code:
Hope it will match your problem statment.
fig = plt.figure(figsize=(12, 6))
marker_colors = range(0,10)
x=x1=np.random.randint(0,10,10)
y=y1=np.random.randint(0,10,10)
plt.subplot(1, 2, 1)
g1=plt.scatter(x, y, c = marker_colors, cmap = "inferno")
plt.subplot(1, 2, 2)
g2=plt.scatter(x1, y1, c = marker_colors, cmap = "inferno")
g11=plt.colorbar(g1)
g12=plt.colorbar(g2)
g11.ax.set_title('g1')
g12.ax.set_title('g2')

How to plot arrays of different lengths

How do you plot arrays of different lengths but extend properly on the x-axis? The code below generates 2 data sets, the second one being shorter. I run an interpolation over each set resampling the data with multiple samples per data point. When I plot all of the data the data sets that are shorter don't extend to the end of the plot. I don't want subplots, I need to overlay the data onto each other.
#!/usr/bin/env python3
from scipy import interpolate
import matplotlib.pyplot as plt
import numpy as np
num_points = 100
# Generate an array of data, interpolate, re-sample and graph
x1 = np.arange(0, num_points)
y1 = np.cos(x1)
f1 = interpolate.interp1d(x1, y1, kind='cubic')
xnew1 = np.arange(0, num_points - 1, 0.2)
ynew1 = f1(xnew1)
plt.plot(x1, y1, color='g', label='input 1')
plt.plot(x1, y1, 'o', color='g')
plt.plot(xnew1, ynew1, color='m', label='interp 1')
plt.plot(xnew1, ynew1, '+', color='m')
# Generate ana array different size of data, interpolate, re-sample and graph
x2 = np.arange(0, num_points/2)
y2 = np.sin(x2)
f2 = interpolate.interp1d(x2, y2, kind='cubic')
xnew2 = np.arange(0, (num_points/2) - 1, 0.2)
ynew2 = f2(xnew2)
plt.plot(x2, y2, color='k', label='input 2')
plt.plot(x2, y2, 'o', color='k')
plt.plot(xnew2, ynew2, color='r', label='interp 2')
plt.plot(xnew2, ynew2, '+', color='r')
plt.legend(loc='upper left')
plt.show()
If I am understanding correctly, this can be done by using two different axes which share the same y-axis, as outlined in this matplotlib example.
In your case you can accomplish this by making the following modifications:
from scipy import interpolate
import matplotlib.pyplot as plt
import numpy as np
num_points = 100
# Generate an array of data, interpolate, re-sample and graph
x1 = np.arange(0, num_points)
y1 = np.cos(x1)
f1 = interpolate.interp1d(x1, y1, kind='cubic')
xnew1 = np.arange(0, num_points - 1, 0.2)
ynew1 = f1(xnew1)
fig, ax1 = plt.subplots() # Create the first axis
ax1.plot(x1, y1, color='g', label='input 1')
ax1.plot(x1, y1, 'o', color='g')
ax1.plot(xnew1, ynew1, color='m', label='interp 1')
ax1.plot(xnew1, ynew1, '+', color='m')
ax2 = ax1.twiny() # Create a twin which shares the y-axis
# Generate an array different size of data, interpolate, re-sample and graph
x2 = np.arange(0, num_points/2)
y2 = np.sin(x2)
f2 = interpolate.interp1d(x2, y2, kind='cubic')
xnew2 = np.arange(0, (num_points/2) - 1, 0.2)
ynew2 = f2(xnew2)
ax2.plot(x2, y2, color='k', label='input 2')
ax2.plot(x2, y2, 'o', color='k')
ax2.plot(xnew2, ynew2, color='r', label='interp 2')
ax2.plot(xnew2, ynew2, '+', color='r')
plt.figlegend(loc='upper left', bbox_to_anchor=(0.065, 0.3, 0.5, 0.5))
plt.show()
This will give you something that looks like
Edit
In order to properly display the legend you can construct one legend for all the subplots, as outlined in this demo. Note that using this method will require some manhandling of the bounding box for the legend, and there are much cleaner ways to do this than specifying a 4-tuple of floats as I have in the line
plt.figlegend(loc='upper left', bbox_to_anchor=(0.065, 0.3, 0.5, 0.5))

Add a row to xtick labels and change the color

I'm trying to plot two sets of data that occur over different times. My goal is to have two sets of xaxis labels that correspond to that data point at that time in color, with one on top of the other. So far I have this:
import numpy as np
import matplotlib.pyplot as plt
data1 = [4, 5, 3, 6]
data2 = [1, 6, 7, 2]
xlabel = ['2120', '2125', '2129', '2133']
xlabel2 = ['\n 2115', '\n 2118', '\n 2121', '\n 2124']
xticks = np.arange(0, len(data1) ,1)
fig = plt.figure(figsize=[8.0,5.0])
ax = fig.add_subplot(111)
ax.plot(xticks, data1, color='b', label='Data 1')
ax.plot(xticks, data2, color='r', label='Data 2')
xmajor_ticks=np.arange(0,4,1)
ax.set_xticks(xmajor_ticks)
ax.set_xticklabels(xlabel, color='b')
ax.set_xticklabels(xlabel2, color='r')
ax.set_xlim([-0.5,3.5])
ax.set_xlabel('Time')
ax.legend(loc='upper left')
ax.grid()
This is overwriting the first ax.set_xticklabels and only plots the data2 red times. How can I keep the data1 times in blue with the data2 times in red below on the xaxis?
The problem is that you are indeed overwriting the ticklabels. In order to prevent this, one option is to use another axes object to draw the data from the second plot to. One can do that via ax2 = ax.twiny() which creates another axes linked to the y axis of the first axes. This would now result in labels beeing drawn on top of the plot, which is mostly fine.
Here it is asked to draw labels below the plot as well.
Note that the following solution is kind of a hack to get the required behaviour and that is has some drawbacks, like a non-fixed position of the labels when resizing.
import numpy as np
import matplotlib.pyplot as plt
data1 = [4, 5, 3, 6]
data2 = [1, 6, 7, 2]
xlabel = ['2120', '2125', '2129', '2133']
xlabel2 = ['2115', '2118', '2121', '2124']
xticks = np.arange(0, len(data1) ,1)
fig = plt.figure(figsize=[8.0,5.0])
ax = fig.add_subplot(111)
ax2 = ax.twiny()
pl1, = ax.plot(xticks, data1, color='b', label='Data 1')
pl2, = ax2.plot(xticks, data2, color='r', label='Data 2')
ax2.spines['top'].set_position(('axes',-0.1))
ax2.spines['top'].set_visible(False)
ax2.xaxis.set_tick_params(size=0)
xmajor_ticks=np.arange(0,4,1)
ax.set_xticks(xmajor_ticks)
ax2.set_xticks(xmajor_ticks)
ax.set_xticklabels(xlabel, color='b')
ax2.set_xticklabels(xlabel2, color='r')
ax.set_xlim([-0.5,3.5])
ax2.set_xlim([-0.5,3.5])
ax.set_xlabel('Time')
ax.legend(handles=[pl1, pl2], labels= ['Data 1' ,'Data 2'], loc='upper left')
ax.grid()
plt.show()

How to plot contourf colorbar in different subplot - matplotlib

This is a very similar question to "How to plot pcolor colorbar in a different subplot - matplotlib". I am trying to plot a filled contour plot and a line plot with a shared axis and the colorbar in a separate subplot (i.e. so it doesn't take up space for the contourf axis and thus muck up the x-axis sharing). However, the x-axis in my code does not rescale nicely:
import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt
z = np.random.rand(20, 20)
x, y = np.arange(20), np.arange(20)
y2 = np.random.rand(20)
fig = plt.figure(figsize=(8, 8))
gs = mpl.gridspec.GridSpec(2, 2, height_ratios=[1, 2], width_ratios=[2, 1])
ax1 = fig.add_subplot(gs[1, 0])
ax2 = fig.add_subplot(gs[0, 0], sharex=ax1)
ax3 = fig.add_subplot(gs[1, 1])
cont = ax1.contourf(x, y, z, 20)
plt.tick_params(which='both', top=False, right=False)
ax2.plot(x, y2, color='g')
plt.tick_params(which='both', top=False, right=False)
cbar = plt.colorbar(cont, cax=ax3)
cbar.set_label('Intensity', rotation=270, labelpad=20)
plt.tight_layout()
plt.show()
which produces an x-axis scaled from 0 to 20 (inclusive) rather than 0 to 19, which means there is unsightly whitespace in the filled contour plot. Commenting out the sharex=ax1 in the above code means that the x-axis for the contour plot is scaled nicely, but not for the line plot above it and the plt.tick_params code has no effect on either axis.
Is there a way of solving this?
You could also turn off the autoscaling of x-axis for all subsequent call of plot on this axis so that it keeps the range set by contourf and sharex=True :
ax2.set_autoscalex_on(False)
This comes even before your call to ax2.plot() and I think it is better than calling ax2.set_xlim(0, 19) since you do not need to know what are the actual limit of your x-axis that may be needed.
import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1.inset_locator import inset_axes
z = np.random.rand(20, 20)
x, y = np.arange(20), np.arange(20)
y2 = np.random.rand(20)
fig = plt.figure(figsize=(8, 8))
gs = mpl.gridspec.GridSpec(2, 1, height_ratios=[1, 2], width_ratios=[2])
ax1 = fig.add_subplot(gs[1, 0])
ax2 = fig.add_subplot(gs[0, 0], sharex=ax1)
cont = ax1.contourf(x, y, z, 20)
plt.tick_params(which='both', top=False, right=False)
ax2.set_autoscalex_on(False)
ax2.plot(x, y2, color='g')
axins = inset_axes(ax1,
width="5%", # width = 10% of parent_bbox width
height="100%", # height : 50%
loc=6,
bbox_to_anchor=(1.05, 0., 1, 1),
bbox_transform=ax1.transAxes,
borderpad=0,
)
cbar = plt.colorbar(cont, cax=axins)
plt.show()
You can use inset_axes for this without added another axis.
import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1.inset_locator import inset_axes
z = np.random.rand(20, 20)
x, y = np.arange(20), np.arange(20)
y2 = np.random.rand(20)
fig = plt.figure(figsize=(8, 8))
gs = mpl.gridspec.GridSpec(2, 2, height_ratios=[1, 2], width_ratios=[2, 1])
ax1 = fig.add_subplot(gs[1, 0])
ax2 = fig.add_subplot(gs[0, 0], sharex=ax1)
cont = ax1.contourf(x, y, z, 20)
plt.tick_params(which='both', top=False, right=False)
ax2.plot(x, y2, color='g')
plt.tick_params(which='both', top=False, right=False)
axins = inset_axes(ax1,
width="5%", # width = 10% of parent_bbox width
height="100%", # height : 50%
loc=6,
bbox_to_anchor=(1.05, 0., 1, 1),
bbox_transform=ax1.transAxes,
borderpad=0,
)
cbar = plt.colorbar(cont, cax=axins)
plt.savefig('figure.jpg',bbox_inches='tight',dpi=200)

Matplotlib Scatterplot legend for points

I am programmatically creating a scatterplot like this:
(Ipython sample code)
%matplotlib inline
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1, axisbg="1.0")
d1 = [range(1,11)]
d2 = [range(1,11)]
dcolor = ['red','red','red','green','green','green','blue','blue','blue', 'blue']
colordict{'red': 'monkey', 'green':'whale', 'blue':'cat'}
ax.scatter(d1,d2,alpha=0.8, c=dcolor,edgecolors='none',s=30)
I would like to add a legend for each different point, so that the legend contains a point in the given color and the name from colordict. Is that possible without splitting the creation of the scatterplot into multiple calls to scatter? Since this happens in a automated library, I would rather avoid to have different calls to scatter().
I would probably do the following.
%matplotlib inline
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1, axisbg="1.0")
g1 = ([1,2,3], [1,2,3])
g2 = ([4,5,6], [4,5,6])
g3 = ([7,8,9,10], [7,8,9,10])
data = (g1, g2, g3)
colors = ("red", "green", "blue")
groups = ("monkey", "whale", "cat")
for data, color, group in zip(data, colors, groups):
x, y = data
ax.scatter(x, y, alpha=0.8, c=color, edgecolors='none', s=30, label=group)
plt.legend(loc=2)
I like keeping the data and its symbols (color, label) even tighter than cel does. I find the code more readable and more checkable, and often I'm getting them together out of some datasource anyway:
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1, axisbg="1.0")
zoo=[]
zoo.append(([4,5,6], [4,5,6], "blue","ape"))
zoo.append(([1,2,3], [1,2,3], "red","monkey"))
for x,y,c,l in zoo:
plt.scatter(x,y,c=c,label=l)
plt.legend(loc="upper left")
Finally, I have used the following code:
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1, axisbg="1.0")
d1 = [range(1,11)]
d2 = [range(1,11)]
dcolor = ['red','red','red','green','green','green','blue','blue','blue', 'blue']
ax.scatter(d1,d2,alpha=0.8, c=dcolor,edgecolors='none',s=30)
import matplotlib.patches as mpatches
patch = mpatches.Patch(color='red', label='a')
patch2 = mpatches.Patch(color='red', label='a')
fig.legend( [patch, patch2],['abc', 'xyz'], loc = 'lower center', ncol=5, labelspacing=0. )
Here it is not yet in a loop, but that is easily doable.

Categories