From 647930b5c0181bbd6a2a500a29c647ba4568a204 Mon Sep 17 00:00:00 2001 From: Wojciech Lange Date: Fri, 21 Dec 2018 10:02:21 +0100 Subject: [PATCH 1/2] Add legend handler for Annotation Co-authored-by: Maged Hennawy --- .../text_labels_and_annotations/legend.py | 6 ++- lib/matplotlib/legend.py | 9 ++-- lib/matplotlib/legend_handler.py | 53 ++++++++++++++++++- 3 files changed, 63 insertions(+), 5 deletions(-) diff --git a/examples/text_labels_and_annotations/legend.py b/examples/text_labels_and_annotations/legend.py index 33d8af3204e4..dffb375a0da1 100644 --- a/examples/text_labels_and_annotations/legend.py +++ b/examples/text_labels_and_annotations/legend.py @@ -21,7 +21,11 @@ ax.plot(a, d, 'k:', label='Data length') ax.plot(a, c + d, 'k', label='Total message length') -legend = ax.legend(loc='upper center', shadow=True, fontsize='x-large') +#Create an arrow with pre-defined label. +ax.annotate("", xy=(1.5, 4.5), xytext=(1.5, 9.0), + arrowprops={'arrowstyle':'<->', 'color':'C7'}, label='Distance') + +legend = ax.legend(loc='upper center', shadow=True, fontsize='large') # Put a nicer background color on the legend. legend.get_frame().set_facecolor('C0') diff --git a/lib/matplotlib/legend.py b/lib/matplotlib/legend.py index 7aef9ed7e8b3..e0e774d6e7cf 100644 --- a/lib/matplotlib/legend.py +++ b/lib/matplotlib/legend.py @@ -34,11 +34,11 @@ from matplotlib.font_manager import FontProperties from matplotlib.lines import Line2D from matplotlib.patches import (Patch, Rectangle, Shadow, FancyBboxPatch, - StepPatch) + StepPatch, FancyArrowPatch) from matplotlib.collections import ( Collection, CircleCollection, LineCollection, PathCollection, PolyCollection, RegularPolyCollection) -from matplotlib.text import Text +from matplotlib.text import Annotation, Text from matplotlib.transforms import Bbox, BboxBase, TransformedBbox from matplotlib.transforms import BboxTransformTo, BboxTransformFrom from matplotlib.offsetbox import ( @@ -649,7 +649,9 @@ def draw(self, renderer): 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(), + Annotation: legend_handler.HandlerAnnotation() } # (get|set|update)_default_handler_maps are public interfaces to @@ -799,6 +801,7 @@ def _init_legend_box(self, handles, labels, markerfirst=True): self._legend_handle_box]) self._legend_box.set_figure(self.figure) self._legend_box.axes = self.axes + self._legend_box.set_offset(self._findoffset) self.texts = text_list self.legendHandles = handle_list diff --git a/lib/matplotlib/legend_handler.py b/lib/matplotlib/legend_handler.py index c21c6d1212d3..5660655bfe02 100644 --- a/lib/matplotlib/legend_handler.py +++ b/lib/matplotlib/legend_handler.py @@ -34,7 +34,7 @@ def legend_artist(self, legend, orig_handle, fontsize, handlebox) from matplotlib import _api, cbook from matplotlib.lines import Line2D -from matplotlib.patches import Rectangle +from matplotlib.patches import Rectangle, FancyArrowPatch import matplotlib.collections as mcoll @@ -806,3 +806,54 @@ 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 HandlerAnnotation(HandlerBase): + """ + Handler for `.Annotation` instances. + Defers to `HandlerFancyArrowPatch` to draw the annotation arrow (if any). + + Parameters + ---------- + pad : float, optional + If None, fall back to :rc:`legend.borderpad` as the default. + In units of fraction of font size. Default is None + . + width_ratios : two-tuple, optional + The relative width of the respective text/arrow legend annotation pair. + Default is (1, 4). + """ + + def __init__(self, pad=None, width_ratios=(1, 4), **kwargs): + + self._pad = pad + self._width_ratios = width_ratios + + HandlerBase.__init__(self, **kwargs) + + def create_artists(self, legend, orig_handle, xdescent, ydescent, width, + height, fontsize, trans): + + if orig_handle.arrow_patch is not None: + + # Arrow without text + + handler = HandlerFancyArrowPatch() + handle = orig_handle.arrow_patch + + return handler.create_artists(legend, handle, xdescent, ydescent, + width, height, fontsize, trans) From 88b37839b958a8b7c0fb1c85b6e561a12cae7e6c Mon Sep 17 00:00:00 2001 From: Oscar Gustafsson Date: Sun, 29 May 2022 17:01:33 +0200 Subject: [PATCH 2/2] Cleanup and add test --- .../text_labels_and_annotations/legend.py | 4 +-- lib/matplotlib/legend_handler.py | 28 +++++-------------- lib/matplotlib/tests/test_legend.py | 27 ++++++++++++++++++ 3 files changed, 36 insertions(+), 23 deletions(-) diff --git a/examples/text_labels_and_annotations/legend.py b/examples/text_labels_and_annotations/legend.py index dffb375a0da1..e2a1d1319324 100644 --- a/examples/text_labels_and_annotations/legend.py +++ b/examples/text_labels_and_annotations/legend.py @@ -21,9 +21,9 @@ ax.plot(a, d, 'k:', label='Data length') ax.plot(a, c + d, 'k', label='Total message length') -#Create an arrow with pre-defined label. +# Create an arrow with pre-defined label. ax.annotate("", xy=(1.5, 4.5), xytext=(1.5, 9.0), - arrowprops={'arrowstyle':'<->', 'color':'C7'}, label='Distance') + arrowprops={'arrowstyle': '<->', 'color': 'C7'}, label='Distance') legend = ax.legend(loc='upper center', shadow=True, fontsize='large') diff --git a/lib/matplotlib/legend_handler.py b/lib/matplotlib/legend_handler.py index 5660655bfe02..fafb685f3b8d 100644 --- a/lib/matplotlib/legend_handler.py +++ b/lib/matplotlib/legend_handler.py @@ -812,7 +812,8 @@ class HandlerFancyArrowPatch(HandlerPatch): """ Handler for `~.FancyArrowPatch` instances. """ - def _create_patch(self, legend, orig_handle, xdescent, ydescent, width, height, fontsize): + def _create_patch(self, legend, orig_handle, xdescent, ydescent, width, + height, fontsize): arrow = FancyArrowPatch([-xdescent, -ydescent + height / 2], [-xdescent + width, @@ -826,34 +827,19 @@ class HandlerAnnotation(HandlerBase): """ Handler for `.Annotation` instances. Defers to `HandlerFancyArrowPatch` to draw the annotation arrow (if any). - - Parameters - ---------- - pad : float, optional - If None, fall back to :rc:`legend.borderpad` as the default. - In units of fraction of font size. Default is None - . - width_ratios : two-tuple, optional - The relative width of the respective text/arrow legend annotation pair. - Default is (1, 4). """ - - def __init__(self, pad=None, width_ratios=(1, 4), **kwargs): - - self._pad = pad - self._width_ratios = width_ratios - - HandlerBase.__init__(self, **kwargs) - def create_artists(self, legend, orig_handle, xdescent, ydescent, width, height, fontsize, trans): if orig_handle.arrow_patch is not None: - # Arrow without text - handler = HandlerFancyArrowPatch() handle = orig_handle.arrow_patch + else: + # No arrow + handler = HandlerPatch() + # Dummy patch to copy properties from to rectangle patch + handle = Rectangle(width=0, height=0, xy=(0, 0), color='none') return handler.create_artists(legend, handle, xdescent, ydescent, width, height, fontsize, trans) diff --git a/lib/matplotlib/tests/test_legend.py b/lib/matplotlib/tests/test_legend.py index 004b9407fddb..97d5061d9ab2 100644 --- a/lib/matplotlib/tests/test_legend.py +++ b/lib/matplotlib/tests/test_legend.py @@ -927,3 +927,30 @@ def test_legend_markers_from_line2d(): assert markers == new_markers == _markers assert labels == new_labels + + +def test_annotation_legend(): + fig, ax = plt.subplots() + # Add annotation with arrow and label + ax.annotate("", xy=(0.5, 0.5), xytext=(0.5, 0.7), + arrowprops={'arrowstyle': '<->'}, label="Bar") + legend = ax.legend() + assert len(legend.get_texts()) == 1 + # No arrow, no label + ax.annotate("Foo", xy=(0.3, 0.3)) + legend = ax.legend() + assert len(legend.get_texts()) == 1 + # Arrow, no label + ax.annotate("FooBar", xy=(0.7, 0.7), xytext=(0.7, 0.9), + arrowprops={'arrowstyle': '->'}) + legend = ax.legend() + assert len(legend.get_texts()) == 1 + # Add another annotation with arrow and label. now with non-empty text + ax.annotate("Foo", xy=(0.1, 0.1), xytext=(0.1, 0.7), + arrowprops={'arrowstyle': '<-'}, label="Foo") + legend = ax.legend() + assert len(legend.get_texts()) == 2 + # Add annotation without arrow, but with label + ax.annotate("Foo", xy=(0.2, 0.2), xytext=(0.2, 0.6), label="Foo") + legend = ax.legend() + assert len(legend.get_texts()) == 3