diff --git a/.travis.yml b/.travis.yml index b86d322a95..c058b7d4f9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,6 +12,9 @@ os: dist: xenial +addons: + firefox: latest + notifications: email: on_failure: change # [always|never|change] default: always @@ -23,6 +26,7 @@ env: - CHANS="-c pyviz" - MPLBACKEND="Agg" - PYTHON_VERSION=3.7 + - MOZ_HEADLESS=1 stages: - test @@ -66,6 +70,7 @@ jobs: - source activate test-environment - travis_wait 30 doit develop_install $CHANS_DEV -o $HV_REQUIREMENTS - doit env_capture + - hash -r script: - doit test_all_recommended after_success: coveralls @@ -88,10 +93,11 @@ jobs: <<: *default stage: docs_dev env: DESC="docs" HV_DOC_GALLERY="false" HV_DOC_REF_GALLERY="true" HV_REQUIREMENTS="doc" PANEL_EMBED="true" PANEL_EMBED_JSON="true" PANEL_EMBED_JSON_PREFIX="json" CHANS_DEV="-c pyviz/label/dev -c bokeh -c conda-forge" + before-script: + - conda install -c conda-forge geckodriver selenium script: - - conda install $CHANS_DEV firefox geckodriver mpl_sample_data - bokeh sampledata - - nbsite generate-rst --org pyviz --project-name holoviews --skip ^reference + - nbsite generate-rst --org holoviz --project-name holoviews --skip ^reference - python ./doc/generate_modules.py holoviews -d ./doc/reference_manual -n holoviews -e tests - nbsite build --what=html --output=builtdocs after_success: @@ -102,7 +108,6 @@ jobs: stage: gallery_dev env: DESC="gallery" HV_DOC_GALLERY="true" HV_DOC_REF_GALLERY="false" BUCKET="dev." HV_REQUIREMENTS="doc" PANEL_EMBED="true" PANEL_EMBED_JSON="true" PANEL_EMBED_JSON_PREFIX="json" CHANS_DEV="-c pyviz/label/dev -c bokeh -c conda-forge" script: - - conda install $CHANS_DEV firefox geckodriver mpl_sample_data - bokeh sampledata - aws s3 sync --quiet s3://holoviews-doc-builds/$TRAVIS_BUILD_NUMBER ./ - git reset --hard --recurse-submodule diff --git a/doc/FAQ.rst b/doc/FAQ.rst index f4bc86bb0e..f7aa290c60 100644 --- a/doc/FAQ.rst +++ b/doc/FAQ.rst @@ -62,7 +62,7 @@ set the ``xlim`` and ``ylim`` plot options: .. code:: python - hv.Curve(df, 'x_col', 'y_col').options(xlim=(0, None), ylim=(0, 10)) + hv.Curve(df, 'x_col', 'y_col').opts(xlim=(0, None), ylim=(0, 10)) This approach allows you to customize objects easily as a final step, but note that the values won't be applied to the underlying data, and thus won't be inherited if this object is subsequently used in an operation or data selection command. @@ -118,10 +118,10 @@ determine the appropriate axis range yourself and set that, e.g. with .. code:: python # for matplotlib: - hv_obj = hv_obj.options(fig_size=500) + hv_obj = hv_obj.opts(fig_size=500) # for bokeh: - hv_obj = hv_obj.options(width=1000, height=500) + hv_obj = hv_obj.opts(width=1000, height=500) **Q: How do I get a legend on my overlay figure?** @@ -184,7 +184,7 @@ plotted, instead consider that it is possible to write so called ``hooks``: # artist, axis, legend and in bokeh x_range, y_range, glyph, cds etc. plot.handles - hv.Curve(df, 'x_col', 'y_col').options(hooks=[hook]) + hv.Curve(df, 'x_col', 'y_col').opts(hooks=[hook]) These hooks can modify the backend specific representation, e.g. the matplotlib figure, before it is displayed, allowing arbitrary customizations to be @@ -229,7 +229,7 @@ for that HoloViews object: b.axis[0].ticker = FixedTicker(ticks=list(range(0, 10))) h = hv.Curve([1,2,7], 'x_col', 'y_col') - h = h.options(hooks=[update_axis]) + h = h.opts(hooks=[update_axis]) h Here, you've wrapped your Bokeh-API calls into a function, then @@ -360,7 +360,7 @@ they don't get applied until the object is returned (during IPython's "display hooks" processing). So to make sure that options get applied, (a) return the object from a cell, and then (b) access it (e.g. for exporting) after the object has been returned. -To avoid confusion, you may prefer to use .options() directly on the +To avoid confusion, you may prefer to use .opts() directly on the object to ensure that the options have been applied before exporting. Example code below: @@ -451,7 +451,7 @@ element: hv.extension('matplotlib') Store.add_style_opts(hv.Image, ['filternorm'], backend='matplotlib') -Now you can freely use ``'filternorm'`` in ``.options()`` and in the +Now you can freely use ``'filternorm'`` in ``.opts()`` and in the ``%opts`` line/cell magic, including tab-completion! diff --git a/doc/conf.py b/doc/conf.py index f16cb52082..81433840c7 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -4,8 +4,8 @@ # Declare information specific to this project. project = u'HoloViews' -authors = u'PyViz developers' -copyright = u'2019 ' + authors +authors = u'HoloViz developers' +copyright = u'2020 ' + authors description = 'Stop plotting your data - annotate your data and let it visualize itself.' import param @@ -16,6 +16,7 @@ import holoviews version = release = holoviews.__version__ +holoviews.extension.inline = False html_theme = 'sphinx_holoviz_theme' html_static_path += ['_static'] diff --git a/examples/gallery/demos/bokeh/lesmis_example.ipynb b/examples/gallery/demos/bokeh/lesmis_example.ipynb index 71bfb97d0e..bfb7ac5f5c 100644 --- a/examples/gallery/demos/bokeh/lesmis_example.ipynb +++ b/examples/gallery/demos/bokeh/lesmis_example.ipynb @@ -78,10 +78,9 @@ "\n", "combined = hv.Overlay([o.opts(cmap=cm).sort() for o, cm in zip(overlaid, cmaps)], label='LesMis Occurences')\n", "styled = combined.opts(\n", - " opts.HeatMap(logz=True, clipping_colors={'NaN':(1,1,1,0.)},\n", - " xaxis='top', xrotation=90,\n", + " opts.HeatMap(logz=True, clim=(0.1, None), clipping_colors={'NaN':(1,1,1,0.)}, xaxis='top', xrotation=90,\n", " fontsize={'ticks': '7pt', 'title': '18pt'}, invert_xaxis=True, tools=['hover'],\n", - " labelled=[],),\n", + " labelled=[], axiswise=True),\n", " opts.Overlay(height=800, width=800)\n", ")\n", "styled" diff --git a/examples/reference/elements/bokeh/Spread.ipynb b/examples/reference/elements/bokeh/Spread.ipynb index 2b3d8253b5..0b70f1c228 100644 --- a/examples/reference/elements/bokeh/Spread.ipynb +++ b/examples/reference/elements/bokeh/Spread.ipynb @@ -33,7 +33,7 @@ "source": [ "``Spread`` elements have the same data format as the [``ErrorBars``](ErrorBars.ipynb) element, namely x- and y-values with associated symmetric or asymmetric errors, but are interpreted as samples from a continuous distribution (just as ``Curve`` is the continuous version of ``Scatter``). These are often paired with an overlaid ``Curve`` to show an average trend along with a corresponding spread of values; see the [Tabular Datasets](../../../user_guide/08-Tabular_Datasets.ipynb) user guide for examples. \n", "\n", - "Note that as the ``Spread`` element is used to add information to a plot (typically a ``Curve``) the default alpha value is less that one, making it partially transparent. \n", + "Note that as the ``Spread`` element is used to add information to a plot (typically a ``Curve``) the default alpha value is less than one, making it partially transparent. \n", "\n", "\n", "##### Symmetric" diff --git a/examples/reference/elements/matplotlib/Spread.ipynb b/examples/reference/elements/matplotlib/Spread.ipynb index cf2c028093..8a1a0b9bd5 100644 --- a/examples/reference/elements/matplotlib/Spread.ipynb +++ b/examples/reference/elements/matplotlib/Spread.ipynb @@ -33,7 +33,7 @@ "source": [ "``Spread`` elements have the same data format as the [``ErrorBars``](ErrorBars.ipynb) element, namely x- and y-values with associated symmetric or asymmetric errors, but are interpreted as samples from a continuous distribution (just as ``Curve`` is the continuous version of ``Scatter``). These are often paired with an overlaid ``Curve`` to show an average trend along with a corresponding spread of values; see the [Tabular Datasets](../../../user_guide/07-Tabular_Datasets.ipynb) user guide for examples.\n", "\n", - "Note that as the ``Spread`` element is used to add information to a plot (typically a ``Curve``) the default alpha value is less that one, making it partially transparent. \n", + "Note that as the ``Spread`` element is used to add information to a plot (typically a ``Curve``) the default alpha value is less than one, making it partially transparent. \n", "\n", "##### Symmetric" ] diff --git a/examples/reference/elements/plotly/Spread.ipynb b/examples/reference/elements/plotly/Spread.ipynb index b1b701579a..1b95e61c29 100644 --- a/examples/reference/elements/plotly/Spread.ipynb +++ b/examples/reference/elements/plotly/Spread.ipynb @@ -30,7 +30,7 @@ "source": [ "``Spread`` elements have the same data format as the [``ErrorBars``](ErrorBars.ipynb) element, namely x- and y-values with associated symmetric or asymmetric errors, but are interpreted as samples from a continuous distribution (just as ``Curve`` is the continuous version of ``Scatter``). These are often paired with an overlaid ``Curve`` to show an average trend along with a corresponding spread of values; see the [Tabular Datasets](../../../user_guide/08-Tabular_Datasets.ipynb) user guide for examples. \n", "\n", - "Note that as the ``Spread`` element is used to add information to a plot (typically a ``Curve``) the default alpha value is less that one, making it partially transparent. \n", + "Note that as the ``Spread`` element is used to add information to a plot (typically a ``Curve``) the default alpha value is less than one, making it partially transparent. \n", "\n", "\n", "##### Symmetric" diff --git a/examples/user_guide/Continuous_Coordinates.ipynb b/examples/user_guide/Continuous_Coordinates.ipynb index 47a069a5b8..2684232c43 100644 --- a/examples/user_guide/Continuous_Coordinates.ipynb +++ b/examples/user_guide/Continuous_Coordinates.ipynb @@ -53,7 +53,7 @@ "|||\n", "|:--------------:|:----------------|\n", "| **``f(x,y)``** | a simple function that accepts a location in a 2D plane specified in millimeters (mm) |\n", - "| **``region``** | a 1mm×1mm square region of this 2D plane, centered at the origin, and |\n", + "| **``region``** | a 1mm×1mm square region of this 2D plane, centered at the origin, and |\n", "| **``coords``** | a function returning a square (s×s) grid of (x,y) coordinates regularly sampling the region in the given bounds, at the centers of each grid cell |\n", "||||\n", "\n" @@ -332,6 +332,8 @@ "\n", "They also work the same for the n-dimensional coordinates and slicing supported by the [container](Containers) types ``HoloMap``, ``NdLayout``, and ``NdOverlay``, implemented in ``holoviews.core.dimension.Dimensioned`` and again allowing arbitrary irregular spacing. \n", "\n", + "``QuadMesh`` elements are similar but allow more general types of mapping between the underlying array and the continuous space, with arbitrary spacing along each of the axes or even over the entire array. See the ``QuadMesh`` element for more details.\n", + "\n", "Together, these powerful continuous-coordinate indexing and slicing operations allow you to work naturally and simply in the full *n*-dimensional space that characterizes your data and parameter values." ] }, diff --git a/holoviews/core/data/cudf.py b/holoviews/core/data/cudf.py index bc801e2b88..0f5687b8e5 100644 --- a/holoviews/core/data/cudf.py +++ b/holoviews/core/data/cudf.py @@ -163,7 +163,7 @@ def groupby(cls, dataset, dimensions, container_type, group_type, **kwargs): group_kwargs['dataset'] = dataset.dataset # Find all the keys along supplied dimensions - keys = product(*(dataset.data[dimensions[0]].unique() for d in dimensions)) + keys = product(*(dataset.data[dimensions[0]].unique().values_host for d in dimensions)) # Iterate over the unique entries applying selection masks grouped_data = [] diff --git a/holoviews/core/util.py b/holoviews/core/util.py index 10f34d74d6..fdd6024532 100644 --- a/holoviews/core/util.py +++ b/holoviews/core/util.py @@ -1544,10 +1544,16 @@ def resolve_dependent_value(value): A new dictionary where any parameter dependencies have been resolved. """ + range_widget = False if 'panel' in sys.modules: - from panel.widgets.base import Widget - if isinstance(value, Widget): - value = value.param.value + from panel.widgets import RangeSlider, Widget + range_widget = isinstance(value, RangeSlider) + try: + from panel.depends import param_value_if_widget + value = param_value_if_widget(value) + except Exception: + if isinstance(value, Widget): + value = value.param.value if is_param_method(value, has_deps=True): value = value() elif isinstance(value, param.Parameter) and isinstance(value.owner, param.Parameterized): @@ -1557,6 +1563,8 @@ def resolve_dependent_value(value): args = (getattr(p.owner, p.name) for p in deps.get('dependencies', [])) kwargs = {k: getattr(p.owner, p.name) for k, p in deps.get('kw', {}).items()} value = value(*args, **kwargs) + if isinstance(value, tuple) and range_widget: + value = slice(*value) return value diff --git a/holoviews/element/raster.py b/holoviews/element/raster.py index 1069b047a0..fc3e2e6f9e 100644 --- a/holoviews/element/raster.py +++ b/holoviews/element/raster.py @@ -291,9 +291,10 @@ def __init__(self, data, kdims=None, vdims=None, bounds=None, extents=None, Dataset.__init__(self, data, kdims=kdims, vdims=vdims, extents=extents, **params) if not self.interface.gridded: - raise DataError("%s type expects gridded data, %s is columnar." + raise DataError("%s type expects gridded data, %s is columnar. " "To display columnar data as gridded use the HeatMap " - "element or aggregate the data." % + "element or aggregate the data (e.g. using rasterize " + "or np.histogram2d)." % (type(self).__name__, self.interface.__name__)) dim2, dim1 = self.interface.shape(self, gridded=True)[:2] @@ -783,9 +784,10 @@ def __init__(self, data, kdims=None, vdims=None, **params): data = ([], [], np.zeros((0, 0))) super(QuadMesh, self).__init__(data, kdims, vdims, **params) if not self.interface.gridded: - raise DataError("%s type expects gridded data, %s is columnar." + raise DataError("%s type expects gridded data, %s is columnar. " "To display columnar data as gridded use the HeatMap " - "element or aggregate the data." % + "element or aggregate the data (e.g. using " + "np.histogram2d)." % (type(self).__name__, self.interface.__name__)) diff --git a/holoviews/plotting/bokeh/callbacks.py b/holoviews/plotting/bokeh/callbacks.py index 3fb83a63eb..9eb35b157d 100644 --- a/holoviews/plotting/bokeh/callbacks.py +++ b/holoviews/plotting/bokeh/callbacks.py @@ -111,6 +111,8 @@ def reset(self): if self.handle_ids: handles = self._init_plot_handles() for handle_name in self.models: + if not (handle_name in handles): + continue handle = handles[handle_name] cb_hash = (id(handle), id(type(self))) self._callbacks.pop(cb_hash, None) @@ -1198,8 +1200,14 @@ def _process_msg(self, msg): new_values = [] for vals in values: if isinstance(vals, dict): + + shape = vals.pop('shape', None) + dtype = vals.pop('dtype', None) + vals.pop('dimension', None) vals = sorted([(int(k), v) for k, v in vals.items()]) vals = [v for k, v in vals] + if dtype is not None: + vals = np.array(vals, dtype=dtype).reshape(shape) new_values.append(vals) values = new_values elif any(isinstance(v, (int, float)) for v in values): diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index 66f6a29dec..5bd65de50d 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -1380,6 +1380,9 @@ def initialize_plot(self, ranges=None, plot=None, plots=None, source=None): for cb in self.callbacks: cb.initialize() + if self.top_level: + self.init_links() + if not self.overlaid: self._set_active_tools(plot) self._process_legend() diff --git a/holoviews/plotting/bokeh/stats.py b/holoviews/plotting/bokeh/stats.py index 1cfe233640..3c1998d427 100644 --- a/holoviews/plotting/bokeh/stats.py +++ b/holoviews/plotting/bokeh/stats.py @@ -548,6 +548,8 @@ def get_data(self, element, ranges, style): kde_data, line_data, seg_data, bar_data, scatter_data = (defaultdict(list) for i in range(5)) for i, (key, g) in enumerate(groups.items()): key = decode_bytes(key) + if element.kdims: + key = tuple(d.pprint_value(k) for d, k in zip(element.kdims, key)) kde, line, segs, bars, scatter = self._kde_data(element, g, key, split_dim, split_cats, **kwargs) for k, v in segs.items(): seg_data[k] += v diff --git a/holoviews/plotting/plotly/renderer.py b/holoviews/plotting/plotly/renderer.py index 505b632199..723946a1e0 100644 --- a/holoviews/plotting/plotly/renderer.py +++ b/holoviews/plotting/plotly/renderer.py @@ -159,6 +159,8 @@ def load_nb(cls, inline=True): """ import panel.models.plotly # noqa cls._loaded = True + if 'plotly' not in getattr(pn.extension, '_loaded_extensions', ['plotly']): + pn.extension._loaded_extensions.append('plotly') def _activate_plotly_backend(renderer): diff --git a/holoviews/selection.py b/holoviews/selection.py index a444ba47b6..863d413ae9 100644 --- a/holoviews/selection.py +++ b/holoviews/selection.py @@ -5,13 +5,15 @@ from param.parameterized import bothmethod +from .core.data import Dataset from .core.dimension import OrderedDict from .core.element import Element, Layout from .core.options import CallbackError, Store from .core.overlay import NdOverlay, Overlay from .core.spaces import GridSpace from .streams import ( - Stream, SelectionExprSequence, CrossFilterSet, Derived, PlotReset, SelectMode + Stream, SelectionExprSequence, CrossFilterSet, + Derived, PlotReset, SelectMode, Pipe ) from .util import DynamicMap @@ -159,7 +161,7 @@ def _selection_transform(self, hvobj, operations=()): chart = Store.registry[Store.current_backend][hvobj.type] return chart.selection_display(hvobj).build_selection( self._selection_streams, hvobj, operations, - self._selection_expr_streams.get(hvobj, None), + self._selection_expr_streams.get(hvobj, None), cache=self._cache ) else: # This is a DynamicMap that we don't know how to recurse into. @@ -172,7 +174,7 @@ def _selection_transform(self, hvobj, operations=()): self._register(element) return chart.selection_display(element).build_selection( self._selection_streams, element, operations, - self._selection_expr_streams.get(element, None), + self._selection_expr_streams.get(element, None), cache=self._cache ) return hvobj elif isinstance(hvobj, (Layout, Overlay, NdOverlay, GridSpace)): @@ -267,10 +269,49 @@ def instance(self_or_cls, **params): inst._obj_regions = {} inst._reset_regions = True + # _datasets caches + inst._datasets = [] + inst._cache = {} + self_or_cls._install_param_callbacks(inst) return inst + @param.depends('selection_expr', watch=True) + def _update_pipes(self): + sel_expr = self.selection_expr + for pipe, ds, raw in self._datasets: + ref = ds._plot_id + self._cache[ref] = ds_cache = self._cache.get(ref, {}) + if sel_expr in ds_cache: + data = ds_cache[sel_expr] + return pipe.event(data=data.data) + else: + ds_cache.clear() + sel_ds = SelectionDisplay._select(ds, sel_expr, self._cache) + ds_cache[sel_expr] = sel_ds + pipe.event(data=sel_ds.data if raw else sel_ds) + + def selection_param(self, data): + """ + Returns a parameter which reflects the current selection + when applied to the supplied data, making it easy to create + a callback which depends on the current selection. + + Args: + data: A Dataset type or data which can be cast to a Dataset + + Returns: + A parameter which reflects the current selection + """ + raw = False + if not isinstance(data, Dataset): + raw = True + data = Dataset(data) + pipe = Pipe(data=data.data) + self._datasets.append((pipe, data, raw)) + return pipe.param.data + @bothmethod def _install_param_callbacks(self_or_cls, inst): def update_selection_mode(*_): @@ -395,7 +436,7 @@ class SelectionDisplay(object): def __call__(self, element): return self - def build_selection(self, selection_streams, hvobj, operations, region_stream=None): + def build_selection(self, selection_streams, hvobj, operations, region_stream=None, cache={}): raise NotImplementedError() @staticmethod @@ -468,13 +509,12 @@ def __init__(self, color_prop='color', is_cmap=False, supports_region=True): self.color_props = color_prop self.is_cmap = is_cmap self.supports_region = supports_region - self._cache = {} def _get_color_kwarg(self, color): return {color_prop: [color] if self.is_cmap else color for color_prop in self.color_props} - def build_selection(self, selection_streams, hvobj, operations, region_stream=None): + def build_selection(self, selection_streams, hvobj, operations, region_stream=None, cache=None): from .element import Histogram num_layers = len(selection_streams.style_stream.colors) @@ -488,7 +528,7 @@ def build_selection(self, selection_streams, hvobj, operations, region_stream=No cmap_stream = selection_streams.cmap_streams[layer_number] layer = obj.apply( self._build_layer_callback, streams=[cmap_stream]+streams, - layer_number=layer_number, per_element=True + layer_number=layer_number, cache=cache, per_element=True ) layers.append(layer) @@ -529,8 +569,8 @@ def _inject_cmap_in_pipeline(cls, pipeline, cmap): operations.append(op) return pipeline.instance(operations=operations) - def _build_layer_callback(self, element, exprs, layer_number, cmap, **kwargs): - selection = self._select(element, exprs[layer_number], self._cache) + def _build_layer_callback(self, element, exprs, layer_number, cmap, cache, **kwargs): + selection = self._select(element, exprs[layer_number], cache) pipeline = element.pipeline if cmap is not None: pipeline = self._inject_cmap_in_pipeline(pipeline, cmap) @@ -568,7 +608,7 @@ def __init__(self, color_prop='color', alpha_prop='alpha', backend=None): self.alpha_props = [alpha_prop] self.backend = backend - def build_selection(self, selection_streams, hvobj, operations, region_stream=None): + def build_selection(self, selection_streams, hvobj, operations, region_stream=None, cache={}): def _build_selection(el, colors, alpha, exprs, **kwargs): from .plotting.util import linear_gradient ds = el.dataset @@ -595,7 +635,7 @@ def _build_selection(el, colors, alpha, exprs, **kwargs): return el.pipeline(ds).opts(backend=self.backend, clone=True, **color_opts) sel_streams = [selection_streams.style_stream, selection_streams.exprs_stream] - hvobj = hvobj.apply(_build_selection, streams=sel_streams, per_element=True) + hvobj = hvobj.apply(_build_selection, streams=sel_streams, per_element=True, cache=cache) for op in operations: hvobj = op(hvobj) diff --git a/setup.py b/setup.py index fa71017e4d..93c94f1136 100644 --- a/setup.py +++ b/setup.py @@ -100,10 +100,9 @@ 'awscli', 'pscript', 'graphviz', - 'selenium', 'bokeh <2.2', - 'firefox', - 'geckodriver' + 'nbconvert <6.0', + 'mpl_sample_data' ] extras_require['build'] = [