Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 34 additions & 14 deletions python/xy/pyplot/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1729,7 +1729,7 @@ def table(
)


def legend(*args: Any, **kwargs: Any) -> None:
def legend(*args: Any, **kwargs: Any) -> Legend:
"""Show the legend of the current axes.

Call forms: ``legend()`` (labeled artists), ``legend(labels)``, or
Expand Down Expand Up @@ -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:
Expand All @@ -2721,12 +2728,19 @@ 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()
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()


def yticks(
Expand All @@ -2735,9 +2749,15 @@ 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()
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()


def tight_layout(**kwargs: Any) -> None:
Expand Down
24 changes: 18 additions & 6 deletions python/xy/pyplot/_axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))``;
Expand All @@ -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()
Expand Down Expand Up @@ -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."""
Expand All @@ -4151,14 +4156,18 @@ 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
the limits apply to its own right-hand y-axis.
"""
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)
Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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,
Expand All @@ -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)
Expand Down Expand Up @@ -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."""
Expand Down
8 changes: 4 additions & 4 deletions python/xy/pyplot/_plot_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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: ...

Expand Down
68 changes: 68 additions & 0 deletions tests/pyplot/test_reference_semantics.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,74 @@ 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_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")

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()
Expand Down