Repository: holoviz/holoviews

Issue:
No hover data on Segments
Bug report on hover of Segments element

**Software version info**

python 3.7.4
holoviews 1.13.0
bokeh 2.0.0

**Description of expected behavior and the observed behavior**

When hovering Segments elements, the shown hover tooltip should contain data.
Instead, "???" are shown for every field.

**Complete, minimal, self-contained example code that reproduces the issue**
This is the code from http://build.holoviews.org/reference/elements/bokeh/Segments.html with just `tools=["hover"]` added.
```
import numpy as np
import holoviews as hv
from holoviews import dim
hv.extension('bokeh')

event = ['A', 'B']
data = dict(
    start=[np.datetime64('1999'), np.datetime64('2001')],
    end=[np.datetime64('2010'), np.datetime64('2020')],
    start_event = event,
    end_event = event
)

seg = hv.Segments(data, [hv.Dimension('start', label='Year'), 
                         hv.Dimension('start_event', label='Event'), 'end', 'end_event']).opts(tools=["hover"])
```

**Screenshots or screencasts of the bug in action**
![bugq](https://user-images.githubusercontent.com/11155537/77335227-2da20880-6d26-11ea-860e-1516a9ab96a8.png)



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
@@ -293,10 +293,6 @@ def _get_hover_data(self, data, element, dimensions=None):
             dim = util.dimension_sanitizer(d.name)
             if dim not in data:
                 data[dim] = element.dimension_values(d)
-            values = np.asarray(data[dim])
-            if (values.dtype.kind == 'M' or (
-                    len(values) and isinstance(values[0], util.datetime_types))):
-                data[dim+'_dt_strings'] = [d.pprint_value(v) for v in values]
 
         for k, v in self.overlay_dims.items():
             dim = util.dimension_sanitizer(k.name)
@@ -1243,21 +1239,19 @@ def _postprocess_hover(self, renderer, source):
         hover = self.handles.get('hover')
         if hover is None:
             return
+        if not isinstance(hover.tooltips, util.basestring) and 'hv_created' in hover.tags:
+            for k, values in source.data.items():
+                key = '@{%s}' % k
+                if key in hover.formatters:
+                    continue
+                if ((isinstance(value, np.ndarray) and value.dtype.kind == 'M') or
+                    (len(values) and isinstance(values[0], util.datetime_types))):
+                    hover.tooltips = [(l, f+'{%F %T}' if f == key else f) for l, f in hover.tooltips]
+                    hover.formatters[key] = "datetime"
         if hover.renderers == 'auto':
             hover.renderers = []
         hover.renderers.append(renderer)
 
-        # If datetime column is in the data replace hover formatter
-        for k, v in source.data.items():
-            if k+'_dt_strings' in source.data:
-                tooltips = []
-                for name, formatter in hover.tooltips:
-                    if formatter == '@{%s}' % k:
-                        formatter = '@{%s_dt_strings}' % k
-                    tooltips.append((name, formatter))
-                hover.tooltips = tooltips
-
-
     def _init_glyphs(self, plot, element, ranges, source):
         style_element = element.last if self.batched else element
 
@@ -2119,11 +2113,8 @@ def _merge_tools(self, subplot):
             self.handles['hover'] = subplot.handles['hover']
         elif 'hover' in subplot.handles and 'hover_tools' in self.handles:
             hover = subplot.handles['hover']
-            # Datetime formatter may have been applied, remove _dt_strings
-            # to match on the hover tooltips, then merge tool renderers
             if hover.tooltips and not isinstance(hover.tooltips, util.basestring):
-                tooltips = tuple((name, spec.replace('_dt_strings', ''))
-                                  for name, spec in hover.tooltips)
+                tooltips = tuple((name, spec.replace('{%F %T}', '')) for name, spec in hover.tooltips)
             else:
                 tooltips = ()
             tool = self.handles['hover_tools'].get(tooltips)
diff --git a/holoviews/plotting/bokeh/geometry.py b/holoviews/plotting/bokeh/geometry.py
--- a/holoviews/plotting/bokeh/geometry.py
+++ b/holoviews/plotting/bokeh/geometry.py
@@ -3,6 +3,7 @@
 import numpy as np
 import param
 
+from ...core.util import dimension_sanitizer
 from ..mixins import GeomMixin
 from .element import ColorbarPlot, LegendPlot
 from .selection import BokehOverlaySelectionDisplay
@@ -29,9 +30,12 @@ class SegmentPlot(GeomMixin, ColorbarPlot):
 
     def get_data(self, element, ranges, style):
         inds = (1, 0, 3, 2) if self.invert_axes else (0, 1, 2, 3)
+        xd0, yd0, xd1, yd1 = (dimension_sanitizer(element.get_dimension(i).name)
+                              for i in inds)
         x0s, y0s, x1s, y1s = (element.dimension_values(kd) for kd in inds)
-        data = {'x0': x0s, 'x1': x1s, 'y0': y0s, 'y1': y1s}
-        mapping = dict(x0='x0', x1='x1', y0='y0', y1='y1')
+        data = {xd0: x0s, xd1: x1s, yd0: y0s, yd1: y1s}
+        mapping = dict(x0=xd0, x1=xd1, y0=yd0, y1=yd1)
+        self._get_hover_data(data, element)
         return (data, mapping, style)
 
 
@@ -59,4 +63,5 @@ def get_data(self, element, ranges, style):
         y0, y1 = np.min([y0, y1], axis=0), np.max([y0, y1], axis=0)
         data = {'x': (x1+x0)/2., 'y': (y1+y0)/2., 'width': x1-x0, 'height': y1-y0}
         mapping = {'x': 'x', 'y': 'y', 'width': 'width', 'height': 'height'}
+        self._get_hover_data(data, element)
         return data, mapping, style
diff --git a/holoviews/plotting/bokeh/path.py b/holoviews/plotting/bokeh/path.py
--- a/holoviews/plotting/bokeh/path.py
+++ b/holoviews/plotting/bokeh/path.py
@@ -115,8 +115,7 @@ def get_data(self, element, ranges, style):
                 values = path.dimension_values(vd)[:-1]
                 vd_name = util.dimension_sanitizer(vd.name)
                 vals[vd_name].append(values)
-                if values.dtype.kind == 'M' or (len(values) and isinstance(values[0], util.datetime_types)):
-                    vals[vd_name+'_dt_strings'].append([vd.pprint_value(v) for v in values])
+
         values = {d: np.concatenate(vs) if len(vs) else [] for d, vs in vals.items()}
         if self.invert_axes:
             xpaths, ypaths = ypaths, xpaths
@@ -202,10 +201,6 @@ def _get_hover_data(self, data, element):
                     data[dim] = element.dimension_values(d, expanded=False)
                 else:
                     data[dim] = element.split(datatype='array', dimensions=[d])
-            values = data[dim]
-            if ((isinstance(values, np.ndarray) and values.dtype.kind == 'M') or
-                  (len(values) and isinstance(values[0], util.datetime_types))):
-                data[dim+'_dt_strings'] = [d.pprint_value(v) for v in values]
 
         for k, v in self.overlay_dims.items():
             dim = util.dimension_sanitizer(k.name)


Test patch:
diff --git a/holoviews/tests/plotting/bokeh/testcurveplot.py b/holoviews/tests/plotting/bokeh/testcurveplot.py
--- a/holoviews/tests/plotting/bokeh/testcurveplot.py
+++ b/holoviews/tests/plotting/bokeh/testcurveplot.py
@@ -97,7 +97,8 @@ def test_curve_overlay_datetime_hover(self):
                         kdims=['Test'])
         opts = {'Curve': {'tools': ['hover']}}
         obj = obj.opts(plot=opts)
