Skip to content

Commit 3030aba

Browse files
committed
Pad stacked colorbars past the previous one's measured extent (#687)
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.
1 parent 62b2140 commit 3030aba

2 files changed

Lines changed: 32 additions & 13 deletions

File tree

src/spatialdata_plot/pl/basic.py

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from dask.dataframe import DataFrame as DaskDataFrame
2020
from geopandas import GeoDataFrame
2121
from matplotlib.axes import Axes
22+
from matplotlib.backend_bases import RendererBase
2223
from matplotlib.colors import Colormap, LogNorm, Normalize
2324
from matplotlib.figure import Figure
2425
from mpl_toolkits.axes_grid1 import make_axes_locatable
@@ -44,7 +45,7 @@
4445
CBAR_DEFAULT_FRACTION,
4546
CBAR_DEFAULT_LOCATION,
4647
CBAR_DEFAULT_PAD,
47-
CBAR_STACK_PAD_INCHES,
48+
CBAR_STACK_GAP_INCHES,
4849
ChannelLegendEntry,
4950
CmapParams,
5051
ColorbarSpec,
@@ -1532,6 +1533,9 @@ def _draw_colorbar(
15321533
side_counts: dict[str, int],
15331534
clearance: dict[str, float],
15341535
axes_size_in: tuple[float, float],
1536+
prev_outer: dict[str, float],
1537+
renderer: RendererBase,
1538+
dpi: float,
15351539
) -> None:
15361540
norm = spec.mappable.norm
15371541
if isinstance(norm, LogNorm):
@@ -1566,19 +1570,18 @@ def _draw_colorbar(
15661570

15671571
# Append the colorbar axes through the panel's shared divider. This steals space from the
15681572
# panel (so the colorbar matches the equal-aspect plot's extent) and keeps it inside the
1569-
# panel's grid cell, never overflowing into a neighbouring panel. The first colorbar on a
1570-
# side is padded past the panel's own decorations (ticks/labels/title) plus the requested
1571-
# pad; each subsequent one uses an absolute pad wide enough to clear the previous
1572-
# colorbar's tick labels instead of overlapping them.
1573-
# Sides with no decorations to clear (typically "right") keep the relative pad, which
1574-
# matches the historical placement; sides whose ticks/labels/title must be cleared
1575-
# (left/top/bottom), and stacked colorbars, use an absolute pad in inches.
1573+
# panel's grid cell, never overflowing into a neighbouring panel.
1574+
# Pad of the first colorbar on a side clears the panel's own decorations (ticks/labels/
1575+
# title); a side with negligible clearance (typically "right") keeps the relative pad to
1576+
# match the historical placement. Each subsequent (stacked) colorbar is padded past the
1577+
# *measured* outer extent of the previous one (its tick labels AND axis label) so it never
1578+
# overlaps it.
15761579
n_on_side = side_counts.get(location, 0)
15771580
ref_in = axes_size_in[0] if location in {"left", "right"} else axes_size_in[1]
15781581
side_clearance = clearance.get(location, 0.0)
15791582
pad_spec: str | Fixed
15801583
if n_on_side:
1581-
pad_spec = Fixed(CBAR_STACK_PAD_INCHES)
1584+
pad_spec = Fixed(prev_outer.get(location, 0.0) + CBAR_STACK_GAP_INCHES)
15821585
elif side_clearance > _CBAR_DECORATION_EPS_INCHES:
15831586
pad_spec = Fixed(side_clearance + max(pad, 0.0) * ref_in)
15841587
else:
@@ -1615,6 +1618,21 @@ def _draw_colorbar(
16151618
with contextlib.suppress(Exception):
16161619
cb.solids.set_alpha(spec.alpha)
16171620

1621+
# Measure how far this colorbar's own ticks/labels/axis-label extend beyond its box on
1622+
# the outer side, so the next stacked colorbar on this side can be padded clear of them.
1623+
fig.canvas.draw()
1624+
cb_box = cax.get_window_extent(renderer)
1625+
cb_tight = cax.get_tightbbox(renderer)
1626+
if location == "right":
1627+
outer = (cb_tight.x1 - cb_box.x1) / dpi
1628+
elif location == "left":
1629+
outer = (cb_box.x0 - cb_tight.x0) / dpi
1630+
elif location == "top":
1631+
outer = (cb_tight.y1 - cb_box.y1) / dpi
1632+
else: # bottom
1633+
outer = (cb_box.y0 - cb_tight.y0) / dpi
1634+
prev_outer[location] = max(0.0, outer)
1635+
16181636
# go through tree
16191637

16201638
for i, cs in enumerate(coordinate_systems):
@@ -1829,8 +1847,9 @@ def _draw_colorbar(
18291847
# One divider per panel so multiple colorbars on the same panel stack via the divider.
18301848
divider = make_axes_locatable(axis)
18311849
side_counts: dict[str, int] = {}
1850+
prev_outer: dict[str, float] = {}
18321851
for spec in unique_specs:
1833-
_draw_colorbar(spec, fig, divider, side_counts, clearance, axes_size_in)
1852+
_draw_colorbar(spec, fig, divider, side_counts, clearance, axes_size_in, prev_outer, renderer, dpi)
18341853

18351854
if fig_params.fig is not None and save is not None:
18361855
save_fig(fig_params.fig, path=save)

src/spatialdata_plot/pl/render_params.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -219,9 +219,9 @@ class ChannelLegendEntry:
219219
CBAR_DEFAULT_LOCATION = "right"
220220
CBAR_DEFAULT_FRACTION = 0.075
221221
CBAR_DEFAULT_PAD = 0.015
222-
# Absolute pad (inches) between stacked colorbars on the same side, wide enough to clear the
223-
# inner colorbar's tick labels so they don't overlap the next colorbar.
224-
CBAR_STACK_PAD_INCHES = 0.5
222+
# Small gap (inches) added beyond a stacked colorbar's measured outer extent (its tick labels and
223+
# axis label) before placing the next colorbar on the same side, so they never overlap.
224+
CBAR_STACK_GAP_INCHES = 0.08
225225

226226

227227
@dataclass

0 commit comments

Comments
 (0)