Skip to content

Commit a794665

Browse files
committed
Added FloatLogSlider
a slider that uses a logarithmic scale. Closes #719
1 parent 7626dc8 commit a794665

File tree

4 files changed

+238
-2
lines changed

4 files changed

+238
-2
lines changed

ipywidgets/widgets/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from .widget_bool import Checkbox, ToggleButton, Valid
1212
from .widget_button import Button, ButtonStyle
1313
from .widget_box import Box, HBox, VBox
14-
from .widget_float import FloatText, BoundedFloatText, FloatSlider, FloatProgress, FloatRangeSlider
14+
from .widget_float import FloatText, BoundedFloatText, FloatSlider, FloatProgress, FloatRangeSlider, FloatLogSlider
1515
from .widget_image import Image
1616
from .widget_int import IntText, BoundedIntText, IntSlider, IntProgress, IntRangeSlider, Play, SliderStyle
1717
from .widget_color import ColorPicker

ipywidgets/widgets/widget_float.py

+75
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,39 @@ def _validate_max(self, proposal):
5858
self.value = max
5959
return max
6060

61+
class _BoundedLogFloat(_Float):
62+
max = CFloat(10.0, help="Max value for the exponent").tag(sync=True)
63+
min = CFloat(0.0, help="Min value for the exponent").tag(sync=True)
64+
base = CFloat(2.0, help="Base of value").tag(sync=True)
65+
66+
@validate('value')
67+
def _validate_value(self, proposal):
68+
"""Cap and floor value"""
69+
value = proposal['value']
70+
if self.base ** self.min > value or self.base ** self.max < value:
71+
value = min(max(value, self.base ** self.min), self.base ** self.max)
72+
return value
73+
74+
@validate('min')
75+
def _validate_min(self, proposal):
76+
"""Enforce base ** min <= value <= base ** max"""
77+
min = proposal['value']
78+
if min > self.max:
79+
raise TraitError('Setting min > max')
80+
if self.base ** min > self.value:
81+
self.value = self.base ** min
82+
return min
83+
84+
@validate('max')
85+
def _validate_max(self, proposal):
86+
"""Enforce min <= value <= max"""
87+
max = proposal['value']
88+
if max < self.min:
89+
raise TraitError('setting max < min')
90+
if self.base ** max < self.value:
91+
self.value = self.base ** max
92+
return max
93+
6194

6295
@register
6396
class FloatText(_Float):
@@ -141,6 +174,48 @@ class FloatSlider(_BoundedFloat):
141174
continuous_update = Bool(True, help="Update the value of the widget as the user is holding the slider.").tag(sync=True)
142175
disabled = Bool(False, help="Enable or disable user changes").tag(sync=True)
143176

177+
style = InstanceDict(SliderStyle).tag(sync=True, **widget_serialization)
178+
179+
180+
@register
181+
class FloatLogSlider(_BoundedLogFloat):
182+
""" Slider/trackbar of logarithmic floating values with the specified range.
183+
184+
Parameters
185+
----------
186+
value : float
187+
position of the slider
188+
base : float
189+
base of the logarithmic scale. Default is 2
190+
min : float
191+
minimal position of the slider in log scale, i.e., actual minimum is base ** min
192+
max : float
193+
maximal position of the slider in log scale, i.e., actual maximum is base ** max
194+
step : float
195+
step of the trackbar, denotes steps for the exponent, not the actual value
196+
description : str
197+
name of the slider
198+
orientation : {'horizontal', 'vertical'}
199+
default is 'horizontal', orientation of the slider
200+
readout : {True, False}
201+
default is True, display the current value of the slider next to it
202+
readout_format : str
203+
default is '.2f', specifier for the format function used to represent
204+
slider value for human consumption, modeled after Python 3's format
205+
specification mini-language (PEP 3101).
206+
"""
207+
_view_name = Unicode('FloatLogSliderView').tag(sync=True)
208+
_model_name = Unicode('FloatLogSliderModel').tag(sync=True)
209+
step = CFloat(0.1, help="Minimum step in the exponent to increment the value").tag(sync=True)
210+
orientation = CaselessStrEnum(values=['horizontal', 'vertical'],
211+
default_value='horizontal', help="Vertical or horizontal.").tag(sync=True)
212+
readout = Bool(True, help="Display the current value of the slider next to it.").tag(sync=True)
213+
readout_format = NumberFormat(
214+
'.2f', help="Format for the readout").tag(sync=True)
215+
continuous_update = Bool(True, help="Update the value of the widget as the user is holding the slider.").tag(sync=True)
216+
disabled = Bool(False, help="Enable or disable user changes").tag(sync=True)
217+
base = CFloat(2., help="Base for the logarithm").tag(sync=True)
218+
144219

