diff --git a/hvplot/plotting/core.py b/hvplot/plotting/core.py index 02d530639..2da4aef4a 100644 --- a/hvplot/plotting/core.py +++ b/hvplot/plotting/core.py @@ -8,7 +8,7 @@ panel_available = False from ..converter import HoloViewsConverter -from ..util import process_dynamic_args +from ..util import is_list_like, process_dynamic_args class hvPlotBase: @@ -33,8 +33,8 @@ def __call__(self, x=None, y=None, kind=None, **kwds): Parameters ---------- - x, y : string, optional - Field name in the data to draw x- and y-positions from + x, y : string, list, or array-like, optional + Field name(s) in the data to draw x- and y-positions from kind : string, optional The kind of plot to generate, e.g. 'line', 'scatter', etc. **kwds : optional @@ -44,6 +44,10 @@ def __call__(self, x=None, y=None, kind=None, **kwds): ------- HoloViews object: Object representing the requested visualization """ + # Convert an array-like to a list + x = list(x) if is_list_like(x) else x + y = list(y) if is_list_like(y) else y + 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))) diff --git a/hvplot/tests/plotting/testcore.py b/hvplot/tests/plotting/testcore.py new file mode 100644 index 000000000..a2ed1ea3a --- /dev/null +++ b/hvplot/tests/plotting/testcore.py @@ -0,0 +1,21 @@ +import numpy as np +import pandas as pd +import hvplot.pandas # noqa + +import pytest + +@pytest.mark.parametrize("y", ( + ["A", "B", "C", "D"], + ("A", "B", "C", "D"), + {"A", "B", "C", "D"}, + np.array(["A", "B", "C", "D"]), + pd.Index(["A", "B", "C", "D"]), + pd.Series(["A", "B", "C", "D"]), + )) +def test_diffent_input_types(y): + df = pd._testing.makeDataFrame() + types = {t for t in dir(df.hvplot) if not t.startswith("_")} + ignore_types = {'bivariate', 'heatmap', 'hexbin', 'labels', 'vectorfield'} + + for t in types - ignore_types: + df.hvplot(y=y, kind=t) diff --git a/hvplot/tests/testutil.py b/hvplot/tests/testutil.py index c0cf65c74..e14cb40bc 100644 --- a/hvplot/tests/testutil.py +++ b/hvplot/tests/testutil.py @@ -4,11 +4,12 @@ import sys import numpy as np +import pandas as pd import pytest from unittest import TestCase, SkipTest -from hvplot.util import check_crs, process_xarray +from hvplot.util import check_crs, is_list_like, process_xarray class TestProcessXarray(TestCase): @@ -275,3 +276,15 @@ def test_check_crs(): assert p.srs == '+proj=utm +zone=15 +datum=NAD83 +units=m +no_defs' p = check_crs('wrong') assert p is None + + +def test_is_list_like(): + assert not is_list_like(0) + assert not is_list_like('string') + assert not is_list_like(np.array('a')) + assert is_list_like(['a', 'b']) + assert is_list_like(('a', 'b')) + assert is_list_like({'a', 'b'}) + assert is_list_like(pd.Series(['a', 'b'])) + assert is_list_like(pd.Index(['a', 'b'])) + assert is_list_like(np.array(['a', 'b'])) diff --git a/hvplot/util.py b/hvplot/util.py index d9b3a28b0..9cc6e2ba6 100644 --- a/hvplot/util.py +++ b/hvplot/util.py @@ -8,6 +8,7 @@ from packaging.version import Version from types import FunctionType +import numpy as np import pandas as pd import holoviews as hv import param @@ -252,6 +253,21 @@ def process_crs(crs): raise ValueError("Projection must be defined as a EPSG code, proj4 string, cartopy CRS or pyproj.Proj.") return crs + +def is_list_like(obj): + """ + Adapted from pandas' is_list_like cython function. + """ + return ( + # equiv: `isinstance(obj, abc.Iterable)` + hasattr(obj, "__iter__") and not isinstance(obj, type) + # we do not count strings/unicode/bytes as list-like + and not isinstance(obj, (str, bytes)) + # exclude zero-dimensional numpy arrays, effectively scalars + and not (isinstance(obj, np.ndarray) and obj.ndim == 0) + ) + + def is_tabular(data): if check_library(data, ['dask', 'streamz', 'pandas', 'geopandas', 'cudf']): return True