Skip to content

Preserve path breaks during interpolation - #1116

Closed
has2k1 wants to merge 1483 commits into
mainfrom
fix-munch-data-breaks
Closed

Preserve path breaks during interpolation#1116
has2k1 wants to merge 1483 commits into
mainfrom
fix-munch-data-breaks

Conversation

@has2k1

@has2k1 has2k1 commented Sep 3, 2026

Copy link
Copy Markdown
Owner

Under a non-linear coordinate system, a geom_polygon with several groups draws an edge from one polygon to the next, and separate geom_path groups join up when an aesthetic varies along the line.

df = pd.DataFrame({
    "x": [1, 2, 2, 1, 3, 4, 4, 3],
    "y": [1, 1, 2, 2, 3, 3, 4, 4],
    "g": list("aaaabbbb"),
})

(
    ggplot(df, aes("x", "y", fill="g"))
    + geom_polygon(colour="black", size=1)
    + coord_trans(x="log10", y="log10")
)

Without this PR, a diagonal edge runs from the bottom-left corner of the first square to the second square.

iangow and others added 30 commits May 27, 2026 13:49
(cherry picked from commit 7a65e28)
Add a scale_position mixin that validates and carries the axis position
("left"/"right" for y, "top"/"bottom" for x), and a scale_position_view
that propagates the resolved position through coord_cartesian to the coord.
Add per-side axis theme hierarchy (axis_text/ticks/line/title
each gain _x_top/_x_bottom, _y_left/_y_right children under the existing
parent). Each child styles its own side's artist; coord.setup_ax makes
the active side's ticks/spine visible per scale.position. coord_flip
rotates the position with the swap. Delete the dead mpl<3.10 tick-param
shim.
… axes

Side-scoped extent accessors on the layout items (label1/label2,
tick1line/tick2line via side_artists); top_space/right_space gain axis
attrs and the side-spaces read the scoped accessors + side-scoped
margins;
per-side axis-title placement; tick text justified into the band past
the
panel's far edge. Anchor top/right titles at the band's panel edge
(y1/x1,
not y2/x2, which cropped them) and align them across compositions via
axis_title_alignment, matching bottom/left. Default plots are
byte-identical.
The axis text/title gap is the single margin between the text and the
panel. Each side reads the margin edge that actually faces the panel: top
for a bottom axis, bottom for a top axis, right for a left axis, left for a
right axis. The value is read from the side-scoped themeable
(axis_text_x_top, axis_title_y_right, ...), which cascades to the parent by
default, so a user can override one side's margin without touching the
others. The themes (gray, matplotlib, seaborn) set both edges of the axis
text/title margins so every position has a gap. Docstrings updated to match.
Default plots unchanged.
New baselines for the axis-position feature: x_axis_top_continuous,
y_axis_right_continuous, x_axis_top_discrete, coord_flip_x_top,
facet_wrap_y_right. theme_seaborn is refreshed for the per-side axis
text/title margins.
Remove the temporal coupling where Layout.setup pre-assigned
self.facet.plot = plot so axis_positions() could reach
self.plot.scales before facet.setup(plot) ran at draw time.

axis_positions() now takes `scales` explicitly and uses an
explicit `is None` check (replacing getattr) so a present scale
missing `position` becomes a type error rather than a silent
default. compute_layout gains `axis_positions: tuple[str, str]`
in the base class and both subclasses (facet_grid, facet_wrap,
facet_null). Layout.setup resolves the positions from
plot.scales and passes them in, dropping the early assignment.
has2k1 added 27 commits August 14, 2026 21:14
Transform bounds, endpoints, and intercepts with `x` and `y` so radial reversal and `theta="y"` preserve their dimensions. Let geoms select transformed dimensions through the coordinate-system contract.
Measure the largest visible polar tick label in each dimension and add the larger configured label margin or non-blank tick length. Reserve that band on every panel edge because polar axes follow arcs and spokes rather than Cartesian sides.

This slightly shrinks every polar panel, so each baseline image changes.
A polar panel reserved its largest tick label on all four sides. One long
label therefore cost a third of the panel and bought space on three sides
that had nothing long on them. Each side now reserves the labels that
reach past it, from where a label attaches to the panel, its own size, and
the gap that holds it off its boundary.

How much of that reach falls inside the panel depends on the panel the
layout is still solving for, so it is taken from the previous render. A
first render over-reserves by that difference rather than clipping.

