Repository: matplotlib/matplotlib

Issue:
[Bug]: _raw_ticker() istep
### Bug summary

Before 3.8.0, in matplotlib.ticker.MaxNLocator._raw_ticker(), there was a for-loop going over different possibilities that breaks out of the loop if a suitable condition was found, now it returns an empty list and fails.

### Code for reproduction

```python
# set up
import matplotlib as mpl
import numpy as np
mpl.rcParams['axes.autolimit_mode'] = 'round_numbers'
mnl = mpl.ticker.MaxNLocator()
mnl._nbins = 1
mnl._extended_steps = np.array([0.1,0.2,0.5,1.,2.,5.,10. ,20.])

# function that causes the bug to appear
mnl._raw_ticks(-9.4, 18.9)
```


### Actual outcome

IndexError: index 0 is out of bounds for axis 0 with size 0

### Expected outcome

not failing, it should just break at line 2135

### Additional information

https://github.com/matplotlib/matplotlib/blob/eb02b108ea181930ab37717c75e07ba792e01f1d/lib/matplotlib/ticker.py#L2109C7-L2109C8

Before 3.8.0, in matplotlib.ticker.MaxNLocator._raw_ticker(), there was a for-loop going over different possibilities that breaks out of the loop if a suitable condition was found, now it returns an empty list. Then, it searches for values in this list. As this list is empty, it gives an error.

This worked before 3.8.0. I'm currently preparing a new release of my own package 'pybinding', and I noticed that this error appeared in the newer versions (available on test.pypi.org).
The error originates from calling the function ax.autoscale_view() or calling other functions from Matplotlib that in turn call ax.autoscale_view().
I just discovered this, and I can probably work around this issue, but I though to inform you to know if this is wanted behavior.

### Operating system

_No response_

### Matplotlib Version

3.8.0

### Matplotlib Backend

_No response_

### Python version

_No response_

### Jupyter version

_No response_

### Installation

None


Discussion:
Since `_raw_ticks()` starts with an underscore we consider it private API that we can change in each release without notifying users, and we don't expect users to use it. Is there an example that uses only public documented code that produces the error you're talking about? otherwise I think we can close this.
Thanks for the fast reply.
Upon further inspection, it appears that there was some usage of other private functions and inheritance from Matplotlib in the background.
However, this bug can still be reached with specific combinations of public functions;
``` 
import matplotlib as mpl
mpl.rcParams['axes.autolimit_mode'] = 'round_numbers'
import matplotlib.pyplot as plt
from matplotlib.collections import CircleCollection
_, ax = plt.subplots()
col = CircleCollection(sizes=[10, 10], offsets=[[0, 0], [1, 0]], offset_transform=ax.transData)
ax.add_collection(col)
ax.set_aspect('equal')
ax.autoscale_view()
```
I'll probably find a workaround for this function in my program as the error only appears in very specific situations.
With the loop we had an implicit fallback case of none of the steps were "good", but with the vectorized approach we can end up with no "good" steps.  

Hopefully we only need a `if not `np.any(large_steps): ...` that reproduces what ever the old fallback was, but I could also see there being some funny behavior with the "round" steps.
These changes seem to be working in our local implementation that overrides `MaxNLocator._raw_ticks`:

```
    if any(large_steps):
        istep = np.nonzero(large_steps)[0][0]
    else:
        istep = len(steps) - 1
```

With this test:

```
def test_temp_raw_ticks():
    import matplotlib as mpl
    import numpy as np

    # valid values:  data | round_numbers
    mpl.rcParams["axes.autolimit_mode"] = "round_numbers"
    mnl = mpl.ticker.MaxNLocator()
    mnl._nbins = 1
    mnl._extended_steps = np.array([0.1, 0.2, 0.5, 1.0, 2.0, 5.0, 10.0, 20.0])
    actual_raw_ticks = mnl._raw_ticks(-9.4, 18.9)
    expected_raw_ticks = np.array([-10.0, 0.0, 10.0, 20.0])
    assert np.array_equal(actual_raw_ticks, expected_raw_ticks)
```

@ddeibert-nso any interest in submitting a PR (if you haven't already?)
I'll submit a PR.  I didn't want to take it on if someone else was already working on it.



Patch:
diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py
index f042372a7be9..2b00937f9e29 100644
--- a/lib/matplotlib/ticker.py
+++ b/lib/matplotlib/ticker.py
@@ -2137,7 +2137,10 @@ def _raw_ticks(self, vmin, vmax):
             large_steps = large_steps & (floored_vmaxs >= _vmax)
 
         # Find index of smallest large step
-        istep = np.nonzero(large_steps)[0][0]
+        if any(large_steps):
+            istep = np.nonzero(large_steps)[0][0]
+        else:
+            istep = len(steps) - 1
 
         # Start at smallest of the steps greater than the raw step, and check
         # if it provides enough ticks. If not, work backwards through


Test patch:
diff --git a/lib/matplotlib/tests/test_ticker.py b/lib/matplotlib/tests/test_ticker.py
index 36b83c95b3d3..ac68a5d90b14 100644
--- a/lib/matplotlib/tests/test_ticker.py
+++ b/lib/matplotlib/tests/test_ticker.py
@@ -130,6 +130,14 @@ def test_view_limits_round_numbers_with_offset(self):
             loc = mticker.MultipleLocator(base=3.147, offset=1.3)
             assert_almost_equal(loc.view_limits(-4, 4), (-4.994, 4.447))
 
+    def test_view_limits_single_bin(self):
+        """
+        Test that 'round_numbers' works properly with a single bin.
+        """
+        with mpl.rc_context({'axes.autolimit_mode': 'round_numbers'}):
+            loc = mticker.MaxNLocator(nbins=1)
+            assert_almost_equal(loc.view_limits(-2.3, 2.3), (-4, 4))
+
     def test_set_params(self):
         """
         Create multiple locator with 0.7 base, and change it to something else.