Skip to content

Commit dcff41f

Browse files
Fix colorbar alignment with suptitle in compressed layout mode (matplotlib#30766)
* matplotlib#30472 fix issue with colorbar+suptext in compressed mode * fix horizontal colorbar --------- Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com>
1 parent 7a7a388 commit dcff41f

File tree

4 files changed

+104
-5
lines changed

4 files changed

+104
-5
lines changed

lib/matplotlib/_constrained_layout.py

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,8 @@ def do_constrained_layout(fig, h_pad, w_pad,
137137
layoutgrids[fig].update_variables()
138138
if check_no_collapsed_axes(layoutgrids, fig):
139139
reposition_axes(layoutgrids, fig, renderer, h_pad=h_pad,
140-
w_pad=w_pad, hspace=hspace, wspace=wspace)
140+
w_pad=w_pad, hspace=hspace, wspace=wspace,
141+
compress=True)
141142
else:
142143
_api.warn_external(warn_collapsed)
143144

@@ -651,7 +652,7 @@ def get_pos_and_bbox(ax, renderer):
651652

652653

653654
def reposition_axes(layoutgrids, fig, renderer, *,
654-
w_pad=0, h_pad=0, hspace=0, wspace=0):
655+
w_pad=0, h_pad=0, hspace=0, wspace=0, compress=False):
655656
"""
656657
Reposition all the Axes based on the new inner bounding box.
657658
"""
@@ -662,7 +663,7 @@ def reposition_axes(layoutgrids, fig, renderer, *,
662663
bbox=bbox.transformed(trans_fig_to_subfig))
663664
reposition_axes(layoutgrids, sfig, renderer,
664665
w_pad=w_pad, h_pad=h_pad,
665-
wspace=wspace, hspace=hspace)
666+
wspace=wspace, hspace=hspace, compress=compress)
666667

667668
for ax in fig._localaxes:
668669
if ax.get_subplotspec() is None or not ax.get_in_layout():
@@ -689,10 +690,10 @@ def reposition_axes(layoutgrids, fig, renderer, *,
689690
for nn, cbax in enumerate(ax._colorbars[::-1]):
690691
if ax == cbax._colorbar_info['parents'][0]:
691692
reposition_colorbar(layoutgrids, cbax, renderer,
692-
offset=offset)
693+
offset=offset, compress=compress)
693694

694695

695-
def reposition_colorbar(layoutgrids, cbax, renderer, *, offset=None):
696+
def reposition_colorbar(layoutgrids, cbax, renderer, *, offset=None, compress=False):
696697
"""
697698
Place the colorbar in its new place.
698699
@@ -706,6 +707,8 @@ def reposition_colorbar(layoutgrids, cbax, renderer, *, offset=None):
706707
offset : array-like
707708
Offset the colorbar needs to be pushed to in order to
708709
account for multiple colorbars.
710+
compress : bool
711+
Whether we're in compressed layout mode.
709712
"""
710713

711714
parents = cbax._colorbar_info['parents']
@@ -724,6 +727,31 @@ def reposition_colorbar(layoutgrids, cbax, renderer, *, offset=None):
724727
aspect = cbax._colorbar_info['aspect']
725728
shrink = cbax._colorbar_info['shrink']
726729

730+
# For colorbars with a single parent in compressed layout,
731+
# use the actual visual size of the parent axis after apply_aspect()
732+
# has been called. This ensures colorbars align with their parent axes.
733+
# This fix is specific to single-parent colorbars where alignment is critical.
734+
if compress and len(parents) == 1:
735+
from matplotlib.transforms import Bbox
736+
# Get the actual parent position after apply_aspect()
737+
parent_ax = parents[0]
738+
actual_pos = parent_ax.get_position(original=False)
739+
# Transform to figure coordinates
740+
actual_pos_fig = actual_pos.transformed(fig.transSubfigure - fig.transFigure)
741+
742+
if location in ('left', 'right'):
743+
# For vertical colorbars, use the actual parent bbox height
744+
# for colorbar sizing
745+
# Keep the pb x-coordinates but use actual y-coordinates
746+
pb = Bbox.from_extents(pb.x0, actual_pos_fig.y0,
747+
pb.x1, actual_pos_fig.y1)
748+
elif location in ('top', 'bottom'):
749+
# For horizontal colorbars, use the actual parent bbox width
750+
# for colorbar sizing
751+
# Keep the pb y-coordinates but use actual x-coordinates
752+
pb = Bbox.from_extents(actual_pos_fig.x0, pb.y0,
753+
actual_pos_fig.x1, pb.y1)
754+
727755
cbpos, cbbbox = get_pos_and_bbox(cbax, renderer)
728756

