From 66e3195960414e20e6f6366c2e83870b43e265d4 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 29 May 2019 17:31:44 +0200 Subject: [PATCH 01/11] Add ability to use functions with param dependencies in DynamicMap --- holoviews/core/spaces.py | 4 +++- holoviews/streams.py | 11 ++++++++--- holoviews/util/__init__.py | 6 ++++-- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/holoviews/core/spaces.py b/holoviews/core/spaces.py index 54fb299b4a..cb780aa945 100644 --- a/holoviews/core/spaces.py +++ b/holoviews/core/spaces.py @@ -7,6 +7,7 @@ from functools import partial from collections import defaultdict from contextlib import contextmanager +from types import FunctionType import numpy as np import param @@ -915,7 +916,8 @@ def __init__(self, callback, initial_items=None, streams=None, **params): streams = (streams or []) # If callback is a parameterized method and watch is disabled add as stream - if util.is_param_method(callback, has_deps=True) and params.get('watch', True): + if (util.is_param_method(callback, has_deps=True) and params.get('watch', True) + or isinstance(callback, FunctionType) and hasattr(callback, '_dinfo')): streams.append(callback) if isinstance(callback, types.GeneratorType): diff --git a/holoviews/streams.py b/holoviews/streams.py index 6e6bad3fa2..0847aae0ee 100644 --- a/holoviews/streams.py +++ b/holoviews/streams.py @@ -9,6 +9,7 @@ from collections import defaultdict from contextlib import contextmanager from itertools import groupby +from types import FunctionType import param import numpy as np @@ -186,6 +187,10 @@ def _process_streams(cls, streams): if not hasattr(s, "_dinfo"): continue s = ParamMethod(s) + elif isinstance(s, FunctionType) and hasattr(s, "_dinfo"): + deps = s._dinfo + dep_params = list(deps['dependencies']) + list(deps.get('kw', {}).values()) + s = Params(parameters=dep_params) else: invalid.append(s) continue @@ -623,13 +628,13 @@ class Params(Stream): parameterized = param.ClassSelector(class_=(param.Parameterized, param.parameterized.ParameterizedMetaclass), - constant=True, doc=""" + constant=True, allow_None=True, doc=""" Parameterized instance to watch for parameter changes.""") parameters = param.List([], constant=True, doc=""" Parameters on the parameterized to watch.""") - def __init__(self, parameterized, parameters=None, watch=True, **params): + def __init__(self, parameterized=None, parameters=None, watch=True, **params): if util.param_version < '1.8.0' and watch: raise RuntimeError('Params stream requires param version >= 1.8.0, ' 'to support watching parameters.') @@ -725,7 +730,7 @@ class ParamMethod(Params): def __init__(self, parameterized, parameters=None, watch=True, **params): if not util.is_param_method(parameterized): - raise ValueError('ParamMethodStream expects a method on a ' + raise ValueError('ParamMethod stream expects a method on a ' 'parameterized class, found %s.' % type(parameterized).__name__) method = parameterized diff --git a/holoviews/util/__init__.py b/holoviews/util/__init__.py index 74b4194f09..ea11d4a6fe 100644 --- a/holoviews/util/__init__.py +++ b/holoviews/util/__init__.py @@ -1,6 +1,7 @@ import os, sys, inspect, shutil from collections import defaultdict +from types import FunctionType try: from pathlib import Path @@ -882,8 +883,9 @@ def _get_streams(self, map_obj, watch=True): streams = list(util.unique_iterator(streams + dim_streams)) # If callback is a parameterized method and watch is disabled add as stream - has_dependencies = util.is_param_method(self.p.operation, has_deps=True) - if has_dependencies and watch: + has_dependencies = (util.is_param_method(self.p.operation, has_deps=True) or + isinstance(callback, FunctionType) and hasattr(callback, '_dinfo')) + if (has_dependencies and watch: streams.append(self.p.operation) # Add any keyword arguments which are parameterized methods From 21b60bb24e90e48127faa92ade716eec4ce70cb2 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 29 May 2019 17:49:02 +0200 Subject: [PATCH 02/11] Add tests --- holoviews/tests/core/testdynamic.py | 14 ++++++++++++++ holoviews/tests/teststreams.py | 12 ++++++++++++ holoviews/util/__init__.py | 15 ++++++++------- 3 files changed, 34 insertions(+), 7 deletions(-) diff --git a/holoviews/tests/core/testdynamic.py b/holoviews/tests/core/testdynamic.py index 72813709ec..495543a8df 100644 --- a/holoviews/tests/core/testdynamic.py +++ b/holoviews/tests/core/testdynamic.py @@ -190,6 +190,20 @@ def test_deep_apply_element_function(self): curve = fn(10) self.assertEqual(mapped[10], curve.clone(curve.data*2)) + def test_deep_apply_element_param_function(self): + fn = lambda i: Curve(np.arange(i)) + class Test(param.Parameterized): + a = param.Integer(default=1) + test = Test() + @param.depends(test.param.a) + def op(obj, a): + return obj.clone(obj.data*2) + dmap = DynamicMap(fn, kdims=[Dimension('Test', range=(10, 20))]) + mapped = dmap.apply(op) + test.a = 2 + curve = fn(10) + self.assertEqual(mapped[10], curve.clone(curve.data*2)) + def test_deep_apply_element_function_with_kwarg(self): fn = lambda i: Curve(np.arange(i)) dmap = DynamicMap(fn, kdims=[Dimension('Test', range=(10, 20))]) diff --git a/holoviews/tests/teststreams.py b/holoviews/tests/teststreams.py index 8483c03bcb..94942b5558 100644 --- a/holoviews/tests/teststreams.py +++ b/holoviews/tests/teststreams.py @@ -380,6 +380,18 @@ def subscriber(**kwargs): inner.y = 2 self.assertEqual(values, [{}]) + def test_param_function_depends(self): + inner = self.inner() + + @param.depends(inner.param.x) + def test(x): + return Points([x]) + + dmap = DynamicMap(test) + + inner.x = 10 + self.assertEqual(dmap[()], Points([10])) + def test_param_method_depends_no_deps(self): inner = self.inner() stream = ParamMethod(inner.method_no_deps) diff --git a/holoviews/util/__init__.py b/holoviews/util/__init__.py index ea11d4a6fe..4f17b16abe 100644 --- a/holoviews/util/__init__.py +++ b/holoviews/util/__init__.py @@ -859,15 +859,16 @@ def _get_streams(self, map_obj, watch=True): added to the list. """ streams = [] + op = self.p.operation for stream in self.p.streams: if inspect.isclass(stream) and issubclass(stream, Stream): stream = stream() elif not (isinstance(stream, Stream) or util.is_param_method(stream)): raise ValueError('Streams must be Stream classes or instances, found %s type' % type(stream).__name__) - if isinstance(self.p.operation, Operation): - updates = {k: self.p.operation.p.get(k) for k, v in stream.contents.items() - if v is None and k in self.p.operation.p} + if isinstance(op, Operation): + updates = {k: op.p.get(k) for k, v in stream.contents.items() + if v is None and k in op.p} if updates: reverse = {v: k for k, v in stream._rename.items()} stream.update(**{reverse.get(k, k): v for k, v in updates.items()}) @@ -883,10 +884,10 @@ def _get_streams(self, map_obj, watch=True): streams = list(util.unique_iterator(streams + dim_streams)) # If callback is a parameterized method and watch is disabled add as stream - has_dependencies = (util.is_param_method(self.p.operation, has_deps=True) or - isinstance(callback, FunctionType) and hasattr(callback, '_dinfo')) - if (has_dependencies and watch: - streams.append(self.p.operation) + has_dependencies = (util.is_param_method(op, has_deps=True) or + isinstance(op, FunctionType) and hasattr(op, '_dinfo')) + if has_dependencies and watch: + streams.append(op) # Add any keyword arguments which are parameterized methods # with dependencies as streams From 9ce261734b701dbe6f1a7ed756760c87c9f59923 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 29 May 2019 18:17:08 +0200 Subject: [PATCH 03/11] Allow renaming kwargs in depends --- holoviews/streams.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/holoviews/streams.py b/holoviews/streams.py index 0847aae0ee..c5ac5ee175 100644 --- a/holoviews/streams.py +++ b/holoviews/streams.py @@ -190,7 +190,8 @@ def _process_streams(cls, streams): elif isinstance(s, FunctionType) and hasattr(s, "_dinfo"): deps = s._dinfo dep_params = list(deps['dependencies']) + list(deps.get('kw', {}).values()) - s = Params(parameters=dep_params) + rename = {p.name: k for k, p in deps.get('kw', {}).items()} + s = Params(parameters=dep_params, rename=rename) else: invalid.append(s) continue From 6ad02ff01bfbdb15c28a7c12fba6fd24b1f06d71 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 2 Aug 2019 13:17:07 +0100 Subject: [PATCH 04/11] Ensure that Params stream handles renames on a per owner basis --- holoviews/streams.py | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/holoviews/streams.py b/holoviews/streams.py index c5ac5ee175..b26d307621 100644 --- a/holoviews/streams.py +++ b/holoviews/streams.py @@ -190,7 +190,7 @@ def _process_streams(cls, streams): elif isinstance(s, FunctionType) and hasattr(s, "_dinfo"): deps = s._dinfo dep_params = list(deps['dependencies']) + list(deps.get('kw', {}).values()) - rename = {p.name: k for k, p in deps.get('kw', {}).items()} + rename = {(p.owner, p.name): k for k, p in deps.get('kw', {}).items()} s = Params(parameters=dep_params, rename=rename) else: invalid.append(s) @@ -644,6 +644,17 @@ def __init__(self, parameterized=None, parameters=None, watch=True, **params): else: parameters = [p if isinstance(p, param.Parameter) else parameterized.param[p] for p in parameters] + + if 'rename' in params: + rename = {} + owners = [p.owner for p in parameters] + for k, v in params['rename'].items(): + if isinstance(k, tuple): + rename[k] = v + else: + rename.update({(o, k): v for o in owners}) + params['rename'] = rename + super(Params, self).__init__(parameterized=parameterized, parameters=parameters, **params) self._memoize_counter = 0 self._events = [] @@ -679,9 +690,10 @@ def from_params(cls, params): def _validate_rename(self, mapping): pnames = [p.name for p in self.parameters] for k, v in mapping.items(): - if k not in pnames: - raise KeyError('Cannot rename %r as it is not a stream parameter' % k) - if k != v and v in pnames: + n = k[1] if isinstance(k, tuple) else k + if n not in pnames: + raise KeyError('Cannot rename %r as it is not a stream parameter' % n) + if n != v and v in pnames: raise KeyError('Cannot rename to %r as it clashes with a ' 'stream parameter of the same name' % v) return mapping @@ -701,9 +713,9 @@ def _on_trigger(self): @property def hashkey(self): - hashkey = {p.name: getattr(p.owner, p.name) for p in self.parameters} - hashkey = {self._rename.get(k, k): v for (k, v) in hashkey.items() - if self._rename.get(k, True) is not None} + hashkey = {(p.owner, p.name): getattr(p.owner, p.name) for p in self.parameters} + hashkey = {self._rename.get((o, n), n): v for (o, n), v in hashkey.items() + if self._rename.get((o, n), True) is not None} hashkey['_memoize_key'] = self._memoize_counter return hashkey @@ -716,9 +728,9 @@ def update(self, **kwargs): @property def contents(self): - filtered = {p.name: getattr(p.owner, p.name) for p in self.parameters} - return {self._rename.get(k, k): v for (k, v) in filtered.items() - if self._rename.get(k, True) is not None} + filtered = {(p.owner, p.name): getattr(p.owner, p.name) for p in self.parameters} + return {self._rename.get((o, n), n): v for (o, n), v in filtered.items() + if self._rename.get((o, n), True) is not None} From 27d085fb8c5c8dba7b9d86a7babdee0fcf1789c4 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 2 Aug 2019 13:19:08 +0100 Subject: [PATCH 05/11] Updated docs --- examples/user_guide/14-Data_Pipelines.ipynb | 53 ++++++------------- examples/user_guide/17-Dashboards.ipynb | 57 ++++++++++++++++----- 2 files changed, 59 insertions(+), 51 deletions(-) diff --git a/examples/user_guide/14-Data_Pipelines.ipynb b/examples/user_guide/14-Data_Pipelines.ipynb index a40b147dff..f128ecc748 100644 --- a/examples/user_guide/14-Data_Pipelines.ipynb +++ b/examples/user_guide/14-Data_Pipelines.ipynb @@ -105,14 +105,16 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Controlling operations via Streams" + "### Dynamically evaluating operations and parameters with ``.apply``" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "In the previous section we briefly mentioned that in addition to regular widgets ``DynamicMap`` also supports streams, which allow us to define custom events our ``DynamicMap`` should subscribe to. To learn more about streams see the [Responding to Events](./12-Responding_to_Events.ipynb). Here we will declare a stream that controls the rolling window:" + "The ``.apply`` method allows us to automatically build a pipeline given an object and some operation or function along with parameter instances passed in as keyword argument. Internally it will then build a Stream to ensure that whenever the parameter changes the plot is updated. To learn more about streams see the [Responding to Events](./12-Responding_to_Events.ipynb).\n", + "\n", + "This mechanism allows us to build powerful pipelines by linking parameters on a user defined class or even an external widget, e.g. here we import an ``IntSlider`` widget from [``panel``](https://pyviz.panel.org):" ] }, { @@ -121,15 +123,16 @@ "metadata": {}, "outputs": [], "source": [ - "rolling_stream = Stream.define('rolling', rolling_window=5)\n", - "stream = rolling_stream()" + "import panel as pn\n", + "\n", + "slider = pn.widgets.IntSlider(name='rolling_window', start=1, end=100, value=50)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Now we can define a function that both loads the symbol and applies the ``rolling`` operation passing our ``rolling_window`` parameter to the operation:" + "Using the ``.apply`` method we can now apply the ``rolling`` operation to the DynamicMap and set the ``value`` parameter of the slider to supply the rolling_window value. Note that as long as the applied function is a HoloViews ``Operation`` we could also apply it directly but for pure functions we have to use ``.apply``." ] }, { @@ -138,12 +141,7 @@ "metadata": {}, "outputs": [], "source": [ - "def rolled_data(symbol, rolling_window, **kwargs):\n", - " curve = load_symbol(symbol)\n", - " return rolling(curve, rolling_window=rolling_window)\n", - " \n", - "rolled_dmap = hv.DynamicMap(rolled_data, kdims='Symbol',\n", - " streams=[stream]).redim.values(Symbol=stock_symbols)\n", + "rolled_dmap = dmap.apply(rolling, rolling_window=slider.param.value)\n", "\n", "rolled_dmap" ] @@ -152,7 +150,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Since we have a handle on the ``Stream`` we can now send events to it and watch the plot above update, let's start by setting the ``rolling_window=50``. " + "Since the widget's value is now linked to the plot via a ``Stream`` we can display the widget and watch the plot update:" ] }, { @@ -161,14 +159,14 @@ "metadata": {}, "outputs": [], "source": [ - "stream.event(rolling_window=50)" + "slider" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Instead of manually defining a function we can also do something much simpler, namely we can just apply the rolling operation to the original ``DynamicMap`` we defined and pass our ``rolling_stream`` to the operation. To make things a bit more interesting we will also apply the ``rolling_outlier_std`` function which computes outliers within the ``rolling_window``. We supply our stream to both:" + "The power of building pipelines is that different visual components can share the same inputs but compute very different things from that data. The part of the pipeline that is shared is only evaluated once making it easy to build efficient data processing code. To illustrate this we will also apply the ``rolling_outlier_std`` operation which computes outliers within the ``rolling_window`` and again we will supply the widget ``value``:" ] }, { @@ -177,37 +175,18 @@ "metadata": {}, "outputs": [], "source": [ - "stream = rolling_stream()\n", - "\n", - "smoothed = rolling(dmap, streams=[stream])\n", - "outliers = rolling_outlier_std(dmap, streams=[stream])\n", + "outliers = dmap.apply(rolling_outlier_std, rolling_window=slider.param.value)\n", "\n", - "smoothed * outliers.opts(color='red', marker='triangle')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Since the ``rolling_stream`` instance we created is bound to both operations, triggering an event on the stream will trigger both the ``Curve`` and the ``Scatter`` of outliers to be updated:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "stream.event(rolling_window=50)" + "rolled_dmap * outliers.opts(color='red', marker='triangle')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "We can chain operations like this indefinitely and attach streams to each stage. By chaining we can watch our visualization update whenever we change a stream value anywhere in the pipeline and HoloViews will be smart about which parts of the pipeline are recomputed, which allows us to build complex visualizations very quickly.\n", + "We can chain operations like this indefinitely and attach parameters or explicit streams to each stage. By chaining we can watch our visualization update whenever we change a stream value anywhere in the pipeline and HoloViews will be smart about which parts of the pipeline are recomputed, which allows us to build complex visualizations very quickly.\n", "\n", - "In later guides we will discover how to tie custom streams to custom widgets letting us easily control the stream values and making it trivial to define complex dashboards. ``paramNB`` is only one widget framework we could use: we could also choose ``paramBokeh`` to make use of bokeh widgets and deploy the dashboard on bokeh server, or we could manually link ``ipywidgets`` to our streams. For more information on how to deploy bokeh apps from HoloViews and build dashboards see the [Deploying Bokeh Apps](./Deploying_Bokeh_Apps.ipynb) and [Dashboards](./17-Dashboards.ipynb) guides." + "In later guides we will see how we can combine HoloViews plots and Panel widgets into custom layouts allowing us to define complex dashboards. For more information on how to deploy bokeh apps from HoloViews and build dashboards see the [Deploying Bokeh Apps](./Deploying_Bokeh_Apps.ipynb) and [Dashboards](./17-Dashboards.ipynb) guides." ] } ], diff --git a/examples/user_guide/17-Dashboards.ipynb b/examples/user_guide/17-Dashboards.ipynb index 0689258785..1d00838236 100644 --- a/examples/user_guide/17-Dashboards.ipynb +++ b/examples/user_guide/17-Dashboards.ipynb @@ -65,7 +65,8 @@ "source": [ "import param\n", "import panel as pn\n", - "from holoviews.streams import Params\n", + "\n", + "variables = ['open', 'high', 'low', 'close', 'volume', 'adj_close']\n", "\n", "class StockExplorer(param.Parameterized):\n", "\n", @@ -73,8 +74,7 @@ " \n", " symbol = param.ObjectSelector(default='AAPL', objects=stock_symbols)\n", " \n", - " variable = param.ObjectSelector(default='adj_close', objects=[\n", - " 'date', 'open', 'high', 'low', 'close', 'volume', 'adj_close'])\n", + " variable = param.ObjectSelector(default='adj_close', objects=variables)\n", "\n", " @param.depends('symbol', 'variable')\n", " def load_symbol(self):\n", @@ -107,7 +107,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The ``rolling_window`` parameter is not yet connected to anything however, so just like in the [Data Processing Pipelines section](./14-Data_Pipelines.ipynb) we will see how we can get the widget to control the parameters of an operation. Both the ``rolling`` and ``rolling_outlier_std`` operations accept a ``rolling_window`` parameter, so we create a ``Params`` stream to listen to that parameter and then pass it to the operations. Finally we compose everything into a panel ``Row``:" + "The ``rolling_window`` parameter is not yet connected to anything however, so just like in the [Data Processing Pipelines section](./14-Data_Pipelines.ipynb) we will see how we can get the widget to control the parameters of an operation. Both the ``rolling`` and ``rolling_outlier_std`` operations accept a ``rolling_window`` parameter, so we simply pass that parameter into the operation. Finally we compose everything into a panel ``Row``:" ] }, { @@ -117,15 +117,45 @@ "outputs": [], "source": [ "# Apply rolling mean\n", - "window = Params(explorer, ['rolling_window'])\n", - "smoothed = rolling(stock_dmap, streams=[window])\n", + "smoothed = rolling(stock_dmap, rolling_window=explorer.param.rolling_window)\n", "\n", "# Find outliers\n", - "outliers = rolling_outlier_std(stock_dmap, streams=[window]).opts(\n", - " hv.opts.Scatter(color='red', marker='triangle')\n", - ")\n", + "outliers = rolling_outlier_std(stock_dmap, rolling_window=explorer.param.rolling_window).opts(\n", + " color='red', marker='triangle')\n", + "\n", + "pn.Row(explorer.param, (smoothed * outliers).opts(width=600, padding=0.1))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## A function based approach\n", + "\n", + "Instead of defining a whole Parameterized class we can also use the ``depends`` decorator to directly link the widgets to a DynamicMap callback function. This approach makes the link between the widgets and the computation very explicit at the cost of tying the widget and display code very closely together.\n", + "\n", + "Instead of declaring the dependencies as strings we map the parameter instance to a particular keyword argument in the ``depends`` call. In this way we can link the symbol to the ``RadioButtonGroup`` value and the ``variable`` to the ``Select`` widget value:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "symbol = pn.widgets.RadioButtonGroup(options=stock_symbols)\n", + "variable = pn.widgets.Select(options=variables)\n", + "window = pn.widgets.IntSlider(name='Rolling Window', value=10, start=1, end=365)\n", + "\n", + "@pn.depends(symbol=symbol.param.value, variable=variable.param.value)\n", + "def load_symbol_cb(symbol, variable):\n", + " return load_symbol(symbol, variable)\n", + "\n", + "dmap = hv.DynamicMap(load_symbol_cb)\n", + "\n", + "smoothed = rolling(stock_dmap, rolling_window=window.param.value)\n", "\n", - "pn.Row(explorer.param, (smoothed * outliers).opts(width=600))" + "pn.Row(pn.WidgetBox('## Stock Explorer', symbol, variable, window), smoothed.opts(width=500))" ] }, { @@ -154,13 +184,12 @@ " stocks = hv.DynamicMap(self.load_symbol)\n", "\n", " # Apply rolling mean\n", - " window = Params(self, ['rolling_window'])\n", - " smoothed = rolling(stocks, streams=[window])\n", + " smoothed = rolling(stocks, rolling_window=self.param.rolling_window)\n", " if self.datashade:\n", - " smoothed = dynspread(datashade(smoothed)).opts(framewise=True)\n", + " smoothed = dynspread(datashade(smoothed, aggregator='any')).opts(framewise=True)\n", "\n", " # Find outliers\n", - " outliers = rolling_outlier_std(stocks, streams=[window]).opts(\n", + " outliers = rolling_outlier_std(stocks, rolling_window=self.param.rolling_window).opts(\n", " width=600, color='red', marker='triangle', framewise=True)\n", " return (smoothed * outliers)" ] From edcc5ba490d72294302aa9bb058e202c4b078e6c Mon Sep 17 00:00:00 2001 From: Jean-Luc Stevens Date: Fri, 2 Aug 2019 14:23:37 -0500 Subject: [PATCH 06/11] Update examples/user_guide/14-Data_Pipelines.ipynb Co-Authored-By: Philipp Rudiger --- examples/user_guide/14-Data_Pipelines.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/user_guide/14-Data_Pipelines.ipynb b/examples/user_guide/14-Data_Pipelines.ipynb index f128ecc748..b7c1d67872 100644 --- a/examples/user_guide/14-Data_Pipelines.ipynb +++ b/examples/user_guide/14-Data_Pipelines.ipynb @@ -112,7 +112,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The ``.apply`` method allows us to automatically build a pipeline given an object and some operation or function along with parameter instances passed in as keyword argument. Internally it will then build a Stream to ensure that whenever the parameter changes the plot is updated. To learn more about streams see the [Responding to Events](./12-Responding_to_Events.ipynb).\n", + "The ``.apply`` method allows us to automatically build a pipeline given an object and some operation or function along with parameter instances passed in as keyword arguments. Internally it will then build a Stream to ensure that whenever the parameter changes the plot is updated. To learn more about streams see the [Responding to Events](./12-Responding_to_Events.ipynb).\n", "\n", "This mechanism allows us to build powerful pipelines by linking parameters on a user defined class or even an external widget, e.g. here we import an ``IntSlider`` widget from [``panel``](https://pyviz.panel.org):" ] From 5282a6056f37dc08cd2a85eccd451c9b7604c93c Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 2 Aug 2019 21:17:27 +0100 Subject: [PATCH 07/11] Addressed review comments in Pipelines guide --- examples/user_guide/14-Data_Pipelines.ipynb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/examples/user_guide/14-Data_Pipelines.ipynb b/examples/user_guide/14-Data_Pipelines.ipynb index b7c1d67872..28f83d8396 100644 --- a/examples/user_guide/14-Data_Pipelines.ipynb +++ b/examples/user_guide/14-Data_Pipelines.ipynb @@ -132,7 +132,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Using the ``.apply`` method we can now apply the ``rolling`` operation to the DynamicMap and set the ``value`` parameter of the slider to supply the rolling_window value. Note that as long as the applied function is a HoloViews ``Operation`` we could also apply it directly but for pure functions we have to use ``.apply``." + "Using the ``.apply`` method we can now apply the ``rolling`` operation to the DynamicMap and link the ``value`` parameter of the slider to the operations ``rolling_window`` parameter. Using ``.apply`` we can even use simple functions, when using an operation we can also apply the operation directly (as shown in the comment below):" ] }, { @@ -143,6 +143,8 @@ "source": [ "rolled_dmap = dmap.apply(rolling, rolling_window=slider.param.value)\n", "\n", + "# rolled_dmap = rolling(dmap, rolling_window=slider.param.value)\n", + "\n", "rolled_dmap" ] }, @@ -150,7 +152,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Since the widget's value is now linked to the plot via a ``Stream`` we can display the widget and watch the plot update:" + "The ``rolled_dmap`` is another DynamicMap which defines a simple two step pipeline, which calls the original callback when the ``symbol`` changes and reapplies the ``rolling`` operation when the slider value changes. Since the widget's value is now linked to the plot via a ``Stream`` we can display the widget and watch the plot update:" ] }, { From 1051f14451066b911e5115f19357718ca32c8ba2 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 2 Aug 2019 21:19:47 +0100 Subject: [PATCH 08/11] Small fix in Dashboard guide --- examples/user_guide/17-Dashboards.ipynb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/user_guide/17-Dashboards.ipynb b/examples/user_guide/17-Dashboards.ipynb index 1d00838236..f2fdcd7229 100644 --- a/examples/user_guide/17-Dashboards.ipynb +++ b/examples/user_guide/17-Dashboards.ipynb @@ -145,7 +145,7 @@ "source": [ "symbol = pn.widgets.RadioButtonGroup(options=stock_symbols)\n", "variable = pn.widgets.Select(options=variables)\n", - "window = pn.widgets.IntSlider(name='Rolling Window', value=10, start=1, end=365)\n", + "rolling_window = pn.widgets.IntSlider(name='Rolling Window', value=10, start=1, end=365)\n", "\n", "@pn.depends(symbol=symbol.param.value, variable=variable.param.value)\n", "def load_symbol_cb(symbol, variable):\n", @@ -153,7 +153,7 @@ "\n", "dmap = hv.DynamicMap(load_symbol_cb)\n", "\n", - "smoothed = rolling(stock_dmap, rolling_window=window.param.value)\n", + "smoothed = rolling(stock_dmap, rolling_window=rolling_window.param.value)\n", "\n", "pn.Row(pn.WidgetBox('## Stock Explorer', symbol, variable, window), smoothed.opts(width=500))" ] From fafae3f79de656d1fbcfeef92f76d8eadf98fa08 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sat, 3 Aug 2019 13:32:51 +0100 Subject: [PATCH 09/11] Expanded on apply with function --- examples/user_guide/14-Data_Pipelines.ipynb | 54 +++++++++++++++++++-- 1 file changed, 49 insertions(+), 5 deletions(-) diff --git a/examples/user_guide/14-Data_Pipelines.ipynb b/examples/user_guide/14-Data_Pipelines.ipynb index 28f83d8396..6e55f6a93c 100644 --- a/examples/user_guide/14-Data_Pipelines.ipynb +++ b/examples/user_guide/14-Data_Pipelines.ipynb @@ -112,7 +112,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The ``.apply`` method allows us to automatically build a pipeline given an object and some operation or function along with parameter instances passed in as keyword arguments. Internally it will then build a Stream to ensure that whenever the parameter changes the plot is updated. To learn more about streams see the [Responding to Events](./12-Responding_to_Events.ipynb).\n", + "The ``.apply`` method allows us to automatically build a pipeline given an object and some operation or function along with parameter instances passed in as keyword argument. Internally it will then build a Stream to ensure that whenever the parameter changes the plot is updated. To learn more about streams see the [Responding to Events](./12-Responding_to_Events.ipynb).\n", "\n", "This mechanism allows us to build powerful pipelines by linking parameters on a user defined class or even an external widget, e.g. here we import an ``IntSlider`` widget from [``panel``](https://pyviz.panel.org):" ] @@ -132,7 +132,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Using the ``.apply`` method we can now apply the ``rolling`` operation to the DynamicMap and link the ``value`` parameter of the slider to the operations ``rolling_window`` parameter. Using ``.apply`` we can even use simple functions, when using an operation we can also apply the operation directly (as shown in the comment below):" + "Using the ``.apply`` method we can now apply the ``rolling`` operation to the DynamicMap and link the ``value`` parameter of the slider to the operations ``rolling_window`` parameter (this also works for simple functions):" ] }, { @@ -143,8 +143,6 @@ "source": [ "rolled_dmap = dmap.apply(rolling, rolling_window=slider.param.value)\n", "\n", - "# rolled_dmap = rolling(dmap, rolling_window=slider.param.value)\n", - "\n", "rolled_dmap" ] }, @@ -188,7 +186,53 @@ "source": [ "We can chain operations like this indefinitely and attach parameters or explicit streams to each stage. By chaining we can watch our visualization update whenever we change a stream value anywhere in the pipeline and HoloViews will be smart about which parts of the pipeline are recomputed, which allows us to build complex visualizations very quickly.\n", "\n", - "In later guides we will see how we can combine HoloViews plots and Panel widgets into custom layouts allowing us to define complex dashboards. For more information on how to deploy bokeh apps from HoloViews and build dashboards see the [Deploying Bokeh Apps](./Deploying_Bokeh_Apps.ipynb) and [Dashboards](./17-Dashboards.ipynb) guides." + "The ``.apply`` method is also not limited to operations, we can just as easily apply a simple function to each object in the ``DynamicMap``. Here we define a function to compute the residual between the original ``dmap`` and the ``rolled_dmap``." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def residual_fn(overlay):\n", + " # Get first and second Element in overlay\n", + " el1, el2 = overlay.get(0), overlay.get(1)\n", + "\n", + " # Get x-values and y-values of curves\n", + " xvals = el1.dimension_values(0)\n", + " yvals = el1.dimension_values(1)\n", + " yvals2 = el2.dimension_values(1)\n", + "\n", + " # Return new Element with subtracted y-values\n", + " # and new label\n", + " return el1.clone((xvals, yvals-yvals2),\n", + " vdims='Residual')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If we overlay the two DynamicMaps we can then dynamically broadcast this function to each of the overlays, producing a new DynamicMap which responds to both the symbol selector widget and the slider:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "residual = (dmap * rolled_dmap).apply(residual_fn)\n", + "\n", + "residual" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In later guides we will see how we can combine HoloViews plots and Panel widgets into custom layouts allowing us to define complex dashboards. For more information on how to deploy bokeh apps from HoloViews and build dashboards see the [Deploying Bokeh Apps](./Deploying_Bokeh_Apps.ipynb) and [Dashboards](./17-Dashboards.ipynb) guides. To get a quick idea of what this might look like let's compose all the components we have no built:" ] } ], From 852b72ec2d152e8a31fb1c51fda59530463ae0bf Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sat, 3 Aug 2019 13:43:31 +0100 Subject: [PATCH 10/11] Apply suggestions from code review Co-Authored-By: James A. Bednar --- examples/user_guide/14-Data_Pipelines.ipynb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/user_guide/14-Data_Pipelines.ipynb b/examples/user_guide/14-Data_Pipelines.ipynb index 6e55f6a93c..8d4f455257 100644 --- a/examples/user_guide/14-Data_Pipelines.ipynb +++ b/examples/user_guide/14-Data_Pipelines.ipynb @@ -112,7 +112,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The ``.apply`` method allows us to automatically build a pipeline given an object and some operation or function along with parameter instances passed in as keyword argument. Internally it will then build a Stream to ensure that whenever the parameter changes the plot is updated. To learn more about streams see the [Responding to Events](./12-Responding_to_Events.ipynb).\n", + "The ``.apply`` method allows us to automatically build a pipeline given an object and some operation or function along with parameter instances passed in as keyword arguments. Internally it will then build a Stream to ensure that whenever the parameter changes the plot is updated. To learn more about streams see the [Responding to Events](./12-Responding_to_Events.ipynb).\n", "\n", "This mechanism allows us to build powerful pipelines by linking parameters on a user defined class or even an external widget, e.g. here we import an ``IntSlider`` widget from [``panel``](https://pyviz.panel.org):" ] @@ -132,7 +132,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Using the ``.apply`` method we can now apply the ``rolling`` operation to the DynamicMap and link the ``value`` parameter of the slider to the operations ``rolling_window`` parameter (this also works for simple functions):" + "Using the ``.apply`` method we can now apply the ``rolling`` operation to the DynamicMap and link the ``value`` parameter of the slider to the operation's ``rolling_window`` parameter (which also works for simple functions as will be shown below):" ] }, { @@ -150,7 +150,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The ``rolled_dmap`` is another DynamicMap which defines a simple two step pipeline, which calls the original callback when the ``symbol`` changes and reapplies the ``rolling`` operation when the slider value changes. Since the widget's value is now linked to the plot via a ``Stream`` we can display the widget and watch the plot update:" + "The ``rolled_dmap`` is another DynamicMap that defines a simple two-step pipeline, which calls the original callback when the ``symbol`` changes and reapplies the ``rolling`` operation when the slider value changes. Since the widget's value is now linked to the plot via a ``Stream`` we can display the widget and watch the plot update:" ] }, { From a5b464d7bae186bc0e21b832f89d1757d88d67c4 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sat, 3 Aug 2019 13:48:12 +0100 Subject: [PATCH 11/11] Update examples/user_guide/14-Data_Pipelines.ipynb Co-Authored-By: James A. Bednar --- examples/user_guide/14-Data_Pipelines.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/user_guide/14-Data_Pipelines.ipynb b/examples/user_guide/14-Data_Pipelines.ipynb index 8d4f455257..9f805b4e40 100644 --- a/examples/user_guide/14-Data_Pipelines.ipynb +++ b/examples/user_guide/14-Data_Pipelines.ipynb @@ -186,7 +186,7 @@ "source": [ "We can chain operations like this indefinitely and attach parameters or explicit streams to each stage. By chaining we can watch our visualization update whenever we change a stream value anywhere in the pipeline and HoloViews will be smart about which parts of the pipeline are recomputed, which allows us to build complex visualizations very quickly.\n", "\n", - "The ``.apply`` method is also not limited to operations, we can just as easily apply a simple function to each object in the ``DynamicMap``. Here we define a function to compute the residual between the original ``dmap`` and the ``rolled_dmap``." + "The ``.apply`` method is also not limited to operations. We can just as easily apply a simple Python function to each object in the ``DynamicMap``. Here we define a function to compute the residual between the original ``dmap`` and the ``rolled_dmap``." ] }, {