Repository: matplotlib/matplotlib

Issue:
[Bug]: Turning on minor gridlines in a multi-Axes Figure, created with subplots(), that contains >1 boxplot results in a ValueError
### Bug summary

When creating multiple Axes with boxplots, all on the same Figure, adding minor gridlines using `minorticks_on()` causes a traceback only if more than 1 Axes has boxplots on it. 

### Code for reproduction

```python
from random import random

import matplotlib.pyplot as plt


if __name__ == "__main__":
    fig, axes = plt.subplots(sharex=True, ncols=2, nrows=2)

    def values():
        return [random() for _ in range(9)]

    for x in range(3):
        """
        Code works as is, with commented lines not being run.
        When I uncomment any of the below boxplot() calls, I am getting the error
        mentioned in the bug report.
        """

        # axes[0, 0].boxplot(values(), positions=[x])
        # axes[0, 1].boxplot(values(), positions=[x])
        # axes[1, 0].boxplot(values(), positions=[x])
        axes[1, 1].boxplot(values(), positions=[x])

    for ax in axes.flatten():
        ax.grid(which="major", color="k", alpha=0.3)
        ax.grid(which="minor", color="k", alpha=0.1, linestyle="--")
        ax.minorticks_on()

    plt.show()
```


### Actual outcome

When I run this code exactly as I've pasted into the code for repro box, I get a Figure with 4 Axes. Each of them has major and minor gridlines (expected/desired), and one of them contains a set of boxplots.

When I run this and attempt to create boxplots on a second Axes within the figure, I get the following traceback when the Figure is attempted to be rendered:

```
Traceback (most recent call last):
  File "python3.9/site-packages/matplotlib/backend_bases.py", line 1226, in _on_timer
    ret = func(*args, **kwargs)
  File "python3.9/site-packages/matplotlib/backends/backend_macosx.py", line 68, in callback_func
    callback()
  File "python3.9/site-packages/matplotlib/backends/backend_macosx.py", line 88, in _draw_idle
    self.draw()
  File "python3.9/site-packages/matplotlib/backends/backend_macosx.py", line 50, in draw
    super().draw()
  File "ython3.9/site-packages/matplotlib/backends/backend_agg.py", line 400, in draw
    self.figure.draw(self.renderer)
  File "python3.9/site-packages/matplotlib/artist.py", line 95, in draw_wrapper
    result = draw(artist, renderer, *args, **kwargs)
  File "python3.9/site-packages/matplotlib/artist.py", line 72, in draw_wrapper
    return draw(artist, renderer)
  File "python3.9/site-packages/matplotlib/figure.py", line 3140, in draw
    mimage._draw_list_compositing_images(
  File "python3.9/site-packages/matplotlib/image.py", line 131, in _draw_list_compositing_images
    a.draw(renderer)
  File "python3.9/site-packages/matplotlib/artist.py", line 72, in draw_wrapper
    return draw(artist, renderer)
  File "python3.9/site-packages/matplotlib/axes/_base.py", line 3028, in draw
    self._update_title_position(renderer)
  File "python3.9/site-packages/matplotlib/axes/_base.py", line 2963, in _update_title_position
    bb = ax.xaxis.get_tightbbox(renderer)
  File "python3.9/site-packages/matplotlib/axis.py", line 1323, in get_tightbbox
    ticks_to_draw = self._update_ticks()
  File "python3.9/site-packages/matplotlib/axis.py", line 1270, in _update_ticks
    minor_locs = self.get_minorticklocs()
  File "python3.9/site-packages/matplotlib/axis.py", line 1489, in get_minorticklocs
    minor_locs = np.asarray(self.minor.locator())
  File "python3.9/site-packages/matplotlib/ticker.py", line 2955, in __call__
    locs = np.arange(tmin, tmax, minorstep) + t0
ValueError: arange: cannot compute length`
```

In addition, the 3 commented out `boxplot()` calls can be made successfully without throwing this error at render-time if you don't call `ax.minorticks_on()`, however AFAIK this is the way to ensure the minor gridlines are shown. 

### Expected outcome

A Figure with 4 Axes, each of which containing a series of boxplots with major and minor gridlines drawn as well.

### Additional information

Machine is a Mac with an Apple M1 CPU.

### Operating system

OS/X

### Matplotlib Version

3.7.1

### Matplotlib Backend

MacOSX

### Python version

3.9.6

### Jupyter version

_No response_

### Installation

pip


Discussion:
This is happening because boxplots set the Locator and Formatter of one axis to be FixedLocator/FixedFormatter, which it does without handling repeated values. this leads to multiple repeated values for the Major ticks, which then the `AutoMinorLocator` does `major[1] - major[0]` to get the spacing of the major, which it gets zero and then divides by 0, leading to the error (I'll note on current `main`, I was getting a slightly different error, but the cause is likely the same).

There are a couple of avenues forward, some more disruptive than others:

- Minimalist: call `np.unique` on the major tick locs in `AutoMinorLocator`
   - quick and dirty, but probably solves this particular usecase at least and does not change any other behavior.
   - or some other way of getting _unique_ major ticks
- Changing FixedLocator: consolidate/disallow repeated values
   - probably at least needs to go through deprecation period, though we could probably solve the internal usage in boxplot
   - Not clear to me how you keep the ordering compared to the fixed formatter
- Change how AutoMinorLocator works to do each segment between major ticks
   - Not clear (as discussed in todays community call) that major ticks are _ordered_ (which calls into question both existing minor behavior and would require sorting in this idea)
   - ticks are not necessarily uniformly spaced, which means that a minor spacing based on only the first two major ticks is perhaps not actually ideal
   - Maybe limit changes to fixed formatters
- Change boxplot to avoid repeated values
   - bit more narrow of a change, does not solve it generically

Short term/backwards compatible workaround:

if you loop over axes and have the positioning for loop on the inside, it will put the ticks in an order that is not a problem for `AutoMinorLocator`:

```python
for ax in axes.flat:
    for x in range(3):
       ax.boxplot(values(), positions=[x])
