diff --git a/doc/release/next_whats_new/patchcollection_legend.rst b/doc/release/next_whats_new/patchcollection_legend.rst new file mode 100644 index 000000000000..58574e9e6757 --- /dev/null +++ b/doc/release/next_whats_new/patchcollection_legend.rst @@ -0,0 +1,22 @@ +``PatchCollection`` legends now supported +------------------------------------------ +`.PatchCollection` instances now properly display in legends when given a label. +Previously, labels on `~.PatchCollection` objects were ignored by the legend +system, requiring users to create manual legend entries. + +.. plot:: + :include-source: true + :alt: The legend entry displays a rectangle matching the visual properties (colors, line styles, line widths) of the first patch in the collection. + + import matplotlib.pyplot as plt + import matplotlib.patches as mpatches + from matplotlib.collections import PatchCollection + + fig, ax = plt.subplots() + patches = [mpatches.Circle((0, 0), 0.1), mpatches.Rectangle((0.5, 0.5), 0.2, 0.3)] + pc = PatchCollection(patches, facecolor='blue', edgecolor='black', label='My patches') + ax.add_collection(pc) + ax.legend() # Now displays the label "My patches" + plt.show() + +This fix resolves :ghissue:`23998`. diff --git a/lib/matplotlib/legend.py b/lib/matplotlib/legend.py index 119a27181c80..8564c18c5118 100644 --- a/lib/matplotlib/legend.py +++ b/lib/matplotlib/legend.py @@ -37,7 +37,7 @@ from matplotlib.patches import (Patch, Rectangle, Shadow, FancyBboxPatch, StepPatch) from matplotlib.collections import ( - Collection, CircleCollection, LineCollection, PathCollection, + Collection, CircleCollection, LineCollection, PatchCollection, PathCollection, PolyCollection, RegularPolyCollection) from matplotlib.text import Text from matplotlib.transforms import Bbox, BboxBase, TransformedBbox @@ -787,6 +787,7 @@ def draw(self, renderer): BarContainer: legend_handler.HandlerPatch( update_func=legend_handler.update_from_first_child), tuple: legend_handler.HandlerTuple(), + PatchCollection: legend_handler.HandlerPolyCollection(), PathCollection: legend_handler.HandlerPathCollection(), PolyCollection: legend_handler.HandlerPolyCollection() } diff --git a/lib/matplotlib/tests/test_legend.py b/lib/matplotlib/tests/test_legend.py index 61fae63a298e..a99192c4d571 100644 --- a/lib/matplotlib/tests/test_legend.py +++ b/lib/matplotlib/tests/test_legend.py @@ -1667,3 +1667,55 @@ def test_boxplot_legend_labels(): bp4 = axs[3].boxplot(data, label='box A') assert bp4['medians'][0].get_label() == 'box A' assert all(x.get_label().startswith("_") for x in bp4['medians'][1:]) + + +def test_patchcollection_legend(): + # Test that PatchCollection labels show up in legend and preserve visual + # properties (issue #23998) + fig, ax = plt.subplots() + + pc = mcollections.PatchCollection( + [mpatches.Circle((0, 0), 1), mpatches.Circle((2, 0), 1)], + label="patch collection", + facecolor='red', + edgecolor='blue', + linewidths=3, + linestyle='--', + ) + ax.add_collection(pc) + ax.autoscale_view() + + leg = ax.legend() + + # Check that the legend contains our label + assert len(leg.get_texts()) == 1 + assert leg.get_texts()[0].get_text() == "patch collection" + + # Check that the legend handle exists and has correct visual properties + assert len(leg.legend_handles) == 1 + legend_patch = leg.legend_handles[0] + assert mpl.colors.same_color(legend_patch.get_facecolor(), + pc.get_facecolor()[0]) + assert mpl.colors.same_color(legend_patch.get_edgecolor(), + pc.get_edgecolor()[0]) + assert legend_patch.get_linewidth() == pc.get_linewidths()[0] + assert legend_patch.get_linestyle() == pc.get_linestyles()[0] + + +def test_patchcollection_legend_empty(): + # Test that empty PatchCollection doesn't crash + fig, ax = plt.subplots() + + # Create an empty PatchCollection + pc = mcollections.PatchCollection([], label="empty collection") + ax.add_collection(pc) + + # This should not crash + leg = ax.legend() + + # Check that the label still appears + assert len(leg.get_texts()) == 1 + assert leg.get_texts()[0].get_text() == "empty collection" + + # The legend handle should exist + assert len(leg.legend_handles) == 1