Fix per-panel colorbars overflowing into neighbouring panels - #688

Closed
timtreis wants to merge 12 commits into
mainfrom
fix/issue-687-colorbar-legend-overflow
Closed

Fix per-panel colorbars overflowing into neighbouring panels#688
timtreis wants to merge 12 commits into
mainfrom
fix/issue-687-colorbar-legend-overflow

Conversation

@timtreis

@timtreistimtreis commented May 28, 2026

Copy link
Copy Markdown
Member

Closes#687.

Problem

In multi-panel figures, each panel's colorbar was drawn with inset_axes anchored outside the panel (bbox_to_anchor=(1+pad, ...) on ax.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 panelfig.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 manual set_ticks_position/tick_params).
  • Multiple colorbars on one panel stack via repeated calls (matplotlib-native), replacing the manual offset-tracking.
  • colorbar_params (location/fraction/pad/label/custom kwargs) and per-layer alpha are preserved.
  • Removed the now-unused inset_axes / RendererBase imports 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.

@codecov-commenter

codecov-commenter commented May 28, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.00000% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 75.98%. Comparing base (c499e8c) to head (d6becd6).

Files with missing linesPatch %Lines
src/spatialdata_plot/pl/basic.py91.83%2 Missing and 2 partials ⚠️
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 
Files with missing linesCoverage Δ
src/spatialdata_plot/pl/render_params.py88.75% <100.00%> (+0.04%)⬆️
src/spatialdata_plot/pl/basic.py78.90% <91.83%> (-0.14%)⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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.
@timtreis
timtreisforce-pushed the fix/issue-687-colorbar-legend-overflow branch from 0622a78 to b5710c5CompareMay 28, 2026 22:19
timtreis added 11 commits May 29, 2026 00:32
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.
)
The labels two_calls baseline was not regenerated when the measured-pad fix for
stacked colorbars went in (the layout there changes substantially with two
stacked colorbars). Update it to the CI-rendered version so py3.11/py3.14 stable
match again.
`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`).
@timtreis

Copy link
Copy Markdown
MemberAuthor

Converged to an equal implementation without benefits

Sign up for freeto 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.

Per-panel colorbars/legends overflow into neighbouring panels in multi-panel plots

2 participants

@timtreis@codecov-commenter
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Fix per-panel colorbars overflowing into neighbouring panels - #688

Closed
timtreis wants to merge 12 commits into
mainfrom
fix/issue-687-colorbar-legend-overflow
Closed

Fix per-panel colorbars overflowing into neighbouring panels#688
timtreis wants to merge 12 commits into
mainfrom
fix/issue-687-colorbar-legend-overflow

Conversation

@timtreis

@timtreistimtreis commented May 28, 2026

Copy link
Copy Markdown
Member

Closes#687.

Problem

In multi-panel figures, each panel's colorbar was drawn with inset_axes anchored outside the panel (bbox_to_anchor=(1+pad, ...) on ax.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 panelfig.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 manual set_ticks_position/tick_params).
  • Multiple colorbars on one panel stack via repeated calls (matplotlib-native), replacing the manual offset-tracking.
  • colorbar_params (location/fraction/pad/label/custom kwargs) and per-layer alpha are preserved.
  • Removed the now-unused inset_axes / RendererBase imports 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.

@codecov-commenter

codecov-commenter commented May 28, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.00000% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 75.98%. Comparing base (c499e8c) to head (d6becd6).

Files with missing linesPatch %Lines
src/spatialdata_plot/pl/basic.py91.83%2 Missing and 2 partials ⚠️
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 
Files with missing linesCoverage Δ
src/spatialdata_plot/pl/render_params.py88.75% <100.00%> (+0.04%)⬆️
src/spatialdata_plot/pl/basic.py78.90% <91.83%> (-0.14%)⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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.
@timtreis
timtreisforce-pushed the fix/issue-687-colorbar-legend-overflow branch from 0622a78 to b5710c5CompareMay 28, 2026 22:19
timtreis added 11 commits May 29, 2026 00:32
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.
)
The labels two_calls baseline was not regenerated when the measured-pad fix for
stacked colorbars went in (the layout there changes substantially with two
stacked colorbars). Update it to the CI-rendered version so py3.11/py3.14 stable
match again.
`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`).
@timtreis

Copy link
Copy Markdown
MemberAuthor

Converged to an equal implementation without benefits

Sign up for freeto 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.

Per-panel colorbars/legends overflow into neighbouring panels in multi-panel plots

2 participants