-        self._test_hover_info(obj, [('Test', '@{Test}'), ('x', '@{x_dt_strings}'), ('y', '@{y}')])
+        self._test_hover_info(obj, [('Test', '@{Test}'), ('x', '@{x}{%F %T}'), ('y', '@{y}')],
+                              formatters={'@{x}': "datetime"})
 
     def test_curve_overlay_hover_batched(self):
         obj = NdOverlay({i: Curve(np.random.rand(10,2)) for i in range(5)},
diff --git a/holoviews/tests/plotting/bokeh/testpathplot.py b/holoviews/tests/plotting/bokeh/testpathplot.py
--- a/holoviews/tests/plotting/bokeh/testpathplot.py
+++ b/holoviews/tests/plotting/bokeh/testpathplot.py
@@ -88,8 +88,7 @@ def test_path_colored_by_levels_single_value(self):
         self.assertEqual(source.data['xs'], [np.array([1, 2]), np.array([2, 3]), np.array([3, 4])])
         self.assertEqual(source.data['ys'], [np.array([4, 3]), np.array([3, 2]), np.array([2, 1])])
         self.assertEqual(source.data['color'], np.array([998, 999, 998]))
-        self.assertEqual(source.data['date_dt_strings'],
-                         np.array(['2018-08-01 00:00:00', '2018-08-01 00:00:00', '2018-08-01 00:00:00']))
+        self.assertEqual(source.data['date'], np.array([date]*3))
         self.assertEqual(cmapper.low, 998)
         self.assertEqual(cmapper.high, 999)
         self.assertEqual(cmapper.palette, colors[-1:])
@@ -111,8 +110,7 @@ def test_path_continuously_varying_color_op(self):
         self.assertEqual(source.data['xs'], [np.array([1, 2]), np.array([2, 3]), np.array([3, 4])])
         self.assertEqual(source.data['ys'], [np.array([4, 3]), np.array([3, 2]), np.array([2, 1])])
         self.assertEqual(source.data['color'], np.array([998, 999, 998]))
-        self.assertEqual(source.data['date_dt_strings'],
-                         np.array(['2018-08-01 00:00:00', '2018-08-01 00:00:00', '2018-08-01 00:00:00']))
+        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:])
diff --git a/holoviews/tests/plotting/bokeh/testplot.py b/holoviews/tests/plotting/bokeh/testplot.py
--- a/holoviews/tests/plotting/bokeh/testplot.py
+++ b/holoviews/tests/plotting/bokeh/testplot.py
@@ -61,7 +61,7 @@ def _test_colormapping(self, element, dim, log=False):
         mapper_type = LogColorMapper if log else LinearColorMapper
         self.assertTrue(isinstance(cmapper, mapper_type))
 
