Repository: holoviz/hvplot

Issue:
Users should be able to specify which widgets they want for groupby
I think the syntax will be `widgets={'dim': pn.widgets.DiscreteSlider}`



Discussion:

Resolving patch:
diff --git a/hvplot/plotting/core.py b/hvplot/plotting/core.py
--- a/hvplot/plotting/core.py
+++ b/hvplot/plotting/core.py
@@ -1,8 +1,15 @@
 from __future__ import absolute_import
+from collections import defaultdict
 
 import param
+try:
+    import panel as pn
+    panel_available = True
+except:
+    panel_available = False
 
 from ..converter import HoloViewsConverter
+from ..util import process_dynamic_args
 
 
 class hvPlotBase(object):
@@ -31,8 +38,38 @@ def __call__(self, x=None, y=None, kind=None, **kwds):
         -------
         HoloViews object: Object representing the requested visualization
         """
-        if kind is not None and kind not in self.__all__:
-            raise NotImplementedError(f"kind='{kind}' for data of type {type(self._data)}")
+        if isinstance(kind, str) and kind not in self.__all__:
+            raise NotImplementedError("kind='{kind}' for data of type {type}".format(
+                kind=kind, type=type(self._data)))
+
+        if panel_available:
+            dynamic , arg_deps, arg_names = process_dynamic_args(x, y, kind, **kwds)
+            if dynamic or arg_deps:
+                if kwds.get('groupby', None):
+                    raise ValueError('Groupby is not yet supported when using explicit widgets')
+                @pn.depends(*arg_deps, **dynamic)
+                def callback(*args, **dyn_kwds):
+                    xd = dyn_kwds.pop('x', x)
+                    yd = dyn_kwds.pop('y', y)
+                    kindd = dyn_kwds.pop('kind', kind)
+
+                    combined_kwds = dict(kwds, **dyn_kwds)
+                    fn_args = defaultdict(list)
+                    for name, arg in zip(arg_names, args):
+                        fn_args[(name, kwds[name])].append(arg)
+                    for (name, fn), args in fn_args.items():
+                        combined_kwds[name] = fn(*args)
+                    return self._get_converter(xd, yd, kindd, **combined_kwds)(kindd, xd, yd)
+
+                return pn.panel(callback)
+            elif 'widgets' in kwds:
+                widgets = kwds.pop('widgets')
+                for w in  widgets.values():
+                    if not issubclass(w, pn.widgets.Widget):
+                        raise ValueError('Expected widgets to be dict of form dim: '
+                                         'pn.widgets.Widget Got type {}'.format(w))
+                plot = self._get_converter(x, y, kind, **kwds)(kind, x, y)
+                return pn.panel(plot, widgets=widgets)
 
         return self._get_converter(x, y, kind, **kwds)(kind, x, y)
 
diff --git a/hvplot/util.py b/hvplot/util.py
--- a/hvplot/util.py
+++ b/hvplot/util.py
@@ -4,9 +4,16 @@
 from __future__ import absolute_import
 
 from distutils.version import LooseVersion
+from types import FunctionType
 
 import pandas as pd
 import holoviews as hv
+import param
+try:
+    import panel as pn
+    panel_available = True
+except:
+    panel_available = False
 
 from holoviews.core.util import basestring
 
@@ -384,3 +391,26 @@ def process_derived_datetime_pandas(data, not_found, indexes=None):
     not_found = [var for var in not_found if var not in extra_cols.keys()]
 
     return not_found, data
+
+
+def process_dynamic_args(x, y, kind, **kwds):
+    dynamic = {}
+    arg_deps = []
+    arg_names = []
+
+    for k, v in list(kwds.items()) + [('x', x), ('y', y), ('kind', kind)]:
+        if isinstance(v, param.Parameter):
+            dynamic[k] = v
+        elif panel_available and isinstance(v, pn.widgets.Widget):
+            if LooseVersion(pn.__version__) < '0.6.4':
+                dynamic[k] = v.param.value
+            else:
+                dynamic[k] = v
+
+    for k, v in kwds.items():
+        if k not in dynamic and isinstance(v, FunctionType) and hasattr(v, '_dinfo'):
+            deps = v._dinfo['dependencies']
+            arg_deps += list(deps)
+            arg_names += list(k) * len(deps)
+
+    return dynamic, arg_deps, arg_names
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -130,6 +130,7 @@ def package_assets(example_path):
 
 _examples_extra = [
     'geoviews >=1.6.0',
+    'panel',
     'geopandas',
     'xarray',
     'networkx',


Test patch:
diff --git a/hvplot/tests/testpanel.py b/hvplot/tests/testpanel.py
new file mode 100644
--- /dev/null
+++ b/hvplot/tests/testpanel.py
@@ -0,0 +1,69 @@
+"""
+Tests for panel widgets and param objects as arguments
+"""
+from unittest import TestCase, SkipTest
+
+from hvplot.util import process_xarray  # noqa
+
+def look_for_class(panel, classname, items=None):
+    """
+    Descend a panel object and find any instances of the given class
+    """
+    import panel as pn
+
+    if items is None:
+        items = []
+    if isinstance(panel, pn.layout.ListPanel):
+        for p in panel:
+            items = look_for_class(p, classname, items)
+    elif isinstance(panel, classname):
+        items.append(panel)
+    return items
+
+
+class TestPanelObjects(TestCase):
+
+    def setUp(self):
+        try:
+            import panel as pn  # noqa
+            import hvplot.pandas  # noqa
+        except:
+            raise SkipTest('panel not available')
+
+        from bokeh.sampledata.iris import flowers
+        self.flowers = flowers
+        self.cols = list(self.flowers.columns[:-1])
+
+
+    def test_using_explicit_widgets_works(self):
+        import panel as pn
+
+        x = pn.widgets.Select(name='x', value='sepal_length', options=self.cols)
+        y = pn.widgets.Select(name='y', value='sepal_width', options=self.cols)
+        kind = pn.widgets.Select(name='kind', value='scatter', options=['bivariate', 'scatter'])
+        by_species = pn.widgets.Checkbox(name='By species')
+        color = pn.widgets.ColorPicker(value='#ff0000')
+
+        @pn.depends(by_species.param.value, color.param.value)
+        def by_species_fn(by_species, color):
+            return 'species' if by_species else color
+
+        self.flowers.hvplot(x, y=y, kind=kind.param.value, c=color)
+
+    def test_casting_widgets_to_different_classes(self):
+        import panel as pn
+
+        pane = self.flowers.hvplot.scatter(
+            groupby='species', legend='top_right',
+            widgets={'species': pn.widgets.DiscreteSlider})
+
+        assert len(look_for_class(pane, pn.widgets.DiscreteSlider)) == 1
+
+    def test_using_explicit_widgets_with_groupby_raises_error(self):
+        import panel as pn
+
+        x = pn.widgets.Select(name='x', value='sepal_length', options=self.cols)
+        y = pn.widgets.Select(name='y', value='sepal_width', options=self.cols)
+
+        with self.assertRaisesRegex(ValueError, "Groupby is not yet"):
+            self.flowers.hvplot(x, y, groupby='species')
diff --git a/hvplot/tests/testutil.py b/hvplot/tests/testutil.py
--- a/hvplot/tests/testutil.py
+++ b/hvplot/tests/testutil.py
@@ -207,3 +207,58 @@ def test_proj_to_cartopy(self):
         crs = proj_to_cartopy('+init=epsg:26911')
 
         assert isinstance(crs, self.ccrs.CRS)
+
+
+class TestDynamicArgs(TestCase):
+
+    def setUp(self):
+        try:
+            import panel as pn  # noqa
+        except:
+            raise SkipTest('panel not available')
+
+    def test_dynamic_and_static(self):
+        import panel as pn
+        from ..util import process_dynamic_args
+
+        x = 'sepal_width'
+        y = pn.widgets.Select(name='y', value='sepal_length', options=['sepal_length', 'petal_length'])
+        kind = pn.widgets.Select(name='kind', value='scatter', options=['bivariate', 'scatter'])
+
+        dynamic, arg_deps, arg_names = process_dynamic_args(x, y, kind)
+        assert 'x' not in dynamic
+        assert 'y' in dynamic
+        assert arg_deps == []
+
+    def test_dynamic_kwds(self):
+        import panel as pn
+        from ..util import process_dynamic_args
+
+        x = 'sepal_length'
+        y = 'sepal_width'
+        kind = 'scatter'
+        color = pn.widgets.ColorPicker(value='#ff0000')
+
+        dynamic, arg_deps, arg_names = process_dynamic_args(x, y, kind, c=color)
+        assert 'x' not in dynamic
+        assert 'c' in dynamic
+        assert arg_deps == []
+
+    def test_fn_kwds(self):
+        import panel as pn
+        from ..util import process_dynamic_args
+
+        x = 'sepal_length'
+        y = 'sepal_width'
+        kind = 'scatter'
+        by_species = pn.widgets.Checkbox(name='By species')
+        color = pn.widgets.ColorPicker(value='#ff0000')
+
+        @pn.depends(by_species.param.value, color.param.value)
+        def by_species_fn(by_species, color):
+            return 'species' if by_species else color
+
+        dynamic, arg_deps, arg_names = process_dynamic_args(x, y, kind, c=by_species_fn)
+        assert dynamic == {}
+        assert arg_names == ['c', 'c']
+        assert len(arg_deps) == 2