mirror of
https://github.com/jupyter/notebook.git
synced 2025-03-13 13:17:50 +08:00
Merge pull request #6050 from chronitis/interact-range-widgets
Range widgets
This commit is contained in:
commit
cf0ca9d4a5
8
IPython/html/static/style/ipython.min.css
vendored
8
IPython/html/static/style/ipython.min.css
vendored
@ -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;
|
||||
|
8
IPython/html/static/style/style.min.css
vendored
8
IPython/html/static/style/style.min.css
vendored
@ -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;
|
||||
|
@ -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;
|
||||
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);
|
||||
}
|
||||
else if(value < min){
|
||||
value = min;
|
||||
}
|
||||
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();
|
||||
},
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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')
|
||||
|
@ -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')
|
||||
|
Loading…
Reference in New Issue
Block a user