Fix per-panel colorbars overflowing into neighbouring panels#688
Closed
timtreis wants to merge 12 commits into
Closed
Fix per-panel colorbars overflowing into neighbouring panels#688timtreis wants to merge 12 commits into
timtreis wants to merge 12 commits into
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #688 +/- ##
==========================================
- Coverage 75.98% 75.98% -0.01%
==========================================
Files 14 14
Lines 4156 4164 +8
Branches 964 962 -2
==========================================
+ Hits 3158 3164 +6
- Misses 647 649 +2
Partials 351 351
🚀 New features to boost your workflow:
|
Place each colorbar through a per-panel axes divider (make_axes_locatable(ax).append_axes(location, size, pad)) instead of anchoring an inset axes outside the panel. The divider steals space from the panel, so the colorbar matches the (equal-aspect) plot's drawn extent and stays inside the panel's grid cell — it can no longer overflow into a neighbouring panel. - Colorbar height/width now matches the plot (fixes the equal-aspect mismatch that a plain fig.colorbar(ax=ax) would introduce). - Stacked same-side colorbars use an absolute pad (CBAR_STACK_PAD_INCHES) wide enough to clear the inner colorbar's tick labels, preserving order and keeping both readable. - location handles tick/label side; colorbar_params (location/width/pad/label/ custom kwargs) and per-layer alpha are preserved. Verified: single colorbar, adjust_pad/width, all four locations, two-on-same-side, all-sides, and multiple-images-in-one-cs all render correctly at native size and under the test thumbnail. Categorical legends already use scanpy's shrink-then-place.
0622a78 to
b5710c5
Compare
15 colorbar-bearing baselines updated to the new in-panel colorbar layout (matches the plot extent, stacks with readable labels, no overflow). Verified the control cases render correctly: adjust_pad/width, all/different sides, two-on-same-side, per-side locations, and multiple-images-in-one-cs. Generated from the py3.11-stable CI artifact.
The divider only knows about the axes box, so left/top/bottom colorbars landed on the panel's own y-ticks, title, and x-ticks. Measure each side's decoration clearance (tight bbox vs axes box, in inches) and pad the first colorbar on that side past it, plus the requested pad. Right-side colorbars are unaffected. Colorbar size is now absolute (fraction x axis extent) for consistent stacking.
Keep the colorbar width relative to the panel (size as a percentage) rather than absolute inches; only the pad is absolute (decoration clearance + stacking gap). This matches the previous inset width closely, so single right-side colorbars are visually unchanged and only the genuinely-affected colorbars (left/top/bottom, stacked, all-sides) get new baselines.
Use the absolute (clearance-aware) pad only where it's needed — sides with ticks/labels/title to clear (left/top/bottom) and stacked colorbars. Sides with negligible clearance (typically the default right) keep the relative pad, which matches the historical placement, so single right-side colorbars stay visually unchanged and baseline churn is limited to the genuinely-affected colorbars.
The decoration-clearance fix updates only the genuinely-affected colorbars: the per-side locations (img top/left/bottom, all-sides, different-sides), stacked multiple-images-in-one-cs, and a few colorbar-bearing image/label baselines. Right-side single colorbars are unchanged. Generated from the py3.11-stable CI artifact.
test_plot_can_render_multipolygons colors by a continuous value (right-side colorbar) on a wide, short plot. For non-square aspects the divider's space-stealing diverges slightly more from the old inset placement, so its baseline needs updating too. Verified the colorbar still matches the plot height with no overflow.
The fixed stacking pad cleared an inner colorbar's tick labels but not its axis label (e.g. a rotated "instance_id"), so a longer label still overlapped the next colorbar — worst for vertical (left/right) colorbars. Instead, draw each colorbar and measure how far its ticks/labels/axis-label extend beyond its box on the outer side, then pad the next stacked colorbar past that (plus a small gap). Replaces the fixed CBAR_STACK_PAD_INCHES with the measured extent + CBAR_STACK_GAP_INCHES.
Stacked same-side colorbars (all-sides, two-on-same-side, multiple-images-in-one-cs, stacked render_images, two-call labels) now sit clear of the previous colorbar's tick labels and axis label. Generated from the py3.11-stable CI artifact.
…ct, orientation warning (#688) - Extract `_extent_beyond_box_inches()` used by both the panel-decoration clearance and the stacked-colorbar measurement, with a guard for `get_tightbbox() is None` (invisible/empty axes) and non-positive dpi (returns 0 instead of crashing). - Collapse the four per-location tick/label-position branches into a `_CBAR_TICK_SIDE` lookup. - Warn instead of silently dropping a user-supplied `orientation` that conflicts with the one implied by the colorbar `location`. Behavior-preserving: rendered colorbars are byte-identical across single, stacked, all-sides, and per-location cases (verified locally), so no baselines change. The per-colorbar `fig.canvas.draw()` is kept intentionally — it is load-bearing for the layout engine (skipping it shifts wide colorbars), so it cannot be safely elided.
`fig.colorbar(mappable)` already bakes the mappable's alpha into the colorbar's QuadMesh facecolors (verified: alpha column = mappable.alpha straight after construction). Our subsequent `cb.solids.set_alpha(spec.alpha)` then multiplied on top, so the colorbar rendered at alpha squared and looked much paler than the layer it represents. Only apply `spec.alpha` when the mappable does not carry alpha of its own; if it does, trust the inherited value. Visible improvement on every continuous-color colorbar (especially labels at low fill_alpha, e.g. `Labels_can_handle_dropping_small_labels_after_rasterize_continuous` and `Labels_two_calls_with_coloring_result_in_two_colorbars`).
Member
Author
|
Converged to an equal implementation without benefits |
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.
Closes #687.
Problem
In multi-panel figures, each panel's colorbar was drawn with
inset_axesanchored outside the panel (bbox_to_anchor=(1+pad, ...)onax.transAxes). No layout engine reserves that space, so when panels get small the colorbar and its tick labels spill into the neighbouring panel.Fix
Place the colorbar by stealing space from its own panel —
fig.colorbar(mappable, ax=ax, location=, fraction=, pad=)— exactly as scanpy does (plt.colorbar(cax, ax=ax, ...)). The colorbar now lives inside the panel's grid cell and is accounted for by the layout engine, so it can never overflow into an adjacent panel, at any figure size.location(left/right/top/bottom) sets tick/label side and orientation automatically (removed the manualset_ticks_position/tick_params).colorbar_params(location/fraction/pad/label/custom kwargs) and per-layer alpha are preserved.inset_axes/RendererBaseimports and the offset/tracker bookkeeping (net −51 lines).Categorical legends already use scanpy's shrink-then-place for multi-panel (
multi_panel=True), so they are unchanged.Baselines
Colorbar-bearing visual baselines are regenerated from the CI artifact (placement changed). Legend-only and non-colorbar baselines are untouched.