Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/release-notes/4137.fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Anchor the {func}`scanpy.pl.scatter` colorbar to a caller-supplied `ax` instead of placing it in figure coordinates computed from scanpy's internal panel layout, which previously dropped the colorbar over a sibling axes.
39 changes: 28 additions & 11 deletions src/scanpy/plotting/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -790,6 +790,10 @@ def scatter_base( # noqa: PLR0912, PLR0913, PLR0915
sizes = [sizes[0] for _ in range(len(colors))]
if len(markers) != len(colors) and len(markers) == 1:
markers = [markers[0] for _ in range(len(colors))]
# When the caller provides an ax, panel_pos is computed for scanpy's own
# layout and does not reflect the user's figure, so the rectangle-based
# colorbar placement below would fall outside the user's ax (see #3963).
ax_was_user_supplied = ax is not None
axs, panel_pos, draw_region_width, _figure_width = setup_axes(
ax,
panels=colors,
Expand Down Expand Up @@ -830,17 +834,30 @@ def scatter_base( # noqa: PLR0912, PLR0913, PLR0915
rasterized=settings._vector_friendly,
)
if colorbars[icolor]:
width = 0.006 * draw_region_width / len(colors)
left = (
panel_pos[2][2 * icolor + 1]
+ (1.2 if projection == "3d" else 0.2) * width
)
rectangle = [left, bottom, width, height]
fig = plt.gcf()
ax_cb = fig.add_axes(rectangle)
_ = plt.colorbar(
sct, format=ticker.FuncFormatter(ticks_formatter), cax=ax_cb
)
if ax_was_user_supplied:
# Attach the colorbar to the caller's ax instead of using the
# internal panel_pos rectangle (which is in figure coords for
# scanpy's own layout). Mirrors sc.pl.embedding. See #3963.
_ = plt.colorbar(
sct,
ax=ax,
format=ticker.FuncFormatter(ticks_formatter),
pad=0.01,
fraction=0.08,
aspect=30,
)
else:
width = 0.006 * draw_region_width / len(colors)
left = (
panel_pos[2][2 * icolor + 1]
+ (1.2 if projection == "3d" else 0.2) * width
)
rectangle = [left, bottom, width, height]
fig = plt.gcf()
ax_cb = fig.add_axes(rectangle)
_ = plt.colorbar(
sct, format=ticker.FuncFormatter(ticks_formatter), cax=ax_cb
)
# set the title
if title is not None:
ax.set_title(title[icolor])
Expand Down
35 changes: 35 additions & 0 deletions tests/test_plotting.py
Original file line number Diff line number Diff line change
Expand Up @@ -1419,6 +1419,41 @@ def test_dpt_plots(
save_and_compare_images(func.__name__)


def test_scatter_colorbar_uses_user_ax():
"""Regression test for #3963.

When ``sc.pl.scatter`` receives a user-supplied ``ax``, the colorbar must
be attached to that ax. Previously the colorbar was placed via
``fig.add_axes(rectangle)`` with rectangle in figure coords for scanpy's
own panel layout, landing the colorbar over an unrelated user axes.
"""
rng = np.random.default_rng(0)
adata = AnnData(
rng.random((50, 3), dtype=np.float32),
obs=dict(score=rng.random(50, dtype=np.float32)),
)
adata.obsm["X_umap"] = rng.random((50, 2), dtype=np.float32)

fig, axs = plt.subplots(1, 2, figsize=(10, 4))
sc.pl.scatter(adata, color="score", basis="umap", ax=axs[0], show=False)

extra = [a for a in fig.axes if a not in axs]
assert extra, "sc.pl.scatter should add a colorbar for a continuous color"
cb = extra[0]
user_ax_pos = axs[0].get_position()
cb_pos = cb.get_position()
# The colorbar should sit just to the right of the user's ax, not float
# away from it across the figure.
assert cb_pos.x0 >= user_ax_pos.x0, (
f"Colorbar x0={cb_pos.x0:.3f} is left of user ax x0={user_ax_pos.x0:.3f}"
)
assert cb_pos.x0 < user_ax_pos.x1 + 0.05, (
f"Colorbar x0={cb_pos.x0:.3f} is far past user ax x1={user_ax_pos.x1:.3f}; "
"this would overlap a sibling axes."
)
plt.close(fig)


def test_scatter_raw(tmp_path):
pbmc = pbmc68k_reduced()[:100].copy()
raw_pth = tmp_path / "raw.png"
Expand Down
Loading