Skip to content

Conversation

@FazeelUsmani
Copy link
Contributor

@FazeelUsmani FazeelUsmani commented Nov 17, 2025

PR summary

Closes #23998

Why is this change necessary?

PatchCollection instances do not currently display in legends even when assigned a label. This has been a long-standing limitation that forces users to create manual legend entries, making the API inconsistent with other collection types like PolyCollection and PathCollection.

What problem does it solve?

Before this fix, the following code would not display a legend entry:

import matplotlib.pyplot as plt
from matplotlib.collections import PatchCollection
from matplotlib.patches import Polygon

fig, ax = plt.subplots()
p1 = Polygon([[0, 0], [100, 100], [200, 0]])
p2 = Polygon([[400, 0], [500, 100], [600, 0]])
pc = PatchCollection([p1, p2], label="my patches", facecolor='blue')
ax.add_collection(pc)
ax.legend()  # Legend was empty ❌
plt.show()

After this fix, the legend correctly displays the "my patches" label with a rectangular swatch matching the collection's visual properties.

Implementation reasoning

The implementation adds a new HandlerPatchCollection class that inherits from the existing HandlerPolyCollection. This approach:

  1. Eliminates code duplication: Both PatchCollection and PolyCollection inherit from Collection and share identical APIs for accessing colors, line styles, and other visual properties
  2. Maintains consistency: Uses the same legend representation (Rectangle) as PolyCollection
  3. Ensures maintainability: Bug fixes or enhancements to HandlerPolyCollection automatically benefit HandlerPatchCollection

The handler extracts visual properties (face color, edge color, line width, line style) from the first patch in the collection and creates a rectangular legend entry.

Minimum self-contained example

import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
from matplotlib.collections import PatchCollection

fig, ax = plt.subplots()

# Create various patches
patches = [
    mpatches.Circle((0.2, 0.5), 0.1),
    mpatches.Rectangle((0.5, 0.3), 0.2, 0.3),
]

# Create PatchCollection with label
pc = PatchCollection(patches, facecolor='red', edgecolor='black',
                     linewidths=2, label='My patches')
ax.add_collection(pc)
ax.autoscale_view()

# Legend now works!
ax.legend()
plt.show()

PR checklist

  • "closes Labels for PatchCollection do not show #23998" is in the body of the PR description to link the related issue
  • New and changed code is tested
    • Added 5 comprehensive tests covering:
      • Basic functionality
      • Visual property preservation
      • match_original=True parameter
      • Empty collection edge case
      • Visual regression test with @image_comparison
  • [N/A] Plotting related features are demonstrated in an example
    • This is a bug fix for existing functionality, not a new plotting feature
    • Code examples are included in the What's New entry
  • New Features and API Changes are noted with a directive and release note
    • Added doc/release/next_whats_new/patchcollection_legend.rst
    • Includes user-facing description and code example
  • Documentation complies with general and docstring guidelines
    • HandlerPatchCollection includes comprehensive docstring
    • Follows matplotlib's docstring conventions
    • Includes "See Also" section

@FazeelUsmani FazeelUsmani marked this pull request as draft November 17, 2025 13:42
The @image_comparison test requires a baseline image that doesn't exist yet.
Removing it for now - we still have 4 comprehensive functional tests that
verify the functionality works correctly.
@FazeelUsmani FazeelUsmani marked this pull request as ready for review November 17, 2025 15:51
Close figures explicitly at the end of each test to prevent any
potential state leakage that could affect other tests in parallel
execution environments.
Copy link
Member

@timhoffm timhoffm left a comment

Choose a reason for hiding this comment

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

Thanks! Looks good. Only minor style improvements to the tests.

Comment on lines 1736 to 1737
def test_patchcollection_legend_match_original():
# Test PatchCollection legend with match_original=True
Copy link
Member

Choose a reason for hiding this comment

The 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. match_original applies the properties from the first patch to all other patches. I don't see how any reasonable implementation of match_original could adversingly affect the legend.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've removed the test_patchcollection_legend_match_original test. You're absolutely right - since the legend handler always takes properties from the first patch in the collection, match_original doesn't affect the legend behavior. The test was redundant with test_patchcollection_legend_properties.

Now we have 3 focused tests:

  1. test_patchcollection_legend - Basic functionality (label appears in legend)
  2. test_patchcollection_legend_properties - Visual properties are preserved
  3. test_patchcollection_legend_empty - Edge case: empty collection doesn't crash


# The legend handle should exist (with default/transparent colors)
assert len(leg.legend_handles) == 1
assert isinstance(leg.legend_handles[0], mpatches.Rectangle)
Copy link
Member

