diff --git a/IPython/html/static/style/ipython.min.css b/IPython/html/static/style/ipython.min.css
index 367bf10..f5a42c9 100644
--- a/IPython/html/static/style/ipython.min.css
+++ b/IPython/html/static/style/ipython.min.css
@@ -1365,6 +1365,10 @@ div.cell.text_cell.rendered {
height: 28px !important;
margin-top: -8px !important;
}
+.widget-hslider .ui-slider .ui-slider-range {
+ height: 12px !important;
+ margin-top: -4px !important;
+}
.widget-vslider {
/* Vertical jQuery Slider */
/* Fix the padding of the slide track so the ui-slider is sized
@@ -1442,6 +1446,10 @@ div.cell.text_cell.rendered {
height: 14px !important;
margin-left: -9px;
}
+.widget-vslider .ui-slider .ui-slider-range {
+ width: 12px !important;
+ margin-left: -1px !important;
+}
.widget-text {
/* String Textbox - used for TextBoxView and TextAreaView */
width: 350px;
diff --git a/IPython/html/static/style/style.min.css b/IPython/html/static/style/style.min.css
index 2401e38..77a2e26 100644
--- a/IPython/html/static/style/style.min.css
+++ b/IPython/html/static/style/style.min.css
@@ -9137,6 +9137,10 @@ div.cell.text_cell.rendered {
height: 28px !important;
margin-top: -8px !important;
}
+.widget-hslider .ui-slider .ui-slider-range {
+ height: 12px !important;
+ margin-top: -4px !important;
+}
.widget-vslider {
/* Vertical jQuery Slider */
/* Fix the padding of the slide track so the ui-slider is sized
@@ -9214,6 +9218,10 @@ div.cell.text_cell.rendered {
height: 14px !important;
margin-left: -9px;
}
+.widget-vslider .ui-slider .ui-slider-range {
+ width: 12px !important;
+ margin-left: -1px !important;
+}
.widget-text {
/* String Textbox - used for TextBoxView and TextAreaView */
width: 350px;
diff --git a/IPython/html/static/widgets/js/widget_int.js b/IPython/html/static/widgets/js/widget_int.js
index b2db7a2..c8a736b 100644
--- a/IPython/html/static/widgets/js/widget_int.js
+++ b/IPython/html/static/widgets/js/widget_int.js
@@ -52,6 +52,10 @@ define([
that.$slider.slider("option", key, model_value);
}
});
+ var range_value = this.model.get("_range");
+ if (range_value !== undefined) {
+ this.$slider.slider("option", "range", range_value);
+ }
// WORKAROUND FOR JQUERY SLIDER BUG.
// The horizontal position of the slider handle
@@ -64,17 +68,28 @@ define([
var orientation = this.model.get('orientation');
var min = this.model.get('min');
var max = this.model.get('max');
- this.$slider.slider('option', 'value', min);
+ if (this.model.get('_range')) {
+ this.$slider.slider('option', 'values', [min, min]);
+ } else {
+ this.$slider.slider('option', 'value', min);
+ }
this.$slider.slider('option', 'orientation', orientation);
var value = this.model.get('value');
- if(value > max) {
- value = max;
- }
- else if(value < min){
- value = min;
+ if (this.model.get('_range')) {
+ // values for the range case are validated python-side in
+ // _Bounded{Int,Float}RangeWidget._validate
+ this.$slider.slider('option', 'values', value);
+ this.$readout.text(value.join("-"));
+ } else {
+ if(value > max) {
+ value = max;
+ }
+ else if(value < min){
+ value = min;
+ }
+ this.$slider.slider('option', 'value', value);
+ this.$readout.text(value);
}
- this.$slider.slider('option', 'value', value);
- this.$readout.text(value);
if(this.model.get('value')!=value) {
this.model.set('value', value, {updated_view: this});
@@ -140,9 +155,14 @@ define([
// Calling model.set will trigger all of the other views of the
// model to update.
- var actual_value = this._validate_slide_value(ui.value);
+ if (this.model.get("_range")) {
+ var actual_value = ui.values.map(this._validate_slide_value);
+ this.$readout.text(actual_value.join("-"));
+ } else {
+ var actual_value = this._validate_slide_value(ui.value);
+ this.$readout.text(actual_value);
+ }
this.model.set('value', actual_value, {updated_view: this});
- this.$readout.text(actual_value);
this.touch();
},
diff --git a/IPython/html/static/widgets/less/widgets.less b/IPython/html/static/widgets/less/widgets.less
index 12f8cf9..c26aa27 100644
--- a/IPython/html/static/widgets/less/widgets.less
+++ b/IPython/html/static/widgets/less/widgets.less
@@ -122,6 +122,11 @@
height : 28px !important;
margin-top : -8px !important;
}
+
+ .ui-slider-range {
+ height : 12px !important;
+ margin-top : -4px !important;
+ }
}
}
@@ -160,6 +165,11 @@
height : 14px !important;
margin-left : -9px;
}
+
+ .ui-slider-range {
+ width : 12px !important;
+ margin-left : -1px !important;
+ }
}
}
diff --git a/IPython/html/widgets/__init__.py b/IPython/html/widgets/__init__.py
index c16c7d7..d3cdb11 100644
--- a/IPython/html/widgets/__init__.py
+++ b/IPython/html/widgets/__init__.py
@@ -3,9 +3,9 @@ from .widget import Widget, DOMWidget, CallbackDispatcher
from .widget_bool import Checkbox, ToggleButton
from .widget_button import Button
from .widget_box import Box, Popup, FlexBox, HBox, VBox
-from .widget_float import FloatText, BoundedFloatText, FloatSlider, FloatProgress
+from .widget_float import FloatText, BoundedFloatText, FloatSlider, FloatProgress, FloatRangeSlider
from .widget_image import Image
-from .widget_int import IntText, BoundedIntText, IntSlider, IntProgress
+from .widget_int import IntText, BoundedIntText, IntSlider, IntProgress, IntRangeSlider
from .widget_selection import RadioButtons, ToggleButtons, Dropdown, Select
from .widget_selectioncontainer import Tab, Accordion
from .widget_string import HTML, Latex, Text, Textarea
diff --git a/IPython/html/widgets/tests/test_interaction.py b/IPython/html/widgets/tests/test_interaction.py
index 0ef46d1..7228a58 100644
--- a/IPython/html/widgets/tests/test_interaction.py
+++ b/IPython/html/widgets/tests/test_interaction.py
@@ -480,3 +480,120 @@ def test_custom_description():
value='text',
description='foo',
)
+
+def test_int_range_logic():
+ irsw = widgets.IntRangeSlider
+ w = irsw(value=(2, 4), min=0, max=6)
+ check_widget(w, cls=irsw, value=(2, 4), min=0, max=6)
+ w.value = (4, 2)
+ check_widget(w, cls=irsw, value=(2, 4), min=0, max=6)
+ w.value = (-1, 7)
+ check_widget(w, cls=irsw, value=(0, 6), min=0, max=6)
+ w.min = 3
+ check_widget(w, cls=irsw, value=(3, 6), min=3, max=6)
+ w.max = 3
+ check_widget(w, cls=irsw, value=(3, 3), min=3, max=3)
+
+ w.min = 0
+ w.max = 6
+ w.lower = 2
+ w.upper = 4
+ check_widget(w, cls=irsw, value=(2, 4), min=0, max=6)
+ w.value = (0, 1) #lower non-overlapping range
+ check_widget(w, cls=irsw, value=(0, 1), min=0, max=6)
+ w.value = (5, 6) #upper non-overlapping range
+ check_widget(w, cls=irsw, value=(5, 6), min=0, max=6)
+ w.value = (-1, 4) #semi out-of-range
+ check_widget(w, cls=irsw, value=(0, 4), min=0, max=6)
+ w.lower = 2
+ check_widget(w, cls=irsw, value=(2, 4), min=0, max=6)
+ w.value = (-2, -1) #wholly out of range
+ check_widget(w, cls=irsw, value=(0, 0), min=0, max=6)
+ w.value = (7, 8)
+ check_widget(w, cls=irsw, value=(6, 6), min=0, max=6)
+
+ with nt.assert_raises(ValueError):
+ w.min = 7
+ with nt.assert_raises(ValueError):
+ w.max = -1
+ with nt.assert_raises(ValueError):
+ w.lower = 5
+ with nt.assert_raises(ValueError):
+ w.upper = 1
+
+ w = irsw(min=2, max=3)
+ check_widget(w, min=2, max=3)
+ w = irsw(min=100, max=200)
+ check_widget(w, lower=125, upper=175, value=(125, 175))
+
+ with nt.assert_raises(ValueError):
+ irsw(value=(2, 4), lower=3)
+ with nt.assert_raises(ValueError):
+ irsw(value=(2, 4), upper=3)
+ with nt.assert_raises(ValueError):
+ irsw(value=(2, 4), lower=3, upper=3)
+ with nt.assert_raises(ValueError):
+ irsw(min=2, max=1)
+ with nt.assert_raises(ValueError):
+ irsw(lower=5)
+ with nt.assert_raises(ValueError):
+ irsw(upper=5)
+
+
+def test_float_range_logic():
+ frsw = widgets.FloatRangeSlider
+ w = frsw(value=(.2, .4), min=0., max=.6)
+ check_widget(w, cls=frsw, value=(.2, .4), min=0., max=.6)
+ w.value = (.4, .2)
+ check_widget(w, cls=frsw, value=(.2, .4), min=0., max=.6)
+ w.value = (-.1, .7)
+ check_widget(w, cls=frsw, value=(0., .6), min=0., max=.6)
+ w.min = .3
+ check_widget(w, cls=frsw, value=(.3, .6), min=.3, max=.6)
+ w.max = .3
+ check_widget(w, cls=frsw, value=(.3, .3), min=.3, max=.3)
+
+ w.min = 0.
+ w.max = .6
+ w.lower = .2
+ w.upper = .4
+ check_widget(w, cls=frsw, value=(.2, .4), min=0., max=.6)
+ w.value = (0., .1) #lower non-overlapping range
+ check_widget(w, cls=frsw, value=(0., .1), min=0., max=.6)
+ w.value = (.5, .6) #upper non-overlapping range
+ check_widget(w, cls=frsw, value=(.5, .6), min=0., max=.6)
+ w.value = (-.1, .4) #semi out-of-range
+ check_widget(w, cls=frsw, value=(0., .4), min=0., max=.6)
+ w.lower = .2
+ check_widget(w, cls=frsw, value=(.2, .4), min=0., max=.6)
+ w.value = (-.2, -.1) #wholly out of range
+ check_widget(w, cls=frsw, value=(0., 0.), min=0., max=.6)
+ w.value = (.7, .8)
+ check_widget(w, cls=frsw, value=(.6, .6), min=.0, max=.6)
+
+ with nt.assert_raises(ValueError):
+ w.min = .7
+ with nt.assert_raises(ValueError):
+ w.max = -.1
+ with nt.assert_raises(ValueError):
+ w.lower = .5
+ with nt.assert_raises(ValueError):
+ w.upper = .1
+
+ w = frsw(min=2, max=3)
+ check_widget(w, min=2, max=3)
+ w = frsw(min=1., max=2.)
+ check_widget(w, lower=1.25, upper=1.75, value=(1.25, 1.75))
+
+ with nt.assert_raises(ValueError):
+ frsw(value=(2, 4), lower=3)
+ with nt.assert_raises(ValueError):
+ frsw(value=(2, 4), upper=3)
+ with nt.assert_raises(ValueError):
+ frsw(value=(2, 4), lower=3, upper=3)
+ with nt.assert_raises(ValueError):
+ frsw(min=.2, max=.1)
+ with nt.assert_raises(ValueError):
+ frsw(lower=5)
+ with nt.assert_raises(ValueError):
+ frsw(upper=5)
diff --git a/IPython/html/widgets/widget_float.py b/IPython/html/widgets/widget_float.py
index 0c27456..dc08afd 100644
--- a/IPython/html/widgets/widget_float.py
+++ b/IPython/html/widgets/widget_float.py
@@ -14,7 +14,7 @@ Represents an unbounded float using a widget.
# Imports
#-----------------------------------------------------------------------------
from .widget import DOMWidget
-from IPython.utils.traitlets import Unicode, CFloat, Bool, Enum
+from IPython.utils.traitlets import Unicode, CFloat, Bool, Enum, Tuple
from IPython.utils.warn import DeprecatedClass
#-----------------------------------------------------------------------------
@@ -55,12 +55,113 @@ class FloatSlider(_BoundedFloat):
_view_name = Unicode('FloatSliderView', sync=True)
orientation = Enum([u'horizontal', u'vertical'], u'horizontal',
help="Vertical or horizontal.", sync=True)
+ _range = Bool(False, help="Display a range selector", sync=True)
readout = Bool(True, help="Display the current value of the slider next to it.", sync=True)
class FloatProgress(_BoundedFloat):
_view_name = Unicode('ProgressView', sync=True)
+class _FloatRange(_Float):
+ value = Tuple(CFloat, CFloat, default_value=(0.0, 1.0), help="Tuple of (lower, upper) bounds", sync=True)
+ lower = CFloat(0.0, help="Lower bound", sync=False)
+ upper = CFloat(1.0, help="Upper bound", sync=False)
+
+ def __init__(self, *pargs, **kwargs):
+ value_given = 'value' in kwargs
+ lower_given = 'lower' in kwargs
+ upper_given = 'upper' in kwargs
+ if value_given and (lower_given or upper_given):
+ raise ValueError("Cannot specify both 'value' and 'lower'/'upper' for range widget")
+ if lower_given != upper_given:
+ raise ValueError("Must specify both 'lower' and 'upper' for range widget")
+
+ DOMWidget.__init__(self, *pargs, **kwargs)
+
+ # ensure the traits match, preferring whichever (if any) was given in kwargs
+ if value_given:
+ self.lower, self.upper = self.value
+ else:
+ self.value = (self.lower, self.upper)
+
+ self.on_trait_change(self._validate, ['value', 'upper', 'lower'])
+
+ def _validate(self, name, old, new):
+ if name == 'value':
+ self.lower, self.upper = min(new), max(new)
+ elif name == 'lower':
+ self.value = (new, self.value[1])
+ elif name == 'upper':
+ self.value = (self.value[0], new)
+
+class _BoundedFloatRange(_FloatRange):
+ step = CFloat(1.0, help="Minimum step that the value can take (ignored by some views)", sync=True)
+ max = CFloat(100.0, help="Max value", sync=True)
+ min = CFloat(0.0, help="Min value", sync=True)
+
+ def __init__(self, *pargs, **kwargs):
+ any_value_given = 'value' in kwargs or 'upper' in kwargs or 'lower' in kwargs
+ _FloatRange.__init__(self, *pargs, **kwargs)
+
+ # ensure a minimal amount of sanity
+ if self.min > self.max:
+ raise ValueError("min must be <= max")
+
+ if any_value_given:
+ # if a value was given, clamp it within (min, max)
+ self._validate("value", None, self.value)
+ else:
+ # otherwise, set it to 25-75% to avoid the handles overlapping
+ self.value = (0.75*self.min + 0.25*self.max,
+ 0.25*self.min + 0.75*self.max)
+ # callback already set for 'value', 'lower', 'upper'
+ self.on_trait_change(self._validate, ['min', 'max'])
+
+
+ def _validate(self, name, old, new):
+ if name == "min":
+ if new > self.max:
+ raise ValueError("setting min > max")
+ self.min = new
+ elif name == "max":
+ if new < self.min:
+ raise ValueError("setting max < min")
+ self.max = new
+
+ low, high = self.value
+ if name == "value":
+ low, high = min(new), max(new)
+ elif name == "upper":
+ if new < self.lower:
+ raise ValueError("setting upper < lower")
+ high = new
+ elif name == "lower":
+ if new > self.upper:
+ raise ValueError("setting lower > upper")
+ low = new
+
+ low = max(self.min, min(low, self.max))
+ high = min(self.max, max(high, self.min))
+
+ # determine the order in which we should update the
+ # lower, upper traits to avoid a temporary inverted overlap
+ lower_first = high < self.lower
+
+ self.value = (low, high)
+ if lower_first:
+ self.lower = low
+ self.upper = high
+ else:
+ self.upper = high
+ self.lower = low
+
+
+class FloatRangeSlider(_BoundedFloatRange):
+ _view_name = Unicode('FloatSliderView', sync=True)
+ orientation = Enum([u'horizontal', u'vertical'], u'horizontal',
+ help="Vertical or horizontal.", sync=True)
+ _range = Bool(True, help="Display a range selector", sync=True)
+ readout = Bool(True, help="Display the current value of the slider next to it.", sync=True)
# Remove in IPython 4.0
FloatTextWidget = DeprecatedClass(FloatText, 'FloatTextWidget')
diff --git a/IPython/html/widgets/widget_int.py b/IPython/html/widgets/widget_int.py
index 807b527..5777287 100644
--- a/IPython/html/widgets/widget_int.py
+++ b/IPython/html/widgets/widget_int.py
@@ -14,7 +14,7 @@ Represents an unbounded int using a widget.
# Imports
#-----------------------------------------------------------------------------
from .widget import DOMWidget
-from IPython.utils.traitlets import Unicode, CInt, Bool, Enum
+from IPython.utils.traitlets import Unicode, CInt, Bool, Enum, Tuple
from IPython.utils.warn import DeprecatedClass
#-----------------------------------------------------------------------------
@@ -60,6 +60,7 @@ class IntSlider(_BoundedInt):
_view_name = Unicode('IntSliderView', sync=True)
orientation = Enum([u'horizontal', u'vertical'], u'horizontal',
help="Vertical or horizontal.", sync=True)
+ _range = Bool(False, help="Display a range selector", sync=True)
readout = Bool(True, help="Display the current value of the slider next to it.", sync=True)
@@ -67,6 +68,104 @@ class IntProgress(_BoundedInt):
"""Progress bar that represents a int bounded by a minimum and maximum value."""
_view_name = Unicode('ProgressView', sync=True)
+class _IntRange(_Int):
+ value = Tuple(CInt, CInt, default_value=(0, 1), help="Tuple of (lower, upper) bounds", sync=True)
+ lower = CInt(0, help="Lower bound", sync=False)
+ upper = CInt(1, help="Upper bound", sync=False)
+
+ def __init__(self, *pargs, **kwargs):
+ value_given = 'value' in kwargs
+ lower_given = 'lower' in kwargs
+ upper_given = 'upper' in kwargs
+ if value_given and (lower_given or upper_given):
+ raise ValueError("Cannot specify both 'value' and 'lower'/'upper' for range widget")
+ if lower_given != upper_given:
+ raise ValueError("Must specify both 'lower' and 'upper' for range widget")
+
+ DOMWidget.__init__(self, *pargs, **kwargs)
+
+ # ensure the traits match, preferring whichever (if any) was given in kwargs
+ if value_given:
+ self.lower, self.upper = self.value
+ else:
+ self.value = (self.lower, self.upper)
+
+ self.on_trait_change(self._validate, ['value', 'upper', 'lower'])
+
+ def _validate(self, name, old, new):
+ if name == 'value':
+ self.lower, self.upper = min(new), max(new)
+ elif name == 'lower':
+ self.value = (new, self.value[1])
+ elif name == 'upper':
+ self.value = (self.value[0], new)
+
+class _BoundedIntRange(_IntRange):
+ step = CInt(1, help="Minimum step that the value can take (ignored by some views)", sync=True)
+ max = CInt(100, help="Max value", sync=True)
+ min = CInt(0, help="Min value", sync=True)
+
+ def __init__(self, *pargs, **kwargs):
+ any_value_given = 'value' in kwargs or 'upper' in kwargs or 'lower' in kwargs
+ _IntRange.__init__(self, *pargs, **kwargs)
+
+ # ensure a minimal amount of sanity
+ if self.min > self.max:
+ raise ValueError("min must be <= max")
+
+ if any_value_given:
+ # if a value was given, clamp it within (min, max)
+ self._validate("value", None, self.value)
+ else:
+ # otherwise, set it to 25-75% to avoid the handles overlapping
+ self.value = (0.75*self.min + 0.25*self.max,
+ 0.25*self.min + 0.75*self.max)
+ # callback already set for 'value', 'lower', 'upper'
+ self.on_trait_change(self._validate, ['min', 'max'])
+
+ def _validate(self, name, old, new):
+ if name == "min":
+ if new > self.max:
+ raise ValueError("setting min > max")
+ self.min = new
+ elif name == "max":
+ if new < self.min:
+ raise ValueError("setting max < min")
+ self.max = new
+
+ low, high = self.value
+ if name == "value":
+ low, high = min(new), max(new)
+ elif name == "upper":
+ if new < self.lower:
+ raise ValueError("setting upper < lower")
+ high = new
+ elif name == "lower":
+ if new > self.upper:
+ raise ValueError("setting lower > upper")
+ low = new
+
+ low = max(self.min, min(low, self.max))
+ high = min(self.max, max(high, self.min))
+
+ # determine the order in which we should update the
+ # lower, upper traits to avoid a temporary inverted overlap
+ lower_first = high < self.lower
+
+ self.value = (low, high)
+ if lower_first:
+ self.lower = low
+ self.upper = high
+ else:
+ self.upper = high
+ self.lower = low
+
+class IntRangeSlider(_BoundedIntRange):
+ _view_name = Unicode('IntSliderView', sync=True)
+ orientation = Enum([u'horizontal', u'vertical'], u'horizontal',
+ help="Vertical or horizontal.", sync=True)
+ _range = Bool(True, help="Display a range selector", sync=True)
+ readout = Bool(True, help="Display the current value of the slider next to it.", sync=True)
# Remove in IPython 4.0
IntTextWidget = DeprecatedClass(IntText, 'IntTextWidget')