diff --git a/doc/api/next_api_changes/behavior/9598-AFV.rst b/doc/api/next_api_changes/behavior/9598-AFV.rst new file mode 100644 index 000000000000..4edb930fe432 --- /dev/null +++ b/doc/api/next_api_changes/behavior/9598-AFV.rst @@ -0,0 +1,6 @@ +Change of ``legend(loc="best")`` behavior +----------------------------------------- + +The algorithm of the auto-legend locator has been tweaked to better handle +non rectangular patches. Additional details on this change can be found in +:ghissue:`9580` and :ghissue:`9598`. diff --git a/lib/matplotlib/legend.py b/lib/matplotlib/legend.py index d0590824ad84..d1283a5a8dbd 100644 --- a/lib/matplotlib/legend.py +++ b/lib/matplotlib/legend.py @@ -865,12 +865,14 @@ def _auto_legend_data(self): bboxes.append( artist.get_bbox().transformed(artist.get_data_transform())) elif isinstance(artist, Patch): - bboxes.append( - artist.get_path().get_extents(artist.get_transform())) + lines.append( + artist.get_transform().transform_path(artist.get_path())) elif isinstance(artist, Collection): - _, offset_trf, hoffsets, _ = artist._prepare_points() - for offset in offset_trf.transform(hoffsets): - offsets.append(offset) + transform, transOffset, hoffsets, _ = artist._prepare_points() + if len(hoffsets): + for offset in transOffset.transform(hoffsets): + offsets.append(offset) + return bboxes, lines, offsets def get_children(self): diff --git a/lib/matplotlib/tests/test_legend.py b/lib/matplotlib/tests/test_legend.py index 6660b91ecdd9..7b4f4a465488 100644 --- a/lib/matplotlib/tests/test_legend.py +++ b/lib/matplotlib/tests/test_legend.py @@ -10,6 +10,7 @@ from matplotlib.testing._markers import needs_usetex import matplotlib.pyplot as plt import matplotlib as mpl +import matplotlib.patches as mpatches import matplotlib.transforms as mtransforms import matplotlib.collections as mcollections import matplotlib.lines as mlines @@ -17,6 +18,7 @@ import matplotlib.legend as mlegend from matplotlib import rc_context from matplotlib.font_manager import FontProperties +from numpy.testing import assert_allclose def test_legend_ordereddict(): @@ -69,6 +71,60 @@ def test_legend_auto3(): ax.legend(loc='best') +def test_legend_auto4(): + """ + Check that the legend location with automatic placement is the same, + whatever the histogram type is. Related to issue #9580. + """ + # NB: barstacked is pointless with a single dataset. + fig, axs = plt.subplots(ncols=3, figsize=(6.4, 2.4)) + leg_bboxes = [] + for ax, ht in zip(axs.flat, ('bar', 'step', 'stepfilled')): + ax.set_title(ht) + # A high bar on the left but an even higher one on the right. + ax.hist([0] + 5*[9], bins=range(10), label="Legend", histtype=ht) + leg = ax.legend(loc="best") + fig.canvas.draw() + leg_bboxes.append( + leg.get_window_extent().transformed(ax.transAxes.inverted())) + + # The histogram type "bar" is assumed to be the correct reference. + assert_allclose(leg_bboxes[1].bounds, leg_bboxes[0].bounds) + assert_allclose(leg_bboxes[2].bounds, leg_bboxes[0].bounds) + + +def test_legend_auto5(): + """ + Check that the automatic placement handle a rather complex + case with non rectangular patch. Related to issue #9580. + """ + fig, axs = plt.subplots(ncols=2, figsize=(9.6, 4.8)) + + leg_bboxes = [] + for ax, loc in zip(axs.flat, ("center", "best")): + # An Ellipse patch at the top, a U-shaped Polygon patch at the + # bottom and a ring-like Wedge patch: the correct placement of + # the legend should be in the center. + for _patch in [ + mpatches.Ellipse( + xy=(0.5, 0.9), width=0.8, height=0.2, fc="C1"), + mpatches.Polygon(np.array([ + [0, 1], [0, 0], [1, 0], [1, 1], [0.9, 1.0], [0.9, 0.1], + [0.1, 0.1], [0.1, 1.0], [0.1, 1.0]]), fc="C1"), + mpatches.Wedge((0.5, 0.5), 0.5, 0, 360, width=0.05, fc="C0") + ]: + ax.add_patch(_patch) + + ax.plot([0.1, 0.9], [0.9, 0.9], label="A segment") # sthg to label + + leg = ax.legend(loc=loc) + fig.canvas.draw() + leg_bboxes.append( + leg.get_window_extent().transformed(ax.transAxes.inverted())) + + assert_allclose(leg_bboxes[1].bounds, leg_bboxes[0].bounds) + + @image_comparison(['legend_various_labels'], remove_text=True) def test_various_labels(): # tests all sorts of label types