From 01dfed28c931757ca1f0643c4e3bbea28b0983e0 Mon Sep 17 00:00:00 2001 From: Harsh Thakare Date: Thu, 6 Aug 2026 08:43:38 +0530 Subject: [PATCH 1/2] Restore pyplot getter semantics for limits and ticks --- python/xy/pyplot/__init__.py | 44 ++++++++++++++------- python/xy/pyplot/_axes.py | 24 +++++++++--- python/xy/pyplot/_plot_types.py | 8 ++-- tests/pyplot/test_reference_semantics.py | 49 ++++++++++++++++++++++++ 4 files changed, 101 insertions(+), 24 deletions(-) diff --git a/python/xy/pyplot/__init__.py b/python/xy/pyplot/__init__.py index b6473fb5..409ccb36 100644 --- a/python/xy/pyplot/__init__.py +++ b/python/xy/pyplot/__init__.py @@ -1729,7 +1729,7 @@ def table( ) -def legend(*args: Any, **kwargs: Any) -> None: +def legend(*args: Any, **kwargs: Any) -> Legend | None: """Show the legend of the current axes. Call forms: ``legend()`` (labeled artists), ``legend(labels)``, or @@ -2691,18 +2691,25 @@ def ylabel(label: str, **kwargs: Any) -> None: gca().set_ylabel(label, **kwargs) -def xlim(*args: Any) -> None: - """Set the x limits of the current axes. +def xlim( + left: float | LimitsLike | None = None, + right: float | None = None, +) -> tuple[float, float]: + """Get or set the x limits of the current axes. Call as ``xlim(left, right)``, ``xlim((left, right))``, or with - ``left=``/``right=``; a descending pair inverts the axis. + ``left=``/``right=``. With no arguments, return the current limits + without changing the automatic view. A descending pair inverts the axis. """ - gca().set_xlim(*args) + return gca().set_xlim(left, right) -def ylim(*args: Any) -> None: - """Set the y limits of the current axes (forms as in `xlim`).""" - gca().set_ylim(*args) +def ylim( + bottom: float | LimitsLike | None = None, + top: float | None = None, +) -> tuple[float, float]: + """Get or set the y limits of the current axes (forms as in `xlim`).""" + return gca().set_ylim(bottom, top) def xscale(scale: str) -> None: @@ -2721,12 +2728,17 @@ def xticks( *, rotation: float | None = None, **kwargs: Any, -) -> None: - """Place the x ticks at the given positions, optionally relabeled. +) -> tuple[np.ndarray, list[Any]]: + """Get or set x ticks, optionally relabeled. + With no tick arguments, return the current locations and label handles. ``rotation`` (degrees) and supported text keywords style the labels. """ - gca().set_xticks(ticks, labels, rotation=rotation, **kwargs) + axes = gca() + if ticks is None and labels is None and rotation is None and not kwargs: + return axes.get_xticks(), axes.get_xticklabels() + axes.set_xticks(ticks, labels, rotation=rotation, **kwargs) + return axes.get_xticks(), axes.get_xticklabels() def yticks( @@ -2735,9 +2747,13 @@ def yticks( *, rotation: float | None = None, **kwargs: Any, -) -> None: - """Place the y ticks at the given positions (see `xticks`).""" - gca().set_yticks(ticks, labels, rotation=rotation, **kwargs) +) -> tuple[np.ndarray, list[Any]]: + """Get or set y ticks (see `xticks`).""" + axes = gca() + if ticks is None and labels is None and rotation is None and not kwargs: + return axes.get_yticks(), axes.get_yticklabels() + axes.set_yticks(ticks, labels, rotation=rotation, **kwargs) + return axes.get_yticks(), axes.get_yticklabels() def tight_layout(**kwargs: Any) -> None: diff --git a/python/xy/pyplot/_axes.py b/python/xy/pyplot/_axes.py index 287c310d..12e169c1 100644 --- a/python/xy/pyplot/_axes.py +++ b/python/xy/pyplot/_axes.py @@ -4088,7 +4088,9 @@ def set(self, **kwargs: Any) -> "Axes": self._invalidate() return self - def set_xlim(self, left: float | LimitsLike | None = None, right: float | None = None) -> None: + def set_xlim( + self, left: float | LimitsLike | None = None, right: float | None = None + ) -> tuple[float, float]: """Set the x view limits. Call as ``set_xlim(left, right)`` or ``set_xlim((left, right))``; @@ -4098,6 +4100,8 @@ def set_xlim(self, left: float | LimitsLike | None = None, right: float | None = """ if isinstance(left, (tuple, list)): left, right = left + if left is None and right is None: + return self.get_xlim() host = (self._y2_of or self)._shared_ticker_source("x") spec = host._scale_specs["x"] current_start, current_end = self.get_xlim() @@ -4134,6 +4138,7 @@ def set_xlim(self, left: float | LimitsLike | None = None, right: float | None = host._explicit_domains.add("x") host._tick_expanded_domains.discard("x") host._invalidate_shared_ticker_axis("x") + return self.get_xlim() def get_xlim(self) -> tuple[float, float]: """The current x view limits, in data space and display order.""" @@ -4151,7 +4156,9 @@ def get_xlim(self) -> tuple[float, float]: ) return (hi, lo) if host._axis["x"].get("reverse") else (lo, hi) - def set_ylim(self, bottom: float | LimitsLike | None = None, top: float | None = None) -> None: + def set_ylim( + self, bottom: float | LimitsLike | None = None, top: float | None = None + ) -> tuple[float, float]: """Set the y view limits (forms as in `set_xlim`). A descending ``(bottom, top)`` pair inverts the axis; on a twin axes @@ -4159,6 +4166,8 @@ def set_ylim(self, bottom: float | LimitsLike | None = None, top: float | None = """ if isinstance(bottom, (tuple, list)): bottom, top = bottom + if bottom is None and top is None: + return self.get_ylim() base = self._y2_of or self key = "y2" if self._y2_of is not None else "y" host = base._shared_ticker_source(key) @@ -4197,6 +4206,7 @@ def set_ylim(self, bottom: float | LimitsLike | None = None, top: float | None = host._explicit_domains.add(key) host._tick_expanded_domains.discard(key) host._invalidate_shared_ticker_axis(key) + return self.get_ylim() def get_ylim(self) -> tuple[float, float]: """The current y view limits, in data space and display order.""" @@ -6533,7 +6543,7 @@ def set_xticks( *, rotation: float | None = None, **kwargs: Any, - ) -> None: + ) -> list[_TickLabel]: """Place the x ticks at the given positions, optionally relabeled. ``labels`` must match ``ticks`` in length and displaces any user @@ -6542,7 +6552,7 @@ def set_xticks( Positions are in data space, so nonlinear scales transform them. """ if kwargs.pop("minor", False): - return + return [] host = (self._y2_of or self)._shared_ticker_source("x") props = host._axis["x"] if ticks is not None: @@ -6570,6 +6580,7 @@ def set_xticks( if rotation is not None: self._axis_props("x")["tick_label_angle"] = float(rotation) host._invalidate_shared_ticker_axis("x") + return self.get_xticklabels() def set_yticks( self, @@ -6578,10 +6589,10 @@ def set_yticks( *, rotation: float | None = None, **kwargs: Any, - ) -> None: + ) -> list[_TickLabel]: """Place the y ticks at the given positions (see `set_xticks`).""" if kwargs.pop("minor", False): - return + return [] base = self._y2_of or self key = "y2" if self._y2_of is not None else "y" host = base._shared_ticker_source(key) @@ -6609,6 +6620,7 @@ def set_yticks( if rotation is not None: self._axis_props("y")["tick_label_angle"] = float(rotation) host._invalidate_shared_ticker_axis(key) + return self.get_yticklabels() def _expand_domain_to_ticks(self, axis: str) -> None: """Apply Matplotlib's mandatory view expansion for explicit ticks.""" diff --git a/python/xy/pyplot/_plot_types.py b/python/xy/pyplot/_plot_types.py index e8b17177..653eccd1 100644 --- a/python/xy/pyplot/_plot_types.py +++ b/python/xy/pyplot/_plot_types.py @@ -1272,13 +1272,13 @@ def imshow(self, *args: Any, **kwargs: Any) -> Any: ... def axhline(self, *args: Any, **kwargs: Any) -> Line2D: ... - def set_xticks(self, *args: Any, **kwargs: Any) -> None: ... + def set_xticks(self, *args: Any, **kwargs: Any) -> list[Any]: ... - def set_yticks(self, *args: Any, **kwargs: Any) -> None: ... + def set_yticks(self, *args: Any, **kwargs: Any) -> list[Any]: ... - def set_xlim(self, *args: Any, **kwargs: Any) -> None: ... + def set_xlim(self, *args: Any, **kwargs: Any) -> tuple[float, float]: ... - def set_ylim(self, *args: Any, **kwargs: Any) -> None: ... + def set_ylim(self, *args: Any, **kwargs: Any) -> tuple[float, float]: ... def set_xscale(self, scale: str, **kwargs: Any) -> None: ... diff --git a/tests/pyplot/test_reference_semantics.py b/tests/pyplot/test_reference_semantics.py index ad1a5afe..5db43efe 100644 --- a/tests/pyplot/test_reference_semantics.py +++ b/tests/pyplot/test_reference_semantics.py @@ -47,6 +47,55 @@ def test_reference_line_data_cycle_limits_ticks_and_shared_axes() -> None: assert xyaxes[0].get_xlabel() == mplaxes[0].get_xlabel() +def test_pyplot_limits_support_getters_keywords_and_return_values() -> None: + _fig, ax = xyplt.subplots() + ax.plot([10.0, 20.0], [30.0, 40.0]) + before_domains = {axis: dict(ax._axis[axis]) for axis in ("x", "y")} + before_explicit = set(ax._explicit_domains) + + assert xyplt.xlim() == ax.get_xlim() + assert xyplt.ylim() == ax.get_ylim() + assert {axis: dict(ax._axis[axis]) for axis in ("x", "y")} == before_domains + assert ax._explicit_domains == before_explicit + + assert xyplt.xlim(left=0.0, right=25.0) == (0.0, 25.0) + assert xyplt.ylim((0.0, 50.0)) == (0.0, 50.0) + assert ax.set_xlim() == (0.0, 25.0) + assert ax.set_ylim() == (0.0, 50.0) + + +def test_pyplot_ticks_getters_are_non_mutating_and_setters_return_handles() -> None: + _fig, ax = xyplt.subplots() + ax.plot([0.0, 1.0], [0.0, 1.0]) + before = {axis: dict(ax._axis[axis]) for axis in ("x", "y")} + + xlocations, xlabels = xyplt.xticks() + ylocations, ylabels = xyplt.yticks() + np.testing.assert_array_equal(xlocations, ax.get_xticks()) + np.testing.assert_array_equal(ylocations, ax.get_yticks()) + assert [label.get_text() for label in xlabels] == [ + label.get_text() for label in ax.get_xticklabels() + ] + assert [label.get_text() for label in ylabels] == [ + label.get_text() for label in ax.get_yticklabels() + ] + assert {axis: dict(ax._axis[axis]) for axis in ("x", "y")} == before + + locations, labels = xyplt.xticks([0.0, 1.0], ["zero", "one"]) + np.testing.assert_array_equal(locations, [0.0, 1.0]) + assert [label.get_text() for label in labels] == ["zero", "one"] + + +def test_pyplot_legend_returns_live_legend_handle() -> None: + _fig, ax = xyplt.subplots() + ax.plot([0.0, 1.0], [0.0, 1.0], label="line") + + legend = xyplt.legend() + + assert isinstance(legend, xyplt.Legend) + assert ax.get_legend() is legend + + def test_reference_bar_geometry_stacking_and_container_shape() -> None: xyfig, xyax = xyplt.subplots() mplfig, mplax = mplplt.subplots() From 18a589a36798ff9b807e629e6ff84daa9dcc4733 Mon Sep 17 00:00:00 2001 From: Harsh Thakare Date: Thu, 6 Aug 2026 08:57:30 +0530 Subject: [PATCH 2/2] Fix pyplot minor tick getters --- python/xy/pyplot/__init__.py | 14 +++++++++----- tests/pyplot/test_reference_semantics.py | 19 +++++++++++++++++++ 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/python/xy/pyplot/__init__.py b/python/xy/pyplot/__init__.py index 409ccb36..54a7135c 100644 --- a/python/xy/pyplot/__init__.py +++ b/python/xy/pyplot/__init__.py @@ -1729,7 +1729,7 @@ def table( ) -def legend(*args: Any, **kwargs: Any) -> Legend | None: +def legend(*args: Any, **kwargs: Any) -> Legend: """Show the legend of the current axes. Call forms: ``legend()`` (labeled artists), ``legend(labels)``, or @@ -2735,8 +2735,10 @@ def xticks( ``rotation`` (degrees) and supported text keywords style the labels. """ axes = gca() - if ticks is None and labels is None and rotation is None and not kwargs: - return axes.get_xticks(), axes.get_xticklabels() + minor = bool(kwargs.get("minor", False)) + if ticks is None and labels is None and rotation is None and not (set(kwargs) - {"minor"}): + labels = axes.xaxis.get_minorticklabels() if minor else axes.get_xticklabels() + return axes.get_xticks(minor=minor), labels axes.set_xticks(ticks, labels, rotation=rotation, **kwargs) return axes.get_xticks(), axes.get_xticklabels() @@ -2750,8 +2752,10 @@ def yticks( ) -> tuple[np.ndarray, list[Any]]: """Get or set y ticks (see `xticks`).""" axes = gca() - if ticks is None and labels is None and rotation is None and not kwargs: - return axes.get_yticks(), axes.get_yticklabels() + minor = bool(kwargs.get("minor", False)) + if ticks is None and labels is None and rotation is None and not (set(kwargs) - {"minor"}): + labels = axes.yaxis.get_minorticklabels() if minor else axes.get_yticklabels() + return axes.get_yticks(minor=minor), labels axes.set_yticks(ticks, labels, rotation=rotation, **kwargs) return axes.get_yticks(), axes.get_yticklabels() diff --git a/tests/pyplot/test_reference_semantics.py b/tests/pyplot/test_reference_semantics.py index 5db43efe..f752949b 100644 --- a/tests/pyplot/test_reference_semantics.py +++ b/tests/pyplot/test_reference_semantics.py @@ -86,6 +86,25 @@ def test_pyplot_ticks_getters_are_non_mutating_and_setters_return_handles() -> N assert [label.get_text() for label in labels] == ["zero", "one"] +def test_pyplot_minor_tick_getters_return_minor_locations() -> None: + _fig, ax = xyplt.subplots() + ax.plot([0.0, 10.0], [0.0, 10.0]) + ax.set_xlim(0.0, 10.0) + ax.set_ylim(0.0, 10.0) + ax.minorticks_on() + ax._build_chart(640, 480) + + xlocations, xlabels = xyplt.xticks(minor=True) + ylocations, ylabels = xyplt.yticks(minor=True) + + np.testing.assert_array_equal(xlocations, ax.get_xticks(minor=True)) + np.testing.assert_array_equal(ylocations, ax.get_yticks(minor=True)) + assert xlocations.size > 0 + assert ylocations.size > 0 + assert xlabels == ax.xaxis.get_minorticklabels() + assert ylabels == ax.yaxis.get_minorticklabels() + + def test_pyplot_legend_returns_live_legend_handle() -> None: _fig, ax = xyplt.subplots() ax.plot([0.0, 1.0], [0.0, 1.0], label="line")