@timtreis@codecov-commenter
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix per-panel colorbars overflowing into neighbouring panels - #688

Closed
timtreis wants to merge 12 commits into
mainfrom
fix/issue-687-colorbar-legend-overflow
Closed

Fix per-panel colorbars overflowing into neighbouring panels#688
timtreis wants to merge 12 commits into
mainfrom
fix/issue-687-colorbar-legend-overflow

Conversation

@timtreis

@timtreistimtreis commented May 28, 2026

Copy link
Copy Markdown
Member

Closes#687.

Problem

In multi-panel figures, each panel's colorbar was drawn with inset_axes anchored outside the panel (bbox_to_anchor=(1+pad, ...) on ax.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 panelfig.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 manual set_ticks_position/tick_params).
  • Multiple colorbars on one panel stack via repeated calls (matplotlib-native), replacing the manual offset-tracking.
  • colorbar_params (location/fraction/pad/label/custom kwargs) and per-layer alpha are preserved.
  • Removed the now-unused inset_axes / RendererBase imports 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.

@codecov-commenter

codecov-commenter commented May 28, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.00000% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 75.98%. Comparing base (c499e8c) to head (d6becd6).

Files with missing linesPatch %Lines
src/spatialdata_plot/pl/basic.py91.83%2 Missing and 2 partials ⚠️
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 
Files with missing linesCoverage Δ
src/spatialdata_plot/pl/render_params.py88.75% <100.00%> (+0.04%)⬆️
src/spatialdata_plot/pl/basic.py78.90% <91.83%> (-0.14%)⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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.
@timtreis
timtreisforce-pushed the fix/issue-687-colorbar-legend-overflow branch from 0622a78 to b5710c5CompareMay 28, 2026 22:19
timtreis added 11 commits May 29, 2026 00:32
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.
)
The labels two_calls baseline was not regenerated when the measured-pad fix for
stacked colorbars went in (the layout there changes substantially with two
stacked colorbars). Update it to the CI-rendered version so py3.11/py3.14 stable
match again.
`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`).
@timtreis

Copy link
Copy Markdown
MemberAuthor

Converged to an equal implementation without benefits

Sign up for freeto 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.

Per-panel colorbars/legends overflow into neighbouring panels in multi-panel plots

2 participants

@timtreis@codecov-commenter
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix per-panel colorbars overflowing into neighbouring panels - #688

Closed
timtreis wants to merge 12 commits into
mainfrom
fix/issue-687-colorbar-legend-overflow
Closed

Fix per-panel colorbars overflowing into neighbouring panels#688
timtreis wants to merge 12 commits into
mainfrom
fix/issue-687-colorbar-legend-overflow

Conversation

@timtreis

@timtreistimtreis commented May 28, 2026

Copy link
Copy Markdown
Member

Closes#687.

Problem

In multi-panel figures, each panel's colorbar was drawn with inset_axes anchored outside the panel (bbox_to_anchor=(1+pad, ...) on ax.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 panelfig.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 manual set_ticks_position/tick_params).
  • Multiple colorbars on one panel stack via repeated calls (matplotlib-native), replacing the manual offset-tracking.
  • colorbar_params (location/fraction/pad/label/custom kwargs) and per-layer alpha are preserved.
  • Removed the now-unused inset_axes / RendererBase imports 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.

@codecov-commenter

codecov-commenter commented May 28, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.00000% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 75.98%. Comparing base (c499e8c) to head (d6becd6).

Files with missing linesPatch %Lines
src/spatialdata_plot/pl/basic.py91.83%2 Missing and 2 partials ⚠️
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 
Files with missing linesCoverage Δ
src/spatialdata_plot/pl/render_params.py88.75% <100.00%> (+0.04%)⬆️
src/spatialdata_plot/pl/basic.py78.90% <91.83%> (-0.14%)⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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.
@timtreis
timtreisforce-pushed the fix/issue-687-colorbar-legend-overflow branch from 0622a78 to b5710c5CompareMay 28, 2026 22:19
timtreis added 11 commits May 29, 2026 00:32
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.
)
The labels two_calls baseline was not regenerated when the measured-pad fix for
stacked colorbars went in (the layout there changes substantially with two
stacked colorbars). Update it to the CI-rendered version so py3.11/py3.14 stable
match again.
`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`).
@timtreis

Copy link
Copy Markdown
MemberAuthor

Converged to an equal implementation without benefits

Sign up for freeto 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.

Per-panel colorbars/legends overflow into neighbouring panels in multi-panel plots

2 participants

