Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 1761822

Browse files
authored
Merge pull request #20699 from anntzer/monolithic-line2d-legend-handler
FIX/ENH: Introduce a monolithic legend handler for Line2D Closes #2035 Closes #11357 Closes #18391 Closes #20552
2 parents 2acc6b0 + a25fd75 commit 1761822

File tree

4 files changed

+128
-4
lines changed

4 files changed

+128
-4
lines changed
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
Change of the (default) legend handler for Line2D instances
2+
-----------------------------------------------------------
3+
4+
The default legend handler for Line2D instances (`.HandlerLine2D`) now
5+
consistently exposes all the attributes and methods related to the line
6+
marker (:ghissue:`11358`). This makes easy to change the marker features
7+
after instantiating a legend.
8+
9+
.. code::
10+
11+
import matplotlib.pyplot as plt
12+
13+
fig, ax = plt.subplots()
14+
15+
ax.plot([1, 3, 2], marker="s", label="Line", color="pink", mec="red", ms=8)
16+
leg = ax.legend()
17+
18+
leg.legendHandles[0].set_color("lightgray")
19+
leg.legendHandles[0].set_mec("black") # marker edge color
20+
21+
The former legend handler for Line2D objects has been renamed
22+
`.HandlerLine2DCompound`. To revert to the previous behavior, one can use
23+
24+
.. code::
25+
26+
import matplotlib.legend as mlegend
27+
from matplotlib.legend_handler import HandlerLine2DCompound
28+
from matplotlib.lines import Line2D
29+
30+
mlegend.Legend.update_default_handler_map({Line2D: HandlerLine2DCompound()})
31+

lib/matplotlib/legend_handler.py

