From 67af4a6db28973bf946c77227d200c5c43c843e5 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Mon, 10 Jun 2019 11:36:40 +0200 Subject: [PATCH 1/5] Document and test _get_packed_offsets() --- lib/matplotlib/offsetbox.py | 53 ++++++++++++++++++++---- lib/matplotlib/tests/test_offsetbox.py | 56 ++++++++++++++++++++++++++ 2 files changed, 102 insertions(+), 7 deletions(-) diff --git a/lib/matplotlib/offsetbox.py b/lib/matplotlib/offsetbox.py index c965cf994e99..7d1a39c607db 100644 --- a/lib/matplotlib/offsetbox.py +++ b/lib/matplotlib/offsetbox.py @@ -1,6 +1,6 @@ """ -The OffsetBox is a simple container artist. The child artist are meant -to be drawn at a relative position to its parent. The [VH]Packer, +The `.OffsetBox` is a simple container artist. Its child artists are +meant to be drawn at a relative position to OffsetBox. The [VH]Packer, DrawingArea and TextArea are derived from the OffsetBox. The [VH]Packer automatically adjust the relative positions of their @@ -49,12 +49,48 @@ def _get_packed_offsets(wd_list, total, sep, mode="fixed"): *mode*. xdescent is analogous to the usual descent, but along the x-direction. xdescent values are currently ignored. - *wd_list* : list of (width, xdescent) of boxes to be packed. - *sep* : spacing between boxes - *total* : Intended total length. None if not used. - *mode* : packing mode. 'fixed', 'expand', or 'equal'. + For simplicity of the description, the terminology used here assumes a + horizontal layout, but the function works equally for a vertical layout. + + There are three packing modes: + + - 'fixed': The elements are packed tight to the left with a spacing of + *sep* in between. If *total* is *None* the returned total will be the + right edge of the last box. A non-None total will be passed unchecked + to the output. In particular this means that right edge of the last + box will may be further to the right than the returned total. + + - 'expand': Distribute the boxes with equal spacing so that the left edge + of the first box is at 0, and the right edge of the last box is at + *total*. The parameter *sep* is ignored in this mode. A total of *None* + is accepted and considered equal to 1. The total is returned unchanged + (except for the conversion *None* to 1). If the total is smaller than + the sum of the widths, the laid out boxed will overlap. + + - 'equal': If *total* is given, the total space is divided in N equal + ranges and each box is left-aligned within its subspace. + Otherwise (*total* is *None*), *sep* must be provided and each box is + left-aligned in its subspace of width ``(max(widths) + sep)``. The + total width is then calculated to be ``N * (max(widths) + sep)``. + + Parameters + ---------- + wd_list : list of (float, float) + (width, xdescent) of boxes to be packed. + total : float or None + Intended total length. None if not used. + sep : float + Spacing between boxes. + mode : {'fixed', 'expand', 'equal'} + The packing mode. + + Returns + ------- + total : float + The total width needed to accommodate the laid out boxes. + offsets : array of float + The left offsets of the boxes. """ - w_list, d_list = zip(*wd_list) # d_list is currently not used. @@ -81,6 +117,9 @@ def _get_packed_offsets(wd_list, total, sep, mode="fixed"): elif mode == "equal": maxh = max(w_list) if total is None: + if sep is None: + raise ValueError("total and sep cannot both be None when " + "using layout mode 'equal'.") total = (maxh + sep) * len(w_list) else: sep = total / len(w_list) - maxh diff --git a/lib/matplotlib/tests/test_offsetbox.py b/lib/matplotlib/tests/test_offsetbox.py index 42fc3b355d0e..ad4fbfd4985f 100644 --- a/lib/matplotlib/tests/test_offsetbox.py +++ b/lib/matplotlib/tests/test_offsetbox.py @@ -1,3 +1,5 @@ +from collections import namedtuple +import numpy.testing as nptest import pytest from matplotlib.testing.decorators import image_comparison import matplotlib.pyplot as plt @@ -123,4 +125,58 @@ def test_get_packed_offsets(wd_list, total, sep, mode): # issue tickets (at least #10476 and #10784) related to corner cases # triggered inside this function when calling higher-level functions # (e.g. `Axes.legend`). + # These are just some additional smoke tests. The output is untested. _get_packed_offsets(wd_list, total, sep, mode=mode) + + +_Params = namedtuple('_params', 'wd_list, total, sep, expected') + + +@pytest.mark.parametrize('wd_list, total, sep, expected', [ + _Params( # total=None + [(3, 0), (1, 0), (2, 0)], total=None, sep=1, expected=(8, [0, 4, 6])), + _Params( # total larger than required + [(3, 0), (1, 0), (2, 0)], total=10, sep=1, expected=(10, [0, 4, 6])), + _Params( # total smaller than required + [(3, 0), (1, 0), (2, 0)], total=5, sep=1, expected=(5, [0, 4, 6])), +]) +def test_get_packed_offsets_fixed(wd_list, total, sep, expected): + result = _get_packed_offsets(wd_list, total, sep, mode='fixed') + assert result[0] == expected[0] + nptest.assert_allclose(result[1], expected[1]) + + +@pytest.mark.parametrize('wd_list, total, sep, expected', [ + _Params( # total=None (implicit 1) + [(.1, 0)] * 3, total=None, sep=None, expected=(1, [0, .45, .9])), + _Params( # total larger than sum of widths + [(3, 0), (1, 0), (2, 0)], total=10, sep=1, expected=(10, [0, 5, 8])), + _Params( # total smaller sum of widths: overlapping boxes + [(3, 0), (1, 0), (2, 0)], total=5, sep=1, expected=(5, [0, 2.5, 3])), +]) +def test_get_packed_offsets_expand(wd_list, total, sep, expected): + result = _get_packed_offsets(wd_list, total, sep, mode='expand') + assert result[0] == expected[0] + nptest.assert_allclose(result[1], expected[1]) + + +@pytest.mark.parametrize('wd_list, total, sep, expected', [ + _Params( # total larger than required + [(3, 0), (2, 0), (1, 0)], total=6, sep=None, expected=(6, [0, 2, 4])), + _Params( # total smaller sum of widths: overlapping boxes + [(3, 0), (2, 0), (1, 0), (.5, 0)], total=2, sep=None, + expected=(2, [0, 0.5, 1, 1.5])), + _Params( # total larger than required + [(.5, 0), (1, 0), (.2, 0)], total=None, sep=1, + expected=(6, [0, 2, 4])), + # the case total=None, sep=None is tested separately below +]) +def test_get_packed_offsets_equal(wd_list, total, sep, expected): + result = _get_packed_offsets(wd_list, total, sep, mode='equal') + assert result[0] == expected[0] + nptest.assert_allclose(result[1], expected[1]) + + +def test_get_packed_offsets_equal_total_none_sep_none(): + with pytest.raises(ValueError): + _get_packed_offsets([(1, 0)] * 3, total=None, sep=None, mode='equal') From 30f5d9a27ee0a6b7697ba43ad398fd67b04b0351 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Wed, 12 Jun 2019 00:23:16 +0200 Subject: [PATCH 2/5] Update lib/matplotlib/offsetbox.py Co-Authored-By: Elliott Sales de Andrade --- lib/matplotlib/offsetbox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/offsetbox.py b/lib/matplotlib/offsetbox.py index 7d1a39c607db..2e17f2df65e0 100644 --- a/lib/matplotlib/offsetbox.py +++ b/lib/matplotlib/offsetbox.py @@ -65,7 +65,7 @@ def _get_packed_offsets(wd_list, total, sep, mode="fixed"): *total*. The parameter *sep* is ignored in this mode. A total of *None* is accepted and considered equal to 1. The total is returned unchanged (except for the conversion *None* to 1). If the total is smaller than - the sum of the widths, the laid out boxed will overlap. + the sum of the widths, the laid out boxes will overlap. - 'equal': If *total* is given, the total space is divided in N equal ranges and each box is left-aligned within its subspace. From 6a96ad9804f45df4f1692c99100524e3ee886fa2 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Mon, 24 Jun 2019 19:52:56 +0200 Subject: [PATCH 3/5] Update lib/matplotlib/offsetbox.py Co-Authored-By: Elliott Sales de Andrade --- lib/matplotlib/offsetbox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/offsetbox.py b/lib/matplotlib/offsetbox.py index 2e17f2df65e0..645b8f23d505 100644 --- a/lib/matplotlib/offsetbox.py +++ b/lib/matplotlib/offsetbox.py @@ -56,7 +56,7 @@ def _get_packed_offsets(wd_list, total, sep, mode="fixed"): - 'fixed': The elements are packed tight to the left with a spacing of *sep* in between. If *total* is *None* the returned total will be the - right edge of the last box. A non-None total will be passed unchecked + right edge of the last box. A non-*None* total will be passed unchecked to the output. In particular this means that right edge of the last box will may be further to the right than the returned total. From a4f7ef575f08ba3fa1fac31d085257b8167f05ea Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Mon, 24 Jun 2019 19:53:06 +0200 Subject: [PATCH 4/5] Update lib/matplotlib/offsetbox.py Co-Authored-By: Elliott Sales de Andrade --- lib/matplotlib/offsetbox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/offsetbox.py b/lib/matplotlib/offsetbox.py index 645b8f23d505..6b3795eea558 100644 --- a/lib/matplotlib/offsetbox.py +++ b/lib/matplotlib/offsetbox.py @@ -58,7 +58,7 @@ def _get_packed_offsets(wd_list, total, sep, mode="fixed"): *sep* in between. If *total* is *None* the returned total will be the right edge of the last box. A non-*None* total will be passed unchecked to the output. In particular this means that right edge of the last - box will may be further to the right than the returned total. + box may be further to the right than the returned total. - 'expand': Distribute the boxes with equal spacing so that the left edge of the first box is at 0, and the right edge of the last box is at From 09230d7c651e139296fbba6cb329172e51a8baeb Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Tue, 25 Jun 2019 07:28:33 +0200 Subject: [PATCH 5/5] Update lib/matplotlib/offsetbox.py Co-Authored-By: Elliott Sales de Andrade --- lib/matplotlib/offsetbox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/offsetbox.py b/lib/matplotlib/offsetbox.py index 6b3795eea558..d5a53b5f7f4e 100644 --- a/lib/matplotlib/offsetbox.py +++ b/lib/matplotlib/offsetbox.py @@ -78,7 +78,7 @@ def _get_packed_offsets(wd_list, total, sep, mode="fixed"): wd_list : list of (float, float) (width, xdescent) of boxes to be packed. total : float or None - Intended total length. None if not used. + Intended total length. *None* if not used. sep : float Spacing between boxes. mode : {'fixed', 'expand', 'equal'}