Repository: matplotlib/matplotlib
Pull request: add setters and getters for _AxLine's xy1, xy2 and slope parameters
## PR summary
Closes #26081. Add setter and getter methods for `_AxLine` to modify the control points and slope of the artist.
Setters will not allow the modification of `xy2` if `slope` is set and vice versa.
`Axline` is now public and documented.
## PR checklist
<!-- Please mark any checkboxes that do not apply to this PR as [N/A].-->

- [x] "closes #0000" is in the body of the PR description to [link the related issue](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue)
- [x] new and changed code is [tested](https://matplotlib.org/devdocs/devel/testing.html)
- [ ] *Plotting related* features are demonstrated in an [example](https://matplotlib.org/devdocs/devel/documenting_mpl.html#writing-examples-and-tutorials)
- [ ] *New Features* and *API Changes* are noted with a [directive and release note](https://matplotlib.org/devdocs/devel/coding_guide.html#new-features-and-api-changes)
- [x] Documentation complies with [general](https://matplotlib.org/devdocs/devel/documenting_mpl.html#writing-rest-pages) and [docstring](https://matplotlib.org/devdocs/devel/documenting_mpl.html#writing-docstrings) guidelines

<!--
Thank you so much for your PR!  To help us review your contribution, please
consider the following points:

- A development guide is available at https://matplotlib.org/devdocs/devel/index.html.

- Help with git and github is available at https://matplotlib.org/devdocs/devel/development_workflow.html

- Create a separate branch for your changes and open the PR from this branch. Please avoid working on `main`.

- The PR title should summarize the changes, for example "Raise ValueError on
  non-numeric input to set_xlim".  Avoid non-descriptive titles such as
  "Addresses issue #8576".

- The summary should provide at least 1-2 sentences describing the pull request
  in detail (Why is this change required?  What problem does it solve?) and
  link to any relevant issues.

- If you are contributing fixes to docstrings, please pay attention to
  https://matplotlib.org/stable/devel/documenting_mpl.html#formatting-conventions.  In particular,
  note the difference between using single backquotes, double backquotes, and
  asterisks in the markup.

We understand that PRs can sometimes be overwhelming, especially as the
reviews start coming in.  Please let us know if the reviews are unclear or
the recommended next step seems overly demanding, if you would like help in
addressing a reviewer's comments, or if you have been waiting too long to hear
back on your PR.
-->


Problem statement:
[ENH]: Add setters for _AxLine._xy1, ._xy2, ._slope
### Problem

Currently the control points / slope of the artist returned by axline() cannot be (publically) modified after being instantiated.  It would be nice if the relevant properties (xy1, xy2, slope) had setters (following normal Artist design).

For simplicity it is probably enough if we don't let one set xy2 if slope is set and vice-versa (i.e. whether axline is specified by 2 points or by point-and-slope is locked in).  Note that while I do have a use case for changing a previously set xy1/xy2, wanting to switch between the two different representations seems rarer to me(?)

This would likely also make _AxLine public.

### Proposed solution

_No response_

Hints from issue discussion:

Commit message: add setters and getters for AxLine

Patch to review:
diff --git a/doc/api/lines_api.rst b/doc/api/lines_api.rst
index 4cde67c7e656..bf7ec73493a3 100644
--- a/doc/api/lines_api.rst
+++ b/doc/api/lines_api.rst
@@ -17,6 +17,7 @@ Classes
 
    Line2D
    VertexSelector
+   AxLine
 
 Functions
 ---------
diff --git a/doc/users/next_whats_new/axline_setters.rst b/doc/users/next_whats_new/axline_setters.rst
new file mode 100644
index 000000000000..a344eb0e2a41
--- /dev/null
+++ b/doc/users/next_whats_new/axline_setters.rst
@@ -0,0 +1,16 @@
+Axline setters and getters
+--------------------------
+The returned object from ``Axes.axline`` now supports getter and setter
+methods for its *xy1*, *xy2* and *slope* attributes:
+
+.. code-block:: python
+
+    line1.get_xy1()
+    line1.get_slope()
+    line2.get_xy2()
+
+.. code-block:: python
+
+    line1.set_xy1(.2, .3)
+    line1.set_slope(2.4)
+    line2.set_xy2(.1, .6)
diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py
index 46fb69852286..b000024a1937 100644
--- a/lib/matplotlib/axes/_axes.py
+++ b/lib/matplotlib/axes/_axes.py
@@ -925,7 +925,7 @@ def axline(self, xy1, xy2=None, *, slope=None, **kwargs):
             # data limits should not be adjusted.
             datalim = []
 
-        line = mlines._AxLine(xy1, xy2, slope, **kwargs)
+        line = mlines.AxLine(xy1, xy2, slope, **kwargs)
         # Like add_line, but correctly handling data limits.
         self._set_artist_props(line)
         if line.get_clip_path() is None:
diff --git a/lib/matplotlib/lines.py b/lib/matplotlib/lines.py
index e18bd58bcdea..e88f69a15680 100644
--- a/lib/matplotlib/lines.py
+++ b/lib/matplotlib/lines.py
@@ -1464,13 +1464,24 @@ def is_dashed(self):
         return self._linestyle in ('--', '-.', ':')
 
 
-class _AxLine(Line2D):
+class AxLine(Line2D):
     """
     A helper class that implements `~.Axes.axline`, by recomputing the artist
     transform at draw time.
     """
 
     def __init__(self, xy1, xy2, slope, **kwargs):
+        """
+        Parameters
+        ----------
+        xy1 : (float, float)
+            The first set of (x, y) coordinates for the line to pass through.
+        xy2 : (float, float) or None
+            The second set of (x, y) coordinates for the line to pass through.
+            Either *xy2* or *slope* has to be given.
+        slope : float or None
+            The slope of the line. Either *xy2* or *slope* has to be given.
+        """
         super().__init__([0, 1], [0, 1], **kwargs)
 
         if (xy2 is None and slope is None or
@@ -1527,6 +1538,65 @@ def draw(self, renderer):
         self._transformed_path = None  # Force regen.
         super().draw(renderer)
 
+    def get_xy1(self):
+        """
+        Return the *xy1* value of the line.
+        """
+        return self._xy1
+
+    def get_xy2(self):
+        """
+        Return the *xy2* value of the line.
+        """
+        return self._xy2
+
+    def get_slope(self):
+        """
+        Return the *slope* value of the line.
+        """
+        return self._slope
+
+    def set_xy1(self, x, y):
+        """
+        Set the *xy1* value of the line.
+
+        Parameters
+        ----------
+        x, y : (float, float)
+            Points for the line to pass through.
+        """
+        self._xy1 = x, y
+
+    def set_xy2(self, x, y):
+        """
+        Set the *xy2* value of the line.
+
+        Parameters
+        ----------
+        x, y : (float, float)
+            Points for the line to pass through.
+        """
+        if self._slope is None:
+            self._xy2 = x, y
+        else:
+            raise ValueError("Cannot set an 'xy2' value while 'slope' is set;"
+                             " they differ but their functionalities overlap")
+
+    def set_slope(self, slope):
+        """
+        Set the *slope* value of the line.
+
+        Parameters
+        ----------
+        slope : float
+            The slope of the line.
+        """
+        if self._xy2 is None:
+            self._slope = slope
+        else:
+            raise ValueError("Cannot set a 'slope' value while 'xy2' is set;"
+                             " they differ but their functionalities overlap")
+
 
 class VertexSelector:
     """
diff --git a/lib/matplotlib/lines.pyi b/lib/matplotlib/lines.pyi
index e2e7bd224c66..fbbfe708d705 100644
--- a/lib/matplotlib/lines.pyi
+++ b/lib/matplotlib/lines.pyi
@@ -120,7 +120,7 @@ class Line2D(Artist):
     def get_solid_capstyle(self) -> Literal["butt", "projecting", "round"]: ...
     def is_dashed(self) -> bool: ...
 
-class _AxLine(Line2D):
+class AxLine(Line2D):
     def __init__(
         self,
         xy1: tuple[float, float],
@@ -128,6 +128,12 @@ class _AxLine(Line2D):
         slope: float | None,
         **kwargs
     ) -> None: ...
+    def get_xy1(self) -> tuple[float, float] | None: ...
+    def get_xy2(self) -> tuple[float, float] | None: ...
+    def get_slope(self) -> float: ...
+    def set_xy1(self, x: float, y: float) -> None: ...
+    def set_xy2(self, x: float, y: float) -> None: ...
+    def set_slope(self, slope: float) -> None: ...
 
 class VertexSelector:
     axes: Axes
diff --git a/lib/matplotlib/tests/test_lines.py b/lib/matplotlib/tests/test_lines.py
index cdf301a1f46b..e9d8ccc8b309 100644
--- a/lib/matplotlib/tests/test_lines.py
+++ b/lib/matplotlib/tests/test_lines.py
@@ -409,3 +409,30 @@ def test_markevery_prop_cycle(fig_test, fig_ref):
     ax = fig_test.add_subplot()
     for i, _ in enumerate(cases):
         ax.plot(y - i, 'o-')
+
+
+def test_axline_setters():
+    fig, ax = plt.subplots()
+    line1 = ax.axline((.1, .1), slope=0.6)
+    line2 = ax.axline((.1, .1), (.8, .4))
+    # Testing xy1, xy2 and slope setters.
+    # This should not produce an error.
+    line1.set_xy1(.2, .3)
+    line1.set_slope(2.4)
+    line2.set_xy1(.3, .2)
+    line2.set_xy2(.6, .8)
+    # Testing xy1, xy2 and slope getters.
+    # Should return the modified values.
+    assert line1.get_xy1() == (.2, .3)
+    assert line1.get_slope() == 2.4
+    assert line2.get_xy1() == (.3, .2)
+    assert line2.get_xy2() == (.6, .8)
+    # Testing setting xy2 and slope together.
+    # These test should raise a ValueError
+    with pytest.raises(ValueError, match="Cannot set an 'xy2' value while "
+                                         "'slope' is set"):
+        line1.set_xy2(.2, .3)
+
+    with pytest.raises(ValueError, match="Cannot set a 'slope' value while "
+                                         "'xy2' is set"):
+        line2.set_slope(3)


Code review comments:
diff_hunk: @@ -0,0 +1,16 @@
+Axline setters and getters
+--------------------------
+The returned object from ``Axes.axline`` now supports getter and setter
path: doc/users/next_whats_new/axline_setters.rst
text: @user1:
Let's make this a link:
```suggestion
The returned object from `.axes.Axes.axline` now supports getter and setter
```

diff_hunk: @@ -1527,6 +1538,65 @@ def draw(self, renderer):
         self._transformed_path = None  # Force regen.
         super().draw(renderer)
 
+    def get_xy1(self):
+        """
+        Return the *xy1* value of the line.
+        """
+        return self._xy1
+
+    def get_xy2(self):
+        """
+        Return the *xy2* value of the line.
+        """
+        return self._xy2
+
+    def get_slope(self):
+        """
+        Return the *slope* value of the line.
+        """
+        return self._slope
+
+    def set_xy1(self, x, y):
+        """
+        Set the *xy1* value of the line.
+
+        Parameters
+        ----------
+        x, y : (float, float)
path: lib/matplotlib/lines.py
text: @user1:
This is only `float`, as it applies to both `x` and `y` individually.
```suggestion
        x, y : float
```

diff_hunk: @@ -1527,6 +1538,65 @@ def draw(self, renderer):
         self._transformed_path = None  # Force regen.
         super().draw(renderer)
 
+    def get_xy1(self):
+        """
+        Return the *xy1* value of the line.
+        """
+        return self._xy1
+
+    def get_xy2(self):
+        """
+        Return the *xy2* value of the line.
+        """
+        return self._xy2
+
+    def get_slope(self):
+        """
+        Return the *slope* value of the line.
+        """
+        return self._slope
+
+    def set_xy1(self, x, y):
+        """
+        Set the *xy1* value of the line.
+
+        Parameters
+        ----------
+        x, y : (float, float)
+            Points for the line to pass through.
+        """
+        self._xy1 = x, y
+
+    def set_xy2(self, x, y):
+        """
+        Set the *xy2* value of the line.
+
+        Parameters
+        ----------
+        x, y : (float, float)
path: lib/matplotlib/lines.py
text: @user1:
```suggestion
        x, y : float
```