Repository: holoviz/holoviews

Issue:
Support datetime column in for rasterize selector
The following will show hover information as numbers and not as datetimes.

``` python
import holoviews as hv
import numpy as np
import pandas as pd
import datashader as ds
from holoviews.operation.datashader import rasterize

hv.extension("bokeh")

n = 10000
df = pd.DataFrame(
    {
        "x": np.random.uniform(-180, 180, n),
        "y": np.random.uniform(-90, 90, n),
        "Timestamp": pd.date_range(start="2023-01-01", periods=n, freq="D"),
        "Value": np.random.rand(n) * 100,
    }
)

rasterize(hv.Points(df), aggregator=ds.count(), selector=ds.first("Value")).opts(tools=["hover"])
```

![image](https://github.com/holoviz/holoviews/assets/19758978/1da517bc-ba96-4d08-b102-167e0eeed05a)

First discovered in https://github.com/holoviz/hvplot/issues/1206


Discussion:

Resolving patch:
diff --git a/holoviews/operation/datashader.py b/holoviews/operation/datashader.py
--- a/holoviews/operation/datashader.py
+++ b/holoviews/operation/datashader.py
@@ -438,6 +438,8 @@ def _apply_datashader(self, dfdata, cvs_fn, agg_fn, agg_kwargs, x, y):
                     val[neg1] = "-"
                 elif val.dtype.kind == "O":
                     val[neg1] = "-"
+                elif val.dtype.kind == "M":
+                    val[neg1] = np.datetime64("NaT")
                 else:
                     val = val.astype(np.float64)
                     val[neg1] = np.nan
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
@@ -6,7 +6,6 @@
 import bokeh.plotting
 import numpy as np
 import param
-from bokeh.core.properties import value
 from bokeh.document.events import ModelChangedEvent
 from bokeh.models import (
     BinnedTicker,
@@ -1784,8 +1783,10 @@ def _postprocess_hover(self, renderer, source):
         if not isinstance(hover.tooltips, str) and 'hv_created' in hover.tags:
             for k, values in source.data.items():
                 key = '@{%s}' % k
-                if ((isinstance(value, np.ndarray) and value.dtype.kind == 'M') or
-                    (len(values) and isinstance(values[0], util.datetime_types))):
+                if (
+                    (len(values) and isinstance(values[0], util.datetime_types)) or
+                    (len(values) and isinstance(values[0], np.ndarray) and values[0].dtype.kind == 'M')
+                ):
                     hover.tooltips = [(l, f+'{%F %T}' if f == key else f) for l, f in hover.tooltips]
                     hover.formatters[key] = "datetime"
 
diff --git a/holoviews/plotting/bokeh/raster.py b/holoviews/plotting/bokeh/raster.py
--- a/holoviews/plotting/bokeh/raster.py
+++ b/holoviews/plotting/bokeh/raster.py
@@ -43,7 +43,7 @@ def _hover_opts(self, element):
         tooltips.append((vdims[0].pprint_label, '@image'))
         for vdim in vdims[1:]:
             vname = dimension_sanitizer(vdim.name)
-            tooltips.append((vdim.pprint_label, f'@{vname}'))
+            tooltips.append((vdim.pprint_label, f'@{{{vname}}}'))
         return tooltips, {}
 
     def _postprocess_hover(self, renderer, source):
@@ -52,8 +52,6 @@ def _postprocess_hover(self, renderer, source):
         if not (hover and isinstance(hover.tooltips, list)):
             return
 
-        element = self.current_frame
-        xdim, ydim = (dimension_sanitizer(kd.name) for kd in element.kdims)
         xaxis = self.handles['xaxis']
         yaxis = self.handles['yaxis']
 
@@ -73,6 +71,20 @@ def _postprocess_hover(self, renderer, source):
                 formatters['$y'] = yhover
                 formatter += '{custom}'
             tooltips.append((name, formatter))
+
+        # https://github.com/bokeh/bokeh/issues/13598
+        datetime_code = """
+        if (value === -9223372036854776) {
+            return "NaT"
+        } else {
+            const date = new Date(value);
+            return date.toISOString().slice(0, 19).replace('T', ' ')
+        }
+        """
+        for key in formatters:
+            if formatters[key].lower() == "datetime":
+                formatters[key] = CustomJSHover(code=datetime_code)
+
         hover.tooltips = tooltips
         hover.formatters = formatters
 


Test patch:
diff --git a/holoviews/tests/operation/test_datashader.py b/holoviews/tests/operation/test_datashader.py
--- a/holoviews/tests/operation/test_datashader.py
+++ b/holoviews/tests/operation/test_datashader.py
@@ -1339,6 +1339,21 @@ def test_rasterize_selector(point_plot, sel_fn):
     np.testing.assert_array_equal(img["Count"], img_count["Count"])
 
 
+def test_rasterize_with_datetime_column():
+    n = 4
+    df = pd.DataFrame({
+        "x": np.random.uniform(-180, 180, n),
+        "y": np.random.uniform(-90, 90, n),
+        "Timestamp": pd.date_range(start="2023-01-01", periods=n, freq="D"),
+        "Value": np.random.rand(n) * 100,
+    })
+    point_plot = Points(df)
+    rast_input = dict(dynamic=False,  x_range=(-1, 1), y_range=(-1, 1), width=2, height=2)
+    img_agg = rasterize(point_plot, selector=ds.first("Value"), **rast_input)
+
+    assert img_agg["Timestamp"].dtype == np.dtype("datetime64[ns]")
+
+
 
 class DatashaderSpreadTests(ComparisonTestCase):
 
diff --git a/holoviews/tests/plotting/bokeh/test_rasterplot.py b/holoviews/tests/plotting/bokeh/test_rasterplot.py
--- a/holoviews/tests/plotting/bokeh/test_rasterplot.py
+++ b/holoviews/tests/plotting/bokeh/test_rasterplot.py
@@ -1,6 +1,9 @@
 from unittest import SkipTest
 
 import numpy as np
+import pandas as pd
+import pytest
+from bokeh.models import CustomJSHover
 
 from holoviews.element import RGB, Image, ImageStack, Raster
 from holoviews.plotting.bokeh.raster import ImageStackPlot
@@ -129,6 +132,24 @@ def test_rgb_invert_yaxis(self):
         assert cdata["dw"] == [1.0]
         assert cdata["y"] == [-0.5]
 
+    def test_image_datetime_hover(self):
+        xr = pytest.importorskip("xarray")
+        ts = pd.Timestamp("2020-01-01")
+        data = xr.Dataset(
+            coords={"x": [-0.5, 0.5], "y": [-0.5, 0.5]},
+            data_vars={
+                "Count": (["y", "x"], [[0, 1], [2, 3]]),
+                "Timestamp": (["y", "x"], [[ts, pd.NaT], [ts, ts]]),
+            },
+        )
+        img = Image(data).opts(tools=["hover"])
+        plot = bokeh_renderer.get_plot(img)
+
+        hover = plot.handles["hover"]
+        assert hover.tooltips[-1] == ("Timestamp", "@{Timestamp}{%F %T}")
+        assert "@{Timestamp}" in hover.formatters
+        assert isinstance(hover.formatters["@{Timestamp}"], CustomJSHover)
+        # assert hover.formatters["@{Timestamp}"] == "datetime"  # https://github.com/bokeh/bokeh/issues/13598
 
 class _ImageStackBase(TestRasterPlot):
     __test__ = False