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. pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy