Repository: nilearn/nilearn

Issue:
`_get_ticks_matplotlib()` is incorrect for integers with a small range
https://github.com/nilearn/nilearn/blob/6aaf8f94433dfaf2967d69c64c0f688d7e1c812d/nilearn/plotting/surf_plotting.py#L341-L350

So a "small range" appears to be defined as less than 5.

For larger ranges, we always get 5 ticks. For example:
```python
>>> ticks = _get_ticks_matplotlib(2.01, 7.02, "%i")
>>> 
>>> ticks
array([2.01  , 3.2625, 4.515 , 5.7675, 7.02  ])
>>> 
>>> len(ticks)
5
>>> 
```

For smaller ranges, we may get **5 ticks or more** (instead of **less** as documented):
```python
>>> ticks = _get_ticks_matplotlib(2.01, 6.99, "%i")
>>> 
>>> ticks
array([2.01, 3.01, 4.01, 5.01, 6.01, 7.01])
>>> 
>>> len(ticks)
6
>>> 
>>> 
>>> ticks = _get_ticks_matplotlib(2.01, 5.02, "%i")
>>> 
>>> ticks
array([2.01, 3.01, 4.01, 5.01, 6.01])
>>> 
>>> len(ticks)
5
>>> 
```



Discussion:

Resolving patch:
diff --git a/nilearn/plotting/surf_plotting.py b/nilearn/plotting/surf_plotting.py
--- a/nilearn/plotting/surf_plotting.py
+++ b/nilearn/plotting/surf_plotting.py
@@ -341,11 +341,11 @@ def _get_ticks_matplotlib(vmin, vmax, cbar_tick_format):
     n_ticks = 5
     # ...unless we are dealing with integers with a small range
     # in this case, we reduce the number of ticks
-    if cbar_tick_format == "%i" and vmax - vmin < n_ticks:
+    if cbar_tick_format == "%i" and vmax - vmin < n_ticks - 1:
         ticks = np.arange(vmin, vmax + 1)
-        n_ticks = len(ticks)
     else:
-        ticks = np.linspace(vmin, vmax, n_ticks)
+        # remove duplicate ticks when vmin == vmax, or almost
+        ticks = np.unique(np.linspace(vmin, vmax, n_ticks))
     return ticks
 
 


Test patch:
diff --git a/nilearn/plotting/tests/test_surf_plotting.py b/nilearn/plotting/tests/test_surf_plotting.py
--- a/nilearn/plotting/tests/test_surf_plotting.py
+++ b/nilearn/plotting/tests/test_surf_plotting.py
@@ -12,7 +12,8 @@
 from nilearn.plotting.img_plotting import MNI152TEMPLATE
 from nilearn.plotting.surf_plotting import (plot_surf, plot_surf_stat_map,
                                             plot_surf_roi, plot_img_on_surf,
-                                            plot_surf_contours)
+                                            plot_surf_contours,
+                                            _get_ticks_matplotlib)
 from nilearn.datasets import fetch_surf_fsaverage
 from nilearn.surface import load_surf_mesh
 from nilearn.surface.testing_utils import generate_surf
@@ -822,3 +823,23 @@ def test_plot_surf_contours_error():
             ValueError,
             match='Levels, labels, and colors argument need to be either the same length or None.'):
         plot_surf_contours(mesh, parcellation, levels=[1, 2], colors=['r'], labels=['1', '2'])
+
+
+@pytest.mark.parametrize("vmin,vmax,cbar_tick_format,expected", [
+    (0, 0, "%i", [0]),
+    (0, 3, "%i", [0, 1, 2, 3]),
+    (0, 4, "%i", [0, 1, 2, 3, 4]),
+    (1, 5, "%i", [1, 2, 3, 4, 5]),
+    (0, 5, "%i", [0, 1.25, 2.5, 3.75, 5]),
+    (0, 10, "%i", [0, 2.5, 5, 7.5, 10]),
+    (0, 0, "%.1f", [0]),
+    (0, 1, "%.1f", [0, 0.25, 0.5, 0.75, 1]),
+    (1, 2, "%.1f", [1, 1.25, 1.5, 1.75, 2]),
+    (1.1, 1.2, "%.1f", [1.1, 1.125, 1.15, 1.175, 1.2]),
+    (0, np.nextafter(0, 1), "%.1f", [0.e+000, 5.e-324]),
+])
+def test_get_ticks_matplotlib(vmin, vmax, cbar_tick_format, expected):
+    ticks = _get_ticks_matplotlib(vmin, vmax, cbar_tick_format)
+    assert 1 <= len(ticks) <= 5
+    assert ticks[0] == vmin and ticks[-1] == vmax
+    assert len(ticks) == len(expected) and (ticks == expected).all()