-    def _test_hover_info(self, element, tooltips, line_policy='nearest'):
+    def _test_hover_info(self, element, tooltips, line_policy='nearest', formatters={}):
         plot = bokeh_renderer.get_plot(element)
         plot.initialize_plot()
         fig = plot.state
@@ -70,6 +70,7 @@ def _test_hover_info(self, element, tooltips, line_policy='nearest'):
         hover = fig.select(dict(type=HoverTool))
         self.assertTrue(len(hover))
         self.assertEqual(hover[0].tooltips, tooltips)
+        self.assertEqual(hover[0].formatters, formatters)
         self.assertEqual(hover[0].line_policy, line_policy)
 
         if isinstance(element, Element):
@@ -79,5 +80,6 @@ def _test_hover_info(self, element, tooltips, line_policy='nearest'):
                     self.assertIn(lookup[2:-1], cds.data)
 
         # Ensure all the glyph renderers have a hover tool
+        print(renderers, hover)
         for renderer in renderers:
             self.assertTrue(any(renderer in h.renderers for h in hover))
diff --git a/holoviews/tests/plotting/bokeh/testpointplot.py b/holoviews/tests/plotting/bokeh/testpointplot.py
--- a/holoviews/tests/plotting/bokeh/testpointplot.py
+++ b/holoviews/tests/plotting/bokeh/testpointplot.py
@@ -138,7 +138,8 @@ def test_points_overlay_datetime_hover(self):
                         kdims=['Test'])
         opts = {'Points': {'tools': ['hover']}}
         obj = obj.opts(plot=opts)
