Repository: holoviz/hvplot

Issue:
Using quadmesh with interactive
`AttributeError: 'function' object has no attribute 'quadmesh'`

```
ds.air.interactive.hvplot.quadmesh()
```


Discussion:
Hmm; none of the accessors seem to work with xarray .interactive (.scatter, .points, etc.). Reproducer:

```
import xarray as xr, hvplot.xarray
ds = xr.tutorial.open_dataset('air_temperature').load()
ds.air.interactive.hvplot.points()
```

Resolving patch:
diff --git a/hvplot/interactive.py b/hvplot/interactive.py
--- a/hvplot/interactive.py
+++ b/hvplot/interactive.py
@@ -5,7 +5,7 @@
 import abc
 import operator
 import sys
-
+from functools import partial
 from types import FunctionType, MethodType
 
 import holoviews as hv
@@ -17,6 +17,7 @@
 from panel.util import get_method_owner, full_groupby
 from panel.widgets.base import Widget
 
+from .converter import HoloViewsConverter
 from .util import _flatten, is_tabular, is_xarray, is_xarray_dataarray
 
 
@@ -44,7 +45,7 @@ def _find_widgets(op):
     return widgets
 
 
-class Interactive():
+class Interactive:
     """
     Interactive is a wrapper around a Python object that lets users create
     interactive pipelines by calling existing APIs on an object with
@@ -112,6 +113,7 @@ def __init__(self, obj, transform=None, fn=None, plot=False, depth=0,
         ds = hv.Dataset(self._obj)
         self._current = self._transform.apply(ds, keep_index=True, compute=False)
         self._init = True
+        self.hvplot = _hvplot(self)
 
     def _update_obj(self, *args):
         self._obj = self._fn.eval(self._fn.object)
@@ -127,7 +129,7 @@ def _fn_params(self):
             parameterized = get_method_owner(self._fn.object)
             deps = parameterized.param.method_dependencies(self._fn.object.__name__)
         return deps
-        
+
     @property
     def _params(self):
         ps = self._fn_params
@@ -394,13 +396,6 @@ def get_ax():
         transform = type(transform)(transform, 'plot', accessor=True)
         return new._clone(transform(*args, **kwargs), plot=True)
 
-    def hvplot(self, *args, **kwargs):
-        new = self._resolve_accessor()
-        transform = new._transform
-        transform = type(transform)(transform, 'hvplot', accessor=True)
-        dmap = 'kind' not in kwargs
-        return new._clone(transform(*args, **kwargs), dmap=dmap)
-
     #----------------------------------------------------------------
     # Public API
     #----------------------------------------------------------------
@@ -512,3 +507,37 @@ def widgets(self):
                 if w not in widgets:
                     widgets.append(w)
         return pn.Column(*widgets)
+
+
+class _hvplot:
+    _kinds = tuple(HoloViewsConverter._kind_mapping)
+
+    __slots__ = ["_interactive"]
+
+    def __init__(self, _interactive):
+        self._interactive = _interactive
+
+    def __call__(self, *args, _kind=None, **kwargs):
+        # The underscore in _kind is to not overwrite it
+        # if 'kind' is in kwargs and the function
+        # is used with partial.
+        if _kind and "kind" in kwargs:
+            raise TypeError(f"{_kind}() got an unexpected keyword argument 'kind'")
+        if _kind:
+            kwargs["kind"] = _kind
+
+        new = self._interactive._resolve_accessor()
+        transform = new._transform
+        transform = type(transform)(transform, 'hvplot', accessor=True)
+        dmap = 'kind' not in kwargs or not isinstance(kwargs['kind'], str)
+        return new._clone(transform(*args, **kwargs), dmap=dmap)
+
+    def __getattr__(self, attr):
+        if attr in self._kinds:
+            return partial(self, _kind=attr)
+        else:
+            raise AttributeError(f"'hvplot' object has no attribute '{attr}'")
+
+    def __dir__(self):
+        # This function is for autocompletion
+        return self._interactive._obj.hvplot.__all__


Test patch:
diff --git a/hvplot/tests/testinteractive.py b/hvplot/tests/testinteractive.py
--- a/hvplot/tests/testinteractive.py
+++ b/hvplot/tests/testinteractive.py
@@ -1,13 +1,21 @@
 import pandas as pd
 import panel as pn
+import pytest
 
 from holoviews.util.transform import dim
 
-import hvplot.pandas
+import hvplot.pandas  # noqa
 from hvplot import bind
 from hvplot.interactive import Interactive
 from hvplot.xarray import XArrayInteractive
 
+try:
+    import xarray as xr
+    import hvplot.xarray  # noqa
+except ImportError:
+    xr = None
+
+xr_available = pytest.mark.skipif(xr is None, reason="requires xarray")
 
 def test_interactive_pandas_dataframe():
     df = pd._testing.makeMixedDataFrame()
@@ -29,8 +37,8 @@ def test_interactive_pandas_series():
     assert dfi._fn is None
     assert dfi._transform == dim('*')
 
+@xr_available
 def test_interactive_xarray_dataarray():
-    import xarray as xr
     ds = xr.tutorial.load_dataset('air_temperature')
 
     dsi = Interactive(ds.air)
@@ -40,8 +48,8 @@ def test_interactive_xarray_dataarray():
     assert dsi._fn is None
     assert dsi._transform == dim('air')
 
+@xr_available
 def test_interactive_xarray_dataset():
-    import xarray as xr
     ds = xr.tutorial.load_dataset('air_temperature')
 
     dsi = Interactive(ds)
@@ -55,7 +63,7 @@ def test_interactive_pandas_function():
     df = pd._testing.makeMixedDataFrame()
 
     select = pn.widgets.Select(options=list(df.columns))
-    
+
     def sel_col(col):
         return df[col]
 
@@ -68,13 +76,13 @@ def sel_col(col):
     select.value = 'B'
     assert dfi._obj is df.B
 
+@xr_available
 def test_interactive_xarray_function():
-    import xarray as xr
     ds = xr.tutorial.load_dataset('air_temperature')
     ds['air2'] = ds.air*2
 
     select = pn.widgets.Select(options=list(ds))
-    
+
     def sel_col(sel):
         return ds[sel]
 
@@ -89,6 +97,27 @@ def sel_col(sel):
     assert dsi._transform == dim('air2')
 
 
+def test_interactive_pandas_dataframe_accessor():
+    df = pd._testing.makeMixedDataFrame()
+    dfi = df.interactive()
+
+    assert dfi.hvplot(kind="scatter")._transform == dfi.hvplot.scatter()._transform
+
+    with pytest.raises(TypeError):
+        dfi.hvplot.scatter(kind="area")
+
+
+@xr_available
+def test_interactive_xarray_dataset_accessor():
+    ds = xr.tutorial.load_dataset('air_temperature')
+    dsi = ds.air.interactive
+
+    assert dsi.hvplot(kind="line")._transform == dsi.hvplot.line()._transform
+
+    with pytest.raises(TypeError):
+        dsi.hvplot.line(kind="area")
+
+
 def test_interactive_with_bound_function_calls():
     df = pd.DataFrame({"species": [1, 1, 2, 2], "sex": 2 * ["MALE", "FEMALE"]})