Combine two matplotlib markers into one in legend - python

To have a custom marker, I made two scatter plots with same data points but different markers. Thus by plotting one marker on top of the other I get the look of a new custom marker. Now I want to use it in legend. Is there a way I can use two markers one on top of the other in legend and show them as a single marker.
Edit:
The question is not regarding how to share the same label for two different markers, but how to plot one marker on top of other in the legend

Using a tuple of markers answers the question;
from numpy.random import randn
m=np.random.uniform(size=10)
x=np.arange(0,10,1)
y=x**2
fig, ax = plt.subplots(1,1)
blue_dot = ax.scatter(x[:5],y[:5], s=m*100, color='b')
red_dot = ax.scatter(x[5:],y[5:], s=200*m, color='r')
black_cross = ax.scatter(x[5:],y[5:], s=400*m, marker='+', color='k')
lgnd = ax.legend([blue_dot, (red_dot, black_cross)], ["Blue Circle", "Red Circle and Black Cross"])
Now I want to change the size of the markers in the legend so that all the markers of equal size. For that, I have tried adding this to above code.
lgnd.legendHandles[0]._sizes = [200]
lgnd.legendHandles[1]._sizes = [200] # this is affecting the size of red_dot only
How do I change the size of black_cross as well in the legend?

Related

Matplotlib, 'Figure' object has no attribute 'figlegend' [duplicate]

