diff --git a/doc/WidgetLinking.ipynb b/doc/WidgetLinking.ipynb new file mode 100644 index 0000000..0be550c --- /dev/null +++ b/doc/WidgetLinking.ipynb @@ -0,0 +1,151 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "import param\n", + "import paramnb" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "# we'd add get_soft_range() to relevant parameters in param\n", + "class ObjectSelector2(param.ObjectSelector):\n", + " __slots__ = ['softbounds']\n", + " def __init__(self,default=None,objects=None,instantiate=False,\n", + " compute_default_fn=None,check_on_set=None,allow_None=None,softbounds=None,**params): \n", + " self.softbounds=softbounds if softbounds is not None else objects \n", + " super(ObjectSelector2,self).__init__(\n", + " default=default,objects=objects,instantiate=instantiate,\n", + " compute_default_fn=compute_default_fn,check_on_set=check_on_set,allow_None=allow_None,**params)\n", + " \n", + " def get_soft_range(self):\n", + " return param.named_objs(self.softbounds)\n", + "\n", + "param.ObjectSelector2 = ObjectSelector2" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class SomeManagement(param.Parameterized):\n", + " employee = param.ObjectSelector2(default='Person Z',objects=['Person X','Person Y','Person Z'])\n", + " project = param.ObjectSelector2(default='A',objects=['A','B','C','D','E','F','G','H'])\n", + " days = param.Integer(default=1,softbounds=(0,10),precedence=1)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "m = SomeManagement()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "capabilities = { \n", + " 'Person X' : ('A','B','C','D'),\n", + " 'Person Y' : ('A','B','E','F'),\n", + " 'Person Z' : ('G','H'),\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "speed = {\n", + " 'Person X' : 10,\n", + " 'Person Y' : 20,\n", + " 'Person Z' : 30,\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "def update_days(parameterized):\n", + " parameterized.params('days').softbounds = (0,speed[parameterized.employee])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "def update_projects(parameterized):\n", + " parameterized.params('project').softbounds = capabilities[parameterized.employee]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": false + }, + "outputs": [], + "source": [ + "paramnb.Widgets(m, watchers={'employee':(update_projects,update_days)})" + ] + } + ], + "metadata": { + "anaconda-cloud": {}, + "hide_input": false, + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.0" + } + }, + "nbformat": 4, + "nbformat_minor": 1 +} diff --git a/paramnb/__init__.py b/paramnb/__init__.py index b0842b1..38eb76c 100644 --- a/paramnb/__init__.py +++ b/paramnb/__init__.py @@ -128,6 +128,15 @@ class Widgets(param.ParameterizedFunction): If true, will continuously update the next_n and/or callback, if any, as a slider widget is dragged.""") + watchers = param.Dict(default={}, doc=""" + Registry of functions to call whenever a parameter's widget value + is changed. + + Functions will be passed the parameterized object. + + Dictionary of lists :( + """) + def __call__(self, parameterized, **params): self.p = param.ParamOverrides(self, params) if self.p.initializer: @@ -163,6 +172,7 @@ def __call__(self, parameterized, **params): if self.p.on_init: self.execute() + self._execute_watchers() def _update_trait(self, p_name, p_value, widget=None): @@ -232,10 +242,13 @@ def change_event(event): # Style widget to denote error state apply_error_style(w, error) - if not error and not self.p.button: - self.execute({p_name: new_values}) - else: - self._changed[p_name] = new_values + if not error: + self._execute_watchers({p_name: new_values}) + if not self.p.button: + self.execute({p_name: new_values}) + else: + self._changed[p_name] = new_values + if hasattr(p_obj, 'callbacks'): p_obj.callbacks[id(self.parameterized)] = functools.partial(self._update_trait, p_name) @@ -277,6 +290,35 @@ def widget(self, param_name): return self._widgets[param_name] + def _execute_watchers(self, changed=None): + param_values = dict(self.parameterized.get_param_values()) + del param_values['name'] + + if changed is None: + changed = param_values # assume all values have changed + else: + param_values.update(changed) + + for param in changed: + for fn in self.p.watchers.get(param,[]): + fn(self.parameterized) + + for param in param_values: + p_obj = self.parameterized.params(param) + + # TODO: as suggested by Philipp, should factor out + # converting parameter attributes to ipywidget attributes + # e.g. have a registry of functions ;) or more likely some + # kind of update method that's used during creation and + # here. + if hasattr(p_obj, 'get_soft_range'): + self._widgets[param].options = p_obj.get_soft_range() + elif hasattr(p_obj, 'get_soft_bounds'): + min,max=p_obj.get_soft_bounds() + self._widgets[param].min = min + self._widgets[param].max = max + + def execute(self, changed={}): run_next_cells(self.p.next_n) if self.p.callback is not None: