Repository: holoviz/holoviews
Pull request #6258 (2024-06-04 19:21:24)

fix vectorfield hover
```
import numpy as np
import holoviews as hv

hv.extension("bokeh")

hv.VectorField(
    ([-1, 0, 1, 2, 3], [-1, 0, 1, 2, 3], np.deg2rad([0, 45, 90, 180, 360]))
).opts(tools=["hover"])
```
Not sure there's a better solution, but I think it's better than no solution because hover is useful:

<img width="470" alt="image" src="https://github.com/holoviz/holoviews/assets/15331990/422b3127-a766-465c-a058-1fa443a46ad5">

Closes https://github.com/holoviz/holoviews/issues/3785

hoxbro: I'm not a fan of this solution. The main reason is you increase the data by a factor of 4.  

If we choose to go this way, we should at least limit this only to be done when we have an actual hover tool. By the looks of it, you send all the data to the front end no matter what. 
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/holoviz/holoviews/pull/6258?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=holoviz) Report
Attention: Patch coverage is `66.66667%` with `1 line` in your changes missing coverage. Please review.
> Project coverage is 88.44%. Comparing base [(`2ba3d78`)](https://app.codecov.io/gh/holoviz/holoviews/commit/2ba3d78f6c30340a607c3e63def5dfadc2c02f58?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=holoviz) to head [(`2a97bbb`)](https://app.codecov.io/gh/holoviz/holoviews/commit/2a97bbb3bff05f2af8041651c526060401f2f9a6?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=holoviz).
> Report is 9 commits behind head on main.

| [Files](https://app.codecov.io/gh/holoviz/holoviews/pull/6258?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=holoviz) | Patch % | Lines |
|---|---|---|
| [holoviews/plotting/bokeh/chart.py](https://app.codecov.io/gh/holoviz/holoviews/pull/6258?src=pr&el=tree&filepath=holoviews%2Fplotting%2Fbokeh%2Fchart.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=holoviz#diff-aG9sb3ZpZXdzL3Bsb3R0aW5nL2Jva2VoL2NoYXJ0LnB5) | 66.66% | [1 Missing :warning: ](https://app.codecov.io/gh/holoviz/holoviews/pull/6258?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=holoviz) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #6258      +/-   ##
==========================================
+ Coverage   88.39%   88.44%   +0.05%     
==========================================
  Files         323      323              
  Lines       67620    67641      +21     
==========================================
+ Hits        59770    59823      +53     
+ Misses       7850     7818      -32     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/holoviz/holoviews/pull/6258?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=holoviz).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=holoviz).


Patch:
diff --git a/doc/install.md b/doc/install.md
index fd1ca54b9..f442bf203 100644
--- a/doc/install.md
+++ b/doc/install.md
@@ -8,12 +8,10 @@ provided by the [Anaconda](https://docs.anaconda.com/anaconda/install/)
 or [Miniconda](https://docs.conda.io/en/latest/miniconda.html)
 scientific Python distributions:
 
-    conda install -c pyviz holoviews
+    conda install holoviews
 
-This recommended installation includes the default
-[Matplotlib](http://matplotlib.org) plotting library backend, the more
-interactive [Bokeh](http://bokeh.pydata.org) plotting library backend,
-and the [Jupyter Notebook](http://jupyter.org).
+This installation includes the default [Matplotlib](http://matplotlib.org)
+plotting library backend, the more interactive [Bokeh](http://bokeh.pydata.org) plotting library backend.
 
 A similar set of packages can be installed using `pip`, if that command
 is available on your system:
diff --git a/holoviews/core/data/cudf.py b/holoviews/core/data/cudf.py
index 72fafab30..0cd8f6d0e 100644
--- a/holoviews/core/data/cudf.py
+++ b/holoviews/core/data/cudf.py
@@ -4,6 +4,7 @@ from itertools import product
 
 import numpy as np
 import pandas as pd
+from pandas.api.types import is_numeric_dtype
 
 from .. import util
 from ..dimension import dimension_name
@@ -285,7 +286,11 @@ class cuDFInterface(PandasInterface):
             grouped = reindexed.groupby(cols, sort=False)
             if not hasattr(grouped, agg):
                 raise ValueError(f'{agg} aggregation is not supported on cudf DataFrame.')
-            df = getattr(grouped, agg)().reset_index()
+            numeric_cols = [
+                c for c, d in zip(reindexed.columns, reindexed.dtypes)
+                if is_numeric_dtype(d) and c not in cols
+            ]
+            df = getattr(grouped[numeric_cols], agg)().reset_index()
         else:
             agg_map = {'amin': 'min', 'amax': 'max', 'size': 'count'}
             agg = agg_map.get(agg, agg)
diff --git a/holoviews/core/data/pandas.py b/holoviews/core/data/pandas.py
index ea552194f..a097bdf95 100644
--- a/holoviews/core/data/pandas.py
+++ b/holoviews/core/data/pandas.py
@@ -195,8 +195,7 @@ class PandasInterface(Interface, PandasAPI):
         else:
             column = dataset.data[dimension.name]
         if column.dtype.kind == 'O':
-            if (not isinstance(dataset.data, pd.DataFrame) or
-                util.pandas_version < Version('0.17.0')):
+            if not isinstance(dataset.data, pd.DataFrame):
                 column = column.sort(inplace=False)
             else:
                 column = column.sort_values()
@@ -221,9 +220,7 @@ class PandasInterface(Interface, PandasAPI):
 
     @classmethod
     def concat_fn(cls, dataframes, **kwargs):
-        if util.pandas_version >= Version('0.23.0'):
-            kwargs['sort'] = False
-        return pd.concat(dataframes, **kwargs)
+        return pd.concat(dataframes, sort=False, **kwargs)
 
 
     @classmethod
@@ -346,8 +343,7 @@ class PandasInterface(Interface, PandasAPI):
             by = []
         cols = [dataset.get_dimension(d, strict=True).name for d in by]
 
-        if (not isinstance(dataset.data, pd.DataFrame) or
-            util.pandas_version < Version('0.17.0')):
+        if not isinstance(dataset.data, pd.DataFrame):
             return dataset.data.sort(columns=cols, ascending=not reverse)
         return dataset.data.sort_values(by=cols, ascending=not reverse)
 
diff --git a/holoviews/core/util.py b/holoviews/core/util.py
index 2e2732bfb..dadba0c08 100644
--- a/holoviews/core/util.py
+++ b/holoviews/core/util.py
@@ -23,6 +23,9 @@ import numpy as np
 import pandas as pd
 import param
 from packaging.version import Version
+from pandas.core.arrays.masked import BaseMaskedArray
+from pandas.core.dtypes.dtypes import DatetimeTZDtype
+from pandas.core.dtypes.generic import ABCExtensionArray, ABCIndex, ABCSeries
 
 # Python 2 builtins
 basestring = str
@@ -31,59 +34,22 @@ unicode = str
 cmp = lambda a, b: (a>b)-(a<b)
 
 get_keywords = operator.attrgetter('varkw')
-generator_types = (zip, range, types.GeneratorType)
-numpy_version = Version(np.__version__)
-param_version = Version(param.__version__)
-
-datetime_types = (np.datetime64, dt.datetime, dt.date, dt.time)
-timedelta_types = (np.timedelta64, dt.timedelta,)
-arraylike_types = (np.ndarray,)
-masked_types = ()
-
-anonymous_dimension_label = '_'
-
-# Argspec was removed in Python 3.11
-ArgSpec = namedtuple('ArgSpec', 'args varargs keywords defaults')
-
-_NP_SIZE_LARGE = 1_000_000
-_NP_SAMPLE_SIZE = 1_000_000
-_PANDAS_ROWS_LARGE = 1_000_000
-_PANDAS_SAMPLE_SIZE = 1_000_000
 
+# Versions
 numpy_version = Version(Version(np.__version__).base_version)
+param_version = Version(param.__version__)
+pandas_version = Version(pd.__version__)
+
 NUMPY_GE_200 = numpy_version >= Version("2")
 
-pandas_version = Version(pd.__version__)
-try:
-    if pandas_version >= Version('1.3.0'):
-        from pandas.core.dtypes.dtypes import DatetimeTZDtype as DatetimeTZDtypeType
-        from pandas.core.dtypes.generic import (
-            ABCIndex as ABCIndexClass,
-            ABCSeries,
-        )
-    elif pandas_version >= Version('0.24.0'):
-        from pandas.core.dtypes.dtypes import DatetimeTZDtype as DatetimeTZDtypeType
-        from pandas.core.dtypes.generic import ABCIndexClass, ABCSeries
-    elif pandas_version > Version('0.20.0'):
-        from pandas.core.dtypes.dtypes import DatetimeTZDtypeType
-        from pandas.core.dtypes.generic import ABCIndexClass, ABCSeries
-    else:
-        from pandas.types.dtypes import DatetimeTZDtypeType
-        from pandas.types.dtypes.generic import ABCIndexClass, ABCSeries
-    pandas_datetime_types = (pd.Timestamp, DatetimeTZDtypeType, pd.Period)
-    pandas_timedelta_types = (pd.Timedelta,)
-    datetime_types = datetime_types + pandas_datetime_types
-    timedelta_types = timedelta_types + pandas_timedelta_types
-    arraylike_types = arraylike_types + (ABCSeries, ABCIndexClass)
-    if pandas_version > Version('0.23.0'):
-        from pandas.core.dtypes.generic import ABCExtensionArray
-        arraylike_types = arraylike_types + (ABCExtensionArray,)
-    if pandas_version > Version('1.0'):
-        from pandas.core.arrays.masked import BaseMaskedArray
-        masked_types = (BaseMaskedArray,)
-except Exception as e:
-    param.main.param.warning('pandas could not register all extension types '
-                                f'imports failed with the following error: {e}')
+# Types
+generator_types = (zip, range, types.GeneratorType)
+pandas_datetime_types = (pd.Timestamp, DatetimeTZDtype, pd.Period)
+pandas_timedelta_types = (pd.Timedelta,)
+datetime_types = (np.datetime64, dt.datetime, dt.date, dt.time, *pandas_datetime_types)
+timedelta_types = (np.timedelta64, dt.timedelta, *pandas_timedelta_types)
+arraylike_types = (np.ndarray, ABCSeries, ABCIndex, ABCExtensionArray)
+masked_types = (BaseMaskedArray,)
 
 try:
     import cftime
@@ -93,6 +59,15 @@ except ImportError:
     cftime_types = ()
 _STANDARD_CALENDARS = {'standard', 'gregorian', 'proleptic_gregorian'}
 
+anonymous_dimension_label = '_'
+
+# Argspec was removed in Python 3.11
+ArgSpec = namedtuple('ArgSpec', 'args varargs keywords defaults')
+
+_NP_SIZE_LARGE = 1_000_000
+_NP_SAMPLE_SIZE = 1_000_000
+_PANDAS_ROWS_LARGE = 1_000_000
+_PANDAS_SAMPLE_SIZE = 1_000_000
 
 # To avoid pandas warning about using DataFrameGroupBy.function
 # introduced in Pandas 2.1.
@@ -890,10 +865,7 @@ def isnat(val):
     """
     if (isinstance(val, (np.datetime64, np.timedelta64)) or
         (isinstance(val, np.ndarray) and val.dtype.kind == 'M')):
-        if numpy_version >= Version('1.13'):
-            return np.isnat(val)
-        else:
-            return val.view('i8') == nat_as_integer
+        return np.isnat(val)
     elif val is pd.NaT:
         return True
     elif isinstance(val, pandas_datetime_types+pandas_timedelta_types):
@@ -928,19 +900,16 @@ def isfinite(val):
         elif val.dtype.kind in 'US':
             return ~pd.isna(val)
         finite = np.isfinite(val)
-        if pandas_version >= Version('1.0.0'):
-            finite &= ~pd.isna(val)
+        finite &= ~pd.isna(val)
         return finite
     elif isinstance(val, datetime_types+timedelta_types):
         return not isnat(val)
     elif isinstance(val, (str, bytes)):
         return True
     finite = np.isfinite(val)
-    if pandas_version >= Version('1.0.0'):
-        if finite is pd.NA:
-            return False
-        return finite & ~pd.isna(np.asarray(val))
-    return finite
+    if finite is pd.NA:
+        return False
+    return finite & ~pd.isna(np.asarray(val))
 
 
 def isdatetime(value):
diff --git a/holoviews/operation/timeseries.py b/holoviews/operation/timeseries.py
index c076e9db0..e6713c01e 100644
--- a/holoviews/operation/timeseries.py
+++ b/holoviews/operation/timeseries.py
@@ -1,11 +1,10 @@
 import numpy as np
 import pandas as pd
 import param
-from packaging.version import Version
 
 from ..core import Element, Operation
 from ..core.data import PandasInterface
-from ..core.util import _PANDAS_FUNC_LOOKUP, pandas_version
+from ..core.util import _PANDAS_FUNC_LOOKUP
 from ..element import Scatter
 
 
@@ -51,8 +50,7 @@ class rolling(Operation,RollingBase):
         df = df.set_index(xdim).rolling(win_type=self.p.window_type,
                                         **self._roll_kwargs())
         if self.p.window_type is None:
-            kwargs = {'raw': True} if pandas_version >= Version('0.23.0') else {}
-            rolled = df.apply(self.p.function, **kwargs)
+            rolled = df.apply(self.p.function, raw=True)
         elif self.p.function is np.mean:
             rolled = df.mean()
         elif self.p.function is np.sum:
diff --git a/holoviews/plotting/bokeh/chart.py b/holoviews/plotting/bokeh/chart.py
index fdd7cd97a..65762e73b 100644
--- a/holoviews/plotting/bokeh/chart.py
+++ b/holoviews/plotting/bokeh/chart.py
@@ -318,7 +318,20 @@ class VectorFieldPlot(ColorbarPlot):
         elif cdim:
             color = cdata.get(cdim.name)
 
-        data = {'x0': x0s, 'x1': x1s, 'y0': y0s, 'y1': y1s}
+        data = {
+            'x0': x0s,
+            'x1': x1s,
+            'y0': y0s,
+            'y1': y1s,
+        }
+        if 'hover' in self.handles:
+            data.update({
+                # tile to match the length of the segments
+                "x": np.tile(xs, 3),
+                "y": np.tile(ys, 3),
+                "Angle": np.tile(rads, 3),
+                "Magnitude": np.tile(lens, 3)
+            })
         mapping = dict(x0='x0', x1='x1', y0='y0', y1='y1')
         if cdim and color is not None:
             data[cdim.name] = color
@@ -327,7 +340,6 @@ class VectorFieldPlot(ColorbarPlot):
         return (data, mapping, style)
 
 
-
 class CurvePlot(ElementPlot):
 
     padding = param.ClassSelector(default=(0, 0.1), class_=(int, float, tuple))
diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py
index e46be7150..48138bfa6 100644
--- a/holoviews/plotting/bokeh/element.py
+++ b/holoviews/plotting/bokeh/element.py
@@ -76,6 +76,7 @@ from .util import (
     get_axis_class,
     get_scale,
     get_tab_title,
+    get_ticker_axis_props,
     glyph_order,
     hold_policy,
     match_ax_type,
@@ -246,6 +247,16 @@ class ElementPlot(BokehPlot, GenericElementPlot):
     hover_mode = param.ObjectSelector(default='mouse', objects=['mouse', 'vline', 'hline'], doc="""
         The hover mode determines how the hover tool is activated.""")
 
+    xticks = param.ClassSelector(class_=(int, list, tuple, np.ndarray, Ticker), default=None, doc="""
+        Ticks along x-axis specified as an integer, explicit list of
+        tick locations, or bokeh Ticker object. If set to None default
+        bokeh ticking behavior is applied.""")
+
+    yticks = param.ClassSelector(class_=(int, list, tuple, np.ndarray, Ticker), default=None, doc="""
+        Ticks along y-axis specified as an integer, explicit list of
+        tick locations, or bokeh Ticker object. If set to None default
+        bokeh ticking behavior is applied.""")
+
     toolbar = param.ObjectSelector(default='right',
                                    objects=["above", "below",
                                             "left", "right", "disable", None],
@@ -1051,29 +1062,9 @@ class ElementPlot(BokehPlot, GenericElementPlot):
             if rotation:
                 axis_props['major_label_orientation'] = np.radians(rotation)
             ticker = self.xticks if axis == 'x' else self.yticks
-            if isinstance(ticker, np.ndarray):
-                ticker = list(ticker)
-            if isinstance(ticker, Ticker):
-                axis_props['ticker'] = ticker
-            elif isinstance(ticker, int):
-                axis_props['ticker'] = BasicTicker(desired_num_ticks=ticker)
-            elif isinstance(ticker, (tuple, list)):
-                if all(isinstance(t, tuple) for t in ticker):
-                    ticks, labels = zip(*ticker)
-                    # Ensure floats which are integers are serialized as ints
-                    # because in JS the lookup fails otherwise
-                    ticks = [int(t) if isinstance(t, float) and t.is_integer() else t
-                             for t in ticks]
-                    labels = [l if isinstance(l, str) else str(l)
-                              for l in labels]
-                else:
-                    ticks, labels = ticker, None
-                if ticks and util.isdatetime(ticks[0]):
-                    ticks = [util.dt_to_int(tick, 'ms') for tick in ticks]
-                axis_props['ticker'] = FixedTicker(ticks=ticks)
-                if labels is not None:
-                    axis_props['major_label_overrides'] = dict(zip(ticks, labels))
-            elif self._subcoord_overlaid and axis == 'y':
+            if not (self._subcoord_overlaid and axis == 'y'):
+                axis_props.update(get_ticker_axis_props(ticker))
+            else:
                 ticks, labels = [], []
                 idx = 0
                 for el, sp in zip(self.current_frame, self.subplots.values()):
@@ -2470,6 +2461,11 @@ class ColorbarPlot(ElementPlot):
         #FFFFFFFF or a length 3 or length 4 tuple specifying values in
         the range 0-1 or a named HTML color.""")
 
+    cticks = param.ClassSelector(class_=(int, list, tuple, np.ndarray, Ticker), default=None, doc="""
+        Ticks along colorbar-axis specified as an integer, explicit list of
+        tick locations, or bokeh Ticker object. If set to None default
+        bokeh ticking behavior is applied.""")
+
     logz = param.Boolean(default=False, doc="""
          Whether to apply log scaling to the z-axis.""")
 
@@ -2513,6 +2509,9 @@ class ColorbarPlot(ElementPlot):
         if self.cformatter is not None:
             self.colorbar_opts.update({'formatter': wrap_formatter(self.cformatter, 'c')})
 
+        if self.cticks is not None:
+            self.colorbar_opts.update(get_ticker_axis_props(self.cticks))
+
         for tk in ['cticks', 'ticks']:
             ticksize = self._fontsize(tk, common=False).get('fontsize')
             if ticksize is not None:
diff --git a/holoviews/plotting/bokeh/util.py b/holoviews/plotting/bokeh/util.py
index 9b5a662b9..5d25b2349 100644
--- a/holoviews/plotting/bokeh/util.py
+++ b/holoviews/plotting/bokeh/util.py
@@ -39,12 +39,14 @@ from bokeh.models import (
 )
 from bokeh.models.formatters import PrintfTickFormatter, TickFormatter
 from bokeh.models.scales import CategoricalScale, LinearScale, LogScale
+from bokeh.models.tickers import BasicTicker, FixedTicker, Ticker
 from bokeh.models.widgets import DataTable, Div
 from bokeh.plotting import figure
 from bokeh.themes import built_in_themes
 from bokeh.themes.theme import Theme
 from packaging.version import Version
 
+from ...core import util
 from ...core.layout import Layout
 from ...core.ndmapping import NdMapping
 from ...core.overlay import NdOverlay, Overlay
@@ -1167,3 +1169,30 @@ def dtype_fix_hook(plot, element):
                 for k, v in data.items():
                     if hasattr(v, "dtype") and v.dtype.kind == "U":
                         data[k] = v.tolist()
+
+
+def get_ticker_axis_props(ticker):
+    axis_props = {}
+    if isinstance(ticker, np.ndarray):
+        ticker = list(ticker)
+    if isinstance(ticker, Ticker):
+        axis_props['ticker'] = ticker
+    elif isinstance(ticker, int):
+        axis_props['ticker'] = BasicTicker(desired_num_ticks=ticker)
+    elif isinstance(ticker, (tuple, list)):
+        if all(isinstance(t, tuple) for t in ticker):
+            ticks, labels = zip(*ticker)
+            # Ensure floats which are integers are serialized as ints
+            # because in JS the lookup fails otherwise
+            ticks = [int(t) if isinstance(t, float) and t.is_integer() else t
+                        for t in ticks]
+            labels = [l if isinstance(l, str) else str(l)
+                        for l in labels]
+        else:
+            ticks, labels = ticker, None
+        if ticks and util.isdatetime(ticks[0]):
+            ticks = [util.dt_to_int(tick, 'ms') for tick in ticks]
+        axis_props['ticker'] = FixedTicker(ticks=ticks)
+        if labels is not None:
+            axis_props['major_label_overrides'] = dict(zip(ticks, labels))
+    return axis_props
diff --git a/holoviews/plotting/plot.py b/holoviews/plotting/plot.py
index 4bd69f675..cc9bdbc48 100644
--- a/holoviews/plotting/plot.py
+++ b/holoviews/plotting/plot.py
@@ -1132,15 +1132,13 @@ class GenericElementPlot(DimensionedPlot):
     yrotation = param.Integer(default=None, bounds=(0, 360), doc="""
         Rotation angle of the yticks.""")
 
-    xticks = param.Parameter(default=None, doc="""
+    xticks = param.ClassSelector(class_=(int, list, tuple, np.ndarray), default=None, doc="""
         Ticks along x-axis specified as an integer, explicit list of
-        tick locations, or bokeh Ticker object. If set to None default
-        bokeh ticking behavior is applied.""")
+        tick locations. If set to None default ticking behavior is applied.""")
 
-    yticks = param.Parameter(default=None, doc="""
+    yticks = param.ClassSelector(class_=(int, list, tuple, np.ndarray), default=None, doc="""
         Ticks along y-axis specified as an integer, explicit list of
-        tick locations, or bokeh Ticker object. If set to None
-        default bokeh ticking behavior is applied.""")
+        tick locations. If set to None default ticking behavior is applied.""")
 
     # A dictionary mapping of the plot methods used to draw the
     # glyphs corresponding to the ElementPlot, can support two
diff --git a/holoviews/plotting/renderer.py b/holoviews/plotting/renderer.py
index 04f86b5b4..7d8fcb97f 100644
--- a/holoviews/plotting/renderer.py
+++ b/holoviews/plotting/renderer.py
@@ -16,21 +16,20 @@ from bokeh.io import curdoc
 from bokeh.resources import CDN, INLINE
 from packaging.version import Version
 from panel import config
-from panel.io.notebook import ipywidget, load_notebook, render_mimebundle, render_model
+from panel.io.notebook import (
+    JupyterCommManagerBinary,
+    ipywidget,
+    load_notebook,
+    render_mimebundle,
+    render_model,
+)
 from panel.io.state import state
 from panel.models.comm_manager import CommManager as PnCommManager
 from panel.pane import HoloViews as HoloViewsPane
 from panel.viewable import Viewable
 from panel.widgets.player import PlayerBase
-from pyviz_comms import CommManager
-
-try:
-    # Added in Panel 1.0 to support JS -> Python binary comms
-    from panel.io.notebook import JupyterCommManagerBinary as JupyterCommManager
-except ImportError:
-    from pyviz_comms import JupyterCommManager
-
 from param.parameterized import bothmethod
+from pyviz_comms import CommManager
 
 from ..core import AdjointLayout, DynamicMap, HoloMap, Layout
 from ..core.data import disable_pipeline
@@ -665,8 +664,8 @@ class Renderer(Exporter):
             if not ip or not hasattr(ip, 'kernel'):
                 return
             cls.notebook_context = True
-            cls.comm_manager = JupyterCommManager
-            state._comm_manager = JupyterCommManager
+            cls.comm_manager = JupyterCommManagerBinary
+            state._comm_manager = JupyterCommManagerBinary
 
     @classmethod
     def _delete_plot(cls, plot_id):
diff --git a/holoviews/util/transform.py b/holoviews/util/transform.py
index 69965af1d..0298aa3fa 100644
--- a/holoviews/util/transform.py
+++ b/holoviews/util/transform.py
@@ -170,18 +170,7 @@ def categorize(values, categories, default=None):
     return result
 
 
-if hasattr(np, 'isin'):
-    isin = _maybe_map(np.isin)
-else:
-    # for 1.4 <= numpy < 1.13; in1d() available since 1.4
-    def _isin(element, test_elements, assume_unique=False, invert=False):
-        # from 1.13's numpy.lib.arraysetops
-        element = np.asarray(element)
-        return np.in1d(element, test_elements, assume_unique=assume_unique,
-                invert=invert).reshape(element.shape)
-    isin = _maybe_map(_isin)
-    del _isin
-
+isin = _maybe_map(np.isin)
 digitize = _maybe_map(np.digitize)
 astype = _maybe_map(np.asarray)
 round_ = _maybe_map(np.round)
diff --git a/pixi.toml b/pixi.toml
index bf8954c53..8a6ac192b 100644
--- a/pixi.toml
+++ b/pixi.toml
@@ -26,6 +26,7 @@ build = ["py311", "build"]
 lint = ["py311", "lint"]
 
 [dependencies]
+bokeh = ">=3.1"
 colorcet = "*"
 numpy = ">=1.0"
 packaging = "*"
@@ -35,10 +36,7 @@ param = ">=2.0,<3.0"
 pip = "*"
 pyviz_comms = ">=2.1"
 # Recommended
-bokeh = ">=3.1"
-ipython = ">=5.4.0"
 matplotlib-base = ">=3"
-notebook = "*"
 plotly = ">=4.0"
 
 [feature.py39.dependencies]
@@ -58,6 +56,7 @@ cftime = "*"
 dask-core = "*"
 datashader = ">=0.11.1"
 ffmpeg = "*"
+ipython = ">=5.4.0"
 netcdf4 = "*"
 networkx = "*"
 notebook = "*"
@@ -133,9 +132,10 @@ NUMBA_CUDA_LOW_OCCUPANCY_WARNINGS = "0"
 
 [feature.test-gpu.dependencies]
 cuda-version = "12.2.*"
-cudf = "24.04.*"
+cudf = "24.06.*"
 cupy = "*"
 cuspatial = "*"
+librmm = { version = "*", channel = "rapidsai" }
 rmm = { version = "*", channel = "rapidsai" }
 
 [feature.test-gpu.tasks]
diff --git a/pyproject.toml b/pyproject.toml
index 319326779..489cf75a5 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -30,13 +30,14 @@ classifiers = [
     "Topic :: Software Development :: Libraries",
 ]
 dependencies = [
-    "param >=2.0,<3.0",
-    "numpy >=1.0",
-    "pyviz_comms >=2.1",
-    "panel >=1.0",
+    "bokeh >=3.1",
     "colorcet",
+    "numpy >=1.21",
     "packaging",
-    "pandas >=0.20.0",
+    "pandas >=1.3",
+    "panel >=1.0",
+    "param >=2.0,<3.0",
+    "pyviz_comms >=2.1",
 ]
 
 [project.urls]
@@ -45,7 +46,7 @@ Source = "https://github.com/holoviz/holoviews"
 HoloViz = "https://holoviz.org/"
 
 [project.optional-dependencies]
-recommended = ["ipython >=5.4.0", "notebook", "matplotlib >=3", "bokeh >=3.1", "plotly >=4.0"]
+recommended = ["matplotlib >=3", "plotly >=4.0"]
 tests = ["pytest", "pytest-rerunfailures"]
 
 [project.scripts]
diff --git a/scripts/conda/recipe/meta.yaml b/scripts/conda/recipe/meta.yaml
index 5b1968dee..8b94a418c 100644
--- a/scripts/conda/recipe/meta.yaml
+++ b/scripts/conda/recipe/meta.yaml
@@ -30,7 +30,9 @@ requirements:
     - {{ dep }}
     {% endfor %}
     {% for dep in project['optional-dependencies']['recommended'] %}
-    - {{ dep }}
+    {% if dep.startswith('matplotlib') %}
+    - {{ dep.replace('matplotlib', 'matplotlib-base') }}
+    {% endif %}
     {% endfor %}
 
 test:


Test patch:
diff --git a/holoviews/tests/plotting/bokeh/test_elementplot.py b/holoviews/tests/plotting/bokeh/test_elementplot.py
index 9cef2f3b6..af8e52b24 100644
--- a/holoviews/tests/plotting/bokeh/test_elementplot.py
+++ b/holoviews/tests/plotting/bokeh/test_elementplot.py
@@ -885,6 +885,51 @@ class TestColorbarPlot(LoggingComparisonTestCase, TestBokehPlot):
         self.assertEqual(cmapper.factors, ['0', '1', '2', '3'])
         self.assertEqual(cmapper.palette, ['blue', 'red', 'green', 'purple'])
 
+    def test_cticks_int(self):
+        img = Image(np.array([[0, 1], [2, 3]])).opts(cticks=3, colorbar=True)
+        plot = bokeh_renderer.get_plot(img)
+        colorbar = plot.handles["colorbar"]
+        ticker = colorbar.ticker
+        assert ticker.desired_num_ticks == 3
+
+    def test_cticks_list(self):
+        img = Image(np.array([[0, 1], [2, 3]])).opts(cticks=[1, 2], colorbar=True)
+        plot = bokeh_renderer.get_plot(img)
+        colorbar = plot.handles["colorbar"]
+        ticker = colorbar.ticker
+        assert ticker.ticks == [1, 2]
+
+    def test_cticks_tuple(self):
+        img = Image(np.array([[0, 1], [2, 3]])).opts(cticks=(1, 2), colorbar=True)
+        plot = bokeh_renderer.get_plot(img)
+        colorbar = plot.handles["colorbar"]
+        ticker = colorbar.ticker
+        assert ticker.ticks == (1, 2)
+
+    def test_cticks_np_array(self):
+        img = Image(np.array([[0, 1], [2, 3]])).opts(cticks=np.array([1, 2]), colorbar=True)
+        plot = bokeh_renderer.get_plot(img)
+        colorbar = plot.handles["colorbar"]
+        ticker = colorbar.ticker
+        assert ticker.ticks == [1, 2]
+
+    def test_cticks_labels(self):
+        img = Image(np.array([[0, 1], [2, 3]])).opts(cticks=[(1, "A"), (2, "B")], colorbar=True)
+        plot = bokeh_renderer.get_plot(img)
+        colorbar = plot.handles["colorbar"]
+        assert colorbar.major_label_overrides == {1: "A", 2: "B"}
+        ticker = colorbar.ticker
+        assert ticker.ticks == [1, 2]
+
+    def test_cticks_ticker(self):
+        img = Image(np.array([[0, 1], [2, 3]])).opts(
+            cticks=FixedTicker(ticks=[0, 1]), colorbar=True
+        )
+        plot = bokeh_renderer.get_plot(img)
+        colorbar = plot.handles["colorbar"]
+        ticker = colorbar.ticker
+        assert ticker.ticks == [0, 1]
+
 
 class TestOverlayPlot(TestBokehPlot):
 
diff --git a/holoviews/tests/plotting/bokeh/test_vectorfieldplot.py b/holoviews/tests/plotting/bokeh/test_vectorfieldplot.py
index d4a71987c..e8eb91e48 100644
--- a/holoviews/tests/plotting/bokeh/test_vectorfieldplot.py
+++ b/holoviews/tests/plotting/bokeh/test_vectorfieldplot.py
@@ -83,3 +83,17 @@ class TestVectorFieldPlot(TestBokehPlot):
             "for 'line_color' option and declare a color_index; ignoring the color_index.\n"
         )
         self.assertEqual(log_msg, warning)
+
+    def test_vectorfield_no_hover_columns(self):
+        vectorfield = VectorField([(0, 0, 0), (0, 1, 1), (0, 2, 2)],
+                        vdims='color').opts(tools=[])
+        plot = bokeh_renderer.get_plot(vectorfield)
+        keys = sorted(plot.handles["cds"].data)
+        assert keys == ["x0", "x1", "y0", "y1"]
+
+    def test_vectorfield_hover_columns(self):
+        vectorfield = VectorField([(0, 0, 0), (0, 1, 1), (0, 2, 2)],
+                        vdims='color').opts(tools=["hover"])
+        plot = bokeh_renderer.get_plot(vectorfield)
+        keys = sorted(plot.handles["cds"].data)
+        assert keys == ["Angle", "Magnitude", "x", "x0", "x1", "y", "y0", "y1"]