From a747d0cdfd9b835ad4c3e78a1fda89b70f741234 Mon Sep 17 00:00:00 2001 From: cvanelteren Date: Sun, 16 Aug 2026 21:28:48 +1000 Subject: [PATCH] add in sticky edges --- docs/1dplots.py | 5 ++- ultraplot/axes/base.py | 15 +++++++++ ultraplot/axes/plot.py | 19 +++++++----- ultraplot/internals/rcsetup.py | 8 +++++ ultraplot/tests/test_1dplots.py | 55 +++++++++++++++++++++++++++++++++ 5 files changed, 93 insertions(+), 9 deletions(-) diff --git a/docs/1dplots.py b/docs/1dplots.py index 7c4ba9ae3..ad9a6f1c7 100644 --- a/docs/1dplots.py +++ b/docs/1dplots.py @@ -276,6 +276,8 @@ # Also, the default *x* bounds for lines drawn with :func:`~ultraplot.axes.PlotAxes.plot` # and *y* bounds for lines drawn with :func:`~ultraplot.axes.PlotAxes.plotx` are now # "sticky", i.e. there is no padding between the lines and axes edges by default. +# This can be disabled globally with :rcraw:`axes.sticky_edges` or per-axes with +# the ``use_sticky_edges`` attribute. # # Step and stem plots can be drawn with :func:`~ultraplot.axes.PlotAxes.step`, # :func:`~ultraplot.axes.PlotAxes.stepx`, :func:`~ultraplot.axes.PlotAxes.stem`, and @@ -505,7 +507,8 @@ # ``stacked=True``. Also note the default *x* bounds for shading drawn with # :func:`~ultraplot.axes.PlotAxes.area` and *y* bounds for shading drawn with # :func:`~ultraplot.axes.PlotAxes.areax` is now "sticky", i.e. there is no padding -# between the shading and axes edges by default. +# between the shading and axes edges by default. This can be disabled globally with +# :rcraw:`axes.sticky_edges` or per-axes with the ``use_sticky_edges`` attribute. # .. important:: # diff --git a/ultraplot/axes/base.py b/ultraplot/axes/base.py index 19a429d15..7bbd8fcf0 100644 --- a/ultraplot/axes/base.py +++ b/ultraplot/axes/base.py @@ -993,6 +993,7 @@ def __init__(self, *args, **kwargs): self._title_loc = None self._title_pad = rc["title.pad"] self._title_pad_current = None + self._use_sticky_edges = rc["axes.sticky_edges"] self._altx_parent = None # for cartesian axes only self._alty_parent = None self._colorbar_fill = None @@ -4664,6 +4665,20 @@ def number(self, num): else: raise ValueError(f"Invalid number {num!r}. Must be integer >=1.") + @property + def use_sticky_edges(self): + """ + Whether plotting commands like `plot`, `plotx`, `vlines`, `hlines`, + `fill_between`, and `fill_betweenx` add "sticky" edges to their artists, + i.e. whether the default axis limits are the artist bounds with no padding. + Initialized from :rcraw:`axes.sticky_edges`. + """ + return self._use_sticky_edges + + @use_sticky_edges.setter + def use_sticky_edges(self, value): + self._use_sticky_edges = rcsetup._validate_bool(value) + # Apply signature obfuscation after storing previous signature # NOTE: This is needed for __init__ diff --git a/ultraplot/axes/plot.py b/ultraplot/axes/plot.py index 6bd62acaf..cea2487ac 100644 --- a/ultraplot/axes/plot.py +++ b/ultraplot/axes/plot.py @@ -2115,14 +2115,15 @@ def curved_quiver( arrows.append(p) lc = mcollections.LineCollection(streamlines, transform=transform, **line_kw) - lc.sticky_edges.x[:] = [ - solver.grid.x_origin, - solver.grid.x_origin + solver.grid.width, - ] - lc.sticky_edges.y[:] = [ - solver.grid.y_origin, - solver.grid.y_origin + solver.grid.height, - ] + if self.use_sticky_edges: + lc.sticky_edges.x[:] = [ + solver.grid.x_origin, + solver.grid.x_origin + solver.grid.width, + ] + lc.sticky_edges.y[:] = [ + solver.grid.y_origin, + solver.grid.y_origin + solver.grid.height, + ] if use_multicolor_lines: lc.set_array(np.ma.hstack(line_colors)) @@ -3506,6 +3507,8 @@ def _fix_sticky_edges(self, objs, axis, *args, only=None): Fix sticky edges for the input artists using the minimum and maximum of the input coordinates. This is used to copy `bar` behavior to `area` and `lines`. """ + if not self.use_sticky_edges: + return for array in args: min_, max_ = inputs._safe_range(array) if min_ is None or max_ is None: diff --git a/ultraplot/internals/rcsetup.py b/ultraplot/internals/rcsetup.py index 7a098d4aa..8db81854e 100644 --- a/ultraplot/internals/rcsetup.py +++ b/ultraplot/internals/rcsetup.py @@ -1244,6 +1244,14 @@ def _validator_accepts(validator, value): _validate_float, "The fractional *x* and *y* axis margins when limits are unset.", ), + "axes.sticky_edges": ( + True, + _validate_bool, + "Whether artists added by plotting commands like `plot`, `plotx`, " + "`vlines`, `hlines`, `fill_between`, and `fill_betweenx` are given " + '"sticky" edges, i.e. whether the default axis limits are the artist ' + "bounds with no padding. See also `Axes.use_sticky_edges`.", + ), "bar.bar_labels": ( False, _validate_bool, diff --git a/ultraplot/tests/test_1dplots.py b/ultraplot/tests/test_1dplots.py index c04486a12..71dab1da2 100644 --- a/ultraplot/tests/test_1dplots.py +++ b/ultraplot/tests/test_1dplots.py @@ -52,6 +52,61 @@ def test_bar_absolute_width_manual_override(): assert pytest.approx(w_abs[0], rel=1e-6) == 0.8 +def test_sticky_edges_rc_and_attribute(): + """ + Sticky edges for line and area plots can be disabled globally with the + ``axes.sticky_edges`` setting or per-axes with ``use_sticky_edges``. + See https://github.com/Ultraplot/UltraPlot/issues/631. + """ + x = y = np.arange(10.0) + + # Default: sticky edges enabled, no padding + fig, axs = uplt.subplots() + ax = axs[0] + assert ax.use_sticky_edges is True + ax.plot(x, y) + assert np.allclose(ax.get_xlim(), (0, 9)) + + # Disabled globally via rc setting + with uplt.rc.context({"axes.sticky_edges": False}): + fig, axs = uplt.subplots() + ax = axs[0] + assert ax.use_sticky_edges is False + ax.plot(x, y) + xmin, xmax = ax.get_xlim() + assert xmin < 0 and xmax > 9 + + # plotx, area, and vlines also respect the setting + fig, axs = uplt.subplots(nrows=3) + axs[0].plotx(y, x) + ymin, ymax = axs[0].get_ylim() + assert ymin < 0 and ymax > 9 + axs[1].area(x, y) + xmin, xmax = axs[1].get_xlim() + assert xmin < 0 and xmax > 9 + axs[2].vlines(x, 0, y) + ymin, ymax = axs[2].get_ylim() + assert ymin < 0 and ymax > 9 + + # Setting is restored after the context exits + fig, axs = uplt.subplots() + assert axs[0].use_sticky_edges is True + + # Disabled per-axes via attribute + fig, axs = uplt.subplots() + ax = axs[0] + ax.use_sticky_edges = False + ax.plot(x, y) + xmin, xmax = ax.get_xlim() + assert xmin < 0 and xmax > 9 + + # Attribute values are validated as booleans + ax.use_sticky_edges = "true" + assert ax.use_sticky_edges is True + with pytest.raises(ValueError): + ax.use_sticky_edges = "bogus" + + import pytest