Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 572ef7e

Browse files
committed
Legend refactoring. Drawing of legends is done by LegendHandlers
1 parent 55ae1ec commit 572ef7e

3 files changed

Lines changed: 424 additions & 139 deletions

File tree

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import matplotlib.pyplot as plt
2+
import numpy as np
3+
4+
ax = plt.subplot(111, aspect=1)
5+
x, y = np.random.randn(2, 20)
6+
#[1.1, 2, 2.8], [1.1, 2, 1.8]
7+
l1, = ax.plot(x,y, "k+", mew=3, ms=12)
8+
l2, = ax.plot(x,y, "w+", mew=1, ms=10)
9+
10+
import matplotlib.patches as mpatches
11+
c = mpatches.Circle((0, 0), 1, fc="g", ec="r", lw=3)
12+
ax.add_patch(c)
13+
14+
15+
16+
from matplotlib.legend_handler import HandlerPatch, HandlerMulti
17+
18+
def make_legend_ellipse(legend, orig_handle,
19+
xdescent, ydescent,
20+
width, height, fontsize):
21+
p = mpatches.Ellipse(xy=(0.5*width-0.5*xdescent, 0.5*height-0.5*ydescent),
22+
width = width+xdescent, height=(height+ydescent))
23+
24+
return p
25+
26+
plt.legend([c, "handle 2"], ["Label 1", "Label 2"],
27+
handler_map={mpatches.Circle:HandlerPatch(patch_func=make_legend_ellipse),
28+
"handle 2":HandlerMulti(l1, l2)})
29+
30+
plt.show()

lib/matplotlib/legend.py

Lines changed: 58 additions & 139 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737

3838
from matplotlib.offsetbox import HPacker, VPacker, TextArea, DrawingArea, DraggableOffsetBox
3939

40+
import legend_handler
41+
4042

4143
class DraggableLegend(DraggableOffsetBox):
4244
def __init__(self, legend, use_blit=False, update="loc"):
@@ -167,6 +169,7 @@ def __init__(self, parent, handles, labels,
167169
bbox_to_anchor = None, # bbox that the legend will be anchored.
168170
bbox_transform = None, # transform for the bbox
169171
frameon = True, # draw frame
172+
handler_map = None,
170173
):
171174
"""
172175
- *parent* : the artist that contains the legend
@@ -236,6 +239,9 @@ def __init__(self, parent, handles, labels,
236239
self.legendHandles = []
237240
self._legend_title_box = None
238241

242+
243+
self._handler_map = handler_map
244+
239245
localdict = locals()
240246

241247
for name in propnames:
@@ -470,6 +476,36 @@ def _approx_text_height(self, renderer=None):
470476
return renderer.points_to_pixels(self._fontsize)
471477

472478

479+
_default_handler_map = {Line2D:legend_handler.HandlerLine2D(),
480+
Patch:legend_handler.HandlerPatch(),
481+
LineCollection:legend_handler.HandlerLineCollection(),
482+
RegularPolyCollection:legend_handler.HandlerRegularPolyCollection(),
483+
CircleCollection:legend_handler.HandlerCircleCollection()
484+
}
485+
486+
def get_legend_handler_map(self):
487+
if self._handler_map:
488+
hm = self._default_handler_map.copy()
489+
hm.update(self._handler_map)
490+
return hm
491+
else:
492+
return self._default_handler_map
493+
494+
def get_legend_handler(self, legend_handler_map, orig_handle):
495+
if orig_handle in legend_handler_map:
496+
handler = legend_handler_map[orig_handle]
497+
else:
498+
499+
for handle_type in type(orig_handle).mro():
500+
if handle_type in legend_handler_map:
501+
handler = legend_handler_map[handle_type]
502+
break
503+
else:
504+
handler = None
505+
506+
return handler
507+
508+
473509
def _init_legend_box(self, handles, labels):
474510
"""
475511
Initiallize the legend_box. The legend_box is an instance of
@@ -516,154 +552,37 @@ def _init_legend_box(self, handles, labels):
516552
# manually set their transform to the self.get_transform().
517553

518554

