Repository: holoviz/holoviews

Issue:
Wide DF vdim mapper incompatible with `hover_tooltips`
version: holoviews 1.19.0rc3

`hover_tooltips` seems to not work with the wide df vdim mapper (e.g. `vdim=(column, common_dim)`), as it results in '???' in the tooltip.

I was using `hover_tooltips` so as to include 'group' in the hover tooltip. Both `label` and `group` args are required per curve element for `hover_tooltips` to refer to them and have them included in the tooltip.

An alternative approach is to use `(group, channel)` to key each of the items in the curves dict and then specify `['Group', 'Channel']` as the kdims arg for the NdOverlay. However, this still requires specifying the `label` arg per curve to prevent the group from getting added to the Ytick of each curve.

<details> <summary> Code </summary>

```python
import numpy as np
import pandas as pd
import holoviews as hv
import colorcet as cc
hv.extension('bokeh')

GROUP_EEG = 'EEG'
N_CHANNELS_EEG = 31
N_SECONDS = 5
SAMPLING_RATE_EEG = 100
INIT_FREQ = 2  # Initial frequency in Hz
FREQ_INC = 5  # Frequency increment
AMPLITUDE_EEG = 1000  # Amplitude multiplier

# Generate data
total_samples_eeg = N_SECONDS * SAMPLING_RATE_EEG
time_eeg = np.linspace(0, N_SECONDS, total_samples_eeg)
def generate_eeg_data(index):
    return AMPLITUDE_EEG * np.sin(2 * np.pi * (INIT_FREQ + index * FREQ_INC) * time_eeg)
eeg_channels = [str(i) for i in np.arange(N_CHANNELS_EEG)]
eeg_data = np.array([generate_eeg_data(i) for i in np.arange(N_CHANNELS_EEG)])
eeg_df = pd.DataFrame(eeg_data.T, index=time_eeg, columns=eeg_channels)
eeg_df.index.name = 'Time'

# Create plot
time_dim = hv.Dimension("Time", unit="s")
curves = {}
for col in eeg_df.columns:
    curve = hv.Curve(eeg_df[col], time_dim, hv.Dimension(col, label='Amplitude', unit='uV'),
                     group='EEG', label=str(col))
    curve = curve.opts(subcoordinate_y=True, tools=['xwheel_zoom'], color='grey',
                      hover_tooltips = [("Group", "$group"), ("Channel", "$label"), ("Time"), ("Amplitude")])
    curves[col] = curve

curves_overlay = hv.NdOverlay(curves, "Channel").opts(
    xlabel=time_dim.pprint_label, ylabel="Channel", show_legend=False, aspect=3, responsive=True,
    min_height=600,
)
print(curves_overlay)
curves_overlay
```

</details>

![image](https://github.com/holoviz/holoviews/assets/6613202/c12fd4af-e411-4ebe-bc86-1c26f84ace27)



Discussion:

Resolving patch:
diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py
--- a/holoviews/plotting/bokeh/element.py
+++ b/holoviews/plotting/bokeh/element.py
@@ -421,7 +421,7 @@ def _prepare_hover_kwargs(self, element):
         tooltips, hover_opts = self._hover_opts(element)
 
         dim_aliases = {
-            f"{dim.label} ({dim.unit})" if dim.unit else dim.label: dim.name
+            f"{dim.label} ({dim.unit})" if dim.unit else dim.label: dim.label
             for dim in element.kdims + element.vdims
         }
 
@@ -430,10 +430,10 @@ def _prepare_hover_kwargs(self, element):
         units_dict = {}
         for ttp in tooltips:
             if isinstance(ttp, tuple):
-                name = ttp[0]
+                label = ttp[0]
                 tuple_ = (ttp[0], ttp[1])
             elif isinstance(ttp, Dimension):
-                name = ttp.name
+                label = ttp.label
                 # three brackets means replacing variable,
                 # and then wrapping in brackets, like @{air}
                 unit = f" ({ttp.unit})" if ttp.unit else ""
@@ -441,19 +441,19 @@ def _prepare_hover_kwargs(self, element):
                     ttp.pprint_label,
                     f"@{{{util.dimension_sanitizer(ttp.name)}}}"
                 )
-                units_dict[name] = unit
+                units_dict[label] = unit
             elif isinstance(ttp, str):
-                name = ttp
+                label = ttp
                 # three brackets means replacing variable,
                 # and then wrapping in brackets, like @{air}
-                tuple_ = (ttp.name, f"@{{{util.dimension_sanitizer(ttp)}}}")
+                tuple_ = (ttp, f"@{{{util.dimension_sanitizer(ttp)}}}")
 
-            if name in dim_aliases:
-                name = dim_aliases[name]
+            if label in dim_aliases:
+                label = dim_aliases[label]
 
             # key is the vanilla data column/dimension name
             # value should always be a tuple (label, value)
-            tooltips_dict[name] = tuple_
+            tooltips_dict[label] = tuple_
 
         # subset the tooltips to only the ones user wants
         if self.hover_tooltips:


Test patch:
diff --git a/holoviews/tests/plotting/bokeh/test_curveplot.py b/holoviews/tests/plotting/bokeh/test_curveplot.py
--- a/holoviews/tests/plotting/bokeh/test_curveplot.py
+++ b/holoviews/tests/plotting/bokeh/test_curveplot.py
@@ -5,7 +5,7 @@
 import pytest
 from bokeh.models import FactorRange, FixedTicker
 
-from holoviews.core import DynamicMap, HoloMap, NdOverlay
+from holoviews.core import Dimension, DynamicMap, HoloMap, NdOverlay
 from holoviews.core.options import AbbreviatedException, Cycle, Palette
 from holoviews.element import Curve
 from holoviews.plotting.bokeh.callbacks import Callback, PointerXCallback
@@ -109,6 +109,18 @@ def test_curve_overlay_hover(self):
         obj = obj.opts(opts)
         self._test_hover_info(obj, [('Test', '@{Test}'), ('x', '@{x}'), ('y', '@{y}')], 'nearest')
 
+    def test_curve_hover_dimension(self):
+        obj = Curve(
+            range(10), Dimension('name', label='label')
+        ).opts(hover_tooltips=['label'])
+        self._test_hover_info(obj, [('label', '@{name}')], 'nearest')
+
+    def test_curve_hover_dimension_unit(self):
+        obj = Curve(
+            range(10), Dimension('name', label='label', unit='unit')
+        ).opts(hover_tooltips=['label'])
+        self._test_hover_info(obj, [('label (unit)', '@{name}')], 'nearest')
+
     def test_curve_categorical_xaxis(self):
         curve = Curve((['A', 'B', 'C'], [1,2,3]))
         plot = bokeh_renderer.get_plot(curve)