729757
# Colorbar gets put at extreme edge of outer bbox of the subplotspec
3.45 KB
Loading
4.9 KB
Loading

lib/matplotlib/tests/test_constrainedlayout.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -688,6 +688,77 @@ def test_compressed_suptitle():
688688
assert title.get_position()[1] == 0.98
689689

690690

691+
@image_comparison(['test_compressed_suptitle_colorbar.png'], style='mpl20')
692+
def test_compressed_suptitle_colorbar():
693+
"""Test that colorbars align with axes in compressed layout with suptitle."""
694+
arr = np.arange(100).reshape((10, 10))
695+
fig, axs = plt.subplots(ncols=2, figsize=(4, 2), layout='compressed')
696+
697+
im0 = axs[0].imshow(arr)
698+
im1 = axs[1].imshow(arr)
699+
700+
cb0 = plt.colorbar(im0, ax=axs[0])
701+
cb1 = plt.colorbar(im1, ax=axs[1])
702+
703+
fig.suptitle('Title')
704+
705+
# Verify colorbar heights match axes heights
706+
# After layout, colorbar should have same height as parent axes
707+
fig.canvas.draw()
708+
709+
for ax, cb in zip(axs, [cb0, cb1]):
710+
ax_pos = ax.get_position()
711+
cb_pos = cb.ax.get_position()
712+
713+
# Check that colorbar height matches axes height (within tolerance)
714+
# Note: We check the actual rendered positions, not the bbox
715+
assert abs(cb_pos.height - ax_pos.height) < 0.01, \
716+
f"Colorbar height {cb_pos.height} doesn't match axes height {ax_pos.height}"
717+
718+
# Also verify vertical alignment (y0 and y1 should match)
719+
assert abs(cb_pos.y0 - ax_pos.y0) < 0.01, \
720+
f"Colorbar y0 {cb_pos.y0} doesn't match axes y0 {ax_pos.y0}"
721+
assert abs(cb_pos.y1 - ax_pos.y1) < 0.01, \
722+
f"Colorbar y1 {cb_pos.y1} doesn't match axes y1 {ax_pos.y1}"
723+
724+
725+
@image_comparison(['test_compressed_supylabel_colorbar.png'], style='mpl20')
726+
def test_compressed_supylabel_colorbar():
727+
"""
728+
Test that horizontal colorbars align with axes
729+
in compressed layout with supylabel.
730+
"""
731+
arr = np.arange(100).reshape((10, 10))
732+
fig, axs = plt.subplots(nrows=2, figsize=(3, 4), layout='compressed')
733+
734+
im0 = axs[0].imshow(arr)
735+
im1 = axs[1].imshow(arr)
736+
737+
cb0 = plt.colorbar(im0, ax=axs[0], orientation='horizontal')
738+
cb1 = plt.colorbar(im1, ax=axs[1], orientation='horizontal')
739+
740+
fig.supylabel('Title')
741+
742+
# Verify colorbar widths match axes widths
743+
# After layout, colorbar should have same width as parent axes
744+
fig.canvas.draw()
745+
746+
for ax, cb in zip(axs, [cb0, cb1]):
747+
ax_pos = ax.get_position()
748+
cb_pos = cb.ax.get_position()
749+
750+
# Check that colorbar width matches axes width (within tolerance)
751+
# Note: We check the actual rendered positions, not the bbox
752+
assert abs(cb_pos.width - ax_pos.width) < 0.01, \
753+
f"Colorbar width {cb_pos.width} doesn't match axes width {ax_pos.width}"
754+
755+
# Also verify horizontal alignment (x0 and x1 should match)
756+
assert abs(cb_pos.x0 - ax_pos.x0) < 0.01, \
757+
f"Colorbar x0 {cb_pos.x0} doesn't match axes x0 {ax_pos.x0}"
758+
assert abs(cb_pos.x1 - ax_pos.x1) < 0.01, \
759+
f"Colorbar x1 {cb_pos.x1} doesn't match axes x1 {ax_pos.x1}"
760+
761+
691762
@pytest.mark.parametrize('arg, state', [
692763
(True, True),
693764
(False, False),

0 commit comments

Comments
 (0)