From 8b8b56074d43b7de4579a2ce0826a5371aa2df8d Mon Sep 17 00:00:00 2001 From: Julien Lecoeur Date: Tue, 14 Mar 2017 14:59:56 +0100 Subject: [PATCH 01/13] Add texts artists to legend --- lib/matplotlib/axes/_axes.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index dc7b82386216..65f32ecb4c4e 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -263,7 +263,8 @@ def _get_legend_handles(self, legend_handler_map=None): """ handles_original = (self.lines + self.patches + - self.collections + self.containers) + self.collections + self.containers + + self.texts) handler_map = mlegend.Legend.get_default_handler_map() if legend_handler_map is not None: From 036daedde208296f1f47cb05e14a3297db6a8957 Mon Sep 17 00:00:00 2001 From: Julien Lecoeur Date: Tue, 14 Mar 2017 15:01:07 +0100 Subject: [PATCH 02/13] Allow different widths in legends handled by HandlerTuple --- lib/matplotlib/legend_handler.py | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/lib/matplotlib/legend_handler.py b/lib/matplotlib/legend_handler.py index 67706de0486c..2abe815e4668 100644 --- a/lib/matplotlib/legend_handler.py +++ b/lib/matplotlib/legend_handler.py @@ -578,18 +578,25 @@ class HandlerTuple(HandlerBase): The number of sections to divide the legend area into. If None, use the length of the input tuple. Default is 1. - 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 + The relative width of sections. Must be of length ndivide. + If None, all sections will have the same width. Default is None. """ - def __init__(self, ndivide=1, pad=None, **kwargs): + def __init__(self, ndivide=1, pad=None, width_ratios=None, **kwargs): self._ndivide = ndivide self._pad = pad + + 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, @@ -608,10 +615,16 @@ 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 = [xdescent - (width + pad) * i for i in range(ndivide)] + xds = [xdescent - (widths[-i-1] + pad) * i for i in range(ndivide)] xds_cycle = cycle(xds) a_list = [] @@ -620,7 +633,8 @@ def create_artists(self, legend, orig_handle, _a_list = handler.create_artists(legend, handle1, six.next(xds_cycle), ydescent, - width, height, + six.next(widths_cycle), + height, fontsize, trans) a_list.extend(_a_list) From d7bd0698dec68fe622ce2f03512e7bc600104143 Mon Sep 17 00:00:00 2001 From: Julien Lecoeur Date: Tue, 14 Mar 2017 15:02:40 +0100 Subject: [PATCH 03/13] Add legend handler for FancyArrowPatch --- lib/matplotlib/legend_handler.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/lib/matplotlib/legend_handler.py b/lib/matplotlib/legend_handler.py index 2abe815e4668..d15f08620fa1 100644 --- a/lib/matplotlib/legend_handler.py +++ b/lib/matplotlib/legend_handler.py @@ -256,6 +256,21 @@ def create_artists(self, legend, orig_handle, 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 HandlerLineCollection(HandlerLine2D): """ Handler for LineCollection instances. From 70dd56d49c1e21783aea8a06c1a5f34f842aafdb Mon Sep 17 00:00:00 2001 From: Julien Lecoeur Date: Tue, 14 Mar 2017 15:24:34 +0100 Subject: [PATCH 04/13] Add legend handler for Text artists --- lib/matplotlib/legend_handler.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/lib/matplotlib/legend_handler.py b/lib/matplotlib/legend_handler.py index d15f08620fa1..63bc5d20f724 100644 --- a/lib/matplotlib/legend_handler.py +++ b/lib/matplotlib/legend_handler.py @@ -690,3 +690,26 @@ def create_artists(self, legend, orig_handle, self.update_prop(p, orig_handle, legend) p.set_transform(trans) return [p] + + +class HandlerText(HandlerBase): + """ + Handler for Text instances. + """ + def create_artists(self, legend, orig_handle, + xdescent, ydescent, width, height, fontsize, trans): + t = Text(x=-xdescent + width / 3, + y=-ydescent + height / 4, + text="a") + + # Use original text if it is short + text = orig_handle.get_text() + if len(text) < 2: + t.set_text(text) + + # Copy text attributes, except fontsize + self.update_prop(t, orig_handle, legend) + t.set_transform(trans) + t.set_fontsize(2 * fontsize / 3) + + return [t] From b07352779b57f2ec9011869fd186a3548cab1af2 Mon Sep 17 00:00:00 2001 From: Julien Lecoeur Date: Tue, 14 Mar 2017 15:27:46 +0100 Subject: [PATCH 05/13] Add legend handler for Annotation artists --- lib/matplotlib/legend_handler.py | 37 +++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/legend_handler.py b/lib/matplotlib/legend_handler.py index 63bc5d20f724..76cb29871c04 100644 --- a/lib/matplotlib/legend_handler.py +++ b/lib/matplotlib/legend_handler.py @@ -34,11 +34,13 @@ 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 +from matplotlib.text import Text, Annotation import matplotlib.collections as mcoll import matplotlib.colors as mcolors + def update_from_first_child(tgt, src): tgt.update_from(src.get_children()[0]) @@ -713,3 +715,36 @@ def create_artists(self, legend, orig_handle, t.set_fontsize(2 * fontsize / 3) return [t] + + +class HandlerAnnotation(HandlerBase): + """ + Handler for FancyArrowPatch instances. + """ + 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=None, width_ratios=[1,4]) + + # 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() + handle = orig_handle + else: + # No text, no arrow + handler = HandlerPatch() + handle = Rectangle(xy=[0,0],width=0,height=0,color='w') + + return handler.create_artists(legend, handle, + xdescent, ydescent, + width, height, + fontsize, trans) From 7b35e233b3edd0033744b1212e93a72e0061c1da Mon Sep 17 00:00:00 2001 From: Julien Lecoeur Date: Tue, 14 Mar 2017 15:39:49 +0100 Subject: [PATCH 06/13] Add Text, FancyArrowPatch and Annotation legend handlers to handler map --- lib/matplotlib/legend.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/legend.py b/lib/matplotlib/legend.py index 5165db5c824d..92b66ac0aff7 100644 --- a/lib/matplotlib/legend.py +++ b/lib/matplotlib/legend.py @@ -36,7 +36,8 @@ from matplotlib.cbook import is_string_like, silent_list, is_hashable 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.text import Text, Annotation from matplotlib.collections import (LineCollection, RegularPolyCollection, CircleCollection, PathCollection, PolyCollection) @@ -493,7 +494,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(), + Text:legend_handler.HandlerText(), + FancyArrowPatch:legend_handler.HandlerFancyArrowPatch(), + Annotation:legend_handler.HandlerAnnotation(), } # (get|set|update)_default_handler_maps are public interfaces to From 240a497406985eb646704f7ce3f1fa753aa669ae Mon Sep 17 00:00:00 2001 From: Julien Lecoeur Date: Tue, 14 Mar 2017 16:23:10 +0100 Subject: [PATCH 07/13] Fix typo and add documentation --- lib/matplotlib/legend_handler.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/legend_handler.py b/lib/matplotlib/legend_handler.py index 76cb29871c04..4786f95f1d04 100644 --- a/lib/matplotlib/legend_handler.py +++ b/lib/matplotlib/legend_handler.py @@ -719,7 +719,11 @@ def create_artists(self, legend, orig_handle, class HandlerAnnotation(HandlerBase): """ - Handler for FancyArrowPatch instances. + 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. """ def create_artists(self, legend, orig_handle, xdescent, ydescent, width, height, fontsize, trans): From 3e612e257282c065ec7f7a7fac569f62004a34f1 Mon Sep 17 00:00:00 2001 From: Julien Lecoeur Date: Wed, 15 Mar 2017 10:00:31 +0100 Subject: [PATCH 08/13] Display transparent rectangle in legend for empty annotations --- lib/matplotlib/legend_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/legend_handler.py b/lib/matplotlib/legend_handler.py index 4786f95f1d04..93534478c5e3 100644 --- a/lib/matplotlib/legend_handler.py +++ b/lib/matplotlib/legend_handler.py @@ -746,7 +746,7 @@ def create_artists(self, legend, orig_handle, else: # No text, no arrow handler = HandlerPatch() - handle = Rectangle(xy=[0,0],width=0,height=0,color='w') + handle = Rectangle(xy=[0, 0], width=0, height=0, color='w', alpha=0.0) return handler.create_artists(legend, handle, xdescent, ydescent, From f2de7f14121c6f78aebc7654b2d26313e3e76c8d Mon Sep 17 00:00:00 2001 From: Julien Lecoeur Date: Wed, 15 Mar 2017 10:01:49 +0100 Subject: [PATCH 09/13] Add params in HandlerText: max string length and replacement string --- lib/matplotlib/legend_handler.py | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/legend_handler.py b/lib/matplotlib/legend_handler.py index 93534478c5e3..5f99f8aa2fde 100644 --- a/lib/matplotlib/legend_handler.py +++ b/lib/matplotlib/legend_handler.py @@ -697,18 +697,39 @@ def create_artists(self, legend, orig_handle, 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 + print('rep_str', rep_str) + + HandlerBase.__init__(self, **kwargs) + def create_artists(self, legend, orig_handle, xdescent, ydescent, width, height, fontsize, trans): t = Text(x=-xdescent + width / 3, y=-ydescent + height / 4, - text="a") + text=self._rep_str) # Use original text if it is short text = orig_handle.get_text() - if len(text) < 2: + if len(text) <= self._rep_maxlen: t.set_text(text) - + print('t', text, t.get_text(), self._rep_str) # Copy text attributes, except fontsize self.update_prop(t, orig_handle, legend) t.set_transform(trans) From 0c623ce1be6a709f34165840c9bfb83fca622523 Mon Sep 17 00:00:00 2001 From: Julien Lecoeur Date: Wed, 15 Mar 2017 10:03:07 +0100 Subject: [PATCH 10/13] HandlerTuple: allow to pass a list of custom handlers --- lib/matplotlib/legend_handler.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/lib/matplotlib/legend_handler.py b/lib/matplotlib/legend_handler.py index 5f99f8aa2fde..89e7b1607690 100644 --- a/lib/matplotlib/legend_handler.py +++ b/lib/matplotlib/legend_handler.py @@ -603,11 +603,17 @@ class HandlerTuple(HandlerBase): The relative width of sections. 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 section. Must be of length ndivide. + If None, the handlers will be fetched automatically fromt the legend handler map. + Default is None. + """ - def __init__(self, ndivide=1, pad=None, width_ratios=None, **kwargs): + def __init__(self, ndivide=1, pad=None, width_ratios=None, handlers=None, **kwargs): - self._ndivide = ndivide - self._pad = pad + 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 @@ -645,8 +651,12 @@ def create_artists(self, legend, orig_handle, xds_cycle = cycle(xds) a_list = [] - for handle1 in orig_handle: - handler = legend.get_legend_handler(handler_map, handle1) + 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, six.next(xds_cycle), ydescent, From aeed3e574f0e1cbd3e00deded27cafed95162a41 Mon Sep 17 00:00:00 2001 From: Julien Lecoeur Date: Wed, 15 Mar 2017 10:05:21 +0100 Subject: [PATCH 11/13] HandlerAnnotation: pass additionnal arguments to handle text correctly --- lib/matplotlib/legend_handler.py | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/lib/matplotlib/legend_handler.py b/lib/matplotlib/legend_handler.py index 89e7b1607690..038526ee824c 100644 --- a/lib/matplotlib/legend_handler.py +++ b/lib/matplotlib/legend_handler.py @@ -748,20 +748,43 @@ def create_artists(self, legend, orig_handle, return [t] -class HandlerAnnotation(HandlerBase): +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 text and arrow sections. 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=None, width_ratios=[1,4]) - + 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) @@ -772,7 +795,7 @@ def create_artists(self, legend, orig_handle, handle = orig_handle.arrow_patch elif orig_handle.get_text() is not "": # Text without arrow - handler = HandlerText() + handler = HandlerText(rep_str=self._rep_str, rep_maxlen=self._rep_maxlen) handle = orig_handle else: # No text, no arrow From db84d07142fbf113a85559e8933c654ce6401c23 Mon Sep 17 00:00:00 2001 From: Julien Lecoeur Date: Wed, 15 Mar 2017 10:33:18 +0100 Subject: [PATCH 12/13] Shift Text legend artist laterally according to its length --- lib/matplotlib/legend_handler.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/lib/matplotlib/legend_handler.py b/lib/matplotlib/legend_handler.py index 038526ee824c..daac1bf53804 100644 --- a/lib/matplotlib/legend_handler.py +++ b/lib/matplotlib/legend_handler.py @@ -725,25 +725,27 @@ def __init__(self, rep_str='Aa', rep_maxlen=2, **kwargs): self._rep_str = rep_str self._rep_maxlen = rep_maxlen - print('rep_str', rep_str) HandlerBase.__init__(self, **kwargs) def create_artists(self, legend, orig_handle, xdescent, ydescent, width, height, fontsize, trans): - t = Text(x=-xdescent + width / 3, - y=-ydescent + height / 4, - text=self._rep_str) - # Use original text if it is short text = orig_handle.get_text() - if len(text) <= self._rep_maxlen: - t.set_text(text) - print('t', text, t.get_text(), self._rep_str) + 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(2 * fontsize / 3) + t.set_fontsize(text_fontsize) return [t] From f666224404b0f043048f349e0bffeb441dd73f2b Mon Sep 17 00:00:00 2001 From: Julien Lecoeur Date: Wed, 15 Mar 2017 10:38:20 +0100 Subject: [PATCH 13/13] typo --- lib/matplotlib/legend_handler.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/matplotlib/legend_handler.py b/lib/matplotlib/legend_handler.py index daac1bf53804..3959340a3802 100644 --- a/lib/matplotlib/legend_handler.py +++ b/lib/matplotlib/legend_handler.py @@ -605,8 +605,7 @@ class HandlerTuple(HandlerBase): handlers : tuple, optionnal The list of handlers to call for each section. Must be of length ndivide. - If None, the handlers will be fetched automatically fromt the legend handler map. - Default is None. + If None, the default handlers will be fetched automatically. Default is None. """ def __init__(self, ndivide=1, pad=None, width_ratios=None, handlers=None, **kwargs):