Repository: holoviz/holoviews

Issue:
Color levels not respected
#### ALL software version info
holoviews                 1.13.5                   pypi_0    pypi
Python 3.8.5 (default, Sep  4 2020, 02:22:02) [Clang 10.0.0 ] :: Anaconda, Inc. on darwin

#### Description of expected behavior and the observed behavior

When passing colour_levels to an image opts `color_levels` I would expect them to be respected eg. the colormap is broken on the limits that are specified.

As you can see in the example below the color_levels specifies `0` as a limit however the purple step includes values greater than 0. (In the data provided this is off the eastern African coast)


#### Complete, minimal, self-contained example code that reproduces the issue

data here: https://github.com/CEREGE-CL/netcdf_editor_app/blob/main/data/standard.nc
```
import xarray as xr
import holoviews as hv
import numpy
hv.extension('bokeh')

ds = xr.open_dataset('data/standard.nc')
var = 'Z'

abs_max = numpy.abs((ds[var].min(), ds[var].max())).max()

delta = 1000
clevels = [-abs_max] + [*numpy.arange(0, abs_max, delta)[::-1] * -1, * numpy.arange(0, abs_max, delta)[1:]] + [abs_max]
print('color_levels: ', clevels)

img = hv.Image(
    ds[var],
    group="Map",
).opts(
    invert_axes=True, tools=['hover'], width=600, aspect=2, colorbar=True, cmap='Category10', clim=(-abs_max, abs_max), color_levels = clevels
)

img

```

#### Screenshots or screencasts of the bug in action

![Screenshot 2021-04-19 at 10 42 18](https://user-images.githubusercontent.com/18068320/115207713-1f2a4a00-a0fc-11eb-9b23-0d7f7a48b800.png)


Discussion:
philippjfr: Thanks @WesleyTheGeolien! Will have to update the related tests:

>FAIL: test_color_intervals_clipped (holoviews.tests.plotting.testplotutils.TestBokehPaletteUtils)
>FAIL: test_path_continuously_varying_color_op (holoviews.tests.plotting.bokeh.testpathplot.TestPathPlot)
philippjfr: Also not totally following the correctness of this fix, wouldn't it be more correct not to round and to use `floor` instead?
hoxbro: With this change the reported problem now looks like this:

![image](https://github.com/holoviz/holoviews/assets/19758978/ec443016-0f53-4997-b37f-e57be3c6c286)

Respecting the limit, and also better colorbar axis.

Patch:
diff --git a/holoviews/plotting/util.py b/holoviews/plotting/util.py
index 6dcd6bb2f..4cb3fc26d 100644
--- a/holoviews/plotting/util.py
+++ b/holoviews/plotting/util.py
@@ -989,8 +989,7 @@ def color_intervals(colors, levels, clip=None, N=255):
     if clip is not None:
         clmin, clmax = clip
         lidx = int(round(N*((clmin-cmin)/interval)))
-        uidx = int(round(N*((cmax-clmax)/interval)))
-        uidx = N-uidx
+        uidx = len(cmap) - int(round(N*((cmax-clmax)/interval)))
         if lidx == uidx:
             uidx = lidx+1
         cmap = cmap[lidx:uidx]


Test patch:
diff --git a/holoviews/tests/plotting/bokeh/test_pathplot.py b/holoviews/tests/plotting/bokeh/test_pathplot.py
index a3adc691a..8185cdcee 100644
--- a/holoviews/tests/plotting/bokeh/test_pathplot.py
+++ b/holoviews/tests/plotting/bokeh/test_pathplot.py
@@ -131,7 +131,7 @@ class TestPathPlot(TestBokehPlot):
         self.assertEqual(source.data['date'], np.array([date]*3))
         self.assertEqual(cmapper.low, 994)
         self.assertEqual(cmapper.high, 999)
-        self.assertEqual(cmapper.palette, colors[-1:])
+        self.assertEqual(np.unique(cmapper.palette), colors[-1:])
 
     def test_path_continuously_varying_alpha_op(self):
         xs = [1, 2, 3, 4]
diff --git a/holoviews/tests/plotting/test_plotutils.py b/holoviews/tests/plotting/test_plotutils.py
index 1f402691a..1651607db 100644
--- a/holoviews/tests/plotting/test_plotutils.py
+++ b/holoviews/tests/plotting/test_plotutils.py
@@ -598,7 +598,7 @@ class TestBokehPaletteUtils(ComparisonTestCase):
         colors = ['#5ebaff', '#00faf4', '#ffffcc', '#ffe775', '#ffc140', '#ff8f20', '#ff6060']
         cmap, lims = color_intervals(colors, levels, clip=(10, 90), N=100)
         self.assertEqual(cmap, ['#5ebaff', '#5ebaff', '#5ebaff', '#00faf4', '#00faf4',
-                                '#00faf4', '#00faf4', '#ffffcc'])
+                                '#00faf4', '#00faf4', '#ffffcc', '#ffffcc'])
         self.assertEqual(lims, (10, 90))