Repository: holoviz/holoviews

Issue:
Interpolation breaks with pandas nullable dtypes
#### ALL software version info
```
Python              :  3.8.10 (default, Nov 22 2023, 10:22:35) 
[GCC 9.4.0]
Operating system    :  Linux-5.15.0-92-generic-x86_64-with-glibc2.29
Panel comms         :  default

holoviews           :  1.17.1

bokeh               :  3.1.1
colorcet            :  3.1.0
cudf                :  -
dask                :  -
datashader          :  -
geoviews            :  -
hvplot              :  0.9.2
ibis                :  -
IPython             :  8.12.3
jupyter_bokeh       :  -
jupyterlab          :  4.1.0
matplotlib          :  3.7.4
networkx            :  -
notebook            :  -
numba               :  -
numpy               :  1.24.4
pandas              :  2.0.3
panel               :  1.2.3
param               :  2.0.2
PIL                 :  10.2.0
plotly              :  -
pyarrow             :  15.0.0
scipy               :  1.10.1
skimage             :  -
spatialpandas       :  -
streamz             :  -
xarray              :  -
```

using firefox on Ubuntu 20.04.

#### Description of expected behavior and the observed behavior

Code raises an exception, but it should accept the input and work.

#### Complete, minimal, self-contained example code that reproduces the issue

```
import holoviews as hv
hv.extension('bokeh')
import pandas as pd

df=pd.DataFrame(dict(a=[1,2,3], b=[3,4,5]), dtype='UInt64')

hv.Curve(df, kdims=['a'], vdims=['b']).opts(interpolation='steps-post')
```