-        self._test_hover_info(obj, [('Test', '@{Test}'), ('x', '@{x_dt_strings}'), ('y', '@{y}')])
+        self._test_hover_info(obj, [('Test', '@{Test}'), ('x', '@{x}{%F %T}'), ('y', '@{y}')],
+                              formatters={'@{x}': "datetime"})
 
     def test_points_overlay_hover_batched(self):
         obj = NdOverlay({i: Points(np.random.rand(10,2)) for i in range(5)},
@@ -319,9 +320,9 @@ def test_points_datetime_hover(self):
         plot = bokeh_renderer.get_plot(points)
         cds = plot.handles['cds']
         self.assertEqual(cds.data['date'].astype('datetime64'), np.array([1483228800000000000]))
-        self.assertEqual(cds.data['date_dt_strings'], ['2017-01-01 00:00:00'])
         hover = plot.handles['hover']
-        self.assertEqual(hover.tooltips, [('x', '@{x}'), ('y', '@{y}'), ('date', '@{date_dt_strings}')])
+        self.assertEqual(hover.tooltips, [('x', '@{x}'), ('y', '@{y}'), ('date', '@{date}{%F %T}')],
+                         {'@{date}': "datetime"})
 
     def test_points_selected(self):
         points = Points([(0, 0), (1, 1), (2, 2)]).opts(selected=[0, 2])
diff --git a/holoviews/tests/plotting/bokeh/testspikesplot.py b/holoviews/tests/plotting/bokeh/testspikesplot.py
--- a/holoviews/tests/plotting/bokeh/testspikesplot.py
+++ b/holoviews/tests/plotting/bokeh/testspikesplot.py
@@ -118,9 +118,9 @@ def test_spikes_datetime_vdim_hover(self):
         plot = bokeh_renderer.get_plot(points)
         cds = plot.handles['cds']
         self.assertEqual(cds.data['date'].astype('datetime64'), np.array([1483228800000000000]))
-        self.assertEqual(cds.data['date_dt_strings'], ['2017-01-01 00:00:00'])
         hover = plot.handles['hover']
-        self.assertEqual(hover.tooltips, [('x', '@{x}'), ('value', '@{value}'), ('date', '@{date_dt_strings}')])
+        self.assertEqual(hover.tooltips, [('x', '@{x}'), ('value', '@{value}'), ('date', '@{date}{%F %T}')])
+        self.assertEqual(hover.formatters, {'@{date}': "datetime"})
 
     def test_spikes_datetime_kdim_hover(self):
         points = Spikes([(dt.datetime(2017, 1, 1), 1)], 'x', 'y').options(tools=['hover'])
@@ -130,9 +130,9 @@ def test_spikes_datetime_kdim_hover(self):
         self.assertEqual(cds.data['y0'], np.array([0]))
         self.assertEqual(cds.data['y1'], np.array([1]))
 
-        self.assertEqual(cds.data['x_dt_strings'], ['2017-01-01 00:00:00'])
         hover = plot.handles['hover']
-        self.assertEqual(hover.tooltips, [('x', '@{x_dt_strings}'), ('y', '@{y}')])
+        self.assertEqual(hover.tooltips, [('x', '@{x}{%F %T}'), ('y', '@{y}')])
+        self.assertEqual(hover.formatters, {'@{x}': "datetime"})
 
     def test_spikes_datetime_kdim_hover_spike_length_override(self):
         points = Spikes([(dt.datetime(2017, 1, 1), 1)], 'x', 'y').options(
@@ -142,10 +142,10 @@ def test_spikes_datetime_kdim_hover_spike_length_override(self):
         self.assertEqual(cds.data['x'].astype('datetime64'), np.array([1483228800000000000]))
         self.assertEqual(cds.data['y0'], np.array([0]))
         self.assertEqual(cds.data['y1'], np.array([0.75]))
-        self.assertEqual(cds.data['x_dt_strings'], ['2017-01-01 00:00:00'])
         hover = plot.handles['hover']
-        self.assertEqual(hover.tooltips, [('x', '@{x_dt_strings}'), ('y', '@{y}')])
-
+        self.assertEqual(hover.tooltips, [('x', '@{x}{%F %T}'), ('y', '@{y}')])
+        self.assertEqual(hover.formatters, {'@{x}': "datetime"})
+        
     ###########################
     #    Styling mapping      #
     ###########################