145220
style = InstanceDict(SliderStyle).tag(sync=True, **widget_serialization)
146221

packages/controls/src/widget_float.ts

+137-1
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,14 @@ import {
1212
import * as _ from 'underscore';
1313

1414
import {
15-
IntSliderView, IntRangeSliderView, IntTextView
15+
IntSliderView, IntRangeSliderView, IntTextView, BaseIntSliderView
1616
} from './widget_int';
1717

1818
import {
1919
format
2020
} from 'd3-format';
2121

22+
2223
export
2324
class FloatModel extends CoreDescriptionModel {
2425
defaults() {
@@ -69,6 +70,36 @@ class FloatSliderModel extends BoundedFloatModel {
6970
readout_formatter: any;
7071
}
7172

73+
export
74+
class FloatLogSliderModel extends BoundedFloatModel {
75+
defaults() {
76+
return _.extend(super.defaults(), {
77+
_model_name: 'FloatLogSliderModel',
78+
_view_name: 'FloatLogSliderView',
79+
step: 1.0,
80+
orientation: 'horizontal',
81+
_range: false,
82+
readout: true,
83+
readout_format: '.2f',
84+
slider_color: null,
85+
continuous_update: true,
86+
disabled: false,
87+
base: 2.,
88+
});
89+
}
90+
initialize(attributes, options) {
91+
super.initialize(attributes, options);
92+
this.on('change:readout_format', this.update_readout_format, this);
93+
this.update_readout_format();
94+
}
95+
96+
update_readout_format() {
97+
this.readout_formatter = format(this.get('readout_format'));
98+
}
99+
100+
readout_formatter: any;
101+
}
102+
72103
export
73104
class FloatRangeSliderModel extends FloatSliderModel {}
74105

@@ -85,6 +116,111 @@ class FloatSliderView extends IntSliderView {
85116
_parse_value = parseFloat;
86117
}
87118

119+
120+
export
121+
class FloatLogSliderView extends BaseIntSliderView {
122+
123+
update(options?) {
124+
super.update(options);
125+
let min = this.model.get('min');
126+
let max = this.model.get('max');
127+
let value = this.model.get('value');
128+
let base = this.model.get('base');
129+
130+
let log_value = Math.log( value ) / Math.log( base );
131+
132+
if(log_value > max) {
133+
log_value = max;
134+
} else if(log_value < min) {
135+
log_value = min;
136+
}
137+
this.$slider.slider('option', 'value', log_value);
138+
this.readout.textContent = this.valueToString(value);
139+
if(this.model.get('value') !== value) {
140+
this.model.set('value', value, {updated_view: this});
141+
this.touch();
142+
}
143+
}
144+
145+
/**
146+
* Write value to a string
147+
*/
148+
valueToString(value: number): string {
149+
let format = this.model.readout_formatter;
150+
return format(value);
151+
}
152+
153+
/**
154+
* Parse value from a string
155+
*/
156+
stringToValue(text: string): number {
157+
return this._parse_value(text);
158+
}
159+
160+
/**
161+
* this handles the entry of text into the contentEditable label first, the
162+
* value is checked if it contains a parseable value then it is clamped
163+
* within the min-max range of the slider finally, the model is updated if
164+
* the value is to be changed
165+
*
166+
* if any of these conditions are not met, the text is reset
167+
*/
168+
handleTextChange() {
169+
let value = this.stringToValue(this.readout.textContent);
170+
let vmin = this.model.get('min');
171+
let vmax = this.model.get('max');
172+
173+
if (isNaN(value as number)) {
174+
this.readout.textContent = this.valueToString(this.model.get('value'));
175+
} else {
176+
value = Math.max(Math.min(value as number, vmax), vmin);
177+
178+
if (value !== this.model.get('value')) {
179+
this.readout.textContent = this.valueToString(value);
180+
this.model.set('value', value, {updated_view: this});
181+
this.touch();
182+
} else {
183+
this.readout.textContent = this.valueToString(this.model.get('value'));
184+
}
185+
}
186+
}
187+
/**
188+
* Called when the slider value is changing.
189+
*/
190+
handleSliderChange(e, ui) {
191+
let base = this.model.get('base');
192+
let actual_value = Math.pow(base,this._validate_slide_value(ui.value));
193+
this.readout.textContent = this.valueToString(actual_value);
194+
195+
// Only persist the value while sliding if the continuous_update
196+
// trait is set to true.
197+
if (this.model.get('continuous_update')) {
198+
this.handleSliderChanged(e, ui);
199+
}
200+
}
201+
202+
/**
203+
* Called when the slider value has changed.
204+
*
205+
* Calling model.set will trigger all of the other views of the
206+
* model to update.
207+
*/
208+
handleSliderChanged(e, ui) {
209+
let base = this.model.get('base');
210+
let actual_value = Math.pow(base,this._validate_slide_value(ui.value));
211+
this.model.set('value', actual_value, {updated_view: this});
212+
this.touch();
213+
}
214+
215+
_validate_slide_value(x) {
216+
return x;
217+
}
218+
219+
_parse_value = parseFloat;
220+
221+
}
222+
223+
88224
export
89225
class FloatRangeSliderView extends IntRangeSliderView {
90226
/**

packages/schema/jupyterwidgetmodels.latest.md

+25
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,31 @@ Attribute | Type | Default | Help
296296
`layout` | reference to Layout widget | reference to new instance |
297297
`style` | reference to DescriptionStyle widget | reference to new instance | Styling customizations
298298

299+
### FloatLogSliderModel (@jupyter-widgets/controls, 1.1.0); FloatLogSliderView (@jupyter-widgets/controls, 1.1.0)
300+
301+
Attribute | Type | Default | Help
302+
-----------------|------------------|------------------|----
303+
`_dom_classes` | array | `[]` | CSS classes applied to widget DOM element
304+
`_model_module` | string | `'@jupyter-widgets/controls'` |
305+
`_model_module_version` | string | `'1.1.0'` |
306+
`_model_name` | string | `'FloatLogSliderModel'` |
307+
`_view_module` | string | `'@jupyter-widgets/controls'` |
308+
`_view_module_version` | string | `'1.1.0'` |
309+
`_view_name` | string | `'FloatLogSliderView'` |
310+
`base` | number (float) | `2.0` | Base for the logarithm
311+
`continuous_update` | boolean | `true` | Update the value of the widget as the user is holding the slider.
312+
`description` | string | `''` | Description of the control.
313+
`disabled` | boolean | `false` | Enable or disable user changes
314+
`layout` | reference to Layout widget | reference to new instance |
315+
`max` | number (float) | `10.0` | Max value for the exponent
316+
`min` | number (float) | `0.0` | Min value for the exponent
317+
`orientation` | string (one of `'horizontal'`, `'vertical'`) | `'horizontal'` | Vertical or horizontal.
318+
`readout` | boolean | `true` | Display the current value of the slider next to it.
319+
`readout_format` | string | `'.2f'` | Format for the readout
320+
`step` | number (float) | `0.1` | Minimum step in the exponent to increment the value
321+
`style` | reference to SliderStyle widget | reference to new instance |
322+
`value` | number (float) | `0.0` | Float value
323+
299324
### FloatProgressModel (@jupyter-widgets/controls, 1.1.0); ProgressView (@jupyter-widgets/controls, 1.1.0)
300325

301326
Attribute | Type | Default | Help

0 commit comments

Comments
 (0)