Here is my attempt to change the legend of a barplot from rectangle to square:
import matplotlib.patches as patches
rect1 = patches.Rectangle((0,0),1,1,facecolor='#FF605E')
rect2 = patches.Rectangle((0,0),1,1,facecolor='#64B2DF')
plt.legend((rect1, rect2), ('2016', '2015'))
But when I plot this, I still see rectangles instead of squares:
Any suggestions on how can I do this?
I tried both solutions provided by #ImportanceOfBeingErnest and #furas, here are the results:
#ImportanceOfBeingErnest's solution is the easiest to do:
plt.rcParams['legend.handlelength'] = 1
plt.rcParams['legend.handleheight'] = 1.125
Here is the result:
My final code looks like this:
plt.legend((df.columns[1], df.columns[0]), handlelength=1, handleheight=1) # the df.columns = the legend text
#furas's solution produces this, I don't know why the texts are further away from the rectangles, but I am sure the gap can be changed somehow:
Matplotlib provides the rcParams
legend.handlelength : 2. # the length of the legend lines in fraction of fontsize
legend.handleheight : 0.7 # the height of the legend handle in fraction of fontsize
You can set those within the call to plt.legend()
plt.legend(handlelength=1, handleheight=1)
or using the rcParams at the beginning of your script
import matplotlib
matplotlib.rcParams['legend.handlelength'] = 1
matplotlib.rcParams['legend.handleheight'] = 1
Unfortunately providing equal handlelength=1, handleheight=1 will not give a perfect rectange. It seems handlelength=1, handleheight=1.125 will do the job, but this may depend on the font being used.
An alternative, if you want to use proxy artists may be to use the square markers from the plot/scatter methods.
bar1 = plt.plot([], marker="s", markersize=15, linestyle="", label="2015")
and supply it to the legend, legend(handles=[bar1]). Using this approach needs to have set matplotlib.rcParams['legend.numpoints'] = 1, otherwise two markers would appear in the legend.
Here is a full example of both methods
import matplotlib.pyplot as plt
plt.rcParams['legend.handlelength'] = 1
plt.rcParams['legend.handleheight'] = 1.125
plt.rcParams['legend.numpoints'] = 1
fig, ax = plt.subplots(ncols=2, figsize=(5,2.5))
# Method 1: Set the handlesizes already in the rcParams
ax[0].set_title("Setting handlesize")
ax[0].bar([0,2], [6,3], width=0.7, color="#a30e73", label="2015", align="center")
ax[0].bar([1,3], [3,2], width=0.7, color="#0943a8", label="2016", align="center" )
ax[0].legend()
# Method 2: use proxy markers. (Needs legend.numpoints to be 1)
ax[1].set_title("Proxy markers")
ax[1].bar([0,2], [6,3], width=0.7, color="#a30e73", align="center" )
ax[1].bar([1,3], [3,2], width=0.7, color="#0943a8", align="center" )
b1, =ax[1].plot([], marker="s", markersize=15, linestyle="", color="#a30e73", label="2015")
b2, =ax[1].plot([], marker="s", markersize=15, linestyle="", color="#0943a8", label="2016")
ax[1].legend(handles=[b1, b2])
[a.set_xticks([0,1,2,3]) for a in ax]
plt.show()
producing
It seems they change it long time ago - and now some elements can't be used directly in legend.
Now it needs handler:
Implementing a custom legend handler
import matplotlib.pyplot as plt
import matplotlib.patches as patches
from matplotlib.legend_handler import HandlerPatch
# --- handlers ---
class HandlerRect(HandlerPatch):
def create_artists(self, legend, orig_handle,
xdescent, ydescent, width, height,
fontsize, trans):
x = width//2
y = 0
w = h = 10
# create
p = patches.Rectangle(xy=(x, y), width=w, height=h)
# update with data from oryginal object
self.update_prop(p, orig_handle, legend)
# move xy to legend
p.set_transform(trans)
return [p]
class HandlerCircle(HandlerPatch):
def create_artists(self, legend, orig_handle,
xdescent, ydescent, width, height,
fontsize, trans):
r = 5
x = r + width//2
y = height//2
# create
p = patches.Circle(xy=(x, y), radius=r)
# update with data from oryginal object
self.update_prop(p, orig_handle, legend)
# move xy to legend
p.set_transform(trans)
return [p]
# --- main ---
rect = patches.Rectangle((0,0), 1, 1, facecolor='#FF605E')
circ = patches.Circle((0,0), 1, facecolor='#64B2DF')
plt.legend((rect, circ), ('2016', '2015'),
handler_map={
patches.Rectangle: HandlerRect(),
patches.Circle: HandlerCircle(),
})
plt.show()
Legend reserves place for rectangle and this method doesn't change it so there is so many empty space.
Related
I need to create a legend for a line segment that has a different marker at the bottom and the top of the line. I am able to create a legend with 1 of the marker symbols repeated but not the two different markers on each end.
Here is a reproducible example.
from matplotlib import pyplot as plt
import numpy as np
#Create some data
x = np.arange(0,11)
y1 = np.sqrt(x/2.)
y2 = x
plt.figure(figsize=(8,8))
ax = plt.subplot(111)
#Plot the lines
for i,x_ in zip(range(11),x):
ax.plot([x_,x_],[y1[i],y2[i]],c='k')
#Plot the end points
ax.scatter(x,y1,marker='s',c='r',s=100,zorder=10)
ax.scatter(x,y2,marker='o',c='r',s=100,zorder=10)
ax.plot([],[],c='k',marker='o',mfc='r',label='Test Range') #Create a single line for the label
ax.legend(loc=2,numpoints=2,prop={'size':16}) # How can I add a label with different symbols the line segments?
plt.show()
The end product should have a legend with a symbol showing a line connecting a circle and a square.
I'm afraid you have to combine different patches of mpatches, I'm not sure whether there is a better solution
import matplotlib.pyplot as plt
import numpy as np
import matplotlib.patches as mpatches
from matplotlib.legend_handler import HandlerPatch
from matplotlib.legend_handler import HandlerLine2D
class HandlerCircle(HandlerPatch):
def create_artists(self,legend,orig_handle,
xdescent,ydescent,width,height,fontsize,trans):
center = 0.5 * width, 0.5 * height
p = mpatches.Circle(xy=center,radius=width*0.3)
self.update_prop(p,orig_handle,legend)
p.set_transform(trans)
return [p]
class HandlerRectangle(HandlerPatch):
def create_artists(self, legend, orig_handle,
xdescent, ydescent, width, height, fontsize, trans):
center = 0,height/2-width*0.5/2
width,height = width*0.5,width*0.5
p = mpatches.Rectangle(xy=center,width=width,height=width)
self.update_prop(p, orig_handle, legend)
p.set_transform(trans)
return [p]
fig,ax = plt.subplots(figsize=(12,8))
texts = ['','','Test Range']
line, = ax.plot([],[],c='k')
c = [mpatches.Circle((0.,0.,),facecolor='r',linewidth=.5),
line,
mpatches.Rectangle((0.,0.),5,5,facecolor='r',linewidth=.5)]
ax.legend(c,texts,bbox_to_anchor=(.25,.95),loc='center',ncol=3,prop={'size':20},
columnspacing=-1,handletextpad=.6,
handler_map={mpatches.Circle: HandlerCircle(),
line: HandlerLine2D(numpoints=0,),
mpatches.Rectangle: HandlerRectangle()}).get_frame().set_facecolor('w')
plt.show()
running this script, you will get
If you use a different figure size or a different legend size, the settings in my script above may not be optimal. In that case, you can adjust the following parameters:
The centers and the sizes of Circle and Rectangle
columnspacing and handletextpad in ax.legend(...)
I wonder if it is possible with corrplot (from the biokit package) to have a colobar with patterns.
In this example below, for the colobar, I would like to have 5 bubbles with differents sizes associated to the matrix's values ([-1, -0.5, 0, +0.5, +1]).
Ideas ?
thanks a lot
import numpy as np
import pandas as pd
from biokit.viz import corrplot
from matplotlib import pyplot as plt
import string
letters = string.ascii_uppercase[0:15]
df = pd.DataFrame(dict(( (k, np.random.random(10)+ord(k)-65) for k in letters)))
df = df.corr()
c = corrplot.Corrplot(df)
c.plot(colorbar=True, upper='circle', rotation=60, cmap='Oranges', fontsize=12)
plt.tight_layout()
plt.show()
Adding filled shapes to a colorbar seems not to be part of the standard interface. However, a legend could serve your goals. Some shapes are supported directly by the legend, others need a special handler, as described in this post.
It seems first some circles need to be created, for which color etc. can be set.
To tell the handler which shape exactly is meant, I'm misusing the label parameter.
From the source of biokit's corrplot, we learn that the ellipse is rotated either + or -45°, and that it is scaled by the absolute value of the correlation.
The following code puts everything together. The colorbar is drawn as a reference, but can be left out once everything is checked. The legend is positioned outside the main plot via bbox_to_anchor=(x, y). These coordinates are in axes coordinates. The ideal location depends on the size of the other elements, so some experimentation could be useful. I didn't draw the corrplot itself, as I don't have it installed, but you can replace the dummy scatter plot with it.
import matplotlib.pyplot as plt
from matplotlib.patches import Ellipse, Circle
import matplotlib as mpl
class HandlerEllipse(mpl.legend_handler.HandlerPatch):
def create_artists(self, legend, orig_handle,
xdescent, ydescent, width, height, fontsize, trans):
d = float(orig_handle.get_label())
center = 0.5 * width - 0.5 * xdescent, 0.5 * height - 0.5 * ydescent
radius = (height + ydescent) * 1.8
p = Ellipse(xy=center, width=radius, height=radius * (1 - abs(d)), angle=45 if d > 0 else -45)
self.update_prop(p, orig_handle, legend)
p.set_transform(trans)
return [p]
#values = [1, 0.5, 0, -0.5, -1]
values = [1, 0.75, 0.5, 0.25, 0, -0.25, -0.5, -0.75, -1]
cmap = plt.cm.Oranges # or plt.cm.PiYG
norm = mpl.colors.Normalize(vmin=-1, vmax=1)
fig, ax = plt.subplots()
plt.scatter([0,1], [0,1], c=[-1,1], cmap=cmap, norm=norm) # a dummy plot as a stand-in for the corrplot
char = plt.colorbar(ticks=values)
shapes = [Circle((0.5, 0.5), 1, facecolor=cmap(norm(d)), edgecolor='k', alpha=1, zorder=2, linewidth=1, label=d)
for d in values]
plt.legend(handles=shapes, labels=values, title='Correlation', framealpha=1,
bbox_to_anchor=(1.25, 1), loc='upper left',
handler_map={Circle: HandlerEllipse()})
plt.tight_layout() # make sure legend and colorbar fit nicely in the plot
plt.show()
I'm using the following code to create a custom matplotlib legend.
import matplotlib.patches as mpatches
import matplotlib.pyplot as plt
colors = ["g", "w"]
texts = ["Green Data Description", "RedData Description"]
patches = [ mpatches.Patch(color=colors[i], label="{:s}".format(texts[i]) ) for i in range(len(texts)) ]
plt.legend(handles=patches, bbox_to_anchor=(0.5, 0.5), loc='center', ncol=2 )
The resulted legend is as follows:
1 - The white symbol in the legend is not shown because that the default legend background is white as well. How can I set the legend background to other color ?
2 - How to change the rectangular symbols in the legend into circular shape ?
Try this:
import matplotlib.pyplot as plt
import numpy as np
import matplotlib.patches as mpatches
from matplotlib.legend_handler import HandlerPatch
colors = ["g", "w"]
texts = ["Green Data Description", "RedData Description"]
class HandlerEllipse(HandlerPatch):
def create_artists(self, legend, orig_handle,
xdescent, ydescent, width, height, fontsize, trans):
center = 0.5 * width - 0.5 * xdescent, 0.5 * height - 0.5 * ydescent
p = mpatches.Ellipse(xy=center, width=width + xdescent,
height=height + ydescent)
self.update_prop(p, orig_handle, legend)
p.set_transform(trans)
return [p]
c = [ mpatches.Circle((0.5, 0.5), 1, facecolor=colors[i], linewidth=3) for i in range(len(texts))]
plt.legend(c,texts,bbox_to_anchor=(0.5, 0.5), loc='center', ncol=2, handler_map={mpatches.Circle: HandlerEllipse()}).get_frame().set_facecolor('#00FFCC')
plt.show()
output:
Update:
To circle, set width equals to height, in mpatches.Ellipse
Remove the outer black line, set edgecolor="none" in mpatches.Circle
code:
import matplotlib.pyplot as plt
import numpy as np
import matplotlib.patches as mpatches
from matplotlib.legend_handler import HandlerPatch
colors = ["g", "w"]
texts = ["Green Data Description", "RedData Description"]
class HandlerEllipse(HandlerPatch):
def create_artists(self, legend, orig_handle,
xdescent, ydescent, width, height, fontsize, trans):
center = 0.5 * width - 0.5 * xdescent, 0.5 * height - 0.5 * ydescent
p = mpatches.Ellipse(xy=center, width=height + xdescent,
height=height + ydescent)
self.update_prop(p, orig_handle, legend)
p.set_transform(trans)
return [p]
c = [ mpatches.Circle((0.5, 0.5), radius = 0.25, facecolor=colors[i], edgecolor="none" ) for i in range(len(texts))]
plt.legend(c,texts,bbox_to_anchor=(0.5, 0.5), loc='center', ncol=2, handler_map={mpatches.Circle: HandlerEllipse()}).get_frame().set_facecolor('#00FFCC')
plt.show()
New Picture:
Setting the legend's background color can be done using the facecolor argument to plt.legend(),
plt.legend(facecolor="plum")
To obtain a circular shaped legend handle, you may use a standard plot with a circular marker as proxy artist,
plt.plot([],[], marker="o", ms=10, ls="")
Complete example:
import matplotlib.patches as mpatches
import matplotlib.pyplot as plt
colors = ["g", "w"]
texts = ["Green Data Description", "RedData Description"]
patches = [ plt.plot([],[], marker="o", ms=10, ls="", mec=None, color=colors[i],
label="{:s}".format(texts[i]) )[0] for i in range(len(texts)) ]
plt.legend(handles=patches, bbox_to_anchor=(0.5, 0.5),
loc='center', ncol=2, facecolor="plum", numpoints=1 )
plt.show()
(Note that mec and numpoints arguments are only required for older versions of matplotlib)
For more complicated shapes in the legend, you may use a custom handler map, see the legend guide or e.g. this answer as an example
As the other answers did not work for me, I am adding an answer that is super simple and straight forward:
import matplotlib.pyplot as plt
handles = []
for x in colors:
handles.append(plt.Line2D([], [], color=x, marker="o", linewidth=0))
You can adjust marker size and whatever else you want (maybe a star etc) and the linewidth removes the line to leave you with only the marker. Works perfectly and is super simple!
Even simpler:
import matplotlib.pyplot as plt
handles = [(plt.Line2D([], [], color=x, marker="o", linewidth=0)) for x in colors]
I'd like to add an arrow to a line plot with matplotlib like in the plot below (drawn with pgfplots).
How can I do (position and direction of the arrow should be parameters ideally)?
Here is some code to experiment.
from matplotlib import pyplot
import numpy as np
t = np.linspace(-2, 2, 100)
plt.plot(t, np.sin(t))
plt.show()
Thanks.
In my experience this works best by using annotate. Thereby you avoid the weird warping you get with ax.arrow which is somehow hard to control.
EDIT: I've wrapped it into a little function.
from matplotlib import pyplot as plt
import numpy as np
def add_arrow(line, position=None, direction='right', size=15, color=None):
"""
add an arrow to a line.
line: Line2D object
position: x-position of the arrow. If None, mean of xdata is taken
direction: 'left' or 'right'
size: size of the arrow in fontsize points
color: if None, line color is taken.
"""
if color is None:
color = line.get_color()
xdata = line.get_xdata()
ydata = line.get_ydata()
if position is None:
position = xdata.mean()
# find closest index
start_ind = np.argmin(np.absolute(xdata - position))
if direction == 'right':
end_ind = start_ind + 1
else:
end_ind = start_ind - 1
line.axes.annotate('',
xytext=(xdata[start_ind], ydata[start_ind]),
xy=(xdata[end_ind], ydata[end_ind]),
arrowprops=dict(arrowstyle="->", color=color),
size=size
)
t = np.linspace(-2, 2, 100)
y = np.sin(t)
# return the handle of the line
line = plt.plot(t, y)[0]
add_arrow(line)
plt.show()
It's not very intuitive but it works. You can then fiddle with the arrowprops dictionary until it looks right.
Just add a plt.arrow():
from matplotlib import pyplot as plt
import numpy as np
# your function
def f(t): return np.sin(t)
t = np.linspace(-2, 2, 100)
plt.plot(t, f(t))
plt.arrow(0, f(0), 0.01, f(0.01)-f(0), shape='full', lw=0, length_includes_head=True, head_width=.05)
plt.show()
EDIT: Changed parameters of arrow to include position & direction of function to draw.
Not the nicest solution, but should work:
import matplotlib.pyplot as plt
import numpy as np
def makeArrow(ax,pos,function,direction):
delta = 0.0001 if direction >= 0 else -0.0001
ax.arrow(pos,function(pos),pos+delta,function(pos+delta),head_width=0.05,head_length=0.1)
fun = np.sin
t = np.linspace(-2, 2, 100)
ax = plt.axes()
ax.plot(t, fun(t))
makeArrow(ax,0,fun,+1)
plt.show()
I know this doesn't exactly answer the question as asked, but I thought this could be useful to other people landing here. I wanted to include the arrow in my plot's legend, but the solutions here don't mention how. There may be an easier way to do this, but here is my solution:
To include the arrow in your legend, you need to make a custom patch handler and use the matplotlib.patches.FancyArrow object. Here is a minimal working solution. This solution piggybacks off of the existing solutions in this thread.
First, the imports...
import matplotlib.pyplot as plt
from matplotlib.legend_handler import HandlerPatch
import matplotlib.patches as patches
from matplotlib.lines import Line2D
import numpy as np
Now, we make a custom legend handler. This handler can create legend artists for any line-patch combination, granted that the line has no markers.
class HandlerLinePatch(HandlerPatch):
def __init__(self, linehandle=None, **kw):
HandlerPatch.__init__(self, **kw)
self.linehandle=linehandle
def create_artists(self, legend, orig_handle,
xdescent, ydescent, width,
height, fontsize, trans):
p = super().create_artists(legend, orig_handle,
xdescent, descent,
width, height, fontsize,
trans)
line = Line2D([0,width],[height/2.,height/2.])
if self.linehandle is None:
line.set_linestyle('-')
line._color = orig_handle._edgecolor
else:
self.update_prop(line, self.linehandle, legend)
line.set_drawstyle('default')
line.set_marker('')
line.set_transform(trans)
return [p[0],line]
Next, we write a function that specifies the type of patch we want to include in the legend - an arrow in our case. This is courtesy of Javier's answer here.
def make_legend_arrow(legend, orig_handle,
xdescent, ydescent,
width, height, fontsize):
p = patches.FancyArrow(width/2., height/2., width/5., 0,
length_includes_head=True, width=0,
head_width=height, head_length=height,
overhang=0.2)
return p
Next, a modified version of the add_arrow function from Thomas' answer that uses the FancyArrow patch rather than annotations. This solution might cause weird wrapping like Thomas warned against, but I couldn't figure out how to put the arrow in the legend if the arrow is an annotation.
def add_arrow(line, ax, position=None, direction='right', color=None, label=''):
"""
add an arrow to a line.
line: Line2D object
position: x-position of the arrow. If None, mean of xdata is taken
direction: 'left' or 'right'
color: if None, line color is taken.
label: label for arrow
"""
if color is None:
color = line.get_color()
xdata = line.get_xdata()
ydata = line.get_ydata()
if position is None:
position = xdata.mean()
# find closest index
start_ind = np.argmin(np.absolute(xdata - position))
if direction == 'right':
end_ind = start_ind + 1
else:
end_ind = start_ind - 1
dx = xdata[end_ind] - xdata[start_ind]
dy = ydata[end_ind] - ydata[start_ind]
size = abs(dx) * 5.
x = xdata[start_ind] + (np.sign(dx) * size/2.)
y = ydata[start_ind] + (np.sign(dy) * size/2.)
arrow = patches.FancyArrow(x, y, dx, dy, color=color, width=0,
head_width=size, head_length=size,
label=label,length_includes_head=True,
overhang=0.3, zorder=10)
ax.add_patch(arrow)
Now, a helper function to plot both the arrow and the line. It returns a Line2D object, which is needed for the legend handler we wrote in the first code block
def plot_line_with_arrow(x,y,ax=None,label='',**kw):
if ax is None:
ax = plt.gca()
line = ax.plot(x,y,**kw)[0]
add_arrow(line, ax, label=label)
return line
Finally, we make the plot and update the legend's handler_map with our custom handler.
t = np.linspace(-2, 2, 100)
y = np.sin(t)
line = plot_line_with_arrow(t,y,label='Path', linestyle=':')
plt.gca().set_aspect('equal')
plt.legend(handler_map={patches.FancyArrow :
HandlerLinePatch(patch_func=make_legend_arrow,
linehandle=line)})
plt.show()
Here is the output:
I've found that quiver() works better than arrow() or annotate() when the x and y axes have very different scales. Here's my helper function for plotting a line with arrows:
def plot_with_arrows(ax, x, y, color="g", label="", n_arrows=2):
ax.plot(x, y, rasterized=True, color=color, label=label)
x_range = x.max() - x.min()
y_range = y.max() - y.min()
for i in np.linspace(x.keys().min(), x.keys().max(), n_arrows * 2 + 1).astype(np.int32)[1::2]:
direction = np.array([(x[i+5] - x[i]), (y[i+5] - y[i])])
direction = direction / (np.sqrt(np.sum(np.power(direction, 2)))) * 0.05
direction[0] /= x_range
direction[1] /= y_range
ax.quiver(x[i], y[i], direction[0], direction[1], color=color)
I would like to make a legend entry in a matplotlib look something like this:
It has multiple colors for a given legend item. Code is shown below which outputs a red rectangle. I'm wondering what I need to do to overlay one color ontop of another? Or is there a better solution?
import matplotlib.patches as mpatches
import matplotlib.pyplot as plt
red_patch = mpatches.Patch(color='red', label='Foo')
plt.legend(handles=[red_patch])
plt.show()
The solution I am proposing is to combine two different proxy-artists for one entry legend, as described here: Combine two Pyplot patches for legend.
The strategy is then to set the fillstyle of the first square marker to left while the other one is set to right (see http://matplotlib.org/1.3.0/examples/pylab_examples/filledmarker_demo.html). Two different colours can then be attributed to each marker in order to produce the desired two-colour legend entry.
The code below show how this can be done. Note that the numpoints=1 argument in plt.legend is important in order to display only one marker for each entry.
import matplotlib.pyplot as plt
plt.close('all')
#---- Generate a Figure ----
fig = plt.figure(figsize=(4, 4))
ax = fig.add_axes([0.15, 0.15, 0.75, 0.75])
ax.axis([0, 1, 0, 1])
#---- Define First Legend Entry ----
m1, = ax.plot([], [], c='red' , marker='s', markersize=20,
fillstyle='left', linestyle='none')
m2, = ax.plot([], [], c='blue' , marker='s', markersize=20,
fillstyle='right', linestyle='none')
#---- Define Second Legend Entry ----
m3, = ax.plot([], [], c='cyan' , marker='s', markersize=20,
fillstyle='left', linestyle='none')
m4, = ax.plot([], [], c='magenta' , marker='s', markersize=20,
fillstyle='right', linestyle='none')
#---- Plot Legend ----
ax.legend(((m2, m1), (m3, m4)), ('Foo', 'Foo2'), numpoints=1, labelspacing=2,
loc='center', fontsize=16)
plt.show(block=False)
Which results in:
Disclaimer: This will only work for a two-colors legend entry. If more than two colours is desired, I cannot think of any other way to do this other than the approach described by #jwinterm (Python Matplotlib Multi-color Legend Entry)
Perhaps another hack to handle more than two patches. Make sure you order the handles/labels according to the number of columns:
from matplotlib.patches import Patch
import matplotlib.pyplot as plt
fig, ax = plt.subplots()
pa1 = Patch(facecolor='red', edgecolor='black')
pa2 = Patch(facecolor='blue', edgecolor='black')
pa3 = Patch(facecolor='green', edgecolor='black')
#
pb1 = Patch(facecolor='pink', edgecolor='black')
pb2 = Patch(facecolor='orange', edgecolor='black')
pb3 = Patch(facecolor='purple', edgecolor='black')
ax.legend(handles=[pa1, pb1, pa2, pb2, pa3, pb3],
labels=['', '', '', '', 'First', 'Second'],
ncol=3, handletextpad=0.5, handlelength=1.0, columnspacing=-0.5,
loc='center', fontsize=16)
plt.show()
which results in:
I absolutely loved #raphael's answer.
Here is a version with circles. Furthermore, I've refactored and trimmed the code a bit to make it more modular.
import matplotlib.pyplot as plt
import matplotlib.colors as mcolors
class MulticolorCircles:
"""
For different shapes, override the ``get_patch`` method, and add the new
class to the handler map, e.g. via
ax_r.legend(ax_r_handles, ax_r_labels, handlelength=CONF.LEGEND_ICON_SIZE,
borderpad=1.2, labelspacing=1.2,
handler_map={MulticolorCircles: MulticolorHandler})
"""
def __init__(self, face_colors, edge_colors=None, face_alpha=1,
radius_factor=1):
"""
"""
assert 0 <= face_alpha <= 1, f"Invalid face_alpha: {face_alpha}"
assert radius_factor > 0, "radius_factor must be positive"
self.rad_factor = radius_factor
self.fc = [mcolors.colorConverter.to_rgba(fc, alpha=face_alpha)
for fc in face_colors]
self.ec = edge_colors
if edge_colors is None:
self.ec = ["none" for _ in self.fc]
self.N = len(self.fc)
def get_patch(self, width, height, idx, fc, ec):
"""
"""
w_chunk = width / self.N
radius = min(w_chunk / 2, height) * self.rad_factor
xy = (w_chunk * idx + radius, radius)
patch = plt.Circle(xy, radius, facecolor=fc, edgecolor=ec)
return patch
def __call__(self, width, height):
"""
"""
patches = []
for i, (fc, ec) in enumerate(zip(self.fc, self.ec)):
patch = self.get_patch(width, height, i, fc, ec)
patches.append(patch)
result = PatchCollection(patches, match_original=True)
#
return result
class MulticolorHandler:
"""
"""
#staticmethod
def legend_artist(legend, orig_handle, fontsize, handlebox):
"""
"""
width, height = handlebox.width, handlebox.height
patch = orig_handle(width, height)
handlebox.add_artist(patch)
return patch
Sample usage and image, note that some of the legend handles have radius_factor=0.5 because the true size would be too small.
ax_handles, ax_labels = ax.get_legend_handles_labels()
ax_labels.append(AUDIOSET_LABEL)
ax_handles.append(MulticolorCircles([AUDIOSET_COLOR],
face_alpha=LEGEND_SHADOW_ALPHA))
ax_labels.append(FRAUNHOFER_LABEL)
ax_handles.append(MulticolorCircles([FRAUNHOFER_COLOR],
face_alpha=LEGEND_SHADOW_ALPHA))
ax_labels.append(TRAIN_SOURCE_NORMAL_LABEL)
ax_handles.append(MulticolorCircles(SHADOW_COLORS["source"],
face_alpha=LEGEND_SHADOW_ALPHA))
ax_labels.append(TRAIN_TARGET_NORMAL_LABEL)
ax_handles.append(MulticolorCircles(SHADOW_COLORS["target"],
face_alpha=LEGEND_SHADOW_ALPHA))
ax_labels.append(TEST_SOURCE_ANOMALY_LABEL)
ax_handles.append(MulticolorCircles(DOT_COLORS["anomaly_source"],
radius_factor=LEGEND_DOT_RATIO))
ax_labels.append(TEST_TARGET_ANOMALY_LABEL)
ax_handles.append(MulticolorCircles(DOT_COLORS["anomaly_target"],
radius_factor=LEGEND_DOT_RATIO))
#
ax.legend(ax_handles, ax_labels, handlelength=LEGEND_ICON_SIZE,
borderpad=1.1, labelspacing=1.1,
handler_map={MulticolorCircles: MulticolorHandler})
There is in fact a proper way to do this by implementing a custom
legend handler as explained in the matplotlib-doc under "implementing a custom legend handler" (here):
import matplotlib.pyplot as plt
from matplotlib.collections import PatchCollection
# define an object that will be used by the legend
class MulticolorPatch(object):
def __init__(self, colors):
self.colors = colors
# define a handler for the MulticolorPatch object
class MulticolorPatchHandler(object):
def legend_artist(self, legend, orig_handle, fontsize, handlebox):
width, height = handlebox.width, handlebox.height
patches = []
for i, c in enumerate(orig_handle.colors):
patches.append(plt.Rectangle([width/len(orig_handle.colors) * i - handlebox.xdescent,
-handlebox.ydescent],
width / len(orig_handle.colors),
height,
facecolor=c,
edgecolor='none'))
patch = PatchCollection(patches,match_original=True)
handlebox.add_artist(patch)
return patch
# ------ choose some colors
colors1 = ['g', 'b', 'c', 'm', 'y']
colors2 = ['k', 'r', 'k', 'r', 'k', 'r']
# ------ create a dummy-plot (just to show that it works)
f, ax = plt.subplots()
ax.plot([1,2,3,4,5], [1,4.5,2,5.5,3], c='g', lw=0.5, ls='--',
label='... just a line')
ax.scatter(range(len(colors1)), range(len(colors1)), c=colors1)
ax.scatter([range(len(colors2))], [.5]*len(colors2), c=colors2, s=50)
# ------ get the legend-entries that are already attached to the axis
h, l = ax.get_legend_handles_labels()
# ------ append the multicolor legend patches
h.append(MulticolorPatch(colors1))
l.append("a nice multicolor legend patch")
h.append(MulticolorPatch(colors2))
l.append("and another one")
# ------ create the legend
f.legend(h, l, loc='upper left',
handler_map={MulticolorPatch: MulticolorPatchHandler()},
bbox_to_anchor=(.125,.875))
Probably not exactly what you're looking for, but you can do it (very) manually by placing patches and text yourself on the plot. For instance:
import matplotlib.patches as mpatches
import matplotlib.pyplot as plt
fig, ax = plt.subplots()
red_patch = mpatches.Patch(color='red', label='Foo')
plt.legend(handles=[red_patch])
r1 = mpatches.Rectangle((0.1, 0.1), 0.18, 0.1, fill=False)
r2 = mpatches.Rectangle((0.12, 0.12), 0.03, 0.06, fill=True, color='red')
r3 = mpatches.Rectangle((0.15, 0.12), 0.03, 0.06, fill=True, color='blue')
ax.add_patch(r1)
ax.add_patch(r2)
ax.add_patch(r3)
ax.annotate('Foo', (0.2, 0.13), fontsize='x-large')
plt.show()