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()}) + diff --git a/lib/matplotlib/legend_handler.py b/lib/matplotlib/legend_handler.py index ad574dcf33d0..cbc909416a50 100644 --- a/lib/matplotlib/legend_handler.py +++ b/lib/matplotlib/legend_handler.py @@ -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): """ @@ -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 + 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. diff --git a/lib/matplotlib/lines.py b/lib/matplotlib/lines.py index b9769574dc90..38069d1aeb5c 100644 --- a/lib/matplotlib/lines.py +++ b/lib/matplotlib/lines.py @@ -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() subsampled = _mark_every_path(markevery, tpath, - affine, self.axes.transAxes) + affine, transform) else: subsampled = tpath diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index cb96355bbd9e..b7712f80b396 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -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) diff --git a/lib/matplotlib/tests/test_legend.py b/lib/matplotlib/tests/test_legend.py index 8b3ab373366c..82ab63303cb1 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.cbook.deprecation import MatplotlibDeprecationWarning @@ -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] @@ -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()