Choose a reason for hiding this comment

The 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 Rectangle is an implementation detail that we don't want to test.

Instead we should check the color.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've removed the isinstance(Rectangle) checks from both tests.

In test_patchcollection_legend, I replaced it with color verification - now checking that the legend patch has the correct facecolor and edgecolor.
In test_patchcollection_legend_empty, I simply verify that the handle exists since the main goal is to test that empty collections don't crash.

This way the tests focus on actual behavior (correct colors) rather than implementation details (patch type).

FazeelUsmani and others added 11 commits November 18, 2025 12:12
Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com>
Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com>
Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com>
Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com>
Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com>
Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com>
Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com>
Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com>
Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com>
As noted by @timhoffm, this test doesn't add value because:
- match_original affects how colors are applied to patches in the collection
- The legend handler always takes properties from the first patch
- Whether match_original is True or False, the legend gets the same result
- This is already covered by test_patchcollection_legend_properties
As suggested by @timhoffm:
- Remove isinstance(Rectangle) checks - this is an implementation detail
- In test_patchcollection_legend: Check colors match instead
- In test_patchcollection_legend_empty: Just verify legend handle exists

This focuses tests on actual behavior (correct colors) rather than
internal implementation choices (what patch type is used).
@FazeelUsmani
Copy link
Contributor Author

Hi @timhoffm,
Do I need to find out another reviewer for this PR? Or the maintainers of this repo will check regularly. What I heard is atelast 2 approvals are required to merge a PR.

@rcomer
Copy link
Member

rcomer commented Nov 19, 2025

Unfortunately the bot that posts it is currently broken, but when you opened your first PR you should have got a comment with this message. So about a week is reasonable to wait for a review.

Comment on lines 825 to 828
# PatchCollection and PolyCollection have the same API for accessing
# colors, line properties, etc., so we can reuse HandlerPolyCollection
# implementation directly via inheritance.
pass
Copy link
Member

@story645 story645 Nov 19, 2025

Choose a reason for hiding this comment

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

Copy link
Contributor Author

@FazeelUsmani FazeelUsmani Nov 20, 2025

Choose a reason for hiding this comment

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

There's no reason. PatchCollection and PolyCollection have identical APIs for accessing colors and line properties, so we can just register HandlerPolyCollection directly. I'll remove the new class.

Comment on lines 1735 to 1737
ax.set_xlim(0, 1)
ax.set_ylim(0, 1)

Copy link
Member

Choose a reason for hiding this comment

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

does this matter if you're just testing object properties?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.


# Verify that visual properties are preserved
legend_patch = leg.legend_handles[0]
assert_allclose(legend_patch.get_facecolor()[:3],
Copy link
Member

Choose a reason for hiding this comment

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

why is alpha getting ignored here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.

- Remove HandlerPatchCollection class - use HandlerPolyCollection directly
  Since PatchCollection and PolyCollection have identical APIs, we can
  register PatchCollection to use the existing handler without creating
  a new class. This follows YAGNI principle.

- Check all 4 color components (RGBA) instead of just RGB
  The alpha channel should also be verified for completeness.

- Remove unnecessary axis limits from empty collection test
  Since we're only testing object properties, not visual rendering,
  the axis limits aren't needed.

Addresses feedback from @story645
@rcomer
Copy link
Member

rcomer commented Nov 20, 2025

You may find the same_color function useful for the tests.

pc.get_edgecolor()[0], rtol=1e-5)


def test_patchcollection_legend_properties():
Copy link
Member

Choose a reason for hiding this comment

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

You can combine test_patchcollection_legend: and test_patchcollection_legend_properties(): b/c text is just another property

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ok

Copy link
Member

@story645 story645 left a comment

Choose a reason for hiding this comment

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

Thanks for the contribution! test failures seem unrelated but I'm gonna let them run again to make sure.

@story645 story645 added this to the v3.11.0 milestone Nov 24, 2025
@rcomer rcomer mentioned this pull request Nov 26, 2025
6 tasks
@story645 story645 merged commit 7a7a388 into matplotlib:main Nov 26, 2025
51 of 56 checks passed
@story645
Copy link
Member

story645 commented Nov 26, 2025

Thanks for the contribution! I should check previous comments 🤦‍♀️ but also still thanks for fixing this and your thoughtful use of AI.

@FazeelUsmani
Copy link
Contributor Author

@story645 You're welcome! Haha, yes, I want to adopt AI in every aspect of my day-to-day life.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Labels for PatchCollection do not show

4 participants