From 9a799a58291f9eb5e0ab504e30f51d5a62fcd500 Mon Sep 17 00:00:00 2001 From: gepcel Date: Sun, 24 May 2026 20:29:53 +0800 Subject: [PATCH 1/3] Fix int/list has no size error, for bar plot of pd.Series --- ultraplot/axes/plot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ultraplot/axes/plot.py b/ultraplot/axes/plot.py index 39e55a896..a1214fec0 100644 --- a/ultraplot/axes/plot.py +++ b/ultraplot/axes/plot.py @@ -3668,7 +3668,7 @@ def _inbounds_xylim(self, extents, x, y, **kwargs): return if self._name != "cartesian": return - if not x.size or not y.size: + if not getattr(x, "size", None) or not getattr(y, "size", None): return kwargs, vert = _get_vert(**kwargs) if not vert: From 725d547f826d2d33ce0b3bb7421a13e3f148b354 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Tue, 2 Jun 2026 21:04:17 +1000 Subject: [PATCH 2/3] Fix scalar bottom/left for bar plots at root cause Issue #731 reproduces because pandas dispatches Series.plot(kind="barh") with a scalar ``bottom`` (or ``left``), which flowed unchanged through ``_apply_bar``: ``_not_none(b, np.array([0.0]))`` returns the scalar as soon as it is not ``None``, then ``for y in (b, b + h)`` later passed it to ``_inbounds_xylim`` where ``x.size`` / ``y.size`` raised ``AttributeError`` for ints and lists. Normalize ``b`` to an ndarray once via ``np.atleast_1d`` so all downstream code sees an array, and restore the original ``x.size`` invariant in ``_inbounds_xylim`` (the defensive ``getattr`` only masked the symptom). Add a regression test exercising both direct scalar ``bottom``/``left`` and the original ``pd.Series.plot(kind="barh")`` reproducer. --- ultraplot/axes/plot.py | 4 ++-- ultraplot/tests/test_1dplots.py | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/ultraplot/axes/plot.py b/ultraplot/axes/plot.py index a1214fec0..f4b8aa5d3 100644 --- a/ultraplot/axes/plot.py +++ b/ultraplot/axes/plot.py @@ -3668,7 +3668,7 @@ def _inbounds_xylim(self, extents, x, y, **kwargs): return if self._name != "cartesian": return - if not getattr(x, "size", None) or not getattr(y, "size", None): + if not x.size or not y.size: return kwargs, vert = _get_vert(**kwargs) if not vert: @@ -6057,7 +6057,7 @@ def _apply_bar( kw = self._parse_cycle(n, **kw) # Adjust x or y coordinates for grouped and stacked bars w = _not_none(w, np.array([0.8])) # same as mpl but in *relative* units - b = _not_none(b, np.array([0.0])) # same as mpl + b = np.atleast_1d(_not_none(b, np.array([0.0]))) # tolerate scalar `bottom`/`left` if not absolute_width: w = self._convert_bar_width(x, w) if stack: diff --git a/ultraplot/tests/test_1dplots.py b/ultraplot/tests/test_1dplots.py index d63256a52..d57309161 100644 --- a/ultraplot/tests/test_1dplots.py +++ b/ultraplot/tests/test_1dplots.py @@ -138,6 +138,24 @@ def test_bar_width(rng): return fig +def test_bar_scalar_bottom(): + """ + Regression for #731: pandas dispatches Series.plot(kind="barh") via + matplotlib with a scalar ``bottom`` (or ``left``), which previously hit + ``AttributeError: 'int' object has no attribute 'size'`` inside + ``_inbounds_xylim``. + """ + # Direct scalar `bottom` / `left` + fig, ax = uplt.subplots() + ax.bar([1, 2, 3], [4, 5, 6], bottom=0) + ax.barh([1, 2, 3], [4, 5, 6], left=0) + + # The original failing reproducer from the issue + series = pd.Series({"a": 1, "b": 2, "c": 3}) + fig, ax = uplt.subplots() + series.plot(kind="barh", ax=ax[0]) + + @pytest.mark.mpl_image_compare def test_bar_vectors(): """ From fe5a08f96512916233b3e2546b1e8b69dc34ee7c Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Wed, 3 Jun 2026 02:56:55 +1000 Subject: [PATCH 3/3] black formatting --- ultraplot/axes/plot.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ultraplot/axes/plot.py b/ultraplot/axes/plot.py index f4b8aa5d3..e9078a1a4 100644 --- a/ultraplot/axes/plot.py +++ b/ultraplot/axes/plot.py @@ -6057,7 +6057,9 @@ def _apply_bar( kw = self._parse_cycle(n, **kw) # Adjust x or y coordinates for grouped and stacked bars w = _not_none(w, np.array([0.8])) # same as mpl but in *relative* units - b = np.atleast_1d(_not_none(b, np.array([0.0]))) # tolerate scalar `bottom`/`left` + b = np.atleast_1d( + _not_none(b, np.array([0.0])) + ) # tolerate scalar `bottom`/`left` if not absolute_width: w = self._convert_bar_width(x, w) if stack: