Repository: matplotlib/matplotlib
Pull request: Strictly increasing check with test coverage for streamplot grid
## PR Summary
This PR fixes #18898, adds check for strictly increasing `x` and `y` in `matplotlib.streamplot.Grid`, and adds test coverage for it.

## PR Checklist

<!-- Please mark any checkboxes that do not apply to this PR as [N/A]. -->

- [x] Has pytest style unit tests (and `pytest` passes).
- [x] Is [Flake 8](https://flake8.pycqa.org/en/latest/) compliant (run `flake8` on changed files to check).
- [x] New features are documented, with examples if plot related.
- [x] Documentation is sphinx and numpydoc compliant (the docs should [build](https://matplotlib.org/devel/documenting_mpl.html#building-the-docs) without error).
- [x] Conforms to Matplotlib style conventions (install `flake8-docstrings` and run `flake8 --docstring-convention=all`).
- [ ] New features have an entry in `doc/users/next_whats_new/` (follow instructions in README.rst there).
- [ ] API changes documented in `doc/api/next_api_changes/` (follow instructions in README.rst there).

<!--
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/devel/gitwash/development_workflow.html.

- Do not create the PR out of master, but out of a separate branch.

- 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
  http://matplotlib.org/devel/documenting_mpl.html#formatting.  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:
wrong bounds checking in streamplot start_points
<!--To help us understand and resolve your issue, please fill out the form to the best of your ability.-->
<!--You can feel free to delete the sections that do not apply.-->

### Bug report

The bounds check for the `start_points` argument is wrong if the `x` or `y` coordinates are not in ascending order:

```python
import matplotlib.pylab as plt
import numpy as np

u = np.ones((2, 2))
v = np.zeros((2, 2))
x = np.array([0, 10])

y = np.array([10, 0])  # (a)
#y = np.array([0, 10])  # (b)

plt.streamplot(x, y, u, v, start_points=[[5, 5]])  # (c)
#plt.streamplot(x, y, u, v)  # (d)

plt.show()
```

The code results in
```
ValueError: Starting point (5.0, 5.0) outside of data boundaries
```

I would have expected that a plot with one streamline is created. The error goes away **either** if I replace `(a)` with `(b)` **or** if I replace `(c)` with `(d)`. Thus if `start_points` are **not** given, plotting a descending range works totally fine.

**Matplotlib version**
<!--Please specify your platform and versions of the relevant libraries you are using:-->
  * Operating system: macOS 10.15.7
  * Matplotlib version: 3.3.2
  * Matplotlib backend (`print(matplotlib.get_backend())`): MacOSX
  * Python version: 3.8.6
  * Other libraries: numpy: 1.19.2

Matplotlib has been installed via `pip`.

Hints from issue discussion:
It looks like the code in `streamplot` (via `Grid`) assumes that `x` and `y` are monotonic, evenly spaced, and increasing.  We do a bunch of checks that it is actually a grid and evenly spaced in https://github.com/matplotlib/matplotlib/blob/eeef6e59386c6cebe2960213128c6a84906ddda9/lib/matplotlib/streamplot.py#L314-L351 but not that it is increasing.  I think the options here are:

 - document that x/y should be increasing and add a check to fail with a better error (my preference as it is the status quo)
 - find a way to make the the decreasing input working (either by normalizing the input or bound checking but this will require checking that it actually works correctly as I am not sure that we have traced through all of the integration code to make sure we do not make any assumptions that the step is positive)
I can take this up, looks pretty straightforward.
Should all the `Grid` objects have this check? Or just the `streamplot` function.
In other words, can there be a `Grid` object which does not adhere to having x/y in a decreasing fashion?
Since `matplotlib.streamplot.Grid` is only used for `streamplot()`, it's reasonable to implement the constraint in `Grid`.

Commit message: Add strictly increasing check, corresponding doc fix

Patch to review:
diff --git a/lib/matplotlib/streamplot.py b/lib/matplotlib/streamplot.py
index 12b1a54560e2..26c0e8635c46 100644
--- a/lib/matplotlib/streamplot.py
+++ b/lib/matplotlib/streamplot.py
@@ -25,8 +25,8 @@ def streamplot(axes, x, y, u, v, density=1, linewidth=None, color=None,
 
     Parameters
     ----------
-    x, y : 1D arrays
-        An evenly spaced grid.
+    x, y : 1D/2D arrays
+        Evenly spaced strictly increasing arrays to make a grid.
     u, v : 2D arrays
         *x* and *y*-velocities. The number of rows and columns must match
         the length of *y* and *x*, respectively.
@@ -333,6 +333,11 @@ def __init__(self, x, y):
         else:
             raise ValueError("'y' can have at maximum 2 dimensions")
 
+        if not (np.diff(x) > 0).all():
+            raise ValueError("'x' must be strictly increasing")
+        if not (np.diff(y) > 0).all():
+            raise ValueError("'y' must be strictly increasing")
+
         self.nx = len(x)
         self.ny = len(y)
 


Code review comments:
diff_hunk: @@ -114,3 +115,49 @@ def test_streamplot_limits():
     # datalim.
     assert_array_almost_equal(ax.dataLim.bounds, (20, 30, 15, 6),
                               decimal=1)
+
+
+def test_streamplot_grid():
+    u = np.ones((2, 2))
+    v = np.zeros((2, 2))
+
+    # Test for same rows and columns
+    x = np.array([[10, 20], [10, 30]])
+    y = np.array([[10, 10], [20, 20]])
+
+    with pytest.raises(ValueError, match="The rows of 'x' must be equal"):
+        plt.streamplot(x, y, u, v)
+
+    x = np.array([[10, 20], [10, 20]])
+    y = np.array([[10, 10], [20, 30]])
+
+    with pytest.raises(ValueError, match="The columns of 'y' must be equal"):
+        plt.streamplot(x, y, u, v)
+
+    x = np.array([[10, 20], [10, 20]])
+    y = np.array([[10, 10], [20, 20]])
+    plt.streamplot(x, y, u, v)
+
+    # Test for maximum dimensions
+    x = np.array([0, 10])
+    y = np.array([[[0, 10]]])
+
+    with pytest.raises(ValueError, match="'y' can have at maximum"
path: lib/matplotlib/tests/test_streamplot.py
text: @user1:
```suggestion
    with pytest.raises(ValueError, match="'y' can have at maximum "
```

diff_hunk: @@ -114,3 +115,53 @@ def test_streamplot_limits():
     # datalim.
     assert_array_almost_equal(ax.dataLim.bounds, (20, 30, 15, 6),
                               decimal=1)
+
+
+def test_streamplot_grid():
+    u = np.ones((2, 2))
+    v = np.zeros((2, 2))
+
+    # Test for same rows and columns
+    x = np.array([[10, 20], [10, 30]])
+    y = np.array([[10, 10], [20, 20]])
+
+    msg = "The rows of 'x' must be equal"
+    with pytest.raises(ValueError, match=msg):
path: lib/matplotlib/tests/test_streamplot.py
text: @user1:
I'd rather inline the message in the function. Having a separate variable is an unnecessary indirection.

@author:
Something like I just pushed?