Repository navigation
Add legend support for PatchCollection #30756
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
Changes from 1 commit
0f13209
3f013f6
567b478
8fc57eb
478de45
8536bfc
71c9a86
618d426
122a7d6
83adf5c
477bf97
1a1b20a
bd2f926
33242ca
b669f4d
92aaeca
555faa6
84a911e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
- Loading branch information
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| ``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. | ||
|
|
||
| The legend entry displays a rectangle matching the visual properties (colors, | ||
| line styles, line widths) of the first patch in the collection. | ||
|
|
||
| .. code-block:: python | ||
|
|
||
| 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`. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -809,3 +809,20 @@ def create_artists(self, legend, orig_handle, | |
| self.update_prop(p, orig_handle, legend) | ||
| p.set_transform(trans) | ||
| return [p] | ||
|
|
||
|
|
||
| class HandlerPatchCollection(HandlerPolyCollection): | ||
| """ | ||
| Handler for `.PatchCollection` instances. | ||
|
|
||
| This handler creates a rectangular legend entry that matches the visual | ||
| properties (colors, line styles, etc.) of the first patch in the collection. | ||
|
|
||
| See Also | ||
| -------- | ||
| matplotlib.collections.PatchCollection : The collection class this handles. | ||
| """ | ||
| # PatchCollection and PolyCollection have the same API for accessing | ||
| # colors, line properties, etc., so we can reuse HandlerPolyCollection | ||
| # implementation directly via inheritance. | ||
| pass | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's the reason to bother w/ a new class vs. just directly registering, e.g.: PatchCollection: legend_handler.HandlerPolyCollection()
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's no reason. |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1667,3 +1667,150 @@ 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 (issue #23998) | ||
| fig, ax = plt.subplots() | ||
|
|
||
| # Create some patches | ||
| p1 = mpatches.Polygon([[0, 0], [100, 100], [200, 0]]) | ||
| p2 = mpatches.Polygon([[400, 0], [500, 100], [600, 0]]) | ||
|
|
||
| # Create a PatchCollection with a label | ||
| pc = mcollections.PatchCollection([p1, p2], label="patch collection", | ||
| facecolor='blue', edgecolor='black') | ||
|
FazeelUsmani marked this conversation as resolved.
Outdated
|
||
| ax.add_collection(pc, autolim=True) | ||
| ax.autoscale_view() | ||
|
|
||
| # Create the legend | ||
|
FazeelUsmani marked this conversation as resolved.
Outdated
|
||
| 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 there's a legend handle | ||
| handles = leg.legend_handles | ||
| assert len(handles) == 1 | ||
| assert isinstance(handles[0], mpatches.Rectangle) | ||
|
|
||
|
|
||
| def test_patchcollection_legend_properties(): | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can combine
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ok |
||
| # Test that PatchCollection legend preserves visual properties | ||
| fig, ax = plt.subplots() | ||
|
|
||
| # Create patches with specific properties | ||
| p1 = mpatches.Circle((0, 0), 1) | ||
| p2 = mpatches.Circle((2, 0), 1) | ||
|
|
||
| # Create a PatchCollection with specific visual properties | ||
| pc = mcollections.PatchCollection( | ||
| [p1, p2], | ||
| label="styled patches", | ||
| facecolor='red', | ||
| edgecolor='blue', | ||
| linewidths=3, | ||
| linestyle='--' | ||
| ) | ||
|
FazeelUsmani marked this conversation as resolved.
Outdated
|
||
| ax.add_collection(pc) | ||
| ax.autoscale_view() | ||
|
|
||
| # Create the legend | ||
|
FazeelUsmani marked this conversation as resolved.
Outdated
|
||
| leg = ax.legend() | ||
| legend_patch = leg.legend_handles[0] | ||
|
|
||
| # Verify that visual properties are preserved | ||
|
FazeelUsmani marked this conversation as resolved.
Outdated
|
||
| assert_allclose(legend_patch.get_facecolor()[:3], | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why is alpha getting ignored here?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It shouldn't be. The alpha channel should also be verified. I'll change it to check all 4 RGBA components instead of just RGB. |
||
| pc.get_facecolor()[0][:3], rtol=1e-5) | ||
| assert_allclose(legend_patch.get_edgecolor()[:3], | ||
| pc.get_edgecolor()[0][:3], rtol=1e-5) | ||
| assert legend_patch.get_linewidth() == pc.get_linewidths()[0] | ||
| assert legend_patch.get_linestyle() == pc.get_linestyles()[0] | ||
|
|
||
|
|
||
| def test_patchcollection_legend_match_original(): | ||
| # Test PatchCollection legend with match_original=True | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need this test? The legend patch takes the properties from the first patch.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've removed the Now we have 3 focused tests:
|
||
| fig, ax = plt.subplots() | ||
|
|
||
| # Create patches with individual colors | ||
| p1 = mpatches.Rectangle((0, 0), 1, 1, facecolor='red', edgecolor='black') | ||
| p2 = mpatches.Rectangle((2, 0), 1, 1, facecolor='green', edgecolor='blue') | ||
|
|
||
| # Create a PatchCollection with match_original=True | ||
| # This should use the first patch's colors | ||
| pc = mcollections.PatchCollection([p1, p2], match_original=True, | ||
| label="original colors") | ||
| ax.add_collection(pc) | ||
| ax.autoscale_view() | ||
|
|
||
| # Create the legend | ||
| leg = ax.legend() | ||
| legend_patch = leg.legend_handles[0] | ||
|
|
||
| # The legend should show the first patch's colors (red face, black edge) | ||
| # Note: colors are normalized to RGBA tuples | ||
| assert_allclose(legend_patch.get_facecolor()[:3], | ||
| pc.get_facecolor()[0][:3], rtol=1e-5) | ||
| assert_allclose(legend_patch.get_edgecolor()[:3], | ||
| pc.get_edgecolor()[0][:3], rtol=1e-5) | ||
|
|
||
|
|
||
| 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) | ||
| ax.set_xlim(0, 1) | ||
| ax.set_ylim(0, 1) | ||
|
|
||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. does this matter if you're just testing object properties?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, it doesn't matter. Since we're only testing that the legend handler creates the correct objects with the right properties, the axis limits aren't needed. I'll remove these lines. |
||
| # 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 (with default/transparent colors) | ||
| assert len(leg.legend_handles) == 1 | ||
| assert isinstance(leg.legend_handles[0], mpatches.Rectangle) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we want to leave this out? I'd claim that being a Instead we should check the color.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've removed the In This way the tests focus on actual behavior (correct colors) rather than implementation details (patch type). |
||
|
|
||
|
|
||
| @image_comparison(['patchcollection_legend.png'], remove_text=True) | ||
| def test_patchcollection_legend_rendering(): | ||
| # Visual regression test for PatchCollection legend (issue #23998) | ||
| fig, ax = plt.subplots() | ||
|
|
||
| # Create various patch shapes | ||
| patches = [ | ||
| mpatches.Circle((0.2, 0.5), 0.15), | ||
| mpatches.Rectangle((0.5, 0.3), 0.2, 0.4), | ||
| mpatches.Polygon([[0.8, 0.2], [1.0, 0.7], [0.9, 0.3]]), | ||
| ] | ||
|
|
||
| # Create three PatchCollections with different styles | ||
| pc1 = mcollections.PatchCollection( | ||
| patches, facecolor='red', edgecolor='darkred', | ||
| linewidths=2, alpha=0.7, label='Red patches' | ||
| ) | ||
|
|
||
| pc2 = mcollections.PatchCollection( | ||
| [mpatches.Rectangle((0, 0), 0.1, 0.1) for _ in range(3)], | ||
| facecolor='blue', edgecolor='navy', | ||
| linewidths=1, linestyle='--', label='Blue dashed' | ||
| ) | ||
|
|
||
| pc3 = mcollections.PatchCollection( | ||
| [mpatches.Circle((0, 0), 0.1) for _ in range(2)], | ||
| facecolor='green', edgecolor='none', | ||
| label='Green no edge' | ||
| ) | ||
|
|
||
| for pc in [pc1, pc2, pc3]: | ||
| ax.add_collection(pc) | ||
|
|
||
| ax.set_xlim(0, 1.1) | ||
| ax.set_ylim(0, 0.8) | ||
| ax.legend(loc='upper left') | ||
Uh oh!
There was an error while loading. Please reload this page.