I am plotting the same type of information, but for different countries, with multiple subplots with Matplotlib. That is, I have nine plots on a 3x3 grid, all with the same for lines (of course, different values per line).
However, I have not figured out how to put a single legend (since all nine subplots have the same lines) on the figure just once.
How do I do that?
There is also a nice function get_legend_handles_labels() you can call on the last axis (if you iterate over them) that would collect everything you need from label= arguments:
handles, labels = ax.get_legend_handles_labels()
fig.legend(handles, labels, loc='upper center')
figlegend may be what you're looking for: matplotlib.pyplot.figlegend
An example is at Figure legend demo.
Another example:
plt.figlegend(lines, labels, loc = 'lower center', ncol=5, labelspacing=0.)
Or:
fig.legend(lines, labels, loc = (0.5, 0), ncol=5)
TL;DR
lines_labels = [ax.get_legend_handles_labels() for ax in fig.axes]
lines, labels = [sum(lol, []) for lol in zip(*lines_labels)]
fig.legend(lines, labels)
I have noticed that none of the other answers displays an image with a single legend referencing many curves in different subplots, so I have to show you one... to make you curious...
Now, if I've teased you enough, here it is the code
from numpy import linspace
import matplotlib.pyplot as plt
# each Axes has a brand new prop_cycle, so to have differently
# colored curves in different Axes, we need our own prop_cycle
# Note: we CALL the axes.prop_cycle to get an itertoools.cycle
color_cycle = plt.rcParams['axes.prop_cycle']()
# I need some curves to plot
x = linspace(0, 1, 51)
functs = [x*(1-x), x**2*(1-x),
0.25-x*(1-x), 0.25-x**2*(1-x)]
labels = ['$x-x²$', '$x²-x³$',
'$\\frac{1}{4} - (x-x²)$', '$\\frac{1}{4} - (x²-x³)$']
# the plot,
fig, (a1,a2) = plt.subplots(2)
for ax, f, l, cc in zip((a1,a1,a2,a2), functs, labels, color_cycle):
ax.plot(x, f, label=l, **cc)
ax.set_aspect(2) # superfluos, but nice
# So far, nothing special except the managed prop_cycle. Now the trick:
lines_labels = [ax.get_legend_handles_labels() for ax in fig.axes]
lines, labels = [sum(lol, []) for lol in zip(*lines_labels)]
# Finally, the legend (that maybe you'll customize differently)
fig.legend(lines, labels, loc='upper center', ncol=4)
plt.show()
If you want to stick with the official Matplotlib API, this is
perfect, otherwise see note no.1 below (there is a private
method...)
The two lines
lines_labels = [ax.get_legend_handles_labels() for ax in fig.axes]
lines, labels = [sum(lol, []) for lol in zip(*lines_labels)]
deserve an explanation, see note 2 below.
I tried the method proposed by the most up-voted and accepted answer,
# fig.legend(lines, labels, loc='upper center', ncol=4)
fig.legend(*a2.get_legend_handles_labels(),
loc='upper center', ncol=4)
and this is what I've got
Note 1
If you don't mind using a private method of the matplotlib.legend module ... it's really much much much easier
from matplotlib.legend import _get_legend_handles_labels
...
fig.legend(*_get_legend_handles_and_labels(fig.axes), ...)
Note 2
I have encapsulated the two tricky lines in a function, just four lines of code, but heavily commented
def fig_legend(fig, **kwdargs):
# Generate a sequence of tuples, each contains
# - a list of handles (lohand) and
# - a list of labels (lolbl)
tuples_lohand_lolbl = (ax.get_legend_handles_labels() for ax in fig.axes)
# E.g., a figure with two axes, ax0 with two curves, ax1 with one curve
# yields: ([ax0h0, ax0h1], [ax0l0, ax0l1]) and ([ax1h0], [ax1l0])
# The legend needs a list of handles and a list of labels,
# so our first step is to transpose our data,
# generating two tuples of lists of homogeneous stuff(tolohs), i.e.,
# we yield ([ax0h0, ax0h1], [ax1h0]) and ([ax0l0, ax0l1], [ax1l0])
tolohs = zip(*tuples_lohand_lolbl)
# Finally, we need to concatenate the individual lists in the two
# lists of lists: [ax0h0, ax0h1, ax1h0] and [ax0l0, ax0l1, ax1l0]
# a possible solution is to sum the sublists - we use unpacking
handles, labels = (sum(list_of_lists, []) for list_of_lists in tolohs)
# Call fig.legend with the keyword arguments, return the legend object
return fig.legend(handles, labels, **kwdargs)
I recognize that sum(list_of_lists, []) is a really inefficient method to flatten a list of lists, but ① I love its compactness, ② usually is a few curves in a few subplots and ③ Matplotlib and efficiency? ;-)
For the automatic positioning of a single legend in a figure with many axes, like those obtained with subplots(), the following solution works really well:
plt.legend(lines, labels, loc = 'lower center', bbox_to_anchor = (0, -0.1, 1, 1),
bbox_transform = plt.gcf().transFigure)
With bbox_to_anchor and bbox_transform=plt.gcf().transFigure, you are defining a new bounding box of the size of your figureto be a reference for loc. Using (0, -0.1, 1, 1) moves this bounding box slightly downwards to prevent the legend to be placed over other artists.
OBS: Use this solution after you use fig.set_size_inches() and before you use fig.tight_layout()
You just have to ask for the legend once, outside of your loop.
For example, in this case I have 4 subplots, with the same lines, and a single legend.
from matplotlib.pyplot import *
ficheiros = ['120318.nc', '120319.nc', '120320.nc', '120321.nc']
fig = figure()
fig.suptitle('concentration profile analysis')
for a in range(len(ficheiros)):
# dados is here defined
level = dados.variables['level'][:]
ax = fig.add_subplot(2,2,a+1)
xticks(range(8), ['0h','3h','6h','9h','12h','15h','18h','21h'])
ax.set_xlabel('time (hours)')
ax.set_ylabel('CONC ($\mu g. m^{-3}$)')
for index in range(len(level)):
conc = dados.variables['CONC'][4:12,index] * 1e9
ax.plot(conc,label=str(level[index])+'m')
dados.close()
ax.legend(bbox_to_anchor=(1.05, 0), loc='lower left', borderaxespad=0.)
# it will place the legend on the outer right-hand side of the last axes
show()
If you are using subplots with bar charts, with a different colour for each bar, it may be faster to create the artefacts yourself using mpatches.
Say you have four bars with different colours as r, m, c, and k, you can set the legend as follows:
import matplotlib.patches as mpatches
import matplotlib.pyplot as plt
labels = ['Red Bar', 'Magenta Bar', 'Cyan Bar', 'Black Bar']
#####################################
# Insert code for the subplots here #
#####################################
# Now, create an artist for each color
red_patch = mpatches.Patch(facecolor='r', edgecolor='#000000') # This will create a red bar with black borders, you can leave out edgecolor if you do not want the borders
black_patch = mpatches.Patch(facecolor='k', edgecolor='#000000')
magenta_patch = mpatches.Patch(facecolor='m', edgecolor='#000000')
cyan_patch = mpatches.Patch(facecolor='c', edgecolor='#000000')
fig.legend(handles = [red_patch, magenta_patch, cyan_patch, black_patch], labels=labels,
loc="center right",
borderaxespad=0.1)
plt.subplots_adjust(right=0.85) # Adjust the subplot to the right for the legend
To build on top of gboffi's and Ben Usman's answer:
In a situation where one has different lines in different subplots with the same color and label, one can do something along the lines of:
labels_handles = {
label: handle for ax in fig.axes for handle, label in zip(*ax.get_legend_handles_labels())
}
fig.legend(
labels_handles.values(),
labels_handles.keys(),
loc = "upper center",
bbox_to_anchor = (0.5, 0),
bbox_transform = plt.gcf().transFigure,
)
Using Matplotlib 2.2.2, this can be achieved using the gridspec feature.
In the example below, the aim is to have four subplots arranged in a 2x2 fashion with the legend shown at the bottom. A 'faux' axis is created at the bottom to place the legend in a fixed spot. The 'faux' axis is then turned off so only the legend shows. Result:
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
# Gridspec demo
fig = plt.figure()
fig.set_size_inches(8, 9)
fig.set_dpi(100)
rows = 17 # The larger the number here, the smaller the spacing around the legend
start1 = 0
end1 = int((rows-1)/2)
start2 = end1
end2 = int(rows-1)
gspec = gridspec.GridSpec(ncols=4, nrows=rows)
axes = []
axes.append(fig.add_subplot(gspec[start1:end1, 0:2]))
axes.append(fig.add_subplot(gspec[start2:end2, 0:2]))
axes.append(fig.add_subplot(gspec[start1:end1, 2:4]))
axes.append(fig.add_subplot(gspec[start2:end2, 2:4]))
axes.append(fig.add_subplot(gspec[end2, 0:4]))
line, = axes[0].plot([0, 1], [0, 1], 'b') # Add some data
axes[-1].legend((line,), ('Test',), loc='center') # Create legend on bottommost axis
axes[-1].set_axis_off() # Don't show the bottom-most axis
fig.tight_layout()
plt.show()
This answer is a complement to user707650's answer on the legend position.
My first try on user707650's solution failed due to overlaps of the legend and the subplot's title.
In fact, the overlaps are caused by fig.tight_layout(), which changes the subplots' layout without considering the figure legend. However, fig.tight_layout() is necessary.
In order to avoid the overlaps, we can tell fig.tight_layout() to leave spaces for the figure's legend by fig.tight_layout(rect=(0,0,1,0.9)).
Description of tight_layout() parameters.
All of the previous answers are way over my head, at this state of my coding journey, so I just added another Matplotlib aspect called patches:
import matplotlib.patches as mpatches
first_leg = mpatches.Patch(color='red', label='1st plot')
second_leg = mpatches.Patch(color='blue', label='2nd plot')
thrid_leg = mpatches.Patch(color='green', label='3rd plot')
plt.legend(handles=[first_leg ,second_leg ,thrid_leg ])
The patches aspect put all the data i needed on my final plot (it was a line plot that combined three different line plots all in the same cell in Jupyter Notebook).
Result
(I changed the names form what I named my own legend.)

