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

Skip to content

FIX/ENH: Introduce a monolithic legend handler for Line2D #11358

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions doc/api/next_api_changes/2018-06-01-AFV.rst
Original file line number Diff line number Diff line change
@@ -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()})

61 changes: 59 additions & 2 deletions lib/matplotlib/legend_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,9 +203,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, **kw):
"""
Expand Down Expand Up @@ -255,6 +256,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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps use one of .. note:: or .. versionadded:: or .. seealso:: (http://www.sphinx-doc.org/en/stable/markup/para.html#paragraph-level-markup). The latter could also be a See also numpydoc section.

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 = np.full_like(xdata, (height - ydescent) / 2)
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.
Expand Down
7 changes: 6 additions & 1 deletion lib/matplotlib/lines.py
Original file line number Diff line number Diff line change
Expand Up @@ -801,8 +801,13 @@ def draw(self, renderer):
# subsample the markers if markevery is not None
markevery = self.get_markevery()
if markevery is not None:
try:
transform = self.axes.transAxes
except AttributeError:
# Typically in the case of a **figure** legend.
transform = self.get_transform()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should just be set to None (or IdentityTransform)? I don't know if we really support float markeverys for figure-level artists (this PR only requires support for list-of-indexes markeverys), so setting it to None to throw something (in the isinstance(step, float) case of _mark_every_path) may be better (well, it probably used to throw here, so it's not worse), or if we want to support that case as well (which may be worth noting as a new, if obscure, feature), I think the figure-level artist's get_transform() already includes everything that's needed so we may not need to redo the transform another time? (in which case IdentityTransform may be correct?)

subsampled = _mark_every_path(markevery, tpath,
affine, self.axes.transAxes)
affine, transform)
else:
subsampled = tpath

Expand Down
6 changes: 5 additions & 1 deletion lib/matplotlib/tests/test_axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1329,9 +1329,13 @@ def test_markevery():
ax.legend()


@image_comparison(baseline_images=['markevery_line'],
@image_comparison(baseline_images=['markevery_line'], tol=0.005,
remove_text=True)
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)

Expand Down
18 changes: 17 additions & 1 deletion lib/matplotlib/tests/test_legend.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.cbook.deprecation import MatplotlibDeprecationWarning
Expand Down Expand Up @@ -55,9 +56,13 @@ def test_legend_auto2():
ax.legend([b1[0], b2[0]], ['up', 'down'], loc='best')


@image_comparison(baseline_images=['legend_auto3'])
@image_comparison(baseline_images=['legend_auto3'], tol=0.002)
def test_legend_auto3():
'Test automatic legend placement'
# 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).
fig = plt.figure()
ax = fig.add_subplot(111)
x = [0.9, 0.1, 0.1, 0.9, 0.9, 0.5]
Expand Down Expand Up @@ -539,3 +544,14 @@ def test_draggable():
with pytest.warns(MatplotlibDeprecationWarning):
legend.draggable()
assert not legend.get_draggable()


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()