Simplify fill_between paths in SVG and PDF backends by 0x7e5oI · Pull Request #31359 · matplotlib/matplotlib
PR summary
Suggestion for closing #22803.
This PR reduces SVG/PDF output size for fill_between by allowing simplification to be applied to FillBetweenPolyCollection paths when path.simplify is enabled.
Why
plot() already benefits from path simplification because Line2D paths are passed to the backends with the relevant simplification behavior already in place. In contrast, fill_between() produces a FillBetweenPolyCollection, and the resulting collection paths were not consistently reaching the vector backends in a form that allowed the same simplification to happen. its paths are closed polygons (they contain CLOSEPATH codes), which disables should_simplify
by design in Path._update_values. Additionally, filled paths always have clip=False in draw_path, so the existing simplification gate never fired.
In practice, this meant that increasing path.simplify_threshold greatly reduced vector output size for line plots, but often had little or no effect for fill_between().
What does this PR change
FillBetweenPolyCollection.set_vertstags each generated path with a_fill_between_simplifyattribute.- In
RendererSVG.draw_pathandRendererPDF.draw_path, paths carrying that attribute (and without a hatch) are simplified by checkingrcParams["path.simplify"]andrcParams["path.simplify_threshold"]directly, bypassing the should_simplifyproperty which is alwaysFalse` for closed polygon paths. - Hatched fill_between paths are explicitly excluded: in SVG, the hatch pattern is clipped to the fill polygon boundary, so simplifying that boundary would change the visible output.
- No changes to path collection optimization or to unmarked paths - existing behavior is fully preserved.
What this approach solves:
For simplifiable fill_between paths, exported SVG/PDF files are reduced substantially. In the strongest benchmarked case (interpolate_cross), output size dropped by about 79.7% abd 75.5% for SVG and PDF, respectively. More fragmented cases still showed reductions, though with smaller gains and more mixed timing results.
Implementation notes:
-the change is intentionally narrow in scoppe
-simplification is only enabled for paths explicitly marked as simplifiable, rather than broadening simplification for all collections
-the PDF and SVG backends keep their existing behavior for unmarked paths
-the tests focus on output-size reduction rather than exact rendering bytes
Opt-in
Simplification only fires when the user has already set rcParams["path.simplify"] = True and rcParams["path.simplify_threshold"] > 0. This matches how simplification works for line plots in the same backends.
| Scenario | Backend | thr=0.0 | thr=1.0 | Reduction |
|---|---|---|---|---|
| interpolate_cross | SVG | 73,531 | 14,943 | 79.7% |
| interpolate_cross | 30,102 | 7,382 | 75.5% | |
| where_random | SVG | 155,694 | 104,644 | 32.8% |
| where_random | 35,829 | 20,425 | 43.0% | |
| multi_regions | SVG | 392,895 | 293,482 | 25.3% |
| multi_regions | 58,940 | 33,757 | 42.7% |
The old numbers (99.6-99.7% for interpolate_cross) were from the previous approach with a much larger dataset or different test setup. The real numbers here are more modest but still meaningful (75-80% for the smooth curve case, 25-43% for the fragmented cases).
Test
lib/matplotlib/tests/test_fill_between_simplify.py verifies that SVG and PDF output size is smaller at simplify_threshold=1.0 than at 0.0, for both single and multi-region fill_between scenarios.
Tested on 2000-point datasets. Reduction magnitude depends heavily on data density and threshold value - smooth curves (e.g. interpolate_cross) see near-complete reduction at threshold=1.0, while fragmented where masks
see smaller gains. The regression tests verify that reduction occurs in both backends across representative scenarios.
AI Disclosure
AI was used to assist with debugging environment issues, crafting the new test and refining the PR description, all the rest being manually tested.
Further explanation on the comments.