#### Stack traceback and/or browser JavaScript console output
```
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
File /work/projects/lisa/.lisa-venv-3.8/lib/python3.8/site-packages/IPython/core/formatters.py:974, in MimeBundleFormatter.__call__(self, obj, include, exclude)
    971     method = get_real_method(obj, self.print_method)
    973     if method is not None:
--> 974         return method(include=include, exclude=exclude)
    975     return None
    976 else:

File /work/projects/lisa/.lisa-venv-3.8/lib/python3.8/site-packages/holoviews/core/dimension.py:1287, in Dimensioned._repr_mimebundle_(self, include, exclude)
   1280 def _repr_mimebundle_(self, include=None, exclude=None):
   1281     """
   1282     Resolves the class hierarchy for the class rendering the
   1283     object using any display hooks registered on Store.display
   1284     hooks.  The output of all registered display_hooks is then
   1285     combined and returned.
   1286     """
-> 1287     return Store.render(self)

File /work/projects/lisa/.lisa-venv-3.8/lib/python3.8/site-packages/holoviews/core/options.py:1423, in Store.render(cls, obj)
   1421 data, metadata = {}, {}
   1422 for hook in hooks:
-> 1423     ret = hook(obj)
   1424     if ret is None:
   1425         continue

File /work/projects/lisa/.lisa-venv-3.8/lib/python3.8/site-packages/holoviews/ipython/display_hooks.py:280, in pprint_display(obj)
    278 if not ip.display_formatter.formatters['text[/plain](http://localhost:8889/plain)'].pprint:
    279     return None
--> 280 return display(obj, raw_output=True)

File /work/projects/lisa/.lisa-venv-3.8/lib/python3.8/site-packages/holoviews/ipython/display_hooks.py:248, in display(obj, raw_output, **kwargs)
    246 elif isinstance(obj, (CompositeOverlay, ViewableElement)):
    247     with option_state(obj):
--> 248         output = element_display(obj)
    249 elif isinstance(obj, (Layout, NdLayout, AdjointLayout)):
    250     with option_state(obj):

File /work/projects/lisa/.lisa-venv-3.8/lib/python3.8/site-packages/holoviews/ipython/display_hooks.py:142, in display_hook.<locals>.wrapped(element)
    140 try:
    141     max_frames = OutputSettings.options['max_frames']
--> 142     mimebundle = fn(element, max_frames=max_frames)
    143     if mimebundle is None:
    144         return {}, {}

File /work/projects/lisa/.lisa-venv-3.8/lib/python3.8/site-packages/holoviews/ipython/display_hooks.py:188, in element_display(element, max_frames)
    185 if type(element) not in Store.registry[backend]:
    186     return None
--> 188 return render(element)

File /work/projects/lisa/.lisa-venv-3.8/lib/python3.8/site-packages/holoviews/ipython/display_hooks.py:69, in render(obj, **kwargs)
     66 if renderer.fig == 'pdf':
     67     renderer = renderer.instance(fig='png')
---> 69 return renderer.components(obj, **kwargs)

File /work/projects/lisa/.lisa-venv-3.8/lib/python3.8/site-packages/holoviews/plotting/renderer.py:397, in Renderer.components(self, obj, fmt, comm, **kwargs)
    394 embed = (not (dynamic or streams or self.widget_mode == 'live') or config.embed)
    396 if embed or config.comms == 'default':
--> 397     return self._render_panel(plot, embed, comm)
    398 return self._render_ipywidget(plot)

File /work/projects/lisa/.lisa-venv-3.8/lib/python3.8/site-packages/holoviews/plotting/renderer.py:404, in Renderer._render_panel(self, plot, embed, comm)
    402 doc = Document()
    403 with config.set(embed=embed):
--> 404     model = plot.layout._render_model(doc, comm)
    405 if embed:
    406     return render_model(model, comm)

File /work/projects/lisa/.lisa-venv-3.8/lib/python3.8/site-packages/panel/viewable.py:737, in Viewable._render_model(self, doc, comm)
    735 if comm is None:
    736     comm = state._comm_manager.get_server_comm()
--> 737 model = self.get_root(doc, comm)
    739 if self._design and self._design.theme.bokeh_theme:
    740     doc.theme = self._design.theme.bokeh_theme

File /work/projects/lisa/.lisa-venv-3.8/lib/python3.8/site-packages/panel/layout/base.py:306, in Panel.get_root(self, doc, comm, preprocess)
    302 def get_root(
    303     self, doc: Optional[Document] = None, comm: Optional[Comm] = None,
    304     preprocess: bool = True
    305 ) -> Model:
--> 306     root = super().get_root(doc, comm, preprocess)
    307     # ALERT: Find a better way to handle this
    308     if hasattr(root, 'styles') and 'overflow-x' in root.styles:

File /work/projects/lisa/.lisa-venv-3.8/lib/python3.8/site-packages/panel/viewable.py:659, in Renderable.get_root(self, doc, comm, preprocess)
    657 wrapper = self._design._wrapper(self)
    658 if wrapper is self:
--> 659     root = self._get_model(doc, comm=comm)
    660     if preprocess:
    661         self._preprocess(root)

File /work/projects/lisa/.lisa-venv-3.8/lib/python3.8/site-packages/panel/layout/base.py:174, in Panel._get_model(self, doc, root, parent, comm)
    172 root = root or model
    173 self._models[root.ref['id']] = (model, parent)
--> 174 objects, _ = self._get_objects(model, [], doc, root, comm)
    175 props = self._get_properties(doc)
    176 props[self._property_mapping['objects']] = objects

File /work/projects/lisa/.lisa-venv-3.8/lib/python3.8/site-packages/panel/layout/base.py:156, in Panel._get_objects(self, model, old_objects, doc, root, comm)
    154 else:
    155     try:
--> 156         child = pane._get_model(doc, root, model, comm)
    157     except RerenderError as e:
    158         if e.layout is not None and e.layout is not self:

File /work/projects/lisa/.lisa-venv-3.8/lib/python3.8/site-packages/panel/pane/holoviews.py:411, in HoloViews._get_model(self, doc, root, parent, comm)
    409     plot = self.object
    410 else:
--> 411     plot = self._render(doc, comm, root)
    413 plot.pane = self
    414 backend = plot.renderer.backend

File /work/projects/lisa/.lisa-venv-3.8/lib/python3.8/site-packages/panel/pane/holoviews.py:506, in HoloViews._render(self, doc, comm, root)
    503     if comm:
    504         kwargs['comm'] = comm
--> 506 return renderer.get_plot(self.object, **kwargs)

File /work/projects/lisa/.lisa-venv-3.8/lib/python3.8/site-packages/holoviews/plotting/bokeh/renderer.py:70, in BokehRenderer.get_plot(self_or_cls, obj, doc, renderer, **kwargs)
     63 @bothmethod
     64 def get_plot(self_or_cls, obj, doc=None, renderer=None, **kwargs):
     65     """
     66     Given a HoloViews Viewable return a corresponding plot instance.
     67     Allows supplying a document attach the plot to, useful when
     68     combining the bokeh model with another plot.
     69     """
---> 70     plot = super().get_plot(obj, doc, renderer, **kwargs)
     71     if plot.document is None:
     72         plot.document = Document() if self_or_cls.notebook_context else curdoc()

File /work/projects/lisa/.lisa-venv-3.8/lib/python3.8/site-packages/holoviews/plotting/renderer.py:241, in Renderer.get_plot(self_or_cls, obj, doc, renderer, comm, **kwargs)
    238     defaults = [kd.default for kd in plot.dimensions]
    239     init_key = tuple(v if d is None else d for v, d in
    240                      zip(plot.keys[0], defaults))
--> 241     plot.update(init_key)
    242 else:
    243     plot = obj

File /work/projects/lisa/.lisa-venv-3.8/lib/python3.8/site-packages/holoviews/plotting/plot.py:943, in DimensionedPlot.update(self, key)
    941 def update(self, key):
    942     if len(self) == 1 and key in (0, self.keys[0]) and not self.drawn:
--> 943         return self.initialize_plot()
    944     item = self.__getitem__(key)
    945     self.traverse(lambda x: setattr(x, '_updated', True))

File /work/projects/lisa/.lisa-venv-3.8/lib/python3.8/site-packages/holoviews/plotting/bokeh/element.py:1778, in ElementPlot.initialize_plot(self, ranges, plot, plots, source)
   1776 if self.autorange:
   1777     self._setup_autorange()
-> 1778 self._init_glyphs(plot, element, ranges, source)
   1779 if not self.overlaid:
   1780     self._update_plot(key, plot, style_element)

File /work/projects/lisa/.lisa-venv-3.8/lib/python3.8/site-packages/holoviews/plotting/bokeh/element.py:1700, in ElementPlot._init_glyphs(self, plot, element, ranges, source)
   1698 else:
   1699     style = self.style[self.cyclic_index]
-> 1700     data, mapping, style = self.get_data(element, ranges, style)
   1701     current_id = element._plot_id
   1703 with abbreviated_exception():

File /work/projects/lisa/.lisa-venv-3.8/lib/python3.8/site-packages/holoviews/plotting/bokeh/chart.py:359, in CurvePlot.get_data(self, element, ranges, style)
    356     return {}, dict(x=x, y=y), style
    358 if 'steps' in self.interpolation:
--> 359     element = interpolate_curve(element, interpolation=self.interpolation)
    360 data = {x: element.dimension_values(xidx),
    361         y: element.dimension_values(yidx)}
    362 self._get_hover_data(data, element)

File /work/projects/lisa/.lisa-venv-3.8/lib/python3.8/site-packages/param/parameterized.py:4431, in ParameterizedFunction.__new__(class_, *args, **params)
   4429 inst = class_.instance()
   4430 inst.param._set_name(class_.__name__)
-> 4431 return inst.__call__(*args,**params)

File /work/projects/lisa/.lisa-venv-3.8/lib/python3.8/site-packages/holoviews/core/operation.py:220, in Operation.__call__(self, element, **kwargs)
    218 kwargs['link_dataset'] = self._propagate_dataset
    219 kwargs['link_inputs'] = self.p.link_inputs
--> 220 return element.apply(self, **kwargs)

File /work/projects/lisa/.lisa-venv-3.8/lib/python3.8/site-packages/holoviews/core/accessors.py:43, in AccessorPipelineMeta.pipelined.<locals>.pipelined_call(*args, **kwargs)
     40     inst._obj._in_method = True
     42 try:
---> 43     result = __call__(*args, **kwargs)
     45     if not in_method:
     46         init_op = factory.instance(
     47             output_type=type(inst),
     48             kwargs={'mode': getattr(inst, 'mode', None)},
     49         )

File /work/projects/lisa/.lisa-venv-3.8/lib/python3.8/site-packages/holoviews/core/accessors.py:199, in Apply.__call__(self, apply_function, streams, link_inputs, link_dataset, dynamic, per_element, **kwargs)
    197 if hasattr(apply_function, 'dynamic'):
    198     inner_kwargs['dynamic'] = False
--> 199 new_obj = apply_function(self._obj, **inner_kwargs)
    200 if (link_dataset and isinstance(self._obj, Dataset) and
    201     isinstance(new_obj, Dataset) and new_obj._dataset is None):
    202     new_obj._dataset = self._obj.dataset

File /work/projects/lisa/.lisa-venv-3.8/lib/python3.8/site-packages/holoviews/core/operation.py:214, in Operation.__call__(self, element, **kwargs)
    210         return element.clone([(k, self._apply(el, key=k))
    211                               for k, el in element.items()])
    212     elif ((self._per_element and isinstance(element, Element)) or
    213           (not self._per_element and isinstance(element, ViewableElement))):
--> 214         return self._apply(element)
    215 elif 'streams' not in kwargs:
    216     kwargs['streams'] = self.p.streams

File /work/projects/lisa/.lisa-venv-3.8/lib/python3.8/site-packages/holoviews/core/operation.py:141, in Operation._apply(self, element, key)
    139     if not in_method:
    140         element._in_method = True
--> 141 ret = self._process(element, key)
    142 if hasattr(element, '_in_method') and not in_method:
    143     element._in_method = in_method

File /work/projects/lisa/.lisa-venv-3.8/lib/python3.8/site-packages/holoviews/operation/element.py:999, in interpolate_curve._process(self, element, key)
    998 def _process(self, element, key=None):
--> 999     return element.map(self._process_layer, Element)

File /work/projects/lisa/.lisa-venv-3.8/lib/python3.8/site-packages/holoviews/core/data/__init__.py:195, in PipelineMeta.pipelined.<locals>.pipelined_fn(*args, **kwargs)
    192     inst._in_method = True
    194 try:
--> 195     result = method_fn(*args, **kwargs)
    196     if PipelineMeta.disable:
    197         return result

File /work/projects/lisa/.lisa-venv-3.8/lib/python3.8/site-packages/holoviews/core/data/__init__.py:1203, in Dataset.map(self, *args, **kwargs)
   1201 @wraps(LabelledData.map)
   1202 def map(self, *args, **kwargs):
-> 1203     return super().map(*args, **kwargs)

File /work/projects/lisa/.lisa-venv-3.8/lib/python3.8/site-packages/holoviews/core/dimension.py:698, in LabelledData.map(self, map_fn, specs, clone)
    696     return deep_mapped
    697 else:
--> 698     return map_fn(self) if applies else self

File /work/projects/lisa/.lisa-venv-3.8/lib/python3.8/site-packages/holoviews/operation/element.py:993, in interpolate_curve._process_layer(self, element, key)
    991     x = x.astype(dt_type)
    992 dvals = tuple(element.dimension_values(d) for d in element.dimensions()[1:])
--> 993 xs, dvals = INTERPOLATE_FUNCS[self.p.interpolation](x, dvals)
    994 if is_datetime:
    995     xs = xs.astype(dt_type)

File /work/projects/lisa/.lisa-venv-3.8/lib/python3.8/site-packages/holoviews/operation/element.py:968, in interpolate_curve.pts_to_poststep(cls, x, values)
    965 @classmethod
    966 def pts_to_poststep(cls, x, values):
    967     steps = np.zeros(2 * len(x) - 1)
--> 968     value_steps = tuple(np.empty(2 * len(x) - 1, dtype=v.dtype) for v in values)
    970     steps[0::2] = x
    971     steps[1::2] = steps[2::2]

File /work/projects/lisa/.lisa-venv-3.8/lib/python3.8/site-packages/holoviews/operation/element.py:968, in <genexpr>(.0)
    965 @classmethod
    966 def pts_to_poststep(cls, x, values):
    967     steps = np.zeros(2 * len(x) - 1)
--> 968     value_steps = tuple(np.empty(2 * len(x) - 1, dtype=v.dtype) for v in values)
    970     steps[0::2] = x
    971     steps[1::2] = steps[2::2]

TypeError: Cannot interpret 'UInt64Dtype()' as a data type
```