Lines changed: 82 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,12 @@
2727
def legend_artist(self, legend, orig_handle, fontsize, handlebox)
2828
"""
2929

30+
from collections.abc import Sequence
3031
from itertools import cycle
3132

3233
import numpy as np
3334

34-
from matplotlib import cbook
35+
from matplotlib import _api, cbook
3536
from matplotlib.lines import Line2D
3637
from matplotlib.patches import Rectangle
3738
import matplotlib.collections as mcoll
@@ -119,6 +120,9 @@ def legend_artist(self, legend, orig_handle,
119120
xdescent, ydescent, width, height,
120121
fontsize, handlebox.get_transform())
121122

123+
if isinstance(artists, _Line2DHandleList):
124+
artists = [artists[0]]
125+
122126
# create_artists will return a list of artists.
123127
for a in artists:
124128
handlebox.add_artist(a)
@@ -204,10 +208,12 @@ def get_ydata(self, legend, xdescent, ydescent, width, height, fontsize):
204208
return ydata
205209

206210

207-
class HandlerLine2D(HandlerNpoints):
211+
class HandlerLine2DCompound(HandlerNpoints):
208212
"""
209-
Handler for `.Line2D` instances.
213+
Original handler for `.Line2D` instances, that relies on combining
214+
a line-only with a marker-only artist. May be deprecated in the future.
210215
"""
216+
211217
def __init__(self, marker_pad=0.3, numpoints=None, **kwargs):
212218
"""
213219
Parameters
@@ -252,6 +258,77 @@ def create_artists(self, legend, orig_handle,
252258
return [legline, legline_marker]
253259

254260

261+
class _Line2DHandleList(Sequence):
262+
def __init__(self, legline):
263+
self._legline = legline
264+
265+
def __len__(self):
266+
return 2
267+
268+
def __getitem__(self, index):
269+
if index != 0:
270+
# Make HandlerLine2D return [self._legline] directly after
271+
# deprecation elapses.
272+
_api.warn_deprecated(
273+
"3.5", message="Access to the second element returned by "
274+
"HandlerLine2D is deprecated since %(since)s; it will be "
275+
"removed %(removal)s.")
276+
return [self._legline, self._legline][index]
277+
278+
279+
class HandlerLine2D(HandlerNpoints):
280+
"""
281+
Handler for `.Line2D` instances.
282+
283+
See Also
284+
--------
285+
HandlerLine2DCompound : An earlier handler implementation, which used one
286+
artist for the line and another for the marker(s).
287+
"""
288+
289+
def __init__(self, marker_pad=0.3, numpoints=None, **kw):
290+
"""
291+
Parameters
292+
----------
293+
marker_pad : float
294+
Padding between points in legend entry.
295+
numpoints : int
296+
Number of points to show in legend entry.
297+
**kwargs
298+
Keyword arguments forwarded to `.HandlerNpoints`.
299+
"""
300+
HandlerNpoints.__init__(self, marker_pad=marker_pad,
301+
numpoints=numpoints, **kw)
302+
303+
def create_artists(self, legend, orig_handle,
304+
xdescent, ydescent, width, height, fontsize,
305+
trans):
306+
307+
xdata, xdata_marker = self.get_xdata(legend, xdescent, ydescent,
308+
width, height, fontsize)
309+
310+
markevery = None
311+
if self.get_numpoints(legend) == 1:
312+
# Special case: one wants a single marker in the center
313+
# and a line that extends on both sides. One will use a
314+
# 3 points line, but only mark the #1 (i.e. middle) point.
315+
xdata = np.linspace(xdata[0], xdata[-1], 3)
316+
markevery = [1]
317+
318+
ydata = np.full_like(xdata, (height - ydescent) / 2)
319+
legline = Line2D(xdata, ydata, markevery=markevery)
320+
321+
self.update_prop(legline, orig_handle, legend)
322+
323+
if legend.markerscale != 1:
324+
newsz = legline.get_markersize() * legend.markerscale
325+
legline.set_markersize(newsz)
326+
327+
legline.set_transform(trans)
328+
329+
return _Line2DHandleList(legline)
330+
331+
255332
class HandlerPatch(HandlerBase):
256333
"""
257334
Handler for `.Patch` instances.
@@ -710,6 +787,8 @@ def create_artists(self, legend, orig_handle,
710787
_a_list = handler.create_artists(
711788
legend, handle1,
712789
next(xds_cycle), ydescent, width, height, fontsize, trans)
790+
if isinstance(_a_list, _Line2DHandleList):
791+
_a_list = [_a_list[0]]
713792
a_list.extend(_a_list)
714793

715794
return a_list

lib/matplotlib/tests/test_axes.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1365,8 +1365,12 @@ def test_markevery():
13651365
ax.legend()
13661366

13671367

1368-
@image_comparison(['markevery_line'], remove_text=True)
1368+
@image_comparison(['markevery_line'], remove_text=True, tol=0.005)
13691369
def test_markevery_line():
1370+
# TODO: a slight change in rendering between Inkscape versions may explain
1371+
# why one had to introduce a small non-zero tolerance for the SVG test
1372+
# to pass. One may try to remove this hack once Travis' Inkscape version
1373+
# is modern enough. FWIW, no failure with 0.92.3 on my computer (#11358).
13701374
x = np.linspace(0, 10, 100)
13711375
y = np.sin(x) * np.sqrt(x/10 + 0.5)
13721376

lib/matplotlib/tests/test_legend.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import matplotlib as mpl
1111
import matplotlib.transforms as mtransforms
1212
import matplotlib.collections as mcollections
13+
import matplotlib.lines as mlines
1314
from matplotlib.legend_handler import HandlerTuple
1415
import matplotlib.legend as mlegend
1516
from matplotlib import rc_context
@@ -861,3 +862,12 @@ def test_legend_text_axes():
861862

862863
assert leg.axes is ax
863864
assert leg.get_texts()[0].axes is ax
865+
866+
867+
def test_handlerline2d():
868+
# Test marker consistency for monolithic Line2D legend handler (#11357).
869+
fig, ax = plt.subplots()
870+
ax.scatter([0, 1], [0, 1], marker="v")
871+
handles = [mlines.Line2D([0], [0], marker="v")]
872+
leg = ax.legend(handles, ["Aardvark"], numpoints=1)
873+
assert handles[0].get_marker() == leg.legendHandles[0].get_marker()

0 commit comments

Comments
 (0)