Repository: Ultraplot/UltraPlot

Issue:
minorticks cannot be setted in colorbar
The minorticks cannot be correctly setted in the colorbar.

m=ax.contourf(x1,t1,z,levels=np.linspace(0,500,200),extend='both')
ax.colorbar(m,ticks=np.arange(0,501,100),minorticks=20)

The minorticks in the plot are not 20, but depend on the "levels".

Discussion:
@cvanelteren: Hi there,  thanks for reporting. Please provide a minimal reproducible example -- it is hard to tell what the intentions are based on the snippets. 

I had a quick peek at the code to see what's going on. The levels will set the space of colors that you can extract from the `contourf` whereas the ticker on the colorbar will subdivide this space. For example

```python
import ultraplot as plt
import numpy as np


x = np.random.rand(10, 10)
fig, ax = plt.subplots()
ticks = np.linspace(0, 10)
y = np.array([0, 2, 10])
m = ax.contourf(x, x, x, levels=y, extend="both")
cb = ax.colorbar(m)
fig.show()
```
will produce a plot in which the levels are indicated by `y` which will generate a colorbar with 3 levels. 

If I would then set the ticks, it will attempt to fill the range set by `levels`, e.g. 

```python
x = np.random.rand(10, 10)
fig, ax = plt.subplots()
ticks = np.linspace(0, 10)
y = np.array([0, 2, 10])
m = ax.contourf(x, x, x, levels=y, extend="both")
cb = ax.colorbar(m, ticks = ticks)
fig.show()
```

This is behaving a it should I think, but I could be misinterpreting what you mean.@cmyangcn: Thank you! Take the demo in the documentation as an example (https://ultraplot.readthedocs.io/en/latest/colorbars_legends.html#Figure-wide-colorbars-and-legends).

```python
import ultraplot as uplt
import numpy as np

state = np.random.RandomState(51423)
fig, axs = uplt.subplots(ncols=3, nrows=3, refwidth=1.4)
for ax in axs:
    m = ax.pcolormesh(
        state.rand(20, 20), cmap="grays", levels=np.linspace(0, 1, 11), extend="both"
    )
fig.format(
    suptitle="Figure colorbars and legends demo",
    abc="a.",
    abcloc="l",
    xlabel="xlabel",
    ylabel="ylabel",
)
#fig.colorbar(m, label="column 1", ticks=0.5, loc="b", col=1)
#fig.colorbar(m, label="columns 2 and 3", ticks=0.2, loc="b", cols=(2, 3))
fig.colorbar(m, label="stacked colorbar", ticks=0.1, loc="b", minorticks=0.05)
#fig.colorbar(m, label="colorbar with length <1", ticks=0.1, loc="r", length=0.7)
```
The minorticks in the colorbar of the figure is not 0.05, but 0.1, as showing in the following image. When I use proplot, it's correct. 
![Image](https://github.com/user-attachments/assets/63695d01-105a-4809-8db7-2084983274e1)@cvanelteren: Can you checkout the PR

```
pip install git+https://github.com/cvanelteren/ultraplot.git@locator-fix
```

@cmyangcn: The changes in the locator-fix branch resolve the issue effectively. @cvanelteren: Fixed in main. 

Solution patch:
diff --git a/ultraplot/axes/base.py b/ultraplot/axes/base.py
index 28ece264..20f7e1b9 100644
--- a/ultraplot/axes/base.py
+++ b/ultraplot/axes/base.py
@@ -1159,6 +1159,7 @@ class Axes(maxes.Axes):
                     locator = mticker.FixedLocator(ticks)
                 else:
                     locator = pticker.DiscreteLocator(ticks)
+
             if tickminor and minorlocator is None:
                 minorlocator = pticker.DiscreteLocator(ticks, minor=True)
 
@@ -1213,6 +1214,10 @@ class Axes(maxes.Axes):
         # obj.minorlocator = minorlocator  # backwards compatibility
         obj.update_ticks = guides._update_ticks.__get__(obj)  # backwards compatible
         if minorlocator is not None:
+            # Note we make use of mpl's setters and getters
+            current = obj.minorlocator
+            if current != minorlocator:
+                obj.minorlocator = minorlocator
             obj.update_ticks()
         elif tickminor:
             obj.minorticks_on()


Test patch:
diff --git a/ultraplot/tests/test_colorbar.py b/ultraplot/tests/test_colorbar.py
index 9a02947a..bb165d85 100644
--- a/ultraplot/tests/test_colorbar.py
+++ b/ultraplot/tests/test_colorbar.py
@@ -251,3 +251,20 @@ def test_reversed_levels():
                 ax.pcolormesh(data, colorbar="b", **kw)
                 i += 1
     return fig
+
+
+@pytest.mark.mpl_image_compare
+def test_minor_override():
+    """Test minor ticks override."""
+    # Setting a custom minor tick should override the settings. Here we set the ticks to 1 and the minorticks to half that. We then check that the minor ticks are set correctly
+    data = state.rand(10, 10)
+    left, right, minor, n = 0, 1, 0.05, 11
+    levels = np.linspace(left, right, n)
+    fig, ax = uplt.subplots()
+    m = ax.pcolormesh(data, colorbar="b", levels=levels)
+    cax = ax.colorbar(m, minorticks=minor)
+    assert np.allclose(
+        cax.minorlocator.tick_values(left, right),
+        np.linspace(left - minor, right + minor, n * 2 + 1),
+    )
+    return fig