Skip to content
This repository has been archived by the owner on Nov 28, 2019. It is now read-only.

Added widgets to represent param.DateRange. #61

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 82 additions & 16 deletions doc/AdditionalFeatures.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,7 @@
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": true
},
"metadata": {},
"outputs": [],
"source": [
"paramnb.Widgets(TooltipExample)"
Expand Down Expand Up @@ -75,9 +73,7 @@
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": true
},
"metadata": {},
"outputs": [],
"source": [
"paramnb.Widgets(Task)"
Expand All @@ -86,9 +82,7 @@
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": true
},
"metadata": {},
"outputs": [],
"source": [
"Task.employee.location.duration"
Expand Down Expand Up @@ -125,9 +119,7 @@
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": true
},
"metadata": {},
"outputs": [],
"source": [
"class HTMLExample(param.Parameterized):\n",
Expand Down Expand Up @@ -155,9 +147,7 @@
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": true
},
"metadata": {},
"outputs": [],
"source": [
"import holoviews as hv\n",
Expand Down Expand Up @@ -186,14 +176,90 @@
"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,
"metadata": {
"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": {
Expand Down
9 changes: 9 additions & 0 deletions paramnb/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
157 changes: 157 additions & 0 deletions paramnb/widgets.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import re
import datetime

import param
from param.parameterized import classlist
Expand Down Expand Up @@ -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):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the purpose of the leading _ here?

"""
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.VBox([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 = """
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down