Polar baseline images shift again: the panels are larger.
Move label-bound measurement out of the theta tick class and define the full-circle and outward-direction calculations beside it. This gives polar label placement one coordinate convention without changing theta-label behaviour.
Theta labels were anchored from the nearest bounding-box corner. Between the cardinal directions, this moved the label edge away from its tick and made the gap depend on label width. Centre each label on its tick, then offset the point facing the panel by the themed padding.
Matplotlib's discrete alignments made the gap between an r label and its spoke depend on the spoke angle and label dimensions. Centre each label on its spoke, then offset the edge facing the panel by the themed padding. Apply this placement to full circles and both boundaries of an arc.

Recompute the placement during each draw because it depends on rendered label dimensions. Recompute it before the final redraw so r labels retain the measured position above geoms.
Polar layout reserved each label's full width or height for every panel edge. Long labels therefore shrank the panel along edges they did not extend towards. Calculate clearance from each label's anchor and outward direction, and measure only the label pair assigned to each polar boundary.

For full circles, anchor r-label clearance to the configured label spoke instead of the start of the theta range.
Make the polar axes responsible for identifying full-circle panels, locating r-axis spokes, and returning outward directions. Tick placement and layout now obtain this geometry from the panel instead of duplicating it or depending on Matplotlib's `PolarAxes` type.
The 42 images cover the polar suite: arcs and donuts, reversed theta
and r, secondary theta and radial axes, per-side axis theming, and the
panel spacing that long tick labels and large theta text demand.
Setting `axis_text_x_bottom` after blanking `axis_text_x` left the
bottom labels hidden and omitted their layout space. Preserve the blank
state on unset branches while allowing the specific element to render.
`theme_void` blanks both x-axis text sides. Verify that setting the
bottom side draws its labels and reserves their layout space while the
top side remains blank.
Specific theme elements now render and reserve layout space when their
general theme element is blank.
plot_layout now accepts and merges axes and axis_title modes. These settings do not affect rendering yet.
Build regular and design-based grids through one composition method so layout consumers share the same item placement.
Let layers fill a label view without reaching through the complete plot object.
Resolve each title from its scale, mapping, coordinate, and secondary-axis settings while leaving unbuilt plots unchanged.
plot_layout(axes=...) hides inner axes with their ticks and labels. Matching titles follow axis_title, but the surviving title remains aligned to its own plot.
Use the combined panel bounds of plots that share a collected title. Keep plot-local alignment for uncollected titles.
Add visual cases for rows, columns, design gaps, nested compositions, facets, secondary axes, and independent title collection. Document the new layout settings.
A plot that retained a shared title also retained references to the other
labelled plots, so copying it copied its neighbours as well.

Store item indexes on the composition instead. During layout, resolve the
title span from the plots' current panel bounds.
Saving `(p1 | p2).save("composition.png", dpi=200)` used the default
resolution. The `dpi` value was applied to an inner plot, but the
composition owns the figure and determines its resolution. Matplotlib's
save resolution could also override the requested value.
Non-linear coordinates interpolated each marked break as if it were the
longest segment in the panel. The added points connected separate polygons
and path groups with a visible edge.

Marked breaks now keep only their starting point. Polygons and paths whose
aesthetics vary along a line therefore stop at each group boundary.
@codecov

codecov Bot commented Sep 3, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.76440% with 120 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.00%. Comparing base (0795dd7) to head (a0602c4).
⚠️ Report is 250 commits behind head on main.

Files with missing lines Patch % Lines
plotnine/themes/themeable.py 89.04% 41 Missing and 15 partials ⚠️
plotnine/_mpl/_radial_axes.py 91.97% 6 Missing and 9 partials ⚠️
plotnine/scales/sec_axis.py 80.32% 6 Missing and 6 partials ⚠️
plotnine/coords/coord_radial.py 95.77% 5 Missing and 4 partials ⚠️
plotnine/_mpl/layout_manager/_plot_side_space.py 97.38% 3 Missing and 2 partials ⚠️
plotnine/_mpl/_radial_axis.py 96.87% 3 Missing and 1 partial ⚠️
plotnine/_mpl/layout_manager/_grid.py 86.66% 2 Missing and 2 partials ⚠️
plotnine/_mpl/axes.py 95.31% 0 Missing and 3 partials ⚠️
plotnine/_mpl/text.py 81.81% 2 Missing ⚠️
plotnine/composition/_axis_collection.py 97.67% 0 Missing and 2 partials ⚠️
... and 4 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1116      +/-   ##
==========================================
+ Coverage   87.10%   88.00%   +0.89%     
==========================================
  Files         208      215       +7     
  Lines       14373    16004    +1631     
  Branches     1794     2051     +257     
==========================================
+ Hits        12520    14084    +1564     
- Misses       1286     1317      +31     
- Partials      567      603      +36     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@has2k1 has2k1 closed this Sep 3, 2026
@has2k1
has2k1 deleted the fix-munch-data-breaks branch September 3, 2026 12:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants