diff --git a/doc/api/next_api_changes/behavior/24570-EG.rst b/doc/api/next_api_changes/behavior/24570-EG.rst new file mode 100644 index 000000000000..2f257606c072 --- /dev/null +++ b/doc/api/next_api_changes/behavior/24570-EG.rst @@ -0,0 +1,7 @@ +HPacker top and bottom alignment now flush the expected edges +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +`matplotlib.offsetbox.HPacker` now aligns child boxes correctly when +``align='top'`` or ``align='bottom'``. Top alignment keeps the top edges flush +and bottom alignment keeps the bottom edges flush; previously the semantics +were reversed. This is treated as a bug fix. diff --git a/lib/matplotlib/offsetbox.py b/lib/matplotlib/offsetbox.py index 3a506543ac32..885048359515 100644 --- a/lib/matplotlib/offsetbox.py +++ b/lib/matplotlib/offsetbox.py @@ -166,10 +166,10 @@ def _get_aligned_offsets(hd_list, height, align="baseline"): descent = max(d for h, d in hd_list) height = height_descent + descent offsets = [0. for h, d in hd_list] - elif align in ["left", "top"]: + elif align in ["left", "bottom"]: descent = 0. offsets = [d for h, d in hd_list] - elif align in ["right", "bottom"]: + elif align in ["right", "top"]: descent = 0. offsets = [height - h + d for h, d in hd_list] elif align == "center": @@ -459,7 +459,8 @@ def get_extent_offsets(self, renderer): class HPacker(PackerBase): """ HPacker packs its children horizontally, automatically adjusting their - relative positions at draw time. + relative positions at draw time. ``align='top'`` keeps the children's top + edges flush, while ``align='bottom'`` keeps their bottom edges flush. """ def get_extent_offsets(self, renderer): diff --git a/lib/matplotlib/tests/test_offsetbox_alignment.py b/lib/matplotlib/tests/test_offsetbox_alignment.py new file mode 100644 index 000000000000..79b017a54c9e --- /dev/null +++ b/lib/matplotlib/tests/test_offsetbox_alignment.py @@ -0,0 +1,73 @@ +import pytest + +from matplotlib.offsetbox import DrawingArea, HPacker, VPacker + + +class _UnitRenderer: + def points_to_pixels(self, points): + return points + + +@pytest.fixture() +def renderer(): + return _UnitRenderer() + + +def _y_offsets(offsets): + return [y for _, y in offsets] + + +def _x_offsets(offsets): + return [x for x, _ in offsets] + + +def test_hpacker_top_bottom(renderer): + short = DrawingArea(1, 10) + tall = DrawingArea(1, 30) + + pack_top = HPacker(children=[short, tall], align="top", pad=0., sep=0.) + *_top, offsets_top = pack_top.get_extent_offsets(renderer) + assert _y_offsets(offsets_top) == [20, 0] + + pack_bottom = HPacker(children=[DrawingArea(1, 10), DrawingArea(1, 30)], + align="bottom", pad=0., sep=0.) + *_bottom, offsets_bottom = pack_bottom.get_extent_offsets(renderer) + assert _y_offsets(offsets_bottom) == [0, 0] + + +def test_hpacker_center(renderer): + short = DrawingArea(1, 10) + tall = DrawingArea(1, 30) + pack_center = HPacker(children=[short, tall], align="center", + pad=0., sep=0.) + *_center, offsets_center = pack_center.get_extent_offsets(renderer) + assert _y_offsets(offsets_center) == [10, 0] + + +def test_hpacker_baseline_descent(renderer): + descender = DrawingArea(1, 10, ydescent=5) + baseline = DrawingArea(1, 20, ydescent=0) + pack_baseline = HPacker(children=[descender, baseline], align="baseline", + pad=0., sep=0.) + + width, height, xdescent, ydescent, offsets = ( + pack_baseline.get_extent_offsets(renderer)) + + assert height == pytest.approx(25) + assert ydescent == pytest.approx(5) + assert _y_offsets(offsets) == [0, 0] + + +def test_vpacker_left_right(renderer): + narrow = DrawingArea(10, 1) + wide = DrawingArea(30, 1) + + pack_left = VPacker(children=[narrow, wide], align="left", + pad=0., sep=0.) + *_left, offsets_left = pack_left.get_extent_offsets(renderer) + assert _x_offsets(offsets_left) == [0, 0] + + pack_right = VPacker(children=[DrawingArea(10, 1), DrawingArea(30, 1)], + align="right", pad=0., sep=0.) + *_right, offsets_right = pack_right.get_extent_offsets(renderer) + assert _x_offsets(offsets_right) == [20, 0]