diff --git a/holoviews/core/data/ibis.py b/holoviews/core/data/ibis.py index 441f99a803..537c67be5a 100644 --- a/holoviews/core/data/ibis.py +++ b/holoviews/core/data/ibis.py @@ -34,8 +34,11 @@ def has_rowid(cls): @classmethod def is_rowid_zero_indexed(cls, data): - from ibis.client import find_backends, validate_backends - (backend,) = validate_backends(list(find_backends(data))) + try: + from ibis.client import find_backends, validate_backends + (backend,) = validate_backends(list(find_backends(data))) + except Exception: + backend = data._find_backend() return type(backend).__module__ in cls.zero_indexed_backend_modules @classmethod diff --git a/holoviews/core/data/xarray.py b/holoviews/core/data/xarray.py index 8c14ce3f68..78ca711968 100644 --- a/holoviews/core/data/xarray.py +++ b/holoviews/core/data/xarray.py @@ -217,9 +217,17 @@ def retrieve_unit_and_label(dim): # not need to be canonicalized if any(len(da.coords[c].shape) > 1 for c in da.coords): continue - undeclared = [ - c for c in da.coords if c not in kdims and len(da[c].shape) == 1 and - da[c].shape[0] > 1] + undeclared = [] + for c in da.coords: + if c in kdims or len(da[c].shape) != 1 or da[c].shape[0] <= 1: + # Skip if coord is declared, represents irregular coordinates or is constant + continue + elif all(d in kdims for d in da[c].dims): + continue # Skip if coord is alias for another dimension + elif any(all(d in da[kd.name].dims for d in da[c].dims) for kd in kdims): + # Skip if all the dims on the coord are present on another coord + continue + undeclared.append(c) if undeclared: raise DataError( 'The coordinates on the %r DataArray do not match the ' diff --git a/holoviews/core/util.py b/holoviews/core/util.py index 32d9667a0d..3563df40ac 100644 --- a/holoviews/core/util.py +++ b/holoviews/core/util.py @@ -1569,17 +1569,27 @@ def is_param_method(obj, has_deps=False): def resolve_dependent_value(value): """Resolves parameter dependencies on the supplied value - Resolves parameter values, Parameterized instance methods and - parameterized functions with dependencies on the supplied value. + Resolves parameter values, Parameterized instance methods, + parameterized functions with dependencies on the supplied value, + including such parameters embedded in a list or tuple. Args: value: A value which will be resolved Returns: - A new dictionary where any parameter dependencies have been + A new value where any parameter dependencies have been resolved. """ range_widget = False + if isinstance(value, list): + value = [resolve_dependent_value(v) for v in value] + elif isinstance(value, tuple): + value = tuple(resolve_dependent_value(v) for v in value) + elif isinstance(value, dict): + value = { + resolve_dependent_value(k): resolve_dependent_value(v) for k, v in value.items() + } + if 'panel' in sys.modules: from panel.widgets import RangeSlider, Widget range_widget = isinstance(value, RangeSlider) @@ -1614,7 +1624,7 @@ def resolve_dependent_kwargs(kwargs): kwargs (dict): A dictionary of keyword arguments Returns: - A new dictionary with where any parameter dependencies have been + A new dictionary where any parameter dependencies have been resolved. """ return {k: resolve_dependent_value(v) for k, v in kwargs.items()} @@ -2294,3 +2304,30 @@ def cast_array_to_int64(array): category=FutureWarning, ) return array.astype('int64') + + +def flatten(line): + """ + Flatten an arbitrarily nested sequence. + + Inspired by: pd.core.common.flatten + + Parameters + ---------- + line : sequence + The sequence to flatten + + Notes + ----- + This only flattens list, tuple, and dict sequences. + + Returns + ------- + flattened : generator + """ + + for element in line: + if any(isinstance(element, tp) for tp in (list, tuple, dict)): + yield from flatten(element) + else: + yield element diff --git a/holoviews/operation/datashader.py b/holoviews/operation/datashader.py index c5fe4706bf..154130f3e6 100644 --- a/holoviews/operation/datashader.py +++ b/holoviews/operation/datashader.py @@ -2,7 +2,7 @@ import warnings -from collections import Callable +from collections.abc import Callable from functools import partial import param diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index d13655ea4c..32931d8f90 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -2,6 +2,8 @@ import sys import warnings + +from itertools import chain from types import FunctionType import param @@ -2317,13 +2319,13 @@ def _get_factors(self, overlay, ranges): if el is not None: elranges = util.match_spec(el, ranges) xfs, yfs = sp._get_factors(el, elranges) - xfactors.append(xfs) - yfactors.append(yfs) - if xfactors: - xfactors = np.concatenate(xfactors) - if yfactors: - yfactors = np.concatenate(yfactors) - return util.unique_array(xfactors), util.unique_array(yfactors) + if len(xfs): + xfactors.append(xfs) + if len(yfs): + yfactors.append(yfs) + xfactors = list(util.unique_iterator(chain(*xfactors))) + yfactors = list(util.unique_iterator(chain(*yfactors))) + return xfactors, yfactors def _get_axis_dims(self, element): diff --git a/holoviews/plotting/mpl/element.py b/holoviews/plotting/mpl/element.py index 73c0faae09..f308a10266 100644 --- a/holoviews/plotting/mpl/element.py +++ b/holoviews/plotting/mpl/element.py @@ -174,7 +174,7 @@ def _finalize_axis(self, key, element=None, title=None, dimensions=None, ranges= if self.logy: axis.set_yscale('log') - if not isinstance(self.projection, str) and self.projection == '3d': + if not (isinstance(self.projection, str) and self.projection == '3d'): self._set_axis_position(axis, 'x', self.xaxis) self._set_axis_position(axis, 'y', self.yaxis) @@ -330,7 +330,7 @@ def _set_axis_limits(self, axis, view, subplots, ranges): coords = [coord if isinstance(coord, np.datetime64) or np.isreal(coord) else np.NaN for coord in extents] coords = [date2num(util.dt64_to_dt(c)) if isinstance(c, np.datetime64) else c for c in coords] - if isinstance(self.projection, str) and self.projection == '3d' or len(extents) == 6: + if (isinstance(self.projection, str) and self.projection == '3d') or len(extents) == 6: l, b, zmin, r, t, zmax = coords if self.invert_zaxis or any(p.invert_zaxis for p in subplots): zmin, zmax = zmax, zmin diff --git a/holoviews/streams.py b/holoviews/streams.py index 95e2c29516..9512356c15 100644 --- a/holoviews/streams.py +++ b/holoviews/streams.py @@ -770,7 +770,7 @@ def hashkey(self): for p in self.parameters: pkey = (p.owner, p.name) pname = self._rename.get(pkey, p.name) - key = ' '.join([p.owner.name, pname]) + key = ' '.join([str(id(p.owner)), pname]) if self._rename.get(pkey, True) is not None: hashkey[key] = getattr(p.owner, p.name) hashkey['_memoize_key'] = self._memoize_counter diff --git a/holoviews/tests/core/data/testxarrayinterface.py b/holoviews/tests/core/data/testxarrayinterface.py index b37eb36c75..be80aa0f1a 100644 --- a/holoviews/tests/core/data/testxarrayinterface.py +++ b/holoviews/tests/core/data/testxarrayinterface.py @@ -60,6 +60,14 @@ def get_multi_dim_irregular_dataset(self): 'time': pd.date_range('2014-09-06', periods=3), 'reference_time': pd.Timestamp('2014-09-05')}) + def test_ignore_dependent_dimensions_if_not_specified(self): + coords = OrderedDict([('time', [0, 1]), ('lat', [0, 1]), ('lon', [0, 1])]) + da = xr.DataArray(np.arange(8).reshape((2, 2, 2)), + coords, ['time', 'lat', 'lon']).assign_coords( + lat1=xr.DataArray([2,3], dims=['lat'])) + assert Dataset(da, ['time', 'lat', 'lon'], vdims='value').kdims == ['time', 'lat', 'lon'] + assert Dataset(da, ['time', 'lat1', 'lon'], vdims='value').kdims == ['time', 'lat1', 'lon'] + def test_xarray_dataset_irregular_shape(self): ds = Dataset(self.get_multi_dim_irregular_dataset()) shape = ds.interface.shape(ds, gridded=True) diff --git a/holoviews/tests/core/testapply.py b/holoviews/tests/core/testapply.py index ea1ae06ecc..fd4d6401ba 100644 --- a/holoviews/tests/core/testapply.py +++ b/holoviews/tests/core/testapply.py @@ -1,12 +1,14 @@ import numpy as np +import pandas as pd import param -from panel.widgets import TextInput +from panel.widgets import RadioButtonGroup, TextInput +from holoviews import Dataset, util from holoviews.core.spaces import DynamicMap, HoloMap -from holoviews.element import Image, Curve +from holoviews.element import Curve, Image from holoviews.element.comparison import ComparisonTestCase -from holoviews.streams import Params, ParamMethod +from holoviews.streams import ParamMethod, Params class ParamClass(param.Parameterized): @@ -275,3 +277,18 @@ def test_dmap_apply_dynamic_with_param_method(self): self.assertEqual(applied[1], self.dmap[1].relabel('Test!')) pinst.label = 'Another label' self.assertEqual(applied[1], self.dmap[1].relabel('Another label!')) + + +def test_nested_widgets(): + df = pd._testing.makeDataFrame() + column = RadioButtonGroup(value="A", options=list("ABC")) + ds = Dataset(df) + transform = util.transform.df_dim("*").groupby(["D", column]).mean() + + params = list(transform.params.values()) + assert len(params) == 1 + assert params[0] == column.param.value + + df1 = transform.apply(ds, keep_index=True, compute=False) + df2 = df.groupby(["D", "A"]).mean() + pd.testing.assert_frame_equal(df1, df2) diff --git a/holoviews/tests/plotting/bokeh/testoverlayplot.py b/holoviews/tests/plotting/bokeh/testoverlayplot.py index 5f9b15a4d4..8599c8bb94 100644 --- a/holoviews/tests/plotting/bokeh/testoverlayplot.py +++ b/holoviews/tests/plotting/bokeh/testoverlayplot.py @@ -3,7 +3,7 @@ from holoviews.core import NdOverlay, HoloMap, DynamicMap, Overlay from holoviews.core.options import Cycle -from holoviews.element import Curve, Points, ErrorBars, Scatter, Text, VLine +from holoviews.element import Bars, Curve, ErrorBars, HLine, Points, Scatter, Text, VLine from holoviews.streams import Stream, Tap from holoviews.util import Dynamic @@ -11,7 +11,7 @@ from .testplot import TestBokehPlot, bokeh_renderer try: - from bokeh.models import FixedTicker, HoverTool, FactorRange, Range1d + from bokeh.models import FixedTicker, HoverTool, FactorRange, Span, Range1d except: pass @@ -192,6 +192,16 @@ def test_points_errorbars_text_ndoverlay_categorical_xaxis(self): for xs, factor in zip(error_plot.handles['source'].data['base'], factors): self.assertEqual(factor, xs) + def test_overlay_categorical_two_level(self): + bars = Bars([('A', 'a', 1), ('B', 'b', 2), ('A', 'b', 3), ('B', 'a', 4)], + kdims=['Upper', 'Lower']) + + plot = bokeh_renderer.get_plot(bars * HLine(2)) + x_range = plot.handles['x_range'] + assert isinstance(x_range, FactorRange) + assert x_range.factors == [('A', 'a'), ('A', 'b'), ('B', 'a'), ('B', 'b')] + assert isinstance(plot.state.renderers[-1], Span) + def test_points_errorbars_text_ndoverlay_categorical_xaxis_invert_axes(self): overlay = NdOverlay({i: Points(([chr(65+i)]*10,np.random.randn(10))) for i in range(5)}) diff --git a/holoviews/tests/plotting/bokeh/testserver.py b/holoviews/tests/plotting/bokeh/testserver.py index c85f7f8998..1ff9ad0d98 100644 --- a/holoviews/tests/plotting/bokeh/testserver.py +++ b/holoviews/tests/plotting/bokeh/testserver.py @@ -1,8 +1,6 @@ import time -import threading from unittest import SkipTest -from threading import Event import param @@ -14,21 +12,18 @@ from holoviews.streams import Stream, RangeXY, PlotReset try: - from bokeh.application.handlers import FunctionHandler - from bokeh.application import Application from bokeh.client import pull_session from bokeh.document import Document from bokeh.io.doc import curdoc, set_curdoc from bokeh.models import ColumnDataSource - from bokeh.server.server import Server from holoviews.plotting.bokeh.callbacks import ( Callback, RangeXYCallback, ResetCallback ) from holoviews.plotting.bokeh.renderer import BokehRenderer from panel.widgets import DiscreteSlider, FloatSlider - from panel.io.server import StoppableThread from panel.io.state import state + from panel import serve bokeh_renderer = BokehRenderer.instance(mode='server') except: bokeh_renderer = None @@ -107,78 +102,39 @@ def setUp(self): if not bokeh_renderer: raise SkipTest("Bokeh required to test plot instantiation") Store.current_backend = 'bokeh' - self._loaded = Event() self._port = None - self._thread = None - self._server = None def tearDown(self): Store.current_backend = self.previous_backend Callback._callbacks = {} - if self._thread is not None: - try: - self._thread.stop() - except: - pass - state._thread_id = None - if self._server is not None: - try: - self._server.stop() - except: - pass + state.kill_all_servers() time.sleep(1) - def _launcher(self, obj, threaded=False, io_loop=None): - if io_loop: - io_loop.make_current() - launched = [] - def modify_doc(doc): - bokeh_renderer(obj, doc=doc) - launched.append(True) - handler = FunctionHandler(modify_doc) - app = Application(handler) - server = Server({'/': app}, port=0, io_loop=io_loop) - server.start() - self._port = server.port - self._server = server - if threaded: - server.io_loop.add_callback(self._loaded.set) - thread = threading.current_thread() - state._thread_id = thread.ident if thread else None - io_loop.start() - else: - url = "http://localhost:" + str(server.port) + "/" - session = pull_session(session_id='Test', url=url, io_loop=server.io_loop) - self.assertTrue(len(launched)==1) - return session, server - return None, server - - def _threaded_launcher(self, obj): - from tornado.ioloop import IOLoop - io_loop = IOLoop() - thread = StoppableThread(target=self._launcher, io_loop=io_loop, - args=(obj, True, io_loop)) - thread.setDaemon(True) - thread.start() - self._loaded.wait() - self._thread = thread - return self.session + def _launcher(self, obj, threaded=True, port=6001): + self._port = port + server = serve(obj, threaded=threaded, show=False, port=port) + time.sleep(0.5) + return server, self.session @property def session(self): url = "http://localhost:" + str(self._port) + "/" return pull_session(session_id='Test', url=url) - + def test_launch_simple_server(self): obj = Curve([]) - self._launcher(obj) + server, _ = self._launcher(obj, port=6001) + server.stop() def test_launch_server_with_stream(self): - obj = Curve([]) - stream = RangeXY(source=obj) + el = Curve([]) + stream = RangeXY(source=el) - _, server = self._launcher(obj) - cb = bokeh_renderer.last_plot.callbacks[0] + obj, _ = bokeh_renderer._validate(el, None) + server, _ = self._launcher(obj, port=6002) + [(plot, _)] = obj._plots.values() + + cb = plot.callbacks[0] self.assertIsInstance(cb, RangeXYCallback) self.assertEqual(cb.streams, [stream]) x_range = bokeh_renderer.last_plot.handles['x_range'] @@ -195,15 +151,15 @@ def test_launch_server_with_complex_plot(self): static = Polygons([]) * Path([]) * Curve([]) layout = overlay + static - _, server = self._launcher(layout) + server, _ = self._launcher(layout, port=6003) server.stop() def test_server_dynamicmap_with_dims(self): dmap = DynamicMap(lambda y: Curve([1, 2, y]), kdims=['y']).redim.range(y=(0.1, 5)) obj, _ = bokeh_renderer._validate(dmap, None) - session = self._threaded_launcher(obj) + server, session = self._launcher(obj, port=6004) [(plot, _)] = obj._plots.values() - [(doc, _)] = obj.layout._documents.items() + [(doc, _)] = obj._documents.items() cds = session.document.roots[0].select_one({'type': ColumnDataSource}) self.assertEqual(cds.data['y'][2], 0.1) @@ -214,13 +170,14 @@ def run(): time.sleep(1) cds = self.session.document.roots[0].select_one({'type': ColumnDataSource}) self.assertEqual(cds.data['y'][2], 3.1) + server.stop() def test_server_dynamicmap_with_stream(self): stream = Stream.define('Custom', y=2)() dmap = DynamicMap(lambda y: Curve([1, 2, y]), kdims=['y'], streams=[stream]) obj, _ = bokeh_renderer._validate(dmap, None) - session = self._threaded_launcher(obj) - [(doc, _)] = obj.layout._documents.items() + server, session = self._launcher(obj, port=6005) + [(doc, _)] = obj._documents.items() cds = session.document.roots[0].select_one({'type': ColumnDataSource}) self.assertEqual(cds.data['y'][2], 2) @@ -230,14 +187,15 @@ def run(): time.sleep(1) cds = self.session.document.roots[0].select_one({'type': ColumnDataSource}) self.assertEqual(cds.data['y'][2], 3) + server.stop() def test_server_dynamicmap_with_stream_dims(self): stream = Stream.define('Custom', y=2)() dmap = DynamicMap(lambda x, y: Curve([x, 1, y]), kdims=['x', 'y'], streams=[stream]).redim.values(x=[1, 2, 3]) obj, _ = bokeh_renderer._validate(dmap, None) - session = self._threaded_launcher(obj) - [(doc, _)] = obj.layout._documents.items() + server, session = self._launcher(obj, port=6006) + [(doc, _)] = obj._documents.items() orig_cds = session.document.roots[0].select_one({'type': ColumnDataSource}) self.assertEqual(orig_cds.data['y'][2], 2) @@ -256,3 +214,4 @@ def run(): time.sleep(1) cds = self.session.document.roots[0].select_one({'type': ColumnDataSource}) self.assertEqual(cds.data['y'][0], 3) + server.stop() diff --git a/holoviews/tests/plotting/matplotlib/testelementplot.py b/holoviews/tests/plotting/matplotlib/testelementplot.py index 7f7104b613..f7be9d6d39 100644 --- a/holoviews/tests/plotting/matplotlib/testelementplot.py +++ b/holoviews/tests/plotting/matplotlib/testelementplot.py @@ -59,6 +59,14 @@ def test_element_font_scaling_fontsize_override_specific(self): self.assertEqual(ax.xaxis._major_tick_kw['labelsize'], 24) self.assertEqual(ax.yaxis._major_tick_kw['labelsize'], 20) + def test_element_no_xaxis_yaxis(self): + element = Curve(range(10)).options(xaxis=None, yaxis=None) + axes = mpl_renderer.get_plot(element).handles['axis'] + xaxis = axes.get_xaxis() + yaxis = axes.get_yaxis() + self.assertEqual(xaxis.get_visible(), False) + self.assertEqual(yaxis.get_visible(), False) + def test_element_xlabel(self): element = Curve(range(10)).options(xlabel='custom x-label') axes = mpl_renderer.get_plot(element).handles['axis'] diff --git a/holoviews/tests/teststreams.py b/holoviews/tests/teststreams.py index 98bf2a50be..2538889bd6 100644 --- a/holoviews/tests/teststreams.py +++ b/holoviews/tests/teststreams.py @@ -5,6 +5,7 @@ from unittest import SkipTest import param +from panel.widgets import IntSlider from holoviews.core.spaces import DynamicMap from holoviews.core.util import LooseVersion, pd @@ -272,7 +273,7 @@ def test_param_stream_action(self): def subscriber(**kwargs): values.append(kwargs) self.assertEqual(set(stream.hashkey), - {'%s action' % inner.name, '_memoize_key'}) + {'%s action' % id(inner), '_memoize_key'}) stream.add_subscriber(subscriber) inner.action(inner) @@ -288,7 +289,7 @@ def subscriber(**kwargs): values.append(kwargs) self.assertEqual( set(stream.hashkey), - {'%s action' % inner.name, '%s x' % inner.name, '_memoize_key'}) + {'%s action' % id(inner), '%s x' % id(inner), '_memoize_key'}) stream.add_subscriber(subscriber) inner.action(inner) @@ -296,6 +297,18 @@ def subscriber(**kwargs): self.assertEqual(values, [{'action': inner.action, 'x': 0}]) + def test_params_no_names(self): + a = IntSlider() + b = IntSlider() + p = Params(parameters=[a.param.value, b.param.value]) + assert len(p.hashkey) == 3 # the two widgets + _memoize_key + + def test_params_identical_names(self): + a = IntSlider(name="Name") + b = IntSlider(name="Name") + p = Params(parameters=[a.param.value, b.param.value]) + assert len(p.hashkey) == 3 # the two widgets + _memoize_key + class TestParamMethodStream(ComparisonTestCase): @@ -501,7 +514,7 @@ def test_dynamicmap_param_method_action_param(self): def subscriber(**kwargs): values.append(kwargs) self.assertEqual(set(stream.hashkey), - {'%s action' % inner.name, '_memoize_key'}) + {'%s action' % id(inner), '_memoize_key'}) stream.add_subscriber(subscriber) inner.action(inner) @@ -521,7 +534,7 @@ def subscriber(**kwargs): values.append(kwargs) self.assertEqual( set(stream.hashkey), - {'%s action' % inner.name, '%s x' % inner.name, '_memoize_key'}) + {'%s action' % id(inner), '%s x' % id(inner), '_memoize_key'}) stream.add_subscriber(subscriber) stream.add_subscriber(lambda **kwargs: dmap[()]) diff --git a/holoviews/util/transform.py b/holoviews/util/transform.py index fef557b145..89033a06b8 100644 --- a/holoviews/util/transform.py +++ b/holoviews/util/transform.py @@ -10,8 +10,7 @@ from ..core.data import PandasInterface from ..core.dimension import Dimension -from ..core.util import basestring, pd, resolve_dependent_value, unique_iterator - +from ..core.util import basestring, flatten, pd, resolve_dependent_value, unique_iterator def _maybe_map(numpy_fn): def fn(values, *args, **kwargs): @@ -332,6 +331,7 @@ def params(self): params = {} for op in self.ops: op_args = list(op['args'])+list(op['kwargs'].values()) + op_args = flatten(op_args) for op_arg in op_args: if 'panel' in sys.modules: from panel.widgets.base import Widget