#### Screenshots or screencasts of the bug in action
N/A

- [ ] I may be interested in making a pull request to address this

Discussion hints:

Solution patch:
diff --git a/holoviews/operation/element.py b/holoviews/operation/element.py
index f078607c5c..b8c866bcf8 100644
--- a/holoviews/operation/element.py
+++ b/holoviews/operation/element.py
@@ -1068,10 +1068,14 @@ class interpolate_curve(Operation):
 
     @staticmethod
     def _get_dtype(obj):
-        if dtype_kind(obj) == "O":
+        dtype = obj.dtype
+        if hasattr(dtype, "numpy_dtype"):
+            # Handle non-numpy objects like Pandas Int64Dtype
+            return dtype.numpy_dtype
+        if dtype_kind(dtype) == "O":
             # Handle non-numpy objects like Pandas 3.0 StringArray
             return "O"
-        return obj.dtype
+        return dtype
 
     @classmethod
     def pts_to_prestep(cls, x, values):


Test patch:
diff --git a/holoviews/tests/operation/test_operation.py b/holoviews/tests/operation/test_operation.py
index 0f5f0b8a0a..a04b2f1630 100644
--- a/holoviews/tests/operation/test_operation.py
+++ b/holoviews/tests/operation/test_operation.py
@@ -765,6 +765,14 @@ def test_interpolate_datetime_curve_post(self):
         curve = Curve((dates_interp, [0, 0, 1, 1, 2, 2, 3]))
         assert_element_equal(interpolated, curve)
 
+    @pytest.mark.parametrize("dtype", [pd.Int64Dtype(), np.int64])
+    def test_interpolate_pandas_dtype_curve_post(self, dtype):
+        df = pd.DataFrame(dict(a=[1, 2, 3], b=[3, 4, 5]), dtype=dtype)
+        interpolated = interpolate_curve(Curve(df), interpolation='steps-post')
+        df = pd.DataFrame(dict(a=[1, 2, 2, 3, 3], b=[3, 3, 4, 4, 5]))
+        expected = Curve(df)
+        assert_element_equal(interpolated, expected)
+
     def test_stack_area_overlay(self):
         areas = Area([1, 2, 3]) * Area([1, 2, 3])
         stacked = Area.stack(areas)