Fill patches in add_patch and flatten their real geometry - #381
Conversation
add_patch renders every patch as edge-only line segments: no fill, angle dropped, and curved patches measured from raw Bezier control points. The returned handle also has to own the outline so remove() clears the whole patch, and the fill must never touch the axes color cycle. These tests pin that behaviour and fail against the current implementation.
Extract data-space rings with Path.to_polygons(patch.get_patch_transform()), matplotlib's own flattening call, then fill each ring with the patch's own face color using the triangle-mesh recipe Axes.fill already uses. Rotation and curvature now survive: Rectangle(angle=30) keeps its rotation and Ellipse(width=2, height=1) covers pi*a*b rather than its control-point hull. Unfilled patches, a "none" face color, and a fully transparent one stay edge-only, degenerate rings draw their edge without raising, and the patch's own face color means the axes color cycle is never advanced. A patch now emits several marks, so add_patch returns a Patch handle that carries the outline entries as companions and sweeps them in remove() and set_zorder(), following the Wedge precedent in _artists.py.
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthrough
ChangesPatch rendering and lifecycle
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Caller
participant AxesAddPatch
participant PatchOutline
participant AxesEntries
participant Patch
Caller->>AxesAddPatch: add_patch
AxesAddPatch->>PatchOutline: derive transformed rings and fill state
PatchOutline-->>AxesAddPatch: normalized rings
AxesAddPatch->>AxesEntries: create triangle_mesh and segments entries
AxesAddPatch-->>Caller: return Patch handle
Caller->>Patch: remove()
Patch->>AxesEntries: remove owned entries
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@python/xy/pyplot/_axes.py`:
- Around line 5629-5630: Update the patch outline width handling near edge and
width extraction to convert linewidth from points to pixels using
self._point_scale(). Apply the same conversion to all outline segment entries
created below, and add a DPI-sensitive regression test confirming consistent
rendered stroke widths.
- Around line 5633-5653: Update the fill handling around the ring loop and
triangle generation so compound paths preserve interior holes instead of
triangulating each ring independently. Ensure PathPatch holes and Wedge annuli
honor the path’s fill rule while retaining the existing outline rendering, and
add regression coverage for both hole-preserving cases.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 19a87fe1-4eea-4575-a28c-6380c29d99a6
📒 Files selected for processing (6)
CHANGELOG.mdpython/xy/pyplot/_artists.pypython/xy/pyplot/_axes.pyspec/matplotlib/compat-changelog.mdspec/matplotlib/compat.mdtests/pyplot/test_launch_compat.py
Three defects surviving the fill work. The outline width goes to the mark in Matplotlib points where it wants pixels, so the stroke ignores figure DPI. A compound path with nested rings paints its hole solid, 116 for a true 84. A full-circle Wedge loses its fill entirely, because Matplotlib repeats a vertex in that path and the triangulator's rejection is swallowed. Two of the six are guards rather than repros: an annular sector and a pair of disjoint rings must keep filling, so they pass before and after.
Three fixes that have to land together. Outline width now multiplies by _point_scale(), the shim's points-to-pixels conversion used at 39 other sites. A 5 pt stroke measures 5 px at 72 dpi and 13 px at 200 dpi, matching ax.plot. _patch_outline drops consecutive duplicate vertices, not just a trailing one. Matplotlib repeats a vertex inside its full-circle paths, so a Wedge annulus reached polygon_triangles carrying a duplicate, was rejected, and the fill vanished into the except ValueError. Rings keep their closing vertex, so the outline pass still draws the closing edge. Nested rings mean holes, and polygon_triangles takes one simple polygon, so add_patch now abstains: it draws the outlines and skips the fill rather than painting the hole solid. Containment is tested with kernels.polygon_select, the native even-odd ray cast the lasso already uses. Disjoint rings still fill both, and an annular sector still fills because Matplotlib returns it as one ring. Without the dedupe this abstention would be untested, and without the abstention the dedupe would fill an annulus as two solid discs.
|
Both findings tested rather than taken on trust. One is right as stated, one is right about the defect and wrong about the example, and chasing the second turned up a third bug neither of us had. All three are fixed in Stroke units, confirmedRight. Thickness in pixels of a 5 pt stroke, measured through the centre column of a rendered PNG:
One correction to the framing. Released Hole topology, right about the defect, wrong about the example
Compound paths are the real case, and there the finding holds: Not implementing hole support here. Abstaining instead. When rings nest, the patch draws its outline and skips the fill, which is exactly the released behaviour, so nothing regresses and no known-wrong pixels get painted. Containment is the test, not ring count, so disjoint rings still fill. The check reuses Third bug, found while testing the secondA full annulus emitted no fill at all, rather than the solid sector predicted: Root cause. Matplotlib repeats a vertex inside its full-circle paths, so 33 vertices reached the triangulator carrying 32 unique points, and Rings now drop every consecutive duplicate, not just a trailing one, and both annulus rings triangulate. They still end up outline-only, but now because the nesting check abstains rather than because an exception vanished. The regression test asserts the ring vertex count for that reason, since Verification
Six tests added, four failing against the previous head: The two that pass either way guard the abstention against over-reaching, so passing before and after is the signal they exist for. Known limitation, now documentedNested rings never fill. A compound Unrelated, spotted while here
|
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@python/xy/pyplot/_axes.py`:
- Line 1113: Update the consecutive-vertex filtering expression in the
ring-processing logic to remove vertices only when they are exactly identical to
the preceding vertex, replacing the tolerance-based np.isclose comparison while
preserving the first vertex and existing ring output behavior.
In `@tests/pyplot/test_launch_compat.py`:
- Around line 298-309: Update
test_full_annulus_draws_both_outlines_without_raising to stop asserting that
each edge has exactly 32 vertices. Instead, assert the duplicate-removal
invariant by verifying each emitted outline has no duplicate endpoint, while
retaining the existing checks that meshes are empty and two edges are produced.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 91a35fcb-c836-4577-a48f-863425b9f3dd
📒 Files selected for processing (5)
CHANGELOG.mdpython/xy/pyplot/_axes.pyspec/matplotlib/compat-changelog.mdspec/matplotlib/compat.mdtests/pyplot/test_launch_compat.py
🚧 Files skipped from review as they are similar to previous changes (3)
- spec/matplotlib/compat.md
- CHANGELOG.md
- spec/matplotlib/compat-changelog.md
…ssertion The magnitude-relative duplicate test erases a patch drawn at genomic or epoch-millisecond coordinates: a Rectangle at x=1e9 collapses from five vertices to three and fills zero area, because 1e-5 of 1e9 is 10,000 data units. Two new tests pin that and a genuinely tiny 1e-9 edge surviving. A third pins the full-circle Wedge filling. Matplotlib's repeated vertex is not bit-exact, so deduplicating only exact matches leaves it in place and the triangulator rejects the whole disc. That test fails against an exact-equality dedupe and against the state before any dedupe. The full-annulus test no longer asserts a vertex count of 32, which hardcoded Matplotlib's current tessellation. It asserts the invariant instead, that no outline segment is a duplicate-length stub, measured against the ring's own span so it stays scale-free. Comparing against zero would not do: a repeat is floating-point noise at 2e-16, not an exact coincidence.
np.isclose is relative to coordinate magnitude, so at x=1e9 it treats two vertices 10,000 data units apart as one and the patch disappears. Deduplicate against an absolute tolerance derived from the ring's own bounding-box diagonal instead. Measured against Matplotlib 3.11.1, repeats sit at 9e-17 of the span while the smallest real edge across the shapes we flatten sits at 2e-3 of it. The threshold takes 1e-12 of the span, ten thousand times above the noise and a billion times below the smallest real edge. Exact equality would not work here: Matplotlib's repeated vertex differs in the last bits, so an exact test catches nothing and the annulus loses its fill again. The comparison is negated so a non-finite gap or span keeps its vertex. The triangulator then rejects the ring on its own terms rather than us silently emptying it.
|
Both fixed in Tolerance-based deletion, confirmedWorse than reported. The patch did not degrade, it disappeared. Genomic coordinates, epoch milliseconds and financial ticks all sit in that range. Why the suggested fix is not the one appliedDeduplicating only exactly identical consecutive vertices catches nothing here. Matplotlib's repeat is floating-point noise, not a coincidence: That is the full-annulus bug from the previous round coming straight back, and it would come back silently. What was appliedAn absolute tolerance of Every repeat is noise at ~1e-16 of the span. The smallest real edge across every shape we flatten is 2e-3. Fourteen orders of magnitude apart, and the threshold sits in the middle. Both populations are recorded in the docstring so the constant stays re-derivable. Multiplying by span rather than dividing the gaps means a zero-span ring needs no special case; the tolerance becomes zero and the test degrades to exact equality. The comparison is negated so a non-finite gap keeps its vertex and the ring is rejected by the triangulator on its own terms rather than being silently emptied. After: The pinned tessellation countRight, and removed. One correction worth recording. The obvious replacement, asserting no zero-length segments, is a dead assertion: it passes against the pre-dedupe commit, because a matplotlib repeat is 2.4e-16 apart rather than coincident, so the stub segment has nonzero length. The test now asserts the shortest segment as a fraction of the outline's bounding diagonal exceeds 1e-6, which is scale-free, pins no number we do not control, and fails against the pre-dedupe state as intended. Verification
Three tests added and one rewritten, failing against the previous head on the collapse itself:
No changelog entry for either. Both were introduced inside this unreleased PR and never shipped, so relative to released |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@python/xy/pyplot/_axes.py`:
- Around line 1123-1127: Update the ring filtering logic around the span
calculation to return the original ring immediately when span is non-finite.
Preserve the existing gap-based vertex filtering only for finite spans,
including the current behavior for non-finite gaps.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 26a6b03f-0574-41bd-aca5-a15edd0811c8
📒 Files selected for processing (2)
python/xy/pyplot/_axes.pytests/pyplot/test_launch_compat.py
An infinite coordinate makes the span infinite, so span * 1e-12 is infinite too, every finite gap reads as a duplicate, and the ring collapses to one vertex. NaN took the opposite path and worked by accident, because every comparison against NaN is false. Return the ring untouched when the span is not finite, which covers both and lets the triangulator reject the geometry on its own terms. With that guard in front, a finite span implies finite gaps, so the negated comparison the NaN case needed is gone and the test is a plain greater-than again. np.hypot is overflow-safe, so a ring at 1e200 still deduplicates normally; infinity only reaches here when the caller's geometry already holds it.
|
Right, fixed in An infinite span makes A genuine overflow does not reach this. With the guard in place the negated comparison went back to a plain On the testThe obvious input does not reach the guard.
Verification
No changelog entry. This one was introduced by the span tolerance in |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
python/xy/pyplot/_axes.py (1)
5692-5693: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winAvoid default-tolerance
np.allclosefor endpoint removal.This bypasses
_without_repeats’s ring-relative threshold. NumPy’s default tolerances can treat valid endpoints as equal at large coordinates or for tiny geometry, changing fill area and outline shape. Use exact equality or the same finitespan * 1e-12criterion, and add a near-but-distinct endpoint regression.Suggested fix
- if len(xv) > 2 and np.allclose((xv[0], yv[0]), (xv[-1], yv[-1])): + if len(xv) > 2 and xv[0] == xv[-1] and yv[0] == yv[-1]:🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@python/xy/pyplot/_axes.py` around lines 5692 - 5693, Replace the default-tolerance np.allclose endpoint check in the polygon cleanup path with exact coordinate equality or the established finite span * 1e-12 ring-relative criterion used by _without_repeats. Preserve removal only for genuinely repeated endpoints, and add a regression covering near-but-distinct endpoints so valid geometry remains unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@tests/pyplot/test_launch_compat.py`:
- Around line 365-377: Update the explanatory comment in
test_patch_with_a_non_finite_coordinate_keeps_its_vertices to state that
_without_repeats returns immediately for every non-finite span, preserving the
rectangle’s four vertices for inf, -inf, and nan. Remove the outdated claim that
infinite and NaN spans take opposite branches or that the infinite case empties
the ring.
---
Outside diff comments:
In `@python/xy/pyplot/_axes.py`:
- Around line 5692-5693: Replace the default-tolerance np.allclose endpoint
check in the polygon cleanup path with exact coordinate equality or the
established finite span * 1e-12 ring-relative criterion used by
_without_repeats. Preserve removal only for genuinely repeated endpoints, and
add a regression covering near-but-distinct endpoints so valid geometry remains
unchanged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: c6d6caf2-2d79-43d1-9ca7-d40dc1392ecf
📒 Files selected for processing (2)
python/xy/pyplot/_axes.pytests/pyplot/test_launch_compat.py
The comment claimed inf and NaN take opposite branches. They did before the guard landed; now both hit the same early return, so it described the bug rather than the fix and implied coverage of two paths where there is one. The three cases still earn their place for a different reason: they are the inputs that yield a span no tolerance can come from, and one predicate has to catch all three. Narrowing it to isnan fails both infinities, narrowing it to isinf fails the NaN. Also disambiguate 'the test needs a tolerance' in _without_repeats, which reads as a unit test rather than the duplicate comparison it means.
|
Right, fixed in Rewritten to what is actually true: # The tolerance is a fraction of the ring's span, and none of these three
# yield a usable one. They share a single guard, so what this pins is that
# its predicate catches all three: an isnan check would let the infinities
# through, an isinf check would let the NaN through.Kept rather than deleted. The test name says the vertices survive but not why they would otherwise be lost, and "the tolerance is derived from the span" is not visible from the test body. The new comment's claim is verified rather than asserted, by narrowing the guard in both directions: All three cases earn their place. Audit of the restChecked every comment, docstring and doc line added across the eight commits, since a guard that changes behaviour can strand prose written against the previous version. One more found, a wording ambiguity rather than staleness. Nothing else was stale. Each remaining comment states a measured number or a mechanism that still reproduces: the 1.000..4.000 unrotated span, 3.2509 for control-point ellipse area, 116 against a true 84 for the hole, the annular sector tracing a single ring, the swallowed rejection on the annulus, 1e-5 of 1e9 being 10,000 data units, the non-bit-exact repeat defeating exact-equality dedupe, and One line re-checked specifically. VerificationNo behaviour change and the numbers confirm it. |
Merging this PR will not alter performance
Comparing Footnotes
|
|
Thanks for the contribution! There are couple tweaks im going to push but overall looks pretty good |
…scale A hidden patch still draws its outline, because set_visible reaches only the first mark the patch produced; set_alpha, set_color, and set_transform share the gap. A filled patch whose edge paints nothing, matplotlib's default, emits an invisible segments mark per ring anyway. And to_polygons flattens in data units, so Circle(radius=1) arrives as sixteen segments overshooting the true radius by 2.5% while the identical circle at radius=1000 arrives smooth; area assertions cannot see this because the overshoot between on-curve points cancels the chord deficit. Also pinned: a fill dropped for a reason the user should hear about, a ring past the triangulator's 10,000-vertex cap or self-intersecting, warns instead of going quietly hollow; a genuinely degenerate ring stays quiet; a patch whose every mark would be invisible still leaves one entry for the handle to stand on; and the output-scale round trip does not cost a small patch at a large offset its area. Existing tests that used the always-emitted outline as their observation surface now ask for one explicitly with edgecolor=.
…scale Four review fixes on the add_patch rework. The Artist base gains a _companion_entries hook, and set_visible, set_alpha, set_color, and set_transform now run over it, so a handle standing for several marks moves all of them. Patch feeds its outline entries through the hook; remove and set_zorder already did this by hand. set_color paints face and edge alike, matching matplotlib's Patch.set_color. _patch_edge_color mirrors _patch_fill_color: an edge that paints nothing, which is matplotlib's default on a filled patch, emits no outline mark. That was a fully transparent segments mark per ring, a third of the exported payload for a figure of default rectangles. A patch that drew no body still emits its outline so the handle has an entry to stand on; a width of zero strokes no pixels, as in matplotlib. _refine_at_pixel_scale re-flattens the path as though the patch filled the figure, then undoes the scale. to_polygons subdivides until flat in the coordinates it is handed, so flattening straight through the patch transform took its tessellation from the numeric magnitude of the data: Circle(radius=1) was a visible 16-gon overshooting the true radius by 2.5% while the same circle at radius=1000 was smooth. The scale is anchored at the patch's own corner because the round trip costs relative precision, which a small patch at a large offset has little of to spare. Curved patches now carry ~10x the triangles they did; straight-edged patches are unchanged. Applying the affine to the control points before rebuilding the path is exact, and avoids the matplotlib import the shim forbids. The fill loop filters non-finite vertices first, as Axes.fill does, skips true degenerates silently, and warns with the reason for anything else the triangulator rejects: its 10,000-vertex cap and self-intersection both fell into a bare except ValueError that read as a deliberate hollow style. The closing-vertex test now uses the ring-relative tolerance _without_repeats established rather than magnitude-relative np.allclose. Docs no longer claim the flattening matches matplotlib's renderers; it resolves at the figure's pixel size before the view is known, which is a different mechanism with the same visual result.
There was a problem hiding this comment.
All reported issues were addressed across 6 files
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
The curves these comments describe — Circle, Ellipse, Wedge, Arc paths — are CURVE4 segments, and the repo writes Bézier with its accent everywhere else.
There was a problem hiding this comment.
All reported issues were addressed across 4 files (changes from recent commits).
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
Two cubic review findings confirmed and fixed, one documented, one already settled in the thread. Rectangle(..., transform=Affine2D().rotate_deg(45)) silently dropped its rotation, because flattening went through get_patch_transform alone. matplotlib's Patch.get_transform composes the artist transform in, so when one is set and composes, that is what the flattening uses. xy's own transform objects will not compose inside matplotlib: transData is the identity and is accepted as the no-op it is, a data-space affine applies to the flattened rings afterwards, and transAxes/transFigure raise NotImplementedError with the same message _transform_points gives every other data artist, since baked fractions go silently stale on the next limit change. _rings_are_nested tested one vertex, so rings that merely overlap read as nested and the whole patch went hollow. Nested now means every vertex of one ring inside another. Overlapping rings fill ring-by-ring, their union, where matplotlib's even-odd rule would leave the intersection unpainted; painting what each ring alone would have painted errs on the visible side. The Wedge annulus still abstains, since its inner ring is wholly inside its outer. The tessellation-staleness finding is real and documented instead of fixed: geometry is built when the patch is added, so a later figure resize, DPI change, or deep zoom reuses the tessellation chosen then. Re-flattening at materialization is an architecture change, not a review fix. The hole-fill finding was already deliberately deferred in the review thread, with the abstention documented in compat.md.
The hyphen crept in as a compound modifier; the term is written open in the graphics literature and in this repo's own prior usage.
Axes.add_patchdraws every patch as edge-only line segments. Its docstring admits the outline approximation, but the gap is wider than that, and I found two further problems while fixing the fill.Measured against Matplotlib 3.11.1:
Rectangle(facecolor="tab:blue")Rectangle((1,2), 3, 4, angle=30)angledroppedCircle(radius=1)Ellipse(width=2, height=1, angle=20)The rotation and curve errors have the same root cause as the missing fill.
add_patchreconstructs geometry by hand fromget_x/get_y/get_width/get_height, which has nowhere to putangle, and falls back to rawpath.vertices, which are Bezier control points rather than the curve. The ellipse is out by a factor of two because nothing applies the patch transform.What changed
Geometry now comes from
patch.get_path().to_polygons(patch.get_patch_transform()), the same flattening Matplotlib's own renderers use. It applies the transform and flattens curves, so one call replaces the hand-rolled branching and fixes all three symptoms at once.Each ring then fills through the recipe
Axes.fillalready uses,kernels.polygon_trianglesinto atriangle_meshmark with_joined_fill=True. No new mark and no Rust change.Extraction and paint are now separate.
_patch_outlinereturns data-space rings,_patch_fill_colorreturns the face color orNone, andadd_patchruns one fill pass and one outline pass over them. Conflating extraction with a single edge-only emit is what let three distinct bugs share one line of code.A filled patch emits more than one mark, so
add_patchreturns aPatchhandle that carries the outline entries and sweeps them inremove()andset_zorder(). This followsWedgein_artists.py. Without it the change would regressremove(), which used to clear a patch completely and would otherwise leave a ghost outline behind.Outlines now take the patch's own
get_linewidth()instead of a fixed 1.0.Preserved behaviour
Patches reporting
fill=False, a"none"face color, or a fully transparent one stay edge-only.StepPatchstill routes tostairs. Unsupported objects still raiseTypeError. The axes color cycle is never advanced, because the fill only ever uses the patch's own face color. A degenerate ring such as a zero-heightRectanglehas no triangulation, so it draws its edge and skips the fill rather than raising.Evidence
tests/pyplotgoes from 1174 to 1183 passing, 3 skipped, andmake checkpasses with 3443 passing. Thetyadvisory count is 77 both before and after.Nine tests added, five of which fail against
main:The other four are regression guards that pass either way. The color-cycle guard is one of them, so a plain before-and-after run proves nothing about it. Swapping
resolve_color(face)forself._next_patch_color()makes it fail with+ #ff7f0e, which is the mistake worth guarding given how close_next_patch_colorsits to this code.Rendered output, not just entry structure. Exporting a rotated
Rectangle, a concavePolygon, and anEllipseat 100 dpi and counting pixels by color, using the polygon as the scale reference:Area alone cannot prove rotation, since an unrotated 3x4 rectangle also covers 12. Measuring the rectangle's x extent against the polygon's known 4-unit span gives 4.588 data units. Rotated is 4.598 and unrotated would be 3.000. In SVG the concave shape arrives as a single 5-vertex
<polygon>rather than three triangles, which confirms_joined_fillsuppresses internal seams through to the exported file.Two related bugs left alone
Axes.fillorphans its own edge mark the same way, and filledstairsorphans its hatch mark. Both predate this change, andPatchnow gives them a precedent to copy. Fixing them here would span concerns the contributor guide asks to keep apart.Separately,
add_patchignoresget_label()entirely, before and after this change, so aRectangle(label="region")used as a legend proxy never appears. That is a feature gap rather than a regression.Docs
spec/matplotlib/compat-changelog.mdgets a dated entry,CHANGELOG.mdaFixedentry, andspec/matplotlib/compat.mdone line, since curve flattening throughto_polygonsbelongs in its list of accepted visual approximations.spec/matplotlib/shim-todo.mdneeds nothing. Line 396 already reads "wideradd_patch/add_collectionmappings for common Matplotlib objects", which stays true.No benchmarks. This is a correctness fix and makes no performance claim.
Summary by CodeRabbit
fill=False,"none", or fully transparent faces stay outline-only.linewidth, scaled for the output DPI.Axes.add_patchnow returns a composite patch handle soremove()/set_zorder()affect the full patch.