diff --git a/holoviews/operation/datashader.py b/holoviews/operation/datashader.py index 441a6d144f..c1d3819c28 100644 --- 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 index c62c3f06c4..3d54ac005a 100644 --- 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 index 463a9a4fb1..1925ddd824 100644 --- 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 diff --git a/holoviews/tests/operation/test_datashader.py b/holoviews/tests/operation/test_datashader.py index 7ad78dfe27..a630f8cb0d 100644 --- 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 index c12f969126..7f1417b165 100644 --- 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