@timtreis@codecov-commenter
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Fix per-panel colorbars overflowing into neighbouring panels - #688

Closed
timtreis wants to merge 12 commits into
mainfrom
fix/issue-687-colorbar-legend-overflow
Closed

Fix per-panel colorbars overflowing into neighbouring panels#688
timtreis wants to merge 12 commits into
mainfrom
fix/issue-687-colorbar-legend-overflow

Conversation

@timtreis

@timtreistimtreis commented May 28, 2026

Copy link
Copy Markdown
Member

Closes#687.

Problem

In multi-panel figures, each panel's colorbar was drawn with inset_axes anchored outside the panel (bbox_to_anchor=(1+pad, ...) on ax.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 panelfig.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 manual set_ticks_position/tick_params).
  • Multiple colorbars on one panel stack via repeated calls (matplotlib-native), replacing the manual offset-tracking.
  • colorbar_params (location/fraction/pad/label/custom kwargs) and per-layer alpha are preserved.
  • Removed the now-unused inset_axes / RendererBase imports 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.

@codecov-commenter

codecov-commenter commented May 28, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.00000% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 75.98%. Comparing base (c499e8c) to head (d6becd6).

Files with missing linesPatch %Lines
src/spatialdata_plot/pl/basic.py91.83%2 Missing and 2 partials ⚠️
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 
Files with missing linesCoverage Δ
src/spatialdata_plot/pl/render_params.py88.75% <100.00%> (+0.04%)⬆️
src/spatialdata_plot/pl/basic.py78.90% <91.83%> (-0.14%)⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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.
@timtreis
timtreisforce-pushed the fix/issue-687-colorbar-legend-overflow branch from 0622a78 to b5710c5CompareMay 28, 2026 22:19
timtreis added 11 commits May 29, 2026 00:32
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.
)
The labels two_calls baseline was not regenerated when the measured-pad fix for
stacked colorbars went in (the layout there changes substantially with two
stacked colorbars). Update it to the CI-rendered version so py3.11/py3.14 stable
match again.
`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`).
@timtreis

Copy link
Copy Markdown
MemberAuthor

Converged to an equal implementation without benefits

Sign up for freeto 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.

Per-panel colorbars/legends overflow into neighbouring panels in multi-panel plots

2 participants

@timtreis@codecov-commenter
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix per-panel colorbars overflowing into neighbouring panels - #688

Closed
timtreis wants to merge 12 commits into
mainfrom
fix/issue-687-colorbar-legend-overflow
Closed

Fix per-panel colorbars overflowing into neighbouring panels#688
timtreis wants to merge 12 commits into
mainfrom
fix/issue-687-colorbar-legend-overflow

Conversation

@timtreis

@timtreistimtreis commented May 28, 2026

Copy link
Copy Markdown
Member

Closes#687.

Problem

In multi-panel figures, each panel's colorbar was drawn with inset_axes anchored outside the panel (bbox_to_anchor=(1+pad, ...) on ax.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 panelfig.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 manual set_ticks_position/tick_params).
  • Multiple colorbars on one panel stack via repeated calls (matplotlib-native), replacing the manual offset-tracking.
  • colorbar_params (location/fraction/pad/label/custom kwargs) and per-layer alpha are preserved.
  • Removed the now-unused inset_axes / RendererBase imports 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.

@codecov-commenter

codecov-commenter commented May 28, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.00000% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 75.98%. Comparing base (c499e8c) to head (d6becd6).

Files with missing linesPatch %Lines
src/spatialdata_plot/pl/basic.py91.83%2 Missing and 2 partials ⚠️
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 
Files with missing linesCoverage Δ
src/spatialdata_plot/pl/render_params.py88.75% <100.00%> (+0.04%)⬆️
src/spatialdata_plot/pl/basic.py78.90% <91.83%> (-0.14%)⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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.
@timtreis
timtreisforce-pushed the fix/issue-687-colorbar-legend-overflow branch from 0622a78 to b5710c5CompareMay 28, 2026 22:19
timtreis added 11 commits May 29, 2026 00:32
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.
)
The labels two_calls baseline was not regenerated when the measured-pad fix for
stacked colorbars went in (the layout there changes substantially with two
stacked colorbars). Update it to the CI-rendered version so py3.11/py3.14 stable
match again.
`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`).
@timtreis

Copy link
Copy Markdown
MemberAuthor

Converged to an equal implementation without benefits

Sign up for freeto 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.

Per-panel colorbars/legends overflow into neighbouring panels in multi-panel plots

2 participants