Non-overlapping legend and axes (e.g. in matplotlib)

I need the plot legend to appear side-by-side to the plot axes, i.e. outside of the axes and non-overlapping.
The width of the axes and the legend should adjust "automatically" so that they both fill the figure w/o them to overlap or the legend to be cut, even when using tight layout. The legend should occupy a minor portion of the figure (let's say max to 1/3 of figure width so that the remaining 2/3 are dedicated to the actual plot).
Eventually, the font of the legend entries can automatically reduce to meet the requirements.
I've read a number of answers regarding legend and bbox_to_anchor in matplotlib with no luck, among which:
how to put the legend out of the plot
moving matplotlib legend outside of the axis makes it cutoff by the figure box
I tried by creating a dedicated axes in which to put the legend so that plt.tight_layout() would do its job properly but then the legend only takes a minor portion of the dedicated axes, with the result that a lot of space is wasted. Or if there isn't enough space (the figure is too small), the legend overlaps the first axes anyway.
import matplotlib.pyplot as plt
import numpy as np
# generate some data
x = np.arange(1, 100)
# create 2 side-by-side axes
fig, ax = plt.subplots(1,2)
# create a plot with a long legend
for ii in range(20):
ax[0].plot(x, x**2, label='20201110_120000')
ax[0].plot(x, x, label='20201104_110000')
# grab handles and labels from the first ax and pass it to the second
hl = ax[0].get_legend_handles_labels()
leg = ax[1].legend(*hl, ncol=2)
plt.tight_layout()
I'm open to use a package different from matplotlib.
Instead of trying to plot the legend in a separate axis, you can pass loc to legend:
# create 2 side-by-side axes
fig, ax = plt.subplots(figsize=(10,6))
# create a plot with a long legend
for ii in range(20):
ax.plot(x, x**2, label='20201110_120000')
ax.plot(x, x, label='20201104_110000')
# grab handles and labels from the first ax and pass it to the second
ax.legend(ncol=2, loc=[1,0])
plt.tight_layout()
Output:

Adjusting subplots to make space for colorbar

I was trying to plot some data and found constrained layout very helpful in maintaining margins and spaces between subplots. However, when I add a colorbar it reduces the width of all subplots and creates extra white space in the subplots above. This bcomes a problem when such plots are put up in reports where a lot of space goes waste because of extra space taken by colorbar.
I was wondering how I can avoid this and make only the image plots to resize when the colorbar is added without afecting the subplots above and no extra white space is created. An example code of the problem I am facing is:
fig, ax = plt.subplots(4,2, constrained_layout=True)
ax[0,0].plot(range(10))
ax[0,1].plot(range(10))
ax[1,0].plot(range(10))
ax[1,1].plot(range(10))
ax[2,0].pcolor(np.random.rand(2,2))
ax[2,1].pcolor(np.random.rand(2,2))
ax[3,0].pcolor(np.random.rand(2,2))
im = ax[3,1].pcolor(np.random.rand(2,2))
bar = fig.colorbar(im,ax=[[ax[2,0],ax[2,1]],[ax[3,0],ax[3,1]]])
It'll be better if I can get this done with contrantrained_layout=True.
I don't have much experience with adjusting the color bar, but what about the idea of adding a new axis and placing the color bar in the center? I set the placement values manually. I wanted to put the color bar in the bottom two graphs to make the widths the same, but I couldn't do that.
import matplotlib.pyplot as plt
fig, ax = plt.subplots(4,2, constrained_layout=True)
ax[0,0].plot(range(10))
ax[0,1].plot(range(10))
ax[1,0].plot(range(10))
ax[1,1].plot(range(10))
ax[2,0].pcolor(np.random.rand(2,2))
ax[2,1].pcolor(np.random.rand(2,2))
ax[3,0].pcolor(np.random.rand(2,2))
im = ax[3,1].pcolor(np.random.rand(2,2))
cax = fig.add_axes([0.48, 0.11, 0.05, 0.36])
bar = fig.colorbar(im,cax=cax,ax=[[ax[2,0],ax[2,1]],[ax[3,0],ax[3,1]]])
fig.subplots_adjust(wspace=0.7, hspace=0.5)
plt.show()

matplotlib hatched and filled histograms

I would like to make histograms that are both hatched and filled (like these bar plots on the left in this matplotlib example):
Here's the code I tried to use:
import matplotlib.pyplot as plt
plt.hist(values, bins, histtype='step', linewidth=2, facecolor='c', hatch='/')
But no matter whether I specify "facecolor" or "color", only the lines of the hatching appear in colour and the histogram is still unfilled. How can I make the hatching show up on top of a filled histogram?
In order to fill the area below the histogram the kwarg fill can be set to True. Then, the facecolor and edgecolor can be set in order to use different colors for the hatch and the background.
plt.hist(np.random.normal(size=500), bins=10, histtype='step', linewidth=2, facecolor='c',
hatch='/', edgecolor='k',fill=True)
This generates the following output:
histtype='step'draws step lines. They are by definition not filled (because they are lines.
Instead, use histtype='bar' (which is the default, so you may equally leave it out completely).

Matplotlib - axvspan vs subplots

I'm writing a pythonic script for a coastal engineering application which should output, amongst other things, a figure with two subplots.
The problem is that I would like to shade a section of both subplots using plt.axvspan() but for some reason it only shades one of them.
Please find below an excerpt of the section of the code where I set up the plots as well as the figure that it's currently outputting (link after code).
Thanks for your help, and sorry if this is a rookie question (but it just happens that I am indeed a rookie in Python... and programming in general) but I couldn't find an answer for this anywhere else.
Feel free to add any comments to the code.
# PLOTTING
# now we generate a figure with the bathymetry vs required m50 and another figure with bathy vs Hs
#1. Generate plots
fig = plt.figure() # Generate Figure
ax = fig.add_subplot(211) # add the first plot to the figure.
depth = ax.plot(results[:,0],results[:,1]*-1,label="Depth [mDMD]") #plot the first set of data onto the first set of axis.
ax2 = ax.twinx() # generate a secondary vertical axis with the same horizontal axis as the first
m50 = ax2.plot(results[:,0],results[:,6],"r",label="M50 [kg]") # plot the second set of data onto the second vertical axis
ax3 = fig.add_subplot(212) # generate the second subplot
hs = ax3.plot(results[:,0],results[:,2],"g",label="Hs(m)")
#Now we want to find where breaking starts to occur so we shade it on the plot.
xBreakingDistance = results[numpy.argmax(breakingIndex),0]
# and now we plot a box from the origin to the depth of breaking.
plt.axvspan(0,xBreakingDistance,facecolor="b",alpha=0.1) # this box is called a span in matplotlib (also works for axhspan)
# and then we write BREAKING ZONE in the box we just created
yLimits = ax.get_ylim() # first we get the range of y being plotted
yMiddle = (float(yLimits[1])-float(yLimits[0])) / 2 + yLimits[0] # then we calculate the middle value in y (to center the text)
xMiddle = xBreakingDistance / 2 # and then the middle value in x (to center the text)
#now we write BREAKING ZONE in the center of the box.
ax.text(xMiddle,yMiddle,"BREAKING ZONE",fontweight="bold",rotation=90,verticalalignment="center",horizontalalignment="center")
#FIGURE FORMATTING
ax.set_xlabel("Distance [m]") # define x label
ax.set_ylabel("Depth [mDMD]") # define y label on the first vertical axis (ax)
ax2.set_ylabel("M50 [kg]") # define y label on the second vertical axis (ax2)
ax.grid() # show grid
ax3.set_xlabel("Distance[m]") #define x label
ax3.set_ylabel("Hs[m]") # define y label
ax3.grid()
plt.tight_layout() # minimize subplot labels overlapping
# generating a label on a plot with 2 vertical axis is not very intuitive. Normally we would just write ax.label(loc=0)
combined_plots = depth+m50 #first we need to combine the plots in a vector
combined_labels = [i.get_label() for i in combined_plots] # and then we combine the labels
ax.legend(combined_plots,combined_labels,loc=0) # and finally we plot the combined_labels of the combined_plots
plt.savefig("Required M50(kg) along the trench.png",dpi=1000)
plt.close(fig)
Output Figure:
By just calling plt.axvspan, you are telling matplotlib to create the axvspan on the currently active axes (i.e. in this case, the last one you created, ax3)
You need to plot the axvspan on both of the axes you would like for it to appear on. In this case, ax and ax3.
So, you could do:
ax.axvspan(0,xBreakingDistance,facecolor="b",alpha=0.1)
ax3.axvspan(0,xBreakingDistance,facecolor="b",alpha=0.1)
or in one line:
[this_ax.axvspan(0,xBreakingDistance,facecolor="b",alpha=0.1) for this_ax in [ax,ax3]]
It's difficult to analyze your code and not being able to reproduce it. I advise you to build a minimal example. In any case notice that you are calling "plt.axvspan(" which is general call to the library.
You need to specifically state that you want this in both "ax" and "ax2" (i think).
Also if you need more control consider using Patches (I don't know axvspan):
import matplotlib.pyplot as plt
import matplotlib.patches as patches
fig1 = plt.figure()
ax1 = fig1.add_subplot(111, aspect='equal')
ax1.add_patch(
patches.Rectangle(
(0.1, 0.1), # (x,y)
0.5, # width
0.5, # height
)
)
fig1.savefig('rect1.png', dpi=90, bbox_inches='tight')
See that call to "ax1" in the example? Just make something similar to yours. Or just add axvspan to each of your plots.

Categories

Resources