holoviz / hvplot

A high-level plotting API for pandas, dask, xarray, and networkx built on HoloViews
https://hvplot.holoviz.org
BSD 3-Clause "New" or "Revised" License
1.08k stars 105 forks source link

hvPlot for derived classes #509

Open jbednar opened 4 years ago

jbednar commented 4 years ago

For hvplot=0.6.0, I expected to be able to use .hvplot on both library-provided classes and on user-derived classes. For instance, I expected to be able to define my own subclass of an xr.DataArray, streamz.dataframe.Dataframe, or pd.DataFrame class, and to use .hvplot() on those objects the same as on the superclass objects as long as I didn't do anything crazy in the subclass.

For a no-op derived class (just "pass"), it seems to work fine for pd.DataFrame:

image

But it doesn't work for the other two types:

image image

---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-2-f306fc2739a7> in <module>
      2     pass
      3 
----> 4 MyDataArray(np.random.randn(2, 3), dims=("x", "y"), coords={"x": [10, 20]}, name="rand").hvplot()

~/miniconda3/envs/holoviz-tutorial/lib/python3.7/site-packages/hvplot/plotting/core.py in __call__(self, x, y, kind, **kwds)
     70                 return pn.panel(plot, **panel_dict)
     71 
---> 72         return self._get_converter(x, y, kind, **kwds)(kind, x, y)
     73 
     74     def _get_converter(self, x=None, y=None, kind=None, **kwds):

~/miniconda3/envs/holoviz-tutorial/lib/python3.7/site-packages/hvplot/plotting/core.py in _get_converter(self, x, y, kind, **kwds)
     78         kind = kind or params.pop('kind', None)
     79         return HoloViewsConverter(
---> 80             self._data, x, y, kind=kind, **params
     81         )
     82 

~/miniconda3/envs/holoviz-tutorial/lib/python3.7/site-packages/hvplot/converter.py in __init__(self, data, x, y, kind, by, use_index, group_label, value_label, backlog, persist, use_dask, crs, fields, groupby, dynamic, grid, legend, rot, title, xlim, ylim, clim, symmetric, logx, logy, loglog, hover, subplots, label, invert, stacked, colorbar, datashade, rasterize, row, col, figsize, debug, framewise, aggregator, projection, global_extent, geo, precompute, flip_xaxis, flip_yaxis, dynspread, hover_cols, x_sampling, y_sampling, project, tools, attr_labels, coastline, tiles, sort_date, check_symmetric_max, **kwds)
    322         self._process_data(kind, data, x, y, by, groupby, row, col,
    323                            use_dask, persist, backlog, label, value_label,
--> 324                            hover_cols, attr_labels, kwds)
    325 
    326         self.dynamic = dynamic

~/miniconda3/envs/holoviz-tutorial/lib/python3.7/site-packages/hvplot/converter.py in _process_data(self, kind, data, x, y, by, groupby, row, col, use_dask, persist, backlog, label, value_label, hover_cols, attr_labels, kwds)
    657             self.data = data
    658         else:
--> 659             raise ValueError('Supplied data type %s not understood' % type(data).__name__)
    660 
    661         # Validate data and arguments

ValueError: Supplied data type MyDataArray not understood

image image

---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-2-c6573a02656f> in <module>
      2     pass
      3 
----> 4 MyRandom(interval='200ms', freq='50ms').hvplot()

~/miniconda3/envs/holoviz-tutorial/lib/python3.7/site-packages/hvplot/plotting/core.py in __call__(self, x, y, kind, **kwds)
     70                 return pn.panel(plot, **panel_dict)
     71 
---> 72         return self._get_converter(x, y, kind, **kwds)(kind, x, y)
     73 
     74     def _get_converter(self, x=None, y=None, kind=None, **kwds):

~/miniconda3/envs/holoviz-tutorial/lib/python3.7/site-packages/hvplot/plotting/core.py in _get_converter(self, x, y, kind, **kwds)
     78         kind = kind or params.pop('kind', None)
     79         return HoloViewsConverter(
---> 80             self._data, x, y, kind=kind, **params
     81         )
     82 

~/miniconda3/envs/holoviz-tutorial/lib/python3.7/site-packages/hvplot/converter.py in __init__(self, data, x, y, kind, by, use_index, group_label, value_label, backlog, persist, use_dask, crs, fields, groupby, dynamic, grid, legend, rot, title, xlim, ylim, clim, symmetric, logx, logy, loglog, hover, subplots, label, invert, stacked, colorbar, datashade, rasterize, row, col, figsize, debug, framewise, aggregator, projection, global_extent, geo, precompute, flip_xaxis, flip_yaxis, dynspread, hover_cols, x_sampling, y_sampling, project, tools, attr_labels, coastline, tiles, sort_date, check_symmetric_max, **kwds)
    322         self._process_data(kind, data, x, y, by, groupby, row, col,
    323                            use_dask, persist, backlog, label, value_label,
--> 324                            hover_cols, attr_labels, kwds)
    325 
    326         self.dynamic = dynamic

~/miniconda3/envs/holoviz-tutorial/lib/python3.7/site-packages/hvplot/converter.py in _process_data(self, kind, data, x, y, by, groupby, row, col, use_dask, persist, backlog, label, value_label, hover_cols, attr_labels, kwds)
    657             self.data = data
    658         else:
--> 659             raise ValueError('Supplied data type %s not understood' % type(data).__name__)
    660 
    661         # Validate data and arguments

ValueError: Supplied data type MyRandom not understood

My guess is that it's because this code in hvplot/util.py is only checking by the source module, not isinstance:

def check_library(obj, library):
    if not isinstance(library, list):
        library = [library]
    return any([obj.__module__.split('.')[0].startswith(l) for l in library])

But if so I don't have any explanation for why classes derived from pd.DataFrame seem to work, given that all the classes defined above should be in module __main__.

jbednar commented 4 years ago

Ah, looks like hvplot/converter.py is using isinstance(data, pd.DataFrame) for pandas, but check_library for the rest. That explains the different behavior, leaving this as a request for hvplot to check by class in all the cases, at least if the module lookup fails. Looks tricky, as we'll now need to store a list of the (super)classes supported, not just the modules, but I think it's important.

jbednar commented 4 years ago

For now a workaround is to falsely claim your subclass is defined where the superclass is, as in MyRandom.__module__= 'streamz.dataframe.core'. Seems to work, but obviously not recommended in the long term!

maximlt commented 1 year ago

It seems to be that check_library could be replaced by:

def check_module(data, module):
    if module =='dask' and module in sys.modules:
        import dask.dataframe as dd
        return isinstance(data, (dd.Series, dd.DataFrame))
   elif ...

as we'll now need to store a list of the (super)classes supported, not just the modules, but I think it's important.

Having a mapping of modules and their supported classes wouldn't be a bad idea!