@timtreis@codecov-commenter
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix per-panel colorbars overflowing into neighbouring panels - #688

Closed
timtreis wants to merge 12 commits into
mainfrom
fix/issue-687-colorbar-legend-overflow
Closed

Fix per-panel colorbars overflowing into neighbouring panels#688
timtreis wants to merge 12 commits into
mainfrom
fix/issue-687-colorbar-legend-overflow

Conversation

@timtreis

@timtreistimtreis commented May 28, 2026

Copy link
Copy Markdown
Member

Closes#687.

Problem

In multi-panel figures, each panel's colorbar was drawn with inset_axes anchored outside the panel (bbox_to_anchor=(1+pad, ...) on ax.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 panelfig.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 manual set_ticks_position/tick_params).
  • Multiple colorbars on one panel stack via repeated calls (matplotlib-native), replacing the manual offset-tracking.
  • colorbar_params (location/fraction/pad/label/custom kwargs) and per-layer alpha are preserved.
  • Removed the now-unused inset_axes / RendererBase imports 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.

@codecov-commenter

codecov-commenter commented May 28, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.00000% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 75.98%. Comparing base (c499e8c) to head (d6becd6).

Files with missing linesPatch %Lines
src/spatialdata_plot/pl/basic.py91.83%2 Missing and 2 partials ⚠️
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 
Files with missing linesCoverage Δ
src/spatialdata_plot/pl/render_params.py88.75% <100.00%> (+0.04%)⬆️
src/spatialdata_plot/pl/basic.py78.90% <91.83%> (-0.14%)⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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.
@timtreis
timtreisforce-pushed the fix/issue-687-colorbar-legend-overflow branch from 0622a78 to b5710c5CompareMay 28, 2026 22:19
timtreis added 11 commits May 29, 2026 00:32
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.
)
The labels two_calls baseline was not regenerated when the measured-pad fix for
stacked colorbars went in (the layout there changes substantially with two
stacked colorbars). Update it to the CI-rendered version so py3.11/py3.14 stable
match again.
`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`).
@timtreis

Copy link
Copy Markdown
MemberAuthor

Converged to an equal implementation without benefits

Sign up for freeto 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.

Per-panel colorbars/legends overflow into neighbouring panels in multi-panel plots

2 participants

@timtreis@codecov-commenter
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Fix per-panel colorbars overflowing into neighbouring panels - #688

Closed
timtreis wants to merge 12 commits into
mainfrom
fix/issue-687-colorbar-legend-overflow
Closed

Fix per-panel colorbars overflowing into neighbouring panels#688
timtreis wants to merge 12 commits into
mainfrom
fix/issue-687-colorbar-legend-overflow

Conversation

@timtreis

@timtreistimtreis commented May 28, 2026

Copy link
Copy Markdown
Member

Closes#687.

Problem

In multi-panel figures, each panel's colorbar was drawn with inset_axes anchored outside the panel (bbox_to_anchor=(1+pad, ...) on ax.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 panelfig.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 manual set_ticks_position/tick_params).
  • Multiple colorbars on one panel stack via repeated calls (matplotlib-native), replacing the manual offset-tracking.
  • colorbar_params (location/fraction/pad/label/custom kwargs) and per-layer alpha are preserved.
  • Removed the now-unused inset_axes / RendererBase imports 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.

@codecov-commenter

codecov-commenter commented May 28, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.00000% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 75.98%. Comparing base (c499e8c) to head (d6becd6).

Files with missing linesPatch %Lines
src/spatialdata_plot/pl/basic.py91.83%2 Missing and 2 partials ⚠️
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 
Files with missing linesCoverage Δ
src/spatialdata_plot/pl/render_params.py88.75% <100.00%> (+0.04%)⬆️
src/spatialdata_plot/pl/basic.py78.90% <91.83%> (-0.14%)⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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.
@timtreis
timtreisforce-pushed the fix/issue-687-colorbar-legend-overflow branch from 0622a78 to b5710c5CompareMay 28, 2026 22:19
timtreis added 11 commits May 29, 2026 00:32
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.
)
The labels two_calls baseline was not regenerated when the measured-pad fix for
stacked colorbars went in (the layout there changes substantially with two
stacked colorbars). Update it to the CI-rendered version so py3.11/py3.14 stable
match again.
`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`).
@timtreis

Copy link
Copy Markdown
MemberAuthor

Converged to an equal implementation without benefits

Sign up for freeto 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.

Per-panel colorbars/legends overflow into neighbouring panels in multi-panel plots

2 participants

@timtreis@codecov-commenter