diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 78d98f37a2..f39843f275 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,6 +20,8 @@ jobs: exclude: - os: windows-latest python-version: 2.7 + - os: macos-latest + python-version: 3.7 timeout-minutes: 30 defaults: run: @@ -28,7 +30,7 @@ jobs: DESC: "Python ${{ matrix.python-version }} tests" HV_REQUIREMENTS: "unit_tests" PYTHON_VERSION: ${{ matrix.python-version }} - CHANS_DEV: "-c pyviz/label/dev -c bokeh" + CHANS_DEV: "-c pyviz/label/dev" CHANS: "-c pyviz" MPLBACKEND: "Agg" GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -61,6 +63,12 @@ jobs: git describe echo "======" conda list + - name: bokeh update + if: startsWith(matrix.python-version, 3.) + run: | + eval "$(conda shell.bash hook)" + conda activate test-environment + conda install "bokeh>=2.2" - name: matplotlib patch if: startsWith(matrix.python-version, 3.) run: | diff --git a/.gitignore b/.gitignore index a8d7dba515..924a1fdad9 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,6 @@ holoviews.rc ghostdriver.log holoviews/.version .dir-locals.el +.doit.db +.vscode/settings.json +holoviews/.vscode/settings.json diff --git a/.travis.yml b/.travis.yml index a6c4b75893..ff56160f5f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,7 +22,7 @@ notifications: env: global: - PKG_TEST_PYTHON="--test-python=py37 --test-python=py27" - - CHANS_DEV="-c pyviz/label/dev -c bokeh" + - CHANS_DEV="-c pyviz/label/dev -c bokeh -c conda-forge" - CHANS="-c pyviz" - MPLBACKEND="Agg" - PYTHON_VERSION=3.7 @@ -69,7 +69,7 @@ jobs: install: - doit env_create $CHANS_DEV --python=$PYTHON_VERSION - source activate test-environment - - travis_wait 30 doit develop_install $CHANS_DEV -o $HV_REQUIREMENTS + - travis_wait 45 doit develop_install $CHANS_DEV -o $HV_REQUIREMENTS - if [ "$PYTHON_VERSION" == "3.6" ]; then conda uninstall matplotlib matplotlib-base --force; conda install $CHANS_DEV matplotlib=3.0.3 --no-deps; fi; - doit env_capture - hash -r diff --git a/binder/environment.yml b/binder/environment.yml index c3928e98c4..65ccbd5653 100644 --- a/binder/environment.yml +++ b/binder/environment.yml @@ -32,3 +32,4 @@ dependencies: - bzip2 - dask - scipy + - ibis-framework >= 1.3 \ No newline at end of file diff --git a/holoviews/core/data/__init__.py b/holoviews/core/data/__init__.py index 29f43e9b66..1f1600cd87 100644 --- a/holoviews/core/data/__init__.py +++ b/holoviews/core/data/__init__.py @@ -10,6 +10,7 @@ import numpy as np import param +import pandas as pd # noqa from param.parameterized import add_metaclass, ParameterizedMetaclass @@ -21,57 +22,24 @@ from ..element import Element from ..ndmapping import OrderedDict, MultiDimensionalMapping from ..spaces import HoloMap, DynamicMap -from .interface import Interface, iloc, ndloc -from .array import ArrayInterface -from .dictionary import DictInterface -from .grid import GridInterface + +from .array import ArrayInterface # noqa (API import) +from .cudf import cuDFInterface # noqa (API import) +from .dask import DaskInterface # noqa (API import) +from .dictionary import DictInterface # noqa (API import) +from .grid import GridInterface # noqa (API import) +from .ibis import IbisInterface # noqa (API import) +from .interface import Interface, iloc, ndloc # noqa (API import) from .multipath import MultiInterface # noqa (API import) from .image import ImageInterface # noqa (API import) +from .pandas import PandasInterface # noqa (API import) +from .spatialpandas import SpatialPandasInterface # noqa (API import) +from .xarray import XArrayInterface # noqa (API import) -default_datatype = 'dictionary' -datatypes = ['dictionary', 'grid'] - -try: - import pandas as pd # noqa (Availability import) - from .pandas import PandasInterface - default_datatype = 'dataframe' - datatypes.insert(0, 'dataframe') - DFColumns = PandasInterface -except ImportError: - pd = None -except Exception as e: - pd = None - param.main.param.warning('Pandas interface failed to import with ' - 'following error: %s' % e) - -try: - from .spatialpandas import SpatialPandasInterface # noqa (API import) - datatypes.append('spatialpandas') -except ImportError: - pass - -try: - from .xarray import XArrayInterface # noqa (Conditional API import) - datatypes.append('xarray') -except ImportError: - pass +default_datatype = 'dataframe' -try: - from .cudf import cuDFInterface # noqa (Conditional API import) - datatypes.append('cuDF') -except ImportError: - pass - -try: - from .dask import DaskInterface # noqa (Conditional API import) - datatypes.append('dask') -except ImportError: - pass - -if 'array' not in datatypes: - datatypes.append('array') -if 'multitabular' not in datatypes: - datatypes.append('multitabular') +datatypes = ['dataframe', 'dictionary', 'grid', 'xarray', 'dask', + 'cuDF', 'spatialpandas', 'array', 'multitabular', 'ibis'] def concat(datasets, datatype=None): @@ -370,6 +338,10 @@ def __init__(self, data, kdims=None, vdims=None, **kwargs): ) self._transforms = input_transforms or [] + # On lazy interfaces this allows keeping an evaluated version + # of the dataset in memory + self._cached = None + # Handle initializing the dataset property. self._dataset = input_dataset if self._dataset is None and isinstance(input_data, Dataset) and not dataset_provided: @@ -403,7 +375,6 @@ def dataset(self): return Dataset(self, _validate_vdims=False, **self._dataset) return self._dataset - @property def pipeline(self): """ @@ -413,6 +384,34 @@ def pipeline(self): """ return self._pipeline + def compute(self): + """ + Computes the data to a data format that stores the daata in + memory, e.g. a Dask dataframe or array is converted to a + Pandas DataFrame or NumPy array. + + Returns: + Dataset with the data stored in in-memory format + """ + return self.interface.compute(self) + + def persist(self): + """ + Persists the results of a lazy data interface to memory to + speed up data manipulation and visualization. If the + particular data backend already holds the data in memory + this is a no-op. Unlike the compute method this maintains + the same data type. + + Returns: + Dataset with the data persisted to memory + """ + persisted = self.interface.persist(self) + if persisted.interface is self.interface: + return persisted + self._cached = persisted + return self + def closest(self, coords=[], **kwargs): """Snaps coordinate(s) to closest coordinate in Dataset @@ -441,7 +440,7 @@ def closest(self, coords=[], **kwargs): if xs.dtype.kind in 'SO': raise NotImplementedError("Closest only supported for numeric types") idxs = [np.argmin(np.abs(xs-coord)) for coord in coords] - return [xs[idx] for idx in idxs] + return [type(s)(xs[idx]) for s, idx in zip(coords, idxs)] def sort(self, by=None, reverse=False): @@ -594,15 +593,13 @@ def select(self, selection_expr=None, selection_specs=None, **selection): # Handle selection dim expression if selection_expr is not None: mask = selection_expr.apply(self, compute=False, keep_index=True) - dataset = self[mask] - else: - dataset = self + selection = {'selection_mask': mask} # Handle selection kwargs if selection: - data = dataset.interface.select(dataset, **selection) + data = self.interface.select(self, **selection) else: - data = dataset.data + data = self.data if np.isscalar(data): return data @@ -678,7 +675,7 @@ def __getitem__(self, slices): if not len(slices) == len(self): raise IndexError("Boolean index must match length of sliced object") return self.clone(self.select(selection_mask=slices)) - elif slices in [(), Ellipsis]: + elif (isinstance(slices, ()) and len(slices) == 1) or slices is Ellipsis: return self if not isinstance(slices, tuple): slices = (slices,) value_select = None @@ -770,7 +767,7 @@ def sample(self, samples=[], bounds=None, closest=True, **kwargs): # may be replaced with more general handling # see https://github.com/ioam/holoviews/issues/1173 from ...element import Table, Curve - datatype = ['dataframe', 'dictionary', 'dask'] + datatype = ['dataframe', 'dictionary', 'dask', 'ibis'] if len(samples) == 1: sel = {kd.name: s for kd, s in zip(self.kdims, samples[0])} dims = [kd for kd, v in sel.items() if not np.isscalar(v)] @@ -879,7 +876,7 @@ def aggregate(self, dimensions=None, function=None, spreadfn=None, **kwargs): # Handle functions kdims = [self.get_dimension(d, strict=True) for d in dimensions] - if not len(self): + if not self: if spreadfn: spread_name = spreadfn.__name__ vdims = [d for vd in self.vdims for d in [vd, vd.clone('_'.join([vd.name, spread_name]))]] @@ -905,7 +902,9 @@ def aggregate(self, dimensions=None, function=None, spreadfn=None, **kwargs): for i, d in enumerate(vdims): dim = d.clone('_'.join([d.name, spread_name])) dvals = error.dimension_values(d, flat=False) - combined = combined.add_dimension(dim, ndims+i, dvals, True) + idx = vdims.index(d) + combined = combined.add_dimension(dim, idx+1, dvals, True) + vdims = combined.vdims return combined.clone(new_type=Dataset if generic_type else type(self)) if np.isscalar(aggregated): @@ -1241,10 +1240,3 @@ def ndloc(self): dataset.ndloc[[1, 2, 3], [0, 2, 3]] """ return ndloc(self) - - -# Aliases for pickle backward compatibility -Columns = Dataset -ArrayColumns = ArrayInterface -DictColumns = DictInterface -GridColumns = GridInterface diff --git a/holoviews/core/data/dask.py b/holoviews/core/data/dask.py index 9b7755093d..08f4713537 100644 --- a/holoviews/core/data/dask.py +++ b/holoviews/core/data/dask.py @@ -70,6 +70,14 @@ def init(cls, eltype, data, kdims, vdims): data = reset return data, dims, extra + @classmethod + def compute(cls, dataset): + return dataset.clone(dataset.data.compute()) + + @classmethod + def persiste(cls, dataset): + return dataset.clone(dataset.data.persist()) + @classmethod def shape(cls, dataset): return (len(dataset.data), len(dataset.data.columns)) @@ -263,9 +271,11 @@ def add_dimension(cls, dataset, dimension, dim_pos, values, vdim): data = dataset.data if dimension.name not in data.columns: if not np.isscalar(values): - err = ('Dask dataframe does not support assigning ' - 'non-scalar value.') - raise NotImplementedError(err) + if len(values): + err = ('Dask dataframe does not support assigning ' + 'non-scalar value.') + raise NotImplementedError(err) + values = None data = data.assign(**{dimension.name: values}) return data diff --git a/holoviews/core/data/grid.py b/holoviews/core/data/grid.py index 12ac6447d3..2a33197f17 100644 --- a/holoviews/core/data/grid.py +++ b/holoviews/core/data/grid.py @@ -409,6 +409,17 @@ def ndloc(cls, dataset, indices): selected[d.name] = arr[tuple(adjusted_inds)] return tuple(selected[d.name] for d in dataset.dimensions()) + @classmethod + def persist(cls, dataset): + da = dask_array_module() + return {k: v.persist() if da and isinstance(v, da.Array) else v + for k, v in dataset.data.items()} + + @classmethod + def compute(cls, dataset): + da = dask_array_module() + return {k: v.compute() if da and isinstance(v, da.Array) else v + for k, v in dataset.data.items()} @classmethod def values(cls, dataset, dim, expanded=True, flat=True, compute=True, diff --git a/holoviews/core/data/ibis.py b/holoviews/core/data/ibis.py new file mode 100644 index 0000000000..85afe7e9ff --- /dev/null +++ b/holoviews/core/data/ibis.py @@ -0,0 +1,418 @@ +import sys +import numpy +import ibis + +try: + from collections.abc import Iterable +except ImportError: + from collections import Iterable + +from .. import util +from ..element import Element +from ..ndmapping import NdMapping, item_check, sorted_context +from .interface import Interface, cached +from . import pandas + + +class IbisInterface(Interface): + + types = () + + datatype = "ibis" + + default_partitions = 100 + + # the rowid is needed until ibis updates versions + has_rowid = hasattr(ibis, "rowid") + + @classmethod + def loaded(cls): + return "ibis" in sys.modules + + @classmethod + def applies(cls, obj): + if not cls.loaded(): + return False + from ibis.expr.types import Expr + return isinstance(obj, Expr) + + @classmethod + def init(cls, eltype, data, keys, values): + params = eltype.param.objects() + index = params["kdims"] + columns = params["vdims"] + + if isinstance(index.bounds[1], int): + ndim = min([index.bounds[1], len(index.default)]) + else: + ndim = None + nvdim = columns.bounds[1] if isinstance(columns.bounds[1], int) else None + if keys and values is None: + values = [c for c in data.columns if c not in keys] + elif values and keys is None: + keys = [c for c in data.columns if c not in values][:ndim] + elif keys is None: + keys = list(data.columns[:ndim]) + if values is None: + values = [ + key + for key in data.columns[ndim : ((ndim + nvdim) if nvdim else None)] + if key not in keys + ] + elif keys == [] and values is None: + values = list(data.columns[: nvdim if nvdim else None]) + return data, dict(kdims=keys, vdims=values), {} + + @classmethod + def compute(cls, dataset): + return dataset.clone(dataset.data.execute()) + + @classmethod + def persist(cls, dataset): + return cls.compute(dataset) + + @classmethod + @cached + def length(self, dataset): + # Get the length by counting the length of an empty query. + return dataset.data[[]].count().execute() + + @classmethod + @cached + def nonzero(cls, dataset): + # Make an empty query to see if a row is returned. + return bool(len(dataset.data[[]].head(1).execute())) + + @classmethod + @cached + def range(cls, dataset, dimension): + if cls.dtype(dataset, dimension).kind in 'SUO': + return None, None + column = dataset.data[dataset.get_dimension(dimension, strict=True).name] + return tuple( + dataset.data.aggregate([column.min(), column.max()]).execute().values[0, :] + ) + + @classmethod + @cached + def values( + cls, + dataset, + dimension, + expanded=True, + flat=True, + compute=True, + keep_index=False, + ): + dimension = dataset.get_dimension(dimension, strict=True) + data = dataset.data[dimension.name] + if not expanded: + data = data.distinct() + return data if keep_index or not compute else data.execute().values + + @classmethod + def histogram(cls, expr, bins, density=True, weights=None): + bins = numpy.asarray(bins) + bins = [int(v) if bins.dtype.kind in 'iu' else float(v) for v in bins] + binned = expr.bucket(bins).name('bucket') + hist = numpy.zeros(len(bins)-1) + hist_bins = binned.value_counts().sort_by('bucket').execute() + for b, v in zip(hist_bins['bucket'], hist_bins['count']): + if numpy.isnan(b): + continue + hist[int(b)] = v + if weights is not None: + raise NotImplementedError("Weighted histograms currently " + "not implemented for IbisInterface.") + if density: + hist = hist/expr.count().execute() + return hist, bins + + @classmethod + @cached + def shape(cls, dataset): + return cls.length(dataset), len(dataset.data.columns) + + @classmethod + @cached + def dtype(cls, dataset, dimension): + dimension = dataset.get_dimension(dimension) + return dataset.data.head(0).execute().dtypes[dimension.name] + + dimension_type = dtype + + @classmethod + def sort(cls, dataset, by=[], reverse=False): + return dataset.data.sort_by([(dataset.get_dimension(x).name, not reverse) for x in by]) + + @classmethod + def redim(cls, dataset, dimensions): + return dataset.data.mutate( + **{v.name: dataset.data[k] for k, v in dimensions.items()} + ) + + validate = pandas.PandasInterface.validate + reindex = pandas.PandasInterface.reindex + + @classmethod + def _index_ibis_table(cls, data): + if not cls.has_rowid: + raise ValueError( + "iloc expressions are not supported for ibis version %s." + % ibis.__version__ + ) + + if "hv_row_id__" in data.columns: + return data + return data.mutate(hv_row_id__=ibis.rowid()) + + @classmethod + def iloc(cls, dataset, index): + rows, columns = index + scalar = all(map(util.isscalar, index)) + + if isinstance(columns, slice): + columns = [x.name for x in dataset.dimensions()[columns]] + elif numpy.isscalar(columns): + columns = [dataset.get_dimension(columns).name] + else: + columns = [dataset.get_dimension(d).name for d in columns] + + data = cls._index_ibis_table(dataset.data[columns]) + + if scalar: + return ( + data.filter(data.hv_row_id__ == rows)[columns] + .head(1) + .execute() + .iloc[0, 0] + ) + + if isinstance(rows, slice): + # We should use a pseudo column for the row number but i think that is still awaiting + # a pr on ibis + if any(x is not None for x in (rows.start, rows.stop, rows.step)): + predicates = [] + if rows.start: + predicates += [data.hv_row_id__ >= rows.start] + if rows.stop: + predicates += [data.hv_row_id__ < rows.stop] + + return data.filter(predicates).drop(["hv_row_id__"]) + else: + if not isinstance(rows, Iterable): + rows = [rows] + return data.filter([data.hv_row_id__.isin(rows)]).drop(["hv_row_id__"]) + return data.drop(["hv_row_id__"]) + + @classmethod + def unpack_scalar(cls, dataset, data): + """ + Given a dataset object and data in the appropriate format for + the interface, return a simple scalar. + """ + if len(data.columns) > 1 or data[[]].count().execute() != 1: + return data + return data.execute().iat[0, 0] + + @classmethod + def groupby(cls, dataset, dimensions, container_type, group_type, **kwargs): + # aggregate the necesary dimensions + index_dims = [dataset.get_dimension(d, strict=True) for d in dimensions] + element_dims = [kdim for kdim in dataset.kdims if kdim not in index_dims] + + group_kwargs = {} + if group_type != "raw" and issubclass(group_type, Element): + group_kwargs = dict(util.get_param_values(dataset), kdims=element_dims) + group_kwargs.update(kwargs) + group_kwargs["dataset"] = dataset.dataset + + group_by = [d.name for d in index_dims] + + # execute a query against the table to find the unique groups. + groups = dataset.data.groupby(group_by).aggregate().execute() + + # filter each group based on the predicate defined. + data = [ + ( + tuple(s.values.tolist()), + group_type( + dataset.data.filter( + [dataset.data[k] == v for k, v in s.to_dict().items()] + ), + **group_kwargs + ), + ) + for i, s in groups.iterrows() + ] + if issubclass(container_type, NdMapping): + with item_check(False), sorted_context(False): + return container_type(data, kdims=index_dims) + else: + return container_type(data) + + @classmethod + def assign(cls, dataset, new_data): + return dataset.data.mutate(**new_data) + + @classmethod + def add_dimension(cls, dataset, dimension, dim_pos, values, vdim): + data = dataset.data + if dimension.name not in data.columns: + if not isinstance(values, ibis.Expr) and not numpy.isscalar(values): + raise ValueError("Cannot assign %s type as a Ibis table column, " + "expecting either ibis.Expr or scalar." + % type(values).__name__) + data = data.mutate(**{dimension.name: values}) + return data + + @classmethod + @cached + def isscalar(cls, dataset, dim): + return ( + dataset.data[dataset.get_dimension(dim, strict=True).name] + .distinct() + .count() + .compute() + == 1 + ) + + @classmethod + def select(cls, dataset, selection_mask=None, **selection): + if selection_mask is None: + selection_mask = cls.select_mask(dataset, selection) + indexed = cls.indexed(dataset, selection) + data = dataset.data + + if isinstance(selection_mask, numpy.ndarray): + data = cls._index_ibis_table(data) + if selection_mask.dtype == numpy.dtype("bool"): + selection_mask = numpy.where(selection_mask)[0] + data = data.filter( + data["hv_row_id__"].isin(list(map(int, selection_mask))) + ).drop(["hv_row_id__"]) + elif selection_mask is not None and not (isinstance(selection_mask, list) and not selection_mask): + data = data.filter(selection_mask) + + if indexed and data.count().execute() == 1 and len(dataset.vdims) == 1: + return data[dataset.vdims[0].name].execute().iloc[0] + return data + + @classmethod + def select_mask(cls, dataset, selection): + predicates = [] + for dim, object in selection.items(): + if isinstance(object, tuple): + object = slice(*object) + alias = dataset.get_dimension(dim).name + column = dataset.data[alias] + if isinstance(object, slice): + if object.start is not None: + # Workaround for dask issue #3392 + bound = util.numpy_scalar_to_python(object.start) + predicates.append(bound <= column) + if object.stop is not None: + bound = util.numpy_scalar_to_python(object.stop) + predicates.append(column < bound) + elif isinstance(object, (set, list)): + # rowid conditions + condition = None + for id in object: + predicate = column == id + condition = ( + predicate if condition is None else condition | predicate + ) + if condition is not None: + predicates.append(condition) + elif callable(object): + predicates.append(object(column)) + elif isinstance(object, ibis.Expr): + predicates.append(object) + else: + predicates.append(column == object) + return predicates + + @classmethod + def sample(cls, dataset, samples=[]): + dims = dataset.dimensions() + data = dataset.data + if all(util.isscalar(s) or len(s) == 1 for s in samples): + items = [s[0] if isinstance(s, tuple) else s for s in samples] + return data[data[dims[0].name].isin(items)] + + predicates = None + for sample in samples: + if util.isscalar(sample): + sample = [sample] + if not sample: + continue + predicate = None + for i, v in enumerate(sample): + p = data[dims[i].name] == ibis.literal(util.numpy_scalar_to_python(v)) + if predicate is None: + predicate = p + else: + predicate &= p + if predicates is None: + predicates = predicate + else: + predicates |= predicate + return data if predicates is None else data.filter(predicates) + + @classmethod + def aggregate(cls, dataset, dimensions, function, **kwargs): + data = dataset.data + columns = [d.name for d in dataset.kdims if d in dimensions] + values = dataset.dimensions("value", label="name") + new = data[columns + values] + + function = { + numpy.min: ibis.expr.operations.Min, + numpy.nanmin: ibis.expr.operations.Min, + numpy.max: ibis.expr.operations.Max, + numpy.nanmax: ibis.expr.operations.Max, + numpy.mean: ibis.expr.operations.Mean, + numpy.nanmean: ibis.expr.operations.Mean, + numpy.std: ibis.expr.operations.StandardDev, + numpy.nanstd: ibis.expr.operations.StandardDev, + numpy.sum: ibis.expr.operations.Sum, + numpy.nansum: ibis.expr.operations.Sum, + numpy.var: ibis.expr.operations.Variance, + numpy.nanvar: ibis.expr.operations.Variance, + numpy.cumsum: ibis.expr.operations.CumulativeSum + }.get(function, function) + + if len(dimensions): + selection = new.groupby(columns) + if function is numpy.count_nonzero: + counted = selection.count() + aggregation = counted.mutate( + **{d.name: counted['count'] for d in dataset.vdims} + ) + else: + aggregation = selection.aggregate( + **{ + x: function(new[x]).to_expr() + for x in new.columns + if x not in columns + } + ) + else: + aggregation = new.aggregate( + **{x: function(new[x]).to_expr() for x in new.columns} + ) + + dropped = [x for x in values if x not in data.columns] + return aggregation, dropped + + @classmethod + @cached + def mask(cls, dataset, mask, mask_value=numpy.nan): + raise NotImplementedError('Mask is not implemented for IbisInterface.') + + @classmethod + @cached + def dframe(cls, dataset, dimensions): + return dataset.data[dimensions].execute() + +Interface.register(IbisInterface) diff --git a/holoviews/core/data/interface.py b/holoviews/core/data/interface.py index 6aef2d7920..957587f298 100644 --- a/holoviews/core/data/interface.py +++ b/holoviews/core/data/interface.py @@ -32,6 +32,19 @@ def is_dask(array): return False return da and isinstance(array, da.Array) +def cached(method): + """ + Decorates an Interface method and using a cached version + """ + def cached(*args, **kwargs): + cache = getattr(args[1], '_cached') + if cache is None: + return method(*args, **kwargs) + else: + args = (cache,)+args[2:] + return getattr(cache.interface, method.__name__)(*args, **kwargs) + return cached + class DataError(ValueError): "DataError is raised when the data cannot be interpreted" @@ -304,12 +317,24 @@ def validate(cls, dataset, vdims=True): "dimensions, the following dimensions were " "not found: %s" % repr(not_found), cls) + @classmethod + def persist(cls, dataset): + """ + Should return a persisted version of the Dataset. + """ + return dataset + + @classmethod + def compute(cls, dataset): + """ + Should return a computed version of the Dataset. + """ + return dataset @classmethod def expanded(cls, arrays): return not any(array.shape not in [arrays[0].shape, (1,)] for array in arrays[1:]) - @classmethod def isscalar(cls, dataset, dim): return len(cls.values(dataset, dim, expanded=False)) == 1 @@ -450,6 +475,22 @@ def concatenate(cls, datasets, datatype=None, new_type=None): concat_data = template.interface.concat(data, dimensions, vdims=template.vdims) return template.clone(concat_data, kdims=dimensions+template.kdims, new_type=new_type) + @classmethod + def histogram(cls, array, bins, density=True, weights=None): + if util.is_dask_array(array): + import dask.array as da + histogram = da.histogram + elif util.is_cupy_array(array): + import cupy + histogram = cupy.histogram + else: + histogram = np.histogram + hist, edges = histogram(array, bins=bins, density=density, weights=weights) + if util.is_cupy_array(hist): + edges = cupy.asnumpy(edges) + hist = cupy.asnumpy(hist) + return hist, edges + @classmethod def reduce(cls, dataset, reduce_dims, function, **kwargs): kdims = [kdim for kdim in dataset.kdims if kdim not in reduce_dims] diff --git a/holoviews/core/data/xarray.py b/holoviews/core/data/xarray.py index e5762b94d8..de69df791f 100644 --- a/holoviews/core/data/xarray.py +++ b/holoviews/core/data/xarray.py @@ -236,6 +236,13 @@ def validate(cls, dataset, vdims=True): "non-matching array dimensions:\n\n%s" % ('\n'.join(nonmatching)), cls) + @classmethod + def compute(cls, dataset): + return dataset.clone(dataset.data.compute()) + + @classmethod + def persist(cls, dataset): + return dataset.clone(dataset.data.persist()) @classmethod def range(cls, dataset, dimension): diff --git a/holoviews/core/util.py b/holoviews/core/util.py index aed44e686d..5d92bddeb1 100644 --- a/holoviews/core/util.py +++ b/holoviews/core/util.py @@ -516,9 +516,10 @@ def process_ellipses(obj, key, vdim_selection=False): if getattr(getattr(key, 'dtype', None), 'kind', None) == 'b': return key wrapped_key = wrap_tuple(key) - if wrapped_key.count(Ellipsis)== 0: + ellipse_count = sum(1 for k in wrapped_key if k is Ellipsis) + if ellipse_count == 0: return key - if wrapped_key.count(Ellipsis)!=1: + elif ellipse_count != 1: raise Exception("Only one ellipsis allowed at a time.") dim_count = len(obj.dimensions()) index = wrapped_key.index(Ellipsis) @@ -1501,6 +1502,13 @@ def is_cupy_array(data): return False +def is_ibis_expr(data): + if 'ibis' in sys.modules: + import ibis + return isinstance(data, ibis.expr.types.ColumnExpr) + return False + + def get_param_values(data): params = dict(kdims=data.kdims, vdims=data.vdims, label=data.label) diff --git a/holoviews/element/raster.py b/holoviews/element/raster.py index fc3e2e6f9e..136ecaf048 100644 --- a/holoviews/element/raster.py +++ b/holoviews/element/raster.py @@ -881,7 +881,13 @@ class HeatMap(Selection2DExpr, Dataset, Element2D): def __init__(self, data, kdims=None, vdims=None, **params): super(HeatMap, self).__init__(data, kdims=kdims, vdims=vdims, **params) - self.gridded = categorical_aggregate2d(self) + self._gridded = None + + @property + def gridded(self): + if self._gridded is None: + self._gridded = categorical_aggregate2d(self) + return self._gridded @property def _unique(self): diff --git a/holoviews/operation/element.py b/holoviews/operation/element.py index 33339f26a0..63d0a32ffd 100644 --- a/holoviews/operation/element.py +++ b/holoviews/operation/element.py @@ -14,9 +14,11 @@ from ..core import (Operation, NdOverlay, Overlay, GridMatrix, HoloMap, Dataset, Element, Collator, Dimension) from ..core.data import ArrayInterface, DictInterface, default_datatype +from ..core.data.interface import dask_array_module from ..core.util import (group_sanitizer, label_sanitizer, pd, basestring, datetime_types, isfinite, dt_to_int, - isdatetime, is_dask_array, is_cupy_array) + isdatetime, is_dask_array, is_cupy_array, + is_ibis_expr) from ..element.chart import Histogram, Scatter from ..element.raster import Image, RGB from ..element.path import Contours, Polygons @@ -720,21 +722,25 @@ def _process(self, element, key=None): if not full_cupy_support and (normed or self.p.weight_dimension): data = cupy.asnumpy(data) is_cupy = False - if is_dask_array(data): - import dask.array as da - histogram = da.histogram - elif is_cupy: - import cupy - histogram = cupy.histogram - is_finite = cupy.isfinite - else: - histogram = np.histogram + else: + is_finite = cupy.isfinite # Mask data - mask = is_finite(data) - if self.p.nonzero: - mask = mask & (data > 0) - data = data[mask] + if is_ibis_expr(data): + mask = data.notnull() + if self.p.nonzero: + mask = mask & (data != 0) + data = data.to_projection() + data = data[mask] + no_data = not len(data.head(1).execute()) + data = data[dim.name] + else: + mask = is_finite(data) + if self.p.nonzero: + mask = mask & (data != 0) + data = data[mask] + da = dask_array_module() + no_data = False if da and isinstance(data, da.Array) else not len(data) # Compute weights if self.p.weight_dimension: @@ -742,8 +748,7 @@ def _process(self, element, key=None): weights = element.interface.values(element, self.p.weight_dimension, compute=False) else: weights = element.dimension_values(self.p.weight_dimension) - if self.p.nonzero: - weights = weights[mask] + weights = weights[mask] else: weights = None @@ -769,31 +774,35 @@ def _process(self, element, key=None): edges = np.logspace(np.log10(bin_min), np.log10(end), steps) else: edges = np.linspace(start, end, steps) - if is_cupy: edges = cupy.asarray(edges) - if is_dask_array(data) or len(data): - if is_cupy and not full_cupy_support: - hist, _ = histogram(data, bins=edges) - elif normed: - # This covers True, 'height', 'integral' - hist, edges = histogram(data, density=True, - weights=weights, bins=edges) - if normed == 'height': - hist /= hist.max() - else: - hist, edges = histogram(data, normed=normed, weights=weights, bins=edges) - if self.p.weight_dimension and self.p.mean_weighted: - hist_mean, _ = histogram(data, density=False, bins=self.p.num_bins) - hist /= hist_mean - else: + if not is_dask_array(data) and no_data: nbins = self.p.num_bins if self.p.bins is None else len(self.p.bins)-1 hist = np.zeros(nbins) - - if is_cupy_array(hist): - edges = cupy.asnumpy(edges) - hist = cupy.asnumpy(hist) + elif hasattr(element, 'interface'): + density = True if normed else False + hist, edges = element.interface.histogram( + data, edges, density=density, weights=weights + ) + if normed == 'height': + hist /= hist.max() + if self.p.weight_dimension and self.p.mean_weighted: + hist_mean, _ = element.interface.histogram( + data, density=False, bins=edges + ) + hist /= hist_mean + elif normed: + # This covers True, 'height', 'integral' + hist, edges = np.histogram(data, density=True, + weights=weights, bins=edges) + if normed == 'height': + hist /= hist.max() + else: + hist, edges = np.histogram(data, normed=normed, weights=weights, bins=edges) + if self.p.weight_dimension and self.p.mean_weighted: + hist_mean, _ = np.histogram(data, density=False, bins=self.p.num_bins) + hist /= hist_mean hist[np.isnan(hist)] = 0 if is_datetime: diff --git a/holoviews/tests/core/data/testibisinterface.py b/holoviews/tests/core/data/testibisinterface.py new file mode 100644 index 0000000000..a2840fa8a2 --- /dev/null +++ b/holoviews/tests/core/data/testibisinterface.py @@ -0,0 +1,290 @@ +import sqlite3 +from unittest import SkipTest + +from tempfile import NamedTemporaryFile + +try: + import ibis + from ibis import sqlite +except: + raise SkipTest("Could not import ibis, skipping IbisInterface tests.") + +import numpy as np +import pandas as pd + +from holoviews.core.data import Dataset +from holoviews.core.spaces import HoloMap +from holoviews.core.data.ibis import IbisInterface + +from .base import HeterogeneousColumnTests, ScalarColumnTests, InterfaceTests + + +def create_temp_db(df, name, index=False): + with NamedTemporaryFile(delete=False) as my_file: + filename = my_file.name + con = sqlite3.Connection(filename) + df.to_sql(name, con, index=index) + return sqlite.connect(filename) + + +class IbisDatasetTest(HeterogeneousColumnTests, ScalarColumnTests, InterfaceTests): + """ + Test of the generic dictionary interface. + """ + + datatype = "ibis" + data_type = (ibis.expr.types.Expr,) + + __test__ = True + + def setUp(self): + self.init_column_data() + self.init_grid_data() + self.init_data() + + def tearDown(self): + pass + + def init_column_data(self): + # Create heterogeneously typed table + self.kdims = ["Gender", "Age"] + self.vdims = ["Weight", "Height"] + self.gender, self.age = np.array(["M", "M", "F"]), np.array([10, 16, 12]) + self.weight, self.height = np.array([15, 18, 10]), np.array([0.8, 0.6, 0.8]) + + hetero_df = pd.DataFrame( + { + "Gender": self.gender, + "Age": self.age, + "Weight": self.weight, + "Height": self.height, + }, + columns=["Gender", "Age", "Weight", "Height"], + ) + hetero_db = create_temp_db(hetero_df, "hetero") + self.table = Dataset( + hetero_db.table("hetero"), kdims=self.kdims, vdims=self.vdims + ) + + # Create table with aliased dimenion names + self.alias_kdims = [("gender", "Gender"), ("age", "Age")] + self.alias_vdims = [("weight", "Weight"), ("height", "Height")] + alias_df = pd.DataFrame( + { + "gender": self.gender, + "age": self.age, + "weight": self.weight, + "height": self.height, + }, + columns=["gender", "age", "weight", "height"], + ) + alias_db = create_temp_db(alias_df, "alias") + self.alias_table = Dataset( + alias_db.table("alias"), kdims=self.alias_kdims, vdims=self.alias_vdims + ) + + self.xs = np.array(range(11)) + self.xs_2 = self.xs ** 2 + self.y_ints = self.xs * 2 + self.ys = np.linspace(0, 1, 11) + self.zs = np.sin(self.xs) + + ht_df = pd.DataFrame({"x": self.xs, "y": self.ys}, columns=["x", "y"]) + ht_db = create_temp_db(ht_df, "ht") + self.dataset_ht = Dataset(ht_db.table("ht"), kdims=["x"], vdims=["y"]) + + hm_df = pd.DataFrame({"x": self.xs, "y": self.y_ints}, columns=["x", "y"]) + hm_db = create_temp_db(hm_df, "hm") + self.dataset_hm = Dataset(hm_db.table("hm"), kdims=["x"], vdims=["y"]) + self.dataset_hm_alias = Dataset( + hm_db.table("hm"), kdims=[("x", "X")], vdims=[("y", "Y")] + ) + + def test_dataset_array_init_hm(self): + raise SkipTest("Not supported") + + def test_dataset_dict_dim_not_found_raises_on_scalar(self): + raise SkipTest("Not supported") + + def test_dataset_array_init_hm_tuple_dims(self): + raise SkipTest("Not supported") + + def test_dataset_odict_init(self): + raise SkipTest("Not supported") + + def test_dataset_odict_init_alias(self): + raise SkipTest("Not supported") + + def test_dataset_simple_zip_init(self): + raise SkipTest("Not supported") + + def test_dataset_simple_zip_init_alias(self): + raise SkipTest("Not supported") + + def test_dataset_zip_init(self): + raise SkipTest("Not supported") + + def test_dataset_zip_init_alias(self): + raise SkipTest("Not supported") + + def test_dataset_tuple_init(self): + raise SkipTest("Not supported") + + def test_dataset_tuple_init_alias(self): + raise SkipTest("Not supported") + + def test_dataset_implicit_indexing_init(self): + raise SkipTest("Not supported") + + def test_dataset_dict_init(self): + raise SkipTest("Not supported") + + def test_dataset_dataframe_init_hm(self): + raise SkipTest("Not supported") + + def test_dataset_dataframe_init_hm_alias(self): + raise SkipTest("Not supported") + + def test_dataset_dataframe_init_ht(self): + raise SkipTest("Not supported") + + def test_dataset_dataframe_init_ht_alias(self): + raise SkipTest("Not supported") + + def test_dataset_add_dimensions_values_hm(self): + raise SkipTest("Not supported") + + def test_dataset_add_dimensions_values_ht(self): + raise SkipTest("Not supported") + + def test_dataset_dataset_ht_dtypes(self): + ds = self.table + self.assertEqual(ds.interface.dtype(ds, "Gender"), np.dtype("object")) + self.assertEqual(ds.interface.dtype(ds, "Age"), np.dtype("int32")) + self.assertEqual(ds.interface.dtype(ds, "Weight"), np.dtype("int32")) + self.assertEqual(ds.interface.dtype(ds, "Height"), np.dtype("float64")) + + def test_dataset_dtypes(self): + self.assertEqual( + self.dataset_hm.interface.dtype(self.dataset_hm, "x"), np.dtype("int32") + ) + self.assertEqual( + self.dataset_hm.interface.dtype(self.dataset_hm, "y"), np.dtype("int32") + ) + + def test_dataset_reduce_ht(self): + reduced = Dataset( + {"Age": self.age, "Weight": self.weight, "Height": self.height}, + kdims=self.kdims[1:], + vdims=self.vdims, + ) + self.assertEqual(self.table.reduce(["Gender"], np.mean).sort(), reduced.sort()) + + def test_dataset_aggregate_ht(self): + aggregated = Dataset( + {"Gender": ["M", "F"], "Weight": [16.5, 10], "Height": [0.7, 0.8]}, + kdims=self.kdims[:1], + vdims=self.vdims, + ) + self.compare_dataset( + self.table.aggregate(["Gender"], np.mean).sort(), aggregated.sort() + ) + + def test_dataset_aggregate_ht_alias(self): + aggregated = Dataset( + {"gender": ["M", "F"], "weight": [16.5, 10], "height": [0.7, 0.8]}, + kdims=self.alias_kdims[:1], + vdims=self.alias_vdims, + ) + self.compare_dataset( + self.alias_table.aggregate("Gender", np.mean).sort(), aggregated.sort() + ) + + def test_dataset_groupby(self): + group1 = {"Age": [10, 16], "Weight": [15, 18], "Height": [0.8, 0.6]} + group2 = {"Age": [12], "Weight": [10], "Height": [0.8]} + grouped = HoloMap( + [ + ("M", Dataset(group1, kdims=["Age"], vdims=self.vdims)), + ("F", Dataset(group2, kdims=["Age"], vdims=self.vdims)), + ], + kdims=["Gender"], + ) + self.assertEqual( + self.table.groupby(["Gender"]).apply("sort"), grouped.apply("sort") + ) + + def test_dataset_groupby_alias(self): + group1 = {"age": [10, 16], "weight": [15, 18], "height": [0.8, 0.6]} + group2 = {"age": [12], "weight": [10], "height": [0.8]} + grouped = HoloMap( + [ + ("M", Dataset(group1, kdims=[("age", "Age")], vdims=self.alias_vdims)), + ("F", Dataset(group2, kdims=[("age", "Age")], vdims=self.alias_vdims)), + ], + kdims=[("gender", "Gender")], + ) + self.assertEqual(self.alias_table.groupby("Gender").apply("sort"), grouped) + + def test_dataset_groupby_second_dim(self): + group1 = {"Gender": ["M"], "Weight": [15], "Height": [0.8]} + group2 = {"Gender": ["M"], "Weight": [18], "Height": [0.6]} + group3 = {"Gender": ["F"], "Weight": [10], "Height": [0.8]} + grouped = HoloMap( + [ + (10, Dataset(group1, kdims=["Gender"], vdims=self.vdims)), + (16, Dataset(group2, kdims=["Gender"], vdims=self.vdims)), + (12, Dataset(group3, kdims=["Gender"], vdims=self.vdims)), + ], + kdims=["Age"], + sort=True, + ) + self.assertEqual(self.table.groupby(["Age"]), grouped) + + if not IbisInterface.has_rowid: + + def test_dataset_iloc_slice_rows_slice_cols(self): + raise SkipTest("Not supported") + + def test_dataset_iloc_slice_rows_list_cols(self): + raise SkipTest("Not supported") + + def test_dataset_iloc_slice_rows_index_cols(self): + raise SkipTest("Not supported") + + def test_dataset_iloc_slice_rows(self): + raise SkipTest("Not supported") + + def test_dataset_iloc_list_rows_slice_cols(self): + raise SkipTest("Not supported") + + def test_dataset_iloc_list_rows_list_cols_by_name(self): + raise SkipTest("Not supported") + + def test_dataset_iloc_list_rows_list_cols(self): + raise SkipTest("Not supported") + + def test_dataset_iloc_list_rows(self): + raise SkipTest("Not supported") + + def test_dataset_iloc_list_cols_by_name(self): + raise SkipTest("Not supported") + + def test_dataset_iloc_list_cols(self): + raise SkipTest("Not supported") + + def test_dataset_iloc_index_rows_slice_cols(self): + raise SkipTest("Not supported") + + def test_dataset_iloc_index_rows_index_cols(self): + raise SkipTest("Not supported") + + def test_dataset_iloc_ellipsis_list_cols_by_name(self): + raise SkipTest("Not supported") + + def test_dataset_iloc_ellipsis_list_cols(self): + raise SkipTest("Not supported") + + def test_dataset_boolean_index(self): + raise SkipTest("Not supported") + diff --git a/setup.py b/setup.py index 024fb0d88b..099cc3b38b 100644 --- a/setup.py +++ b/setup.py @@ -11,55 +11,55 @@ setup_args = {} install_requires = [ - 'param >=1.9.3,<2.0', - 'numpy >=1.0', - 'pyviz_comms >=0.7.3', - 'panel >=0.8.0', - 'pandas' + "param >=1.9.3,<2.0", + "numpy >=1.0", + "pyviz_comms >=0.7.3", + "panel >=0.8.0", + "pandas", ] extras_require = {} # Notebook dependencies -extras_require['notebook'] = [ - 'ipython >=5.4.0', - 'notebook' -] +extras_require["notebook"] = ["ipython >=5.4.0", "notebook"] # IPython Notebook + pandas + matplotlib + bokeh -extras_require['recommended'] = extras_require['notebook'] + [ - 'matplotlib >=2.2', - 'bokeh >=1.1.0' +extras_require["recommended"] = extras_require["notebook"] + [ + "matplotlib >=2.2", + "bokeh >=1.1.0", ] # Requirements to run all examples -extras_require['examples'] = extras_require['recommended'] + [ - 'networkx', - 'pillow', - 'xarray >=0.10.4', - 'plotly >=4.0', +extras_require["examples"] = extras_require["recommended"] + [ + "networkx", + "pillow", + "xarray >=0.10.4", + "plotly >=4.0", 'dash >=1.16', - 'streamz >=0.5.0', - 'datashader ==0.11.1', - 'ffmpeg', - 'cftime', - 'netcdf4', - 'dask', - 'scipy', - 'shapely', - 'scikit-image', + "streamz >=0.5.0", + "datashader ==0.11.1", + "ffmpeg", + "cftime", + "netcdf4", + "dask", + "scipy", + "shapely", + "scikit-image" ] if sys.version_info.major > 2: - extras_require['examples'].extend([ - 'spatialpandas', - 'pyarrow <1.0' # spatialpandas incompatibility - ]) + extras_require["examples"].extend( + [ + "spatialpandas", + "pyarrow <1.0", + "ibis-framework >=1.3", + ] # spatialpandas incompatibility + ) # Extra third-party libraries -extras_require['extras'] = extras_require['examples']+[ - 'cyordereddict', - 'pscript ==0.7.1' +extras_require["extras"] = extras_require["examples"] + [ + "cyordereddict", + "pscript ==0.7.1", ] # Test requirements @@ -79,22 +79,22 @@ 'keyring' ] -extras_require['unit_tests'] = extras_require['examples']+extras_require['tests'] - -extras_require['basic_tests'] = extras_require['tests']+[ - 'matplotlib >=2.1', - 'bokeh >=1.1.0', - 'pandas' -] + extras_require['notebook'] - -extras_require['nbtests'] = extras_require['recommended'] + [ - 'nose', - 'awscli', - 'deepdiff', - 'nbconvert ==5.3.1', - 'jsonschema ==2.6.0', - 'cyordereddict', - 'ipython ==5.4.1' +extras_require["unit_tests"] = extras_require["examples"] + extras_require["tests"] + +extras_require["basic_tests"] = ( + extras_require["tests"] + + ["matplotlib >=2.1", "bokeh >=1.1.0", "pandas"] + + extras_require["notebook"] +) + +extras_require["nbtests"] = extras_require["recommended"] + [ + "nose", + "awscli", + "deepdiff", + "nbconvert ==5.3.1", + "jsonschema ==2.6.0", + "cyordereddict", + "ipython ==5.4.1", ] extras_require['doc'] = extras_require['examples'] + [ @@ -107,18 +107,19 @@ 'graphviz', 'bokeh >2.2', 'nbconvert <6.0', - 'mpl_sample_data' ] -extras_require['build'] = [ - 'param >=1.7.0', - 'setuptools >=30.3.0', - 'pyct >=0.4.4', - 'python <3.8' +extras_require["build"] = [ + "param >=1.7.0", + "setuptools >=30.3.0", + "pyct >=0.4.4", + "python <3.8", ] # Everything including cyordereddict (optimization) and nosetests -extras_require['all'] = list(set(extras_require['unit_tests']) | set(extras_require['nbtests'])) +extras_require["all"] = list( + set(extras_require["unit_tests"]) | set(extras_require["nbtests"]) +) def get_setup_version(reponame): @@ -127,69 +128,75 @@ def get_setup_version(reponame): .version file (if available). """ basepath = os.path.split(__file__)[0] - version_file_path = os.path.join(basepath, reponame, '.version') + version_file_path = os.path.join(basepath, reponame, ".version") try: from param import version except: version = None if version is not None: - return version.Version.setup_version(basepath, reponame, archive_commit="$Format:%h$") + return version.Version.setup_version( + basepath, reponame, archive_commit="$Format:%h$" + ) else: - print("WARNING: param>=1.6.0 unavailable. If you are installing a package, this warning can safely be ignored. If you are creating a package or otherwise operating in a git repository, you should install param>=1.6.0.") - return json.load(open(version_file_path, 'r'))['version_string'] - -setup_args.update(dict( - name='holoviews', - version=get_setup_version("holoviews"), - python_requires=">=2.7", - install_requires=install_requires, - extras_require=extras_require, - description='Stop plotting your data - annotate your data and let it visualize itself.', - long_description=open("README.md").read(), - long_description_content_type="text/markdown", - author="Jean-Luc Stevens and Philipp Rudiger", - author_email="holoviews@gmail.com", - maintainer="PyViz Developers", - maintainer_email="developers@pyviz.org", - platforms=['Windows', 'Mac OS X', 'Linux'], - license='BSD', - url='https://www.holoviews.org', - entry_points={ - 'console_scripts': [ - 'holoviews = holoviews.util.command:main' - ]}, - packages=find_packages(), - include_package_data=True, - classifiers=[ - "License :: OSI Approved :: BSD License", - "Development Status :: 5 - Production/Stable", - "Programming Language :: Python :: 2.7", - "Programming Language :: Python :: 3.5", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Operating System :: OS Independent", - "Intended Audience :: Science/Research", - "Intended Audience :: Developers", - "Natural Language :: English", - "Framework :: Matplotlib", - "Topic :: Scientific/Engineering", - "Topic :: Software Development :: Libraries"] -)) + print( + "WARNING: param>=1.6.0 unavailable. If you are installing a package, this warning can safely be ignored. If you are creating a package or otherwise operating in a git repository, you should install param>=1.6.0." + ) + return json.load(open(version_file_path, "r"))["version_string"] + + +setup_args.update( + dict( + name="holoviews", + version=get_setup_version("holoviews"), + python_requires=">=2.7", + install_requires=install_requires, + extras_require=extras_require, + description="Stop plotting your data - annotate your data and let it visualize itself.", + long_description=open("README.md").read(), + long_description_content_type="text/markdown", + author="Jean-Luc Stevens and Philipp Rudiger", + author_email="holoviews@gmail.com", + maintainer="PyViz Developers", + maintainer_email="developers@pyviz.org", + platforms=["Windows", "Mac OS X", "Linux"], + license="BSD", + url="https://www.holoviews.org", + entry_points={"console_scripts": ["holoviews = holoviews.util.command:main"]}, + packages=find_packages(), + include_package_data=True, + classifiers=[ + "License :: OSI Approved :: BSD License", + "Development Status :: 5 - Production/Stable", + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Operating System :: OS Independent", + "Intended Audience :: Science/Research", + "Intended Audience :: Developers", + "Natural Language :: English", + "Framework :: Matplotlib", + "Topic :: Scientific/Engineering", + "Topic :: Software Development :: Libraries", + ], + ) +) if __name__ == "__main__": - example_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), - 'holoviews/examples') + example_path = os.path.join( + os.path.dirname(os.path.abspath(__file__)), "holoviews/examples" + ) - if 'develop' not in sys.argv and 'egg_info' not in sys.argv: + if "develop" not in sys.argv and "egg_info" not in sys.argv: pyct.build.examples(example_path, __file__, force=True) - if 'install' in sys.argv: + if "install" in sys.argv: header = "HOLOVIEWS INSTALLATION INFORMATION" - bars = "="*len(header) + bars = "=" * len(header) - extras = '\n'.join('holoviews[%s]' % e for e in setup_args['extras_require']) + extras = "\n".join("holoviews[%s]" % e for e in setup_args["extras_require"]) print("%s\n%s\n%s" % (bars, header, bars)) @@ -199,7 +206,7 @@ def get_setup_version(reponame): print("By default only a core installation is performed and ") print("only the minimal set of dependencies are fetched.\n\n") print("For more information please visit http://holoviews.org/install.html\n") - print(bars+'\n') + print(bars + "\n") setup(**setup_args)