From 4c2e1d6657a5fffff24d1ffdf2ed3396d35f21c6 Mon Sep 17 00:00:00 2001 From: Travis CI Date: Mon, 19 Mar 2018 19:35:52 -0400 Subject: [PATCH 1/2] Add legend handler and artist for FancyArrow (clean commit) --- lib/matplotlib/legend.py | 16 +- lib/matplotlib/legend_handler.py | 253 ++++++++++++++++-- .../legend_all_annotation_text.png | Bin 0 -> 24064 bytes lib/matplotlib/tests/test_legend.py | 52 +++- 4 files changed, 296 insertions(+), 25 deletions(-) create mode 100644 lib/matplotlib/tests/baseline_images/test_legend/legend_all_annotation_text.png diff --git a/lib/matplotlib/legend.py b/lib/matplotlib/legend.py index a95080ddea1a..e32a34337248 100644 --- a/lib/matplotlib/legend.py +++ b/lib/matplotlib/legend.py @@ -39,7 +39,8 @@ import matplotlib.colors as colors from matplotlib.font_manager import FontProperties from matplotlib.lines import Line2D -from matplotlib.patches import Patch, Rectangle, Shadow, FancyBboxPatch +from matplotlib.patches import (Patch, Rectangle, Shadow, FancyBboxPatch, + FancyArrowPatch) from matplotlib.collections import (LineCollection, RegularPolyCollection, CircleCollection, PathCollection, PolyCollection) @@ -50,6 +51,7 @@ from matplotlib.offsetbox import DraggableOffsetBox from matplotlib.container import ErrorbarContainer, BarContainer, StemContainer +from matplotlib.text import Annotation, Text from . import legend_handler @@ -565,7 +567,6 @@ def _set_loc(self, loc): # value of the find_offset. self._loc_real = loc self.stale = True - self._legend_box.set_offset(self._findoffset) def _get_loc(self): return self._loc_real @@ -649,7 +650,10 @@ def _approx_text_height(self, renderer=None): update_func=legend_handler.update_from_first_child), tuple: legend_handler.HandlerTuple(), PathCollection: legend_handler.HandlerPathCollection(), - PolyCollection: legend_handler.HandlerPolyCollection() + PolyCollection: legend_handler.HandlerPolyCollection(), + FancyArrowPatch: legend_handler.HandlerFancyArrowPatch(), + Text: legend_handler.HandlerText(), + Annotation: legend_handler.HandlerAnnotation() } # (get|set|update)_default_handler_maps are public interfaces to @@ -837,6 +841,7 @@ def _init_legend_box(self, handles, labels, markerfirst=True): children=[self._legend_title_box, self._legend_handle_box]) self._legend_box.set_figure(self.figure) + self._legend_box.set_offset(self._findoffset) self.texts = text_list self.legendHandles = handle_list @@ -1142,12 +1147,13 @@ def _get_legend_handles(axs, legend_handler_map=None): handles_original = [] for ax in axs: handles_original += (ax.lines + ax.patches + - ax.collections + ax.containers) + ax.collections + ax.containers + ax.texts) # support parasite axes: if hasattr(ax, 'parasites'): for axx in ax.parasites: handles_original += (axx.lines + axx.patches + - axx.collections + axx.containers) + axx.collections + axx.containers + + axx.texts) handler_map = Legend.get_default_handler_map() diff --git a/lib/matplotlib/legend_handler.py b/lib/matplotlib/legend_handler.py index 0968a5c4b99b..a1b6a9bd7cd3 100644 --- a/lib/matplotlib/legend_handler.py +++ b/lib/matplotlib/legend_handler.py @@ -33,8 +33,9 @@ def legend_artist(self, legend, orig_handle, fontsize, handlebox): import numpy as np from matplotlib.lines import Line2D -from matplotlib.patches import Rectangle +from matplotlib.patches import Rectangle, FancyArrowPatch import matplotlib.collections as mcoll +from matplotlib.text import Text, Annotation import matplotlib.colors as mcolors @@ -58,6 +59,7 @@ def create_artists(self, legend, orig_handle, width, height) that are scaled by fontsize if necessary. """ + def __init__(self, xpad=0., ypad=0., update_func=None): self._xpad, self._ypad = xpad, ypad self._update_prop_func = update_func @@ -110,10 +112,10 @@ def legend_artist(self, legend, orig_handle, """ xdescent, ydescent, width, height = self.adjust_drawing_area( - legend, orig_handle, - handlebox.xdescent, handlebox.ydescent, - handlebox.width, handlebox.height, - fontsize) + legend, orig_handle, + handlebox.xdescent, handlebox.ydescent, + handlebox.width, handlebox.height, + fontsize) artists = self.create_artists(legend, orig_handle, xdescent, ydescent, width, height, fontsize, handlebox.get_transform()) @@ -135,6 +137,7 @@ class HandlerNpoints(HandlerBase): """ A legend handler that shows *numpoints* points in the legend entry. """ + def __init__(self, marker_pad=0.3, numpoints=None, **kw): """ Parameters @@ -180,6 +183,7 @@ class HandlerNpointsYoffsets(HandlerNpoints): A legend handler that shows *numpoints* in the legend, and allows them to be individually offest in the y-direction. """ + def __init__(self, numpoints=None, yoffsets=None, **kw): """ Parameters @@ -211,6 +215,7 @@ class HandlerLine2D(HandlerNpoints): """ Handler for `.Line2D` instances. """ + def __init__(self, marker_pad=0.3, numpoints=None, **kw): """ Parameters @@ -263,6 +268,7 @@ class HandlerPatch(HandlerBase): """ Handler for `.Patch` instances. """ + def __init__(self, patch_func=None, **kw): """ Parameters @@ -309,6 +315,7 @@ class HandlerLineCollection(HandlerLine2D): """ Handler for `.LineCollection` instances. """ + def get_numpoints(self, legend): if self._numpoints is None: return legend.scatterpoints @@ -341,6 +348,7 @@ class HandlerRegularPolyCollection(HandlerNpointsYoffsets): """ Handler for `.RegularPolyCollections`. """ + def __init__(self, yoffsets=None, sizes=None, **kw): HandlerNpointsYoffsets.__init__(self, yoffsets=yoffsets, **kw) @@ -378,7 +386,7 @@ def update_prop(self, legend_handle, orig_handle, legend): self._update_prop(legend_handle, orig_handle) legend_handle.set_figure(legend.figure) - #legend._set_artist_props(legend_handle) + # legend._set_artist_props(legend_handle) legend_handle.set_clip_box(None) legend_handle.set_clip_path(None) @@ -416,6 +424,7 @@ class HandlerPathCollection(HandlerRegularPolyCollection): """ Handler for `.PathCollections`, which are used by `~.Axes.scatter`. """ + def create_collection(self, orig_handle, sizes, offsets, transOffset): p = type(orig_handle)([orig_handle.get_paths()[0]], sizes=sizes, @@ -429,6 +438,7 @@ class HandlerCircleCollection(HandlerRegularPolyCollection): """ Handler for `.CircleCollections`. """ + def create_collection(self, orig_handle, sizes, offsets, transOffset): p = type(orig_handle)(sizes, offsets=offsets, @@ -441,6 +451,7 @@ class HandlerErrorbar(HandlerLine2D): """ Handler for Errorbars. """ + def __init__(self, xerr_size=0.5, yerr_size=None, marker_pad=0.3, numpoints=None, **kw): @@ -503,8 +514,8 @@ def create_artists(self, legend, orig_handle, handle_caplines = [] if orig_handle.has_xerr: - verts = [ ((x - xerr_size, y), (x + xerr_size, y)) - for x, y in zip(xdata_marker, ydata_marker)] + verts = [((x - xerr_size, y), (x + xerr_size, y)) + for x, y in zip(xdata_marker, ydata_marker)] coll = mcoll.LineCollection(verts) self.update_prop(coll, barlinecols[0], legend) handle_barlinecols.append(coll) @@ -521,8 +532,8 @@ def create_artists(self, legend, orig_handle, handle_caplines.append(capline_right) if orig_handle.has_yerr: - verts = [ ((x, y - yerr_size), (x, y + yerr_size)) - for x, y in zip(xdata_marker, ydata_marker)] + verts = [((x, y - yerr_size), (x, y + yerr_size)) + for x, y in zip(xdata_marker, ydata_marker)] coll = mcoll.LineCollection(verts) self.update_prop(coll, barlinecols[0], legend) handle_barlinecols.append(coll) @@ -554,6 +565,7 @@ class HandlerStem(HandlerNpointsYoffsets): """ Handler for plots produced by `~.Axes.stem`. """ + def __init__(self, marker_pad=0.3, numpoints=None, bottom=None, yoffsets=None, **kw): """ @@ -649,11 +661,37 @@ class HandlerTuple(HandlerBase): pad : float, optional If None, fall back to ``legend.borderpad`` as the default. In units of fraction of font size. Default is None. + + + width_ratios : tuple, optional + Specifies the respective widths of a text/arrow legend annotation pair. + Must be of length ndivide. + If None, all sections will have the same width. Default is None. + + + handlers : tuple, optionnal + The list of handlers to call for each text/arrow legend annotation section. + Must be of length ndivide. + If None, the default handlers will be fetched automatically. Default is None. """ - def __init__(self, ndivide=1, pad=None, **kwargs): + + def __init__( + self, + ndivide=1, + pad=None, + width_ratios=None, + handlers=None, + **kwargs): self._ndivide = ndivide self._pad = pad + self._handlers = handlers + + if (width_ratios is not None) and (len(width_ratios) == ndivide): + self._width_ratios = width_ratios + else: + self._width_ratios = None + HandlerBase.__init__(self, **kwargs) def create_artists(self, legend, orig_handle, @@ -672,17 +710,32 @@ def create_artists(self, legend, orig_handle, else: pad = self._pad * fontsize - if ndivide > 1: - width = (width - pad * (ndivide - 1)) / ndivide + if self._width_ratios is not None: + sumratios = sum(self._width_ratios) + widths = [(width - pad * (ndivide - 1)) * ratio / sumratios + for ratio in self._width_ratios] + else: + widths = [(width - pad * (ndivide - 1)) / ndivide + for _ in range(ndivide)] + widths_cycle = cycle(widths) - xds_cycle = cycle(xdescent - (width + pad) * np.arange(ndivide)) + xds = [xdescent - (widths[-i - 1] + pad) * i for i in range(ndivide)] + xds_cycle = cycle(xds) a_list = [] - for handle1 in orig_handle: - handler = legend.get_legend_handler(handler_map, handle1) - _a_list = handler.create_artists( - legend, handle1, - next(xds_cycle), ydescent, width, height, fontsize, trans) + for i, handle1 in enumerate(orig_handle): + if self._handlers is not None: + handler = self._handlers[i] + else: + handler = legend.get_legend_handler(handler_map, handle1) + + _a_list = handler.create_artists(legend, handle1, + next(xds_cycle), + ydescent, + next(widths_cycle), + height, + fontsize, + trans) a_list.extend(_a_list) return a_list @@ -692,6 +745,7 @@ class HandlerPolyCollection(HandlerBase): """ Handler for `.PolyCollection` used in `~.Axes.fill_between` and `~.Axes.stackplot`. """ + def _update_prop(self, legend_handle, orig_handle): def first_color(colors): if colors is None: @@ -728,3 +782,164 @@ def create_artists(self, legend, orig_handle, self.update_prop(p, orig_handle, legend) p.set_transform(trans) return [p] + + +class HandlerFancyArrowPatch(HandlerPatch): + """ + Handler for FancyArrowPatch instances. + """ + + def _create_patch(self, legend, orig_handle, + xdescent, ydescent, width, height, fontsize): + arrow = FancyArrowPatch([-xdescent, + -ydescent + height / 2], + [-xdescent + width, + -ydescent + height / 2], + mutation_scale=width / 3) + arrow.set_arrowstyle(orig_handle.get_arrowstyle()) + return arrow + + +class HandlerText(HandlerBase): + """ + Handler for Text instances. + + Additional kwargs are passed through to `HandlerBase`. + + Parameters + ---------- + rep_str : string, optional + Replacement string used in the legend when the Text string is longer than rep_maxlen. + Default is 'Aa'. + + + rep_maxlen : int, optional + Maximum length of Text string to be used in the legend. Default is 2. + """ + + def __init__(self, rep_str='Aa', rep_maxlen=2, **kwargs): + + self._rep_str = rep_str + self._rep_maxlen = rep_maxlen + + HandlerBase.__init__(self, **kwargs) + + def create_artists(self, legend, orig_handle, + xdescent, ydescent, width, height, fontsize, trans): + # Use original text if it is short + text = orig_handle.get_text() + if len(text) > self._rep_maxlen: + text = self._rep_str + + # Use smaller fontsize for text repr + text_fontsize = 2 * fontsize / 3 + + t = Text(x=-xdescent + width / 2 - len(text) * text_fontsize / 4, + y=-ydescent + height / 4, + text=text) + + # Copy text attributes, except fontsize + self.update_prop(t, orig_handle, legend) + t.set_transform(trans) + t.set_fontsize(text_fontsize) + + return [t] + + +class HandlerAnnotation(HandlerText): + """ + Handler for Annotation instances. + + Defers to HandlerText to draw the annotation text (if any). + Defers to HandlerFancyArrowPatch to draw the annotation arrow (if any). + For annotations made of both text and arrow, HandlerTuple is used to draw them side by side. + Additional kwargs are passed through to `HandlerText`. + + Parameters + ---------- + + pad : float, optional + If None, fall back to `legend.borderpad` asstr the default. + In units of fraction of font size. + Default is None. + + width_ratios : tuple, optional + The relative width of the respective text/arrow legend annotation pair. + Must be of length 2. + Default is [1,4]. + """ + + def __init__( + self, + pad=None, + width_ratios=[1, 4], + **kwargs + ): + + self._pad = pad + self._width_ratios = width_ratios + + HandlerText.__init__(self, **kwargs) + + def create_artists( + self, + legend, + orig_handle, + xdescent, + ydescent, + width, + height, + fontsize, + trans, + ): + if orig_handle.arrow_patch is not None \ + and orig_handle.get_text() is not '': + + # Draw a tuple (text, arrow) + + handler = HandlerTuple( + ndivide=2, + pad=self._pad, + width_ratios=self._width_ratios, + handlers=[ + HandlerText( + rep_str=self._rep_str, + rep_maxlen=self._rep_maxlen), + HandlerFancyArrowPatch()]) + + # Create a Text instance from annotation text + + text_handle = Text(text=orig_handle.get_text()) + text_handle.update_from(orig_handle) + handle = (text_handle, orig_handle.arrow_patch) + elif orig_handle.arrow_patch is not None: + + # Arrow without text + + handler = HandlerFancyArrowPatch() + handle = orig_handle.arrow_patch + elif orig_handle.get_text() is not '': + + # Text without arrow + + handler = HandlerText(rep_str=self._rep_str, + rep_maxlen=self._rep_maxlen) + handle = orig_handle + else: + + # No text, no arrow + + handler = HandlerPatch() + handle = Rectangle(xy=[0, 0], width=0, height=0, color='w', + alpha=0.0) + + return handler.create_artists( + legend, + handle, + xdescent, + ydescent, + width, + height, + fontsize, + trans, + ) diff --git a/lib/matplotlib/tests/baseline_images/test_legend/legend_all_annotation_text.png b/lib/matplotlib/tests/baseline_images/test_legend/legend_all_annotation_text.png new file mode 100644 index 0000000000000000000000000000000000000000..d49cd37ea3137209854d361ac699ab461fba4211 GIT binary patch literal 24064 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 NyQ>e8@y)&ye*ncK)C~Xt literal 0 HcmV?d00001 diff --git a/lib/matplotlib/tests/test_legend.py b/lib/matplotlib/tests/test_legend.py index 830fe798c44c..8c3232d38b9f 100644 --- a/lib/matplotlib/tests/test_legend.py +++ b/lib/matplotlib/tests/test_legend.py @@ -10,7 +10,8 @@ import matplotlib as mpl import matplotlib.transforms as mtransforms import matplotlib.collections as mcollections -from matplotlib.legend_handler import HandlerTuple +from matplotlib.legend_handler import HandlerTuple, HandlerAnnotation +import matplotlib.patches as mpatches import matplotlib.legend as mlegend @@ -216,6 +217,55 @@ def test_hatching(): ax.legend(handlelength=4, handleheight=4) +@image_comparison(baseline_images=['legend_all_annotation_text'], + extensions=['png'], + style='mpl20') +def test_legend_all_annotation(): + # Related to issue 8236 + # Tests all annotations and text in legend + fig, ax = plt.subplots(1) + ax.plot([0, 1], [0, 0], label='line1') + ax.plot([0, 1], [1, 1], label='line2') + ax.set_xticklabels('') + ax.set_yticklabels('') + # no text, no arrow + ax.annotate("", + xy=(0.1, 0.5), + xytext=(0.1, 0.5), + label='annotation (empty)') + # text, no arrow + my_annotation = ax.annotate("X", + xy=(0.1, 0.5), + xytext=(0.1, 0.5), + color='C2', + label='annotation (text, no arrow)') + # no text, arrow + ax.annotate("", + xy=(0.3, 1.0), + xytext=(0.3, 0.0), + arrowprops={'arrowstyle': '<->', 'color': 'C7'}, + label='annotation (no text, arrow)') + # Fancy arrow patch + arrpatch = mpatches.FancyArrowPatch([0.5, 0.8], [0.9, 0.9], + arrowstyle='<|-', + mutation_scale=20, + color='C3', + label='arrowpatch') + ax.add_patch(arrpatch) + # Long text, will not be used in legend + ax.text(x=0.1, y=0.1, + s='Hello', + color='C5', + label='text') + # Short text, copied in legend + ax.text(x=0.1, y=0.2, + s='Z', + color='C0', + label='short text') + ax.legend(handler_map={my_annotation: HandlerAnnotation(rep_str='Abcde', + rep_maxlen=0)}) + + def test_legend_remove(): fig = plt.figure() ax = fig.add_subplot(1, 1, 1) From 6cf5e2bd80bc696d2bd8b2ad6e5bee0cf5bdb3a3 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sun, 8 Jul 2018 23:54:29 -0400 Subject: [PATCH 2/2] DOC: add whats_new entry --- doc/users/next_whats_new/more_legend_handlers.rst | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 doc/users/next_whats_new/more_legend_handlers.rst diff --git a/doc/users/next_whats_new/more_legend_handlers.rst b/doc/users/next_whats_new/more_legend_handlers.rst new file mode 100644 index 000000000000..1b6051933939 --- /dev/null +++ b/doc/users/next_whats_new/more_legend_handlers.rst @@ -0,0 +1,5 @@ +Add legend handlers for FancyArrowPatch, Text, and Annotation +------------------------------------------------------------- + +By setting the label on FancyArrowPatch, Text, and Annotation objects +they will automatically be included in legends.