519-
for handle, lab in zip(handles, labels):
520-
if isinstance(handle, RegularPolyCollection) or \
521-
isinstance(handle, CircleCollection):
522-
npoints = self.scatterpoints
523-
else:
524-
npoints = self.numpoints
525-
if npoints > 1:
526-
# we put some pad here to compensate the size of the
527-
# marker
528-
xdata = np.linspace(0.3*fontsize,
529-
(self.handlelength-0.3)*fontsize,
530-
npoints)
531-
xdata_marker = xdata
532-
elif npoints == 1:
533-
xdata = np.linspace(0, self.handlelength*fontsize, 2)
534-
xdata_marker = [0.5*self.handlelength*fontsize]
535-
536-
if isinstance(handle, Line2D):
537-
ydata = ((height-descent)/2.)*np.ones(xdata.shape, float)
538-
legline = Line2D(xdata, ydata)
539-
540-
legline.update_from(handle)
541-
self._set_artist_props(legline) # after update
542-
legline.set_clip_box(None)
543-
legline.set_clip_path(None)
544-
legline.set_drawstyle('default')
545-
legline.set_marker('None')
546-
547-
handle_list.append(legline)
548-
549-
legline_marker = Line2D(xdata_marker, ydata[:len(xdata_marker)])
550-
legline_marker.update_from(handle)
551-
self._set_artist_props(legline_marker)
552-
legline_marker.set_clip_box(None)
553-
legline_marker.set_clip_path(None)
554-
legline_marker.set_linestyle('None')
555-
if self.markerscale !=1:
556-
newsz = legline_marker.get_markersize()*self.markerscale
557-
legline_marker.set_markersize(newsz)
558-
# we don't want to add this to the return list because
559-
# the texts and handles are assumed to be in one-to-one
560-
# correpondence.
561-
legline._legmarker = legline_marker
562-
563-
elif isinstance(handle, Patch):
564-
p = Rectangle(xy=(0., 0.),
565-
width = self.handlelength*fontsize,
566-
height=(height-descent),
567-
)
568-
p.update_from(handle)
569-
self._set_artist_props(p)
570-
p.set_clip_box(None)
571-
p.set_clip_path(None)
572-
handle_list.append(p)
573-
elif isinstance(handle, LineCollection):
574-
ydata = ((height-descent)/2.)*np.ones(xdata.shape, float)
575-
legline = Line2D(xdata, ydata)
576-
self._set_artist_props(legline)
577-
legline.set_clip_box(None)
578-
legline.set_clip_path(None)
579-
lw = handle.get_linewidth()[0]
580-
dashes = handle.get_dashes()[0]
581-
color = handle.get_colors()[0]
582-
legline.set_color(color)
583-
legline.set_linewidth(lw)
584-
if dashes[0] is not None: # dashed line
585-
legline.set_dashes(dashes[1])
586-
handle_list.append(legline)
587-
588-
elif isinstance(handle, RegularPolyCollection):
589-
590-
#ydata = self._scatteryoffsets
591-
ydata = height*self._scatteryoffsets
592-
593-
size_max, size_min = max(handle.get_sizes())*self.markerscale**2,\
594-
min(handle.get_sizes())*self.markerscale**2
595-
if self.scatterpoints < 4:
596-
sizes = [.5*(size_max+size_min), size_max,
597-
size_min]
598-
else:
599-
sizes = (size_max-size_min)*np.linspace(0,1,self.scatterpoints)+size_min
600-
601-
p = type(handle)(handle.get_numsides(),
602-
rotation=handle.get_rotation(),
603-
sizes=sizes,
604-
offsets=zip(xdata_marker,ydata),
605-
transOffset=self.get_transform(),
606-
)
607-
608-
p.update_from(handle)
609-
p.set_figure(self.figure)
610-
p.set_clip_box(None)
611-
p.set_clip_path(None)
612-
handle_list.append(p)
613-
614-
elif isinstance(handle, CircleCollection):
615-
616-
ydata = height*self._scatteryoffsets
617-
618-
size_max, size_min = max(handle.get_sizes())*self.markerscale**2,\
619-
min(handle.get_sizes())*self.markerscale**2
620-
if self.scatterpoints < 4:
621-
sizes = [.5*(size_max+size_min), size_max,
622-
size_min]
623-
else:
624-
sizes = (size_max-size_min)*np.linspace(0,1,self.scatterpoints)+size_min
625-
626-
p = type(handle)(sizes,
627-
offsets=zip(xdata_marker,ydata),
628-
transOffset=self.get_transform(),
629-
)
630-
631-
p.update_from(handle)
632-
p.set_figure(self.figure)
633-
p.set_clip_box(None)
634-
p.set_clip_path(None)
635-
handle_list.append(p)
636-
else:
637-
handle_type = type(handle)
638-
warnings.warn("Legend does not support %s\nUse proxy artist instead.\n\nhttp://matplotlib.sourceforge.net/users/legend_guide.html#using-proxy-artist\n" % (str(handle_type),))
639-
handle_list.append(None)
555+
legend_handler_map = self.get_legend_handler_map()
640556

557+
for orig_handle, lab in zip(handles, labels):
641558

559+
handler = self.get_legend_handler(legend_handler_map, orig_handle)
642560

643-
handle = handle_list[-1]
644-
if handle is not None: # handle is None is the artist is not supproted
645-
textbox = TextArea(lab, textprops=label_prop,
646-
multilinebaseline=True, minimumdescent=True)
647-
text_list.append(textbox._text)
561+
if handler is None:
562+
warnings.warn("Legend does not support %s\nUse proxy artist instead.\n\nhttp://matplotlib.sourceforge.net/users/legend_guide.html#using-proxy-artist\n" % (str(orig_handle),))
563+
handle_list.append(None)
564+
continue
565+
566+
textbox = TextArea(lab, textprops=label_prop,
567+
multilinebaseline=True, minimumdescent=True)
568+
text_list.append(textbox._text)
648569

649-
labelboxes.append(textbox)
570+
labelboxes.append(textbox)
650571

651-
handlebox = DrawingArea(width=self.handlelength*fontsize,
652-
height=height,
653-
xdescent=0., ydescent=descent)
572+
handlebox = DrawingArea(width=self.handlelength*fontsize,
573+
height=height,
574+
xdescent=0., ydescent=descent)
654575

655-
handlebox.add_artist(handle)
576+
handle = handler(self, orig_handle, \
577+
#xdescent, ydescent, width, height,
578+
fontsize,
579+
handlebox)
580+
handle_list.append(handle)
581+
656582

657-
# special case for collection instances
658-
if isinstance(handle, RegularPolyCollection) or \
659-
isinstance(handle, CircleCollection):
660-
handle._transOffset = handlebox.get_transform()
661-
handle.set_transform(None)
662583

663584

664-
if hasattr(handle, "_legmarker"):
665-
handlebox.add_artist(handle._legmarker)
666-
handleboxes.append(handlebox)
585+
handleboxes.append(handlebox)
667586

668587

669588
if len(handleboxes) > 0:

0 commit comments

Comments
 (0)