From 908a3d5f0b9bea9a149b502dadcd593d9554d03f Mon Sep 17 00:00:00 2001 From: "C. E. Ball" Date: Fri, 7 Jul 2017 02:19:34 +0100 Subject: [PATCH 1/2] Added widgets to represent param.DateRange. --- doc/AdditionalFeatures.ipynb | 98 ++++++++++++++++++---- paramnb/__init__.py | 9 ++ paramnb/widgets.py | 157 +++++++++++++++++++++++++++++++++++ 3 files changed, 248 insertions(+), 16 deletions(-) diff --git a/doc/AdditionalFeatures.ipynb b/doc/AdditionalFeatures.ipynb index 7483b96..9d27d68 100644 --- a/doc/AdditionalFeatures.ipynb +++ b/doc/AdditionalFeatures.ipynb @@ -36,9 +36,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [ "paramnb.Widgets(TooltipExample)" @@ -75,9 +73,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [ "paramnb.Widgets(Task)" @@ -86,9 +82,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [ "Task.employee.location.duration" @@ -125,9 +119,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [ "class HTMLExample(param.Parameterized):\n", @@ -155,9 +147,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [ "import holoviews as hv\n", @@ -186,6 +176,17 @@ "paramnb.Widgets(example, callback=example.update, on_init=True, view_position='right')" ] }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": true + }, + "source": [ + "# Date ranges\n", + "\n", + "param contains a DateRange parameter, consisting of start and end dates. However, it is often useful to allow users to specify dates in a variety of ways, so paramNB provides a DateRangeSelector widget with multiple possible date entry styles." + ] + }, { "cell_type": "code", "execution_count": null, @@ -193,7 +194,72 @@ "collapsed": true }, "outputs": [], - "source": [] + "source": [ + "import datetime as dt" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "class Test(param.Parameterized):\n", + " date_range = param.DateRange(default=(dt.datetime(2017, 1, 1,),dt.datetime(2017, 12, 31)),\n", + " bounds=(dt.datetime(2016, 1, 1),dt.datetime(2018, 12, 31)))\n", + "\n", + "test = Test(name=\"An example class\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## start, end date" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "paramnb.Widgets(test) # default, \"StartEndDate\"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## duration from start date" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "paramnb.Widgets(test,date_range_style='DurationFromStart')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## duration to end date" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "paramnb.Widgets(test,date_range_style='DurationToEnd')" + ] } ], "metadata": { diff --git a/paramnb/__init__.py b/paramnb/__init__.py index 2c5a6d7..67921ab 100644 --- a/paramnb/__init__.py +++ b/paramnb/__init__.py @@ -131,6 +131,12 @@ class Widgets(param.ParameterizedFunction): If true, will continuously update the next_n and/or callback, if any, as a slider widget is dragged.""") + # passed through to DateRangeSelector widget + date_range_style = param.ObjectSelector( + default='StartEnd', + objects=['StartEnd','DurationFromStart','DurationToEnd'],doc=""" + How to represent DateRange parameters; see widgets.DateRangeSelector.""") + def __call__(self, parameterized, **params): self.p = param.ParamOverrides(self, params) if self.p.initializer: @@ -209,6 +215,9 @@ def action_cb(button): getattr(self.parameterized, p_name)(self.parameterized) kw['value'] = action_cb + if isinstance(p_obj,param.DateRange): + kw['date_range_style'] = self.p.date_range_style + kw['name'] = p_name kw['continuous_update']=self.p.continuous_update diff --git a/paramnb/widgets.py b/paramnb/widgets.py index 05d52a4..bbc4be3 100644 --- a/paramnb/widgets.py +++ b/paramnb/widgets.py @@ -1,4 +1,5 @@ import re +import datetime import param from param.parameterized import classlist @@ -298,7 +299,154 @@ def _ipython_display_(self, **kwargs): def get_state(self, *args, **kw): # support layouts; see CrossSelect.get_state return self._composite.get_state(*args,**kw) + + +class _DateRange(ipywidgets.Widget): + """ + Abstract base class for composite widgets that use two widgets to + represent a (start, end) date range parameter. + """ + __abstract = True + + # TODO: could be tuple; need to decide about value of None + value = traitlets.Any() + + def __init__(self, *args, **kwargs): + self._mindate = kwargs.get('min') + self._maxdate = kwargs.get('max') + self.value = kwargs.pop('value') + assert self.value is not None # for now; see https://github.com/ioam/paramnb/issues/50 + #if self.value is None and self._mindate is not None and self._maxdate is not None: + # self.value = (self._mindate,self._maxdate) + self._w0 = self._create0(*args,**kwargs) + self._w1 = self._create1(*args,**kwargs) + self._composite = ipywidgets.HBox([self._w0,self._w1]) + super(_DateRange, self).__init__() + self.layout = self._composite.layout + self._w0.observe(self._set0,'value') + self._w1.observe(self._set1,'value') + + def _create0(self,*args,**kw): + raise NotImplementedError + + def _create1(self,*args,**kw): + raise NotImplementedError + + def _set0(self,e): + raise NotImplementedError + + def _set1(self,e): + raise NotImplementedError + + def _ipython_display_(self, **kwargs): + self._composite._ipython_display_(**kwargs) + + def get_state(self, *args, **kw): + return self._composite.get_state(*args,**kw) + + +class StartEnd(_DateRange): + """ + Represents a DateRange parameter with start and end date pickers. + """ + def _create0(self,*args,**kwargs): + kw = {'value':self.value[0],'description':'start'} + kw.update(kwargs) + return ipywidgets.DatePicker(*args,**kw) + + def _create1(self,*args,**kwargs): + kw = {'value':self.value[1],'description':'end'} + kw.update(kwargs) + return ipywidgets.DatePicker(*args,**kw) + + def _set0(self,e): + self.value = (e['new'],self.value[1]) + + def _set1(self,e): + self.value = (self.value[0],e['new']) + + +class DurationToEnd(_DateRange): + """ + Represents a DateRange parameter with end date picker plus + duration-to-end-date slider (or text widget if unbounded). + + Duration is in days. + """ + def _create0(self,*args,**kwargs): + kw = {'value':(self.value[1]-self.value[0]).days, + 'description':'duration (days)'} + kw.update(kwargs) + if kw.get('max') is not None: + kw['max'] = (self.value[1]-kw['min']).days + kw['min'] = 0 + return ipywidgets.IntSlider(*args,**kw) + else: + return TextWidget(*args,**kw) + + def _create1(self,*args,**kwargs): + kw = {'value':self.value[1],'description':'end'} + kw.update(kwargs) + return ipywidgets.DatePicker(*args,**kw) + + def _set0(self,e): + self.value = (self.value[1]-datetime.timedelta(int(e['new'])),self.value[1]) + + def _set1(self,e): + self.value = (self.value[0],e['new']) + if self._maxdate is not None: + self._w0.max = (self._maxdate-self.value[0]).days + + +class DurationFromStart(_DateRange): + """ + Represents a DateRange parameter with start date picker plus + duration-from-start-date slider (or text widget if unbounded). + + Duration is in days. + """ + def _create0(self,*args,**kwargs): + kw = {'value':self.value[0],'description':'start'} + kw.update(kwargs) + return ipywidgets.DatePicker(*args,**kw) + + def _create1(self,*args,**kwargs): + kw = {'value':(self.value[1]-self.value[0]).days, + 'description':'duration (days)'} + kw.update(kwargs) + if kw.get('max') is not None: + kw['max'] = (kw['max']-self.value[0]).days + kw['min'] = 0 + return ipywidgets.IntSlider(*args,**kw) + else: + return TextWidget(*args,**kw) + def _set0(self,e): + self.value = (e['new'],self.value[1]) + if self._maxdate is not None: + self._w1.max = (self._maxdate - self.value[0]).days + + def _set1(self,e): + self.value = (self.value[0],self.value[0]+datetime.timedelta(int(e['new']))) + + +class DateRangeSelector(param.ParameterizedFunction): + """ + Returns a _DateRange widget as specified by style parameter. + """ + style = param.ClassSelector(_DateRange,default=StartEnd,is_instance=False,doc=""" + Something.""") + + def __call__(self, *args, **kw): + """ + If date_range_style keyword argument is supplied, will be used to + set style and should be name of a _DateRange class. + """ + date_range_style = kw.pop('date_range_style') + if date_range_style is not None: + self.style = self.params('style').get_range()[date_range_style] + return self.style(*args, **kw) + HTMLVIEW_JS = """ @@ -349,6 +497,9 @@ def apply_error_style(w, error): ImageView: Image } +# TODO: param/widget registry should specify all ideal mappings then +# we should auto add whatever's available + # Handle new parameters introduced in param 1.5 try: from param import Color, Range @@ -368,6 +519,12 @@ def apply_error_style(w, error): except: pass +# Handle new parameters introduced in unreleased param +try: + from param import DateRange + ptype2wtype[DateRange] = DateRangeSelector +except: + pass def wtype(pobj): if pobj.constant: # Ensure constant parameters cannot be edited From fef78c23eba9850f2d7695d1c0ef22d75ea7f2d5 Mon Sep 17 00:00:00 2001 From: "C. E. Ball" Date: Sun, 9 Jul 2017 02:30:11 +0100 Subject: [PATCH 2/2] Changed date range widgets to vertical layout, matching other composite widgets. --- paramnb/widgets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/paramnb/widgets.py b/paramnb/widgets.py index bbc4be3..5f4f626 100644 --- a/paramnb/widgets.py +++ b/paramnb/widgets.py @@ -320,7 +320,7 @@ def __init__(self, *args, **kwargs): # self.value = (self._mindate,self._maxdate) self._w0 = self._create0(*args,**kwargs) self._w1 = self._create1(*args,**kwargs) - self._composite = ipywidgets.HBox([self._w0,self._w1]) + self._composite = ipywidgets.VBox([self._w0,self._w1]) super(_DateRange, self).__init__() self.layout = self._composite.layout self._w0.observe(self._set0,'value')