```

(or anything that does multiple boxplots on the _same_ call to `boxplot`)




Patch:
diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py
index 10ac8cad6ebc..1e9f0892c322 100644
--- a/lib/matplotlib/ticker.py
+++ b/lib/matplotlib/ticker.py
@@ -2903,7 +2903,7 @@ def __call__(self):
             _api.warn_external('AutoMinorLocator does not work on logarithmic scales')
             return []
 
-        majorlocs = self.axis.get_majorticklocs()
+        majorlocs = np.unique(self.axis.get_majorticklocs())
         if len(majorlocs) < 2:
             # Need at least two major ticks to find minor tick locations.
             # TODO: Figure out a way to still be able to display minor ticks with less


Test patch:
diff --git a/lib/matplotlib/tests/test_ticker.py b/lib/matplotlib/tests/test_ticker.py
index 12eafba9ea2b..fcfdf8ee92f5 100644
--- a/lib/matplotlib/tests/test_ticker.py
+++ b/lib/matplotlib/tests/test_ticker.py
@@ -1796,3 +1796,24 @@ def test_set_offset_string(formatter):
     assert formatter.get_offset() == ''
     formatter.set_offset_string('mpl')
     assert formatter.get_offset() == 'mpl'
+
+
+def test_minorticks_on_multi_fig():
+    """
+    Turning on minor gridlines in a multi-Axes Figure
+    that contains more than one boxplot and shares the x-axis
+    should not raise an exception.
+    """
+    fig, ax = plt.subplots()
+
+    ax.boxplot(np.arange(10), positions=[0])
+    ax.boxplot(np.arange(10), positions=[0])
+    ax.boxplot(np.arange(10), positions=[1])
+
+    ax.grid(which="major")
+    ax.grid(which="minor")
+    ax.minorticks_on()
+    fig.draw_without_rendering()
+
+    assert ax.get_xgridlines()
+    assert isinstance(ax.xaxis.get_minor_locator(), mpl.ticker.AutoMinorLocator)