From 0abac25a9826234194e7bef94628c26b1453901a Mon Sep 17 00:00:00 2001 From: "Adrien F. Vincent" Date: Thu, 31 May 2018 20:05:06 -0700 Subject: [PATCH 1/7] introduce default monolithic legend handler for Line2D --- lib/matplotlib/legend_handler.py | 61 ++++++++++++++++++++++++++++++-- 1 file changed, 59 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/legend_handler.py b/lib/matplotlib/legend_handler.py index cfcef1fbde05..71f705a9f7fe 100644 --- a/lib/matplotlib/legend_handler.py +++ b/lib/matplotlib/legend_handler.py @@ -204,9 +204,10 @@ def get_ydata(self, legend, xdescent, ydescent, width, height, fontsize): return ydata -class HandlerLine2D(HandlerNpoints): +class HandlerLine2DCompound(HandlerNpoints): """ - Handler for `.Line2D` instances. + Original handler for `.Line2D` instances, that relies on combining + a line-only with a marker-only artist. """ def __init__(self, marker_pad=0.3, numpoints=None, **kwargs): """ @@ -252,6 +253,62 @@ def create_artists(self, legend, orig_handle, return [legline, legline_marker] +class HandlerLine2D(HandlerNpoints): + """ + Handler for `.Line2D` instances. + + A class similar to the original handler for `.Line2D` instances but + that uses a monolithic artist rather than using one artist for the + line and another one for the marker(s). NB: the former handler, in + use before Matplotlib 3, is still available as `.HandlerLine2DCompound`. + + """ + def __init__(self, marker_pad=0.3, numpoints=None, **kw): + """ + Parameters + ---------- + marker_pad : float + Padding between points in legend entry. + + numpoints : int + Number of points to show in legend entry. + + Notes + ----- + Any other keyword arguments are given to `HandlerNpoints`. + """ + HandlerNpoints.__init__(self, marker_pad=marker_pad, + numpoints=numpoints, **kw) + + def create_artists(self, legend, orig_handle, + xdescent, ydescent, width, height, fontsize, + trans): + + xdata, xdata_marker = self.get_xdata(legend, xdescent, ydescent, + width, height, fontsize) + + markevery = None + if self.get_numpoints(legend) == 1: + # Special case: one wants a single marker in the center + # and a line that extends on both sides. One will use a + # 3 points line, but only mark the #1 (i.e. middle) point. + xdata = np.linspace(xdata[0], xdata[-1], 3) + markevery = [1] + + ydata = ((height - ydescent) / 2.) * np.ones(xdata.shape, float) + legline = Line2D(xdata, ydata, markevery=markevery) + + self.update_prop(legline, orig_handle, legend) + + if legend.markerscale != 1: + newsz = legline.get_markersize() * legend.markerscale + legline.set_markersize(newsz) + + legline.set_transform(trans) + + return [legline] + + class HandlerPatch(HandlerBase): """ Handler for `.Patch` instances. From 54755bc24da5f95bc7807e01fc4b31bd16c1c6b3 Mon Sep 17 00:00:00 2001 From: "Adrien F. Vincent" Date: Thu, 31 May 2018 20:18:16 -0700 Subject: [PATCH 2/7] add test --- lib/matplotlib/tests/test_legend.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/lib/matplotlib/tests/test_legend.py b/lib/matplotlib/tests/test_legend.py index 48172cbd9b24..90e75ddaaec0 100644 --- a/lib/matplotlib/tests/test_legend.py +++ b/lib/matplotlib/tests/test_legend.py @@ -10,6 +10,7 @@ import matplotlib as mpl import matplotlib.transforms as mtransforms import matplotlib.collections as mcollections +import matplotlib.lines as mlines from matplotlib.legend_handler import HandlerTuple import matplotlib.legend as mlegend from matplotlib import rc_context @@ -861,3 +862,14 @@ def test_legend_text_axes(): assert leg.axes is ax assert leg.get_texts()[0].axes is ax + + +def test_handlerline2d(): + '''Test consistency of the marker for the (monolithic) Line2D legend + handler (see #11357). + ''' + fig, ax = plt.subplots() + ax.scatter([0, 1], [0, 1], marker="v") + handles = [mlines.Line2D([0], [0], marker="v")] + leg = ax.legend(handles, ["Aardvark"], numpoints=1) + assert handles[0].get_marker() == leg.legendHandles[0].get_marker() From 594cae079eca95f3c5283e73357673513976588e Mon Sep 17 00:00:00 2001 From: "Adrien F. Vincent" Date: Fri, 1 Jun 2018 15:05:38 -0700 Subject: [PATCH 3/7] add api change note --- doc/api/next_api_changes/2018-06-01-AFV.rst | 32 +++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 doc/api/next_api_changes/2018-06-01-AFV.rst diff --git a/doc/api/next_api_changes/2018-06-01-AFV.rst b/doc/api/next_api_changes/2018-06-01-AFV.rst new file mode 100644 index 000000000000..6f6a9d45ac69 --- /dev/null +++ b/doc/api/next_api_changes/2018-06-01-AFV.rst @@ -0,0 +1,32 @@ +Change of the (default) legend handler for Line2D instances +----------------------------------------------------------- + +The default legend handler for Line2D instances (`.HandlerLine2D`) now +consistently exposes all the attributes and methods related to the line +marker (:ghissue:`11358`). This makes easy to change the marker features +after instantiating a legend. + +.. code:: + + import matplotlib.pyplot as plt + + fig, ax = plt.subplots() + + ax.plot([1, 3, 2], marker="s", label="Line", color="pink", mec="red", ms=8) + leg = ax.legend() + + leg.legendHandles[0].set_color("lightgray") + leg.legendHandles[0].set_mec("black") # marker edge color + +The former legend handler for Line2D objects has been renamed +`.HandlerLine2DCompound`. To revert to the behavior that was used before +Matplotlib 3, one can use + +.. code:: + + import matplotlib.legend as mlegend + from matplotlib.legend_handler import HandlerLine2DCompound + from matplotlib.lines import Line2D + + mlegend.Legend.update_default_handler_map({Line2D: HandlerLine2DCompound()}) + From ae201fc63e352d7f99a1bf959221f0d415ec6773 Mon Sep 17 00:00:00 2001 From: "Adrien F. Vincent" Date: Mon, 4 Jun 2018 19:12:53 -0700 Subject: [PATCH 4/7] slightly increase tolerance for not-really-failing tests --- lib/matplotlib/tests/test_axes.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 9f0272fa1269..78b07349a04d 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -1358,8 +1358,12 @@ def test_markevery(): ax.legend() -@image_comparison(['markevery_line'], remove_text=True) +@image_comparison(['markevery_line'], remove_text=True, tol=0.005) def test_markevery_line(): + # TODO: a slight change in rendering between Inkscape versions may explain + # why one had to introduce a small non-zero tolerance for the SVG test + # to pass. One may try to remove this hack once Travis' Inkscape version + # is modern enough. FWIW, no failure with 0.92.3 on my computer (#11358). x = np.linspace(0, 10, 100) y = np.sin(x) * np.sqrt(x/10 + 0.5) From 512e403512bd142fb44d6bb0252e52cbc3d3e2bf Mon Sep 17 00:00:00 2001 From: "Adrien F. Vincent" Date: Thu, 7 Jun 2018 15:06:53 -0700 Subject: [PATCH 5/7] leverage modern numpy functions --- 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 71f705a9f7fe..cb42c7797b30 100644 --- a/lib/matplotlib/legend_handler.py +++ b/lib/matplotlib/legend_handler.py @@ -295,7 +295,7 @@ def create_artists(self, legend, orig_handle, xdata = np.linspace(xdata[0], xdata[-1], 3) markevery = [1] - ydata = ((height - ydescent) / 2.) * np.ones(xdata.shape, float) + ydata = np.full_like(xdata, (height - ydescent) / 2) legline = Line2D(xdata, ydata, markevery=markevery) self.update_prop(legline, orig_handle, legend) From 3539f18940cbab7af001a569e14af2322aa58957 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Sun, 18 Jul 2021 23:56:11 +0200 Subject: [PATCH 6/7] Small additional doc updates. --- .../20699-AFV.rst} | 3 +-- lib/matplotlib/legend_handler.py | 20 +++++++++---------- lib/matplotlib/tests/test_legend.py | 4 +--- 3 files changed, 11 insertions(+), 16 deletions(-) rename doc/api/next_api_changes/{2018-06-01-AFV.rst => behavior/20699-AFV.rst} (90%) diff --git a/doc/api/next_api_changes/2018-06-01-AFV.rst b/doc/api/next_api_changes/behavior/20699-AFV.rst similarity index 90% rename from doc/api/next_api_changes/2018-06-01-AFV.rst rename to doc/api/next_api_changes/behavior/20699-AFV.rst index 6f6a9d45ac69..9852290baa96 100644 --- a/doc/api/next_api_changes/2018-06-01-AFV.rst +++ b/doc/api/next_api_changes/behavior/20699-AFV.rst @@ -19,8 +19,7 @@ after instantiating a legend. leg.legendHandles[0].set_mec("black") # marker edge color The former legend handler for Line2D objects has been renamed -`.HandlerLine2DCompound`. To revert to the behavior that was used before -Matplotlib 3, one can use +`.HandlerLine2DCompound`. To revert to the previous behavior, one can use .. code:: diff --git a/lib/matplotlib/legend_handler.py b/lib/matplotlib/legend_handler.py index cb42c7797b30..1da87a7483f4 100644 --- a/lib/matplotlib/legend_handler.py +++ b/lib/matplotlib/legend_handler.py @@ -207,8 +207,9 @@ def get_ydata(self, legend, xdescent, ydescent, width, height, fontsize): class HandlerLine2DCompound(HandlerNpoints): """ Original handler for `.Line2D` instances, that relies on combining - a line-only with a marker-only artist. + a line-only with a marker-only artist. May be deprecated in the future. """ + def __init__(self, marker_pad=0.3, numpoints=None, **kwargs): """ Parameters @@ -257,25 +258,22 @@ class HandlerLine2D(HandlerNpoints): """ Handler for `.Line2D` instances. - A class similar to the original handler for `.Line2D` instances but - that uses a monolithic artist rather than using one artist for the - line and another one for the marker(s). NB: the former handler, in - use before Matplotlib 3, is still available as `.HandlerLine2DCompound`. - + See Also + -------- + HandlerLine2DCompound : An earlier handler implementation, which used one + artist for the line and another for the marker(s). """ + def __init__(self, marker_pad=0.3, numpoints=None, **kw): """ Parameters ---------- marker_pad : float Padding between points in legend entry. - numpoints : int Number of points to show in legend entry. - - Notes - ----- - Any other keyword arguments are given to `HandlerNpoints`. + **kwargs + Keyword arguments forwarded to `.HandlerNpoints`. """ HandlerNpoints.__init__(self, marker_pad=marker_pad, numpoints=numpoints, **kw) diff --git a/lib/matplotlib/tests/test_legend.py b/lib/matplotlib/tests/test_legend.py index 90e75ddaaec0..c49ea996d056 100644 --- a/lib/matplotlib/tests/test_legend.py +++ b/lib/matplotlib/tests/test_legend.py @@ -865,9 +865,7 @@ def test_legend_text_axes(): def test_handlerline2d(): - '''Test consistency of the marker for the (monolithic) Line2D legend - handler (see #11357). - ''' + # Test marker consistency for monolithic Line2D legend handler (#11357). fig, ax = plt.subplots() ax.scatter([0, 1], [0, 1], marker="v") handles = [mlines.Line2D([0], [0], marker="v")] From a25fd75e224d8e3567c4df6a78962afb48f44290 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Thu, 29 Jul 2021 11:15:36 +0200 Subject: [PATCH 7/7] Properly deprecate access to second item returned by HandlerLine2D. --- lib/matplotlib/legend_handler.py | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/legend_handler.py b/lib/matplotlib/legend_handler.py index 1da87a7483f4..b289e26cc1f5 100644 --- a/lib/matplotlib/legend_handler.py +++ b/lib/matplotlib/legend_handler.py @@ -27,11 +27,12 @@ def legend_artist(self, legend, orig_handle, fontsize, handlebox) """ +from collections.abc import Sequence from itertools import cycle import numpy as np -from matplotlib import cbook +from matplotlib import _api, cbook from matplotlib.lines import Line2D from matplotlib.patches import Rectangle import matplotlib.collections as mcoll @@ -119,6 +120,9 @@ def legend_artist(self, legend, orig_handle, xdescent, ydescent, width, height, fontsize, handlebox.get_transform()) + if isinstance(artists, _Line2DHandleList): + artists = [artists[0]] + # create_artists will return a list of artists. for a in artists: handlebox.add_artist(a) @@ -254,6 +258,24 @@ def create_artists(self, legend, orig_handle, return [legline, legline_marker] +class _Line2DHandleList(Sequence): + def __init__(self, legline): + self._legline = legline + + def __len__(self): + return 2 + + def __getitem__(self, index): + if index != 0: + # Make HandlerLine2D return [self._legline] directly after + # deprecation elapses. + _api.warn_deprecated( + "3.5", message="Access to the second element returned by " + "HandlerLine2D is deprecated since %(since)s; it will be " + "removed %(removal)s.") + return [self._legline, self._legline][index] + + class HandlerLine2D(HandlerNpoints): """ Handler for `.Line2D` instances. @@ -304,7 +326,7 @@ def create_artists(self, legend, orig_handle, legline.set_transform(trans) - return [legline] + return _Line2DHandleList(legline) class HandlerPatch(HandlerBase): @@ -765,6 +787,8 @@ def create_artists(self, legend, orig_handle, _a_list = handler.create_artists( legend, handle1, next(xds_cycle), ydescent, width, height, fontsize, trans) + if isinstance(_a_list, _Line2DHandleList): + _a_list = [_a_list[0]] a_list.extend(_a_list) return a_list