Add axis sharing to tiled figures - #593
Draft
SimonHeybrock wants to merge 2 commits into
Draft
Conversation
Tiles showing the same dimension repeat their axis labels and ticks on every panel, and each panel autoscales independently, which makes grids hard to compare and wastes space. Add `sharex` and `sharey` to `Tiled`, taking Matplotlib's `subplots` vocabulary (`'all'`/`'row'`/`'col'`/`'none'`, or a bool). Matplotlib's `Axes.sharex` validates nothing and makes the joined axes adopt the reference's range and scale, so the dimension, unit and scale are checked here and the ranges are unioned. Tick labels are dropped only where they would be repeated: down a column for x, across a row for y. Those tiles are then placed flush, with ticks pointing inwards. Constrained layout pads every tile, which keeps tiles apart even at zero grid spacing, so the padding is dropped in the flush direction. A title sits between two rows and a colorbar between two columns; a tile carrying one takes the padding back, so that the decoration does not touch the frame of its neighbour. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Replacing a tile of a figure with shared axes cannot be done correctly: Matplotlib provides no way to un-share axes, so the replaced tile stays joined to its neighbours and remains the reference of its share group. Raise instead of silently building a grid that refers to a discarded tile. The `+` and `/` operators deliberately do not propagate sharing, as the sharing modes of the operands may disagree and their axes need not be compatible. Pin that with tests, along with tiles spanning several cells, which take the group of the first row/column they span. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Member
Author
|
Opened #594 for the pre-existing tile-replacement bug described under Composition, so it can be tracked independently of this branch. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Draft — the layout work is complete and tested for static figures, but two architectural questions below need a decision before this is ready. Opening it now so @nvaytet can weigh in.
What
Adds
sharexandshareytopp.tiled, using Matplotlib'ssubplotsvocabulary:'all'(orTrue),'row','col','none'(orFalse).Why
Tiles showing the same dimension repeat their axis labels and ticks on every panel, and each panel autoscales independently. On anything larger than a 2x2 that is mostly wasted space, and independent ranges make the panels impossible to compare by eye. A 6x4 grid, before and after:
Note the per-tile plot area grows despite the identical figure size. Same effect at 3x3:
What sharing means here
Axes.sharexvalidates nothing, and it makes the joined axes adopt the reference's range, scale and tick locators. Sharing a tile spanning 100-120 m with one spanning 0-20 m silently leaves the second showing(-1, 21), with its data entirely off-screen. SoTileddoes three things Matplotlib does not:shareymismatch is an error rather than a silent mess.'all'/'col'; y labels repeat across a row, so'all'/'row'. Withsharey='col'every column keeps its labels, since each column is an independent axis:Where labels are dropped, the tiles are placed flush and the ticks point inwards. Getting to a genuinely zero gap needed one more thing: constrained layout pads every tile by
w_pad/h_padinches, which surviveswspace=hspace=0, so that padding is dropped in the flush direction.Titles and colorbars
A title sits between two rows, and a colorbar between two columns, so both are starved by the padding removal above. A tile carrying one takes the padding back; tiles with neither stay flush.
Titles necessarily reopen the vertical gap — a title needs the space. Columns stay flush. The same for 2-D tiles, where the colorbar keeps its distance from the next column's frame:
Two architectural questions
Neither is caused by this branch; both are reachable from it and neither felt like my call to make unilaterally.
1. Autoscale ignores the share group.
GraphicalView.autoscalecomputes a bbox from its own artists and assignscanvas.xrange. On shared axes that assignment propagates to the whole group, so any later autoscale collapses the common range to one tile's data — measured(-2.19, 2.20)becoming(-1.10, 1.10)for both tiles. Static figures are unaffected, because sharing is established after construction and nothing autoscales afterwards. Interactive figures driven by widgets are. A correct fix means autoscale has to union across the share group, which is a change to the view/canvas layer, not toTiled.2. A tile's canvas runs
tight_layouton the parent figure.Canvas.to_widgetcallsself.fig.tight_layout(). For a tile,self.figis the shared tiled figure, so adding one tile re-lays-out the whole grid and replaces the constrained layout engine with aPlaceHolderLayoutEngine. Consequences on the widget backend: flush tiles still work (grid spacing survives; measured 0.0 row and column gaps), but the title/colorbar padding does not — a title overlaps the axes above by ~22 pt, worse than the static case this PR fixes.Tiled._set_padscurrently just skips when the engine is not configurable, which is why the interactive tests do not crash.The obvious fix is for a tile's canvas not to
tight_layoutits parent figure, but that is shared code touching every widget figure, and a naive "more than one axes" guard would also catch 2-D figures with colorbars. Worth discussing what the right condition is.Composition
Both ways of building a grid are now covered by tests.
Operators (
+,/,hstack,vstack) do not propagate sharing: the combined figure is unshared, labels and ticks come back, and only the ranges carry over viacopy. That is deliberate rather than incidental — the operands' sharing modes may disagree ('col'combined with'row'has no sensible answer), and propagating would make combining grids with incompatible units raise, which would be a regression. Now pinned by tests.Cell-setters handle tiles spanning several cells; a spanning tile takes the group of the first row/column it spans, and edge detection uses the span, so a tile spanning a whole row keeps its labels only if that row is the last.
Replacing an occupied tile now raises when sharing is on. It cannot be done correctly: Matplotlib provides no way to un-share axes, so the replaced tile stays joined to its neighbours and remains its share group's reference. Before this it silently produced a grid whose share group referred to a discarded tile.
That last point brushes against a pre-existing bug worth a separate issue:
__setitem__always callsadd_subplot, so replacing a tile leaves the old axes on the figure, visible and drawn underneath the new one. Reproducible onmainwith no sharing involved:_historyalso keeps the superseded entry, so+and/replay both. I have left that alone — it is independent of this branch and fixing it properly means deciding what replacement should mean.Notes
figsizeis untouched. It is calibrated for tiles carrying full decorations and shared tiles need less, but shrinking it would be a visible change for anyone adopting the flag.Test plan
sharex/shareyaccept bool and all four mode strings; invalid values raise.'all'/'col'(x) and'all'/'row'(y), and kept otherwise.%matplotlib widget— see question 2 above; expected to show the title overlap.🤖 Generated with Claude Code