Skip to content

Commit 73b8132

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

File tree

4 files changed

+241
-2
lines changed

4 files changed

+241
-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

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

61+
class _BoundedLogFloat(_Float):
62+
max = CFloat(4.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(10.0, help="Base of value").tag(sync=True)
65+
value = CFloat(1.0, help="Float value").tag(sync=True)
66+
67+
@validate('value')
68+
def _validate_value(self, proposal):
69+
"""Cap and floor value"""
70+
value = proposal['value']
71+
if self.base ** self.min > value or self.base ** self.max < value:
72+
value = min(max(value, self.base ** self.min), self.base ** self.max)
73+
return value
74+
75+
@validate('min')
76+
def _validate_min(self, proposal):
77+
"""Enforce base ** min <= value <= base ** max"""
78+
min = proposal['value']
79+
if min > self.max:
80+
raise TraitError('Setting min > max')
81+
if self.base ** min > self.value:
82+
self.value = self.base ** min
83+
return min
84+
85+
@validate('max')
86+
def _validate_max(self, proposal):
87+
"""Enforce base ** min <= value <= base ** max"""
88+
max = proposal['value']
89+
if max < self.min:
90+
raise TraitError('setting max < min')
91+
if self.base ** max < self.value:
92+
self.value = self.base ** max
93+
return max
94+
6195

6296
@register
6397
class FloatText(_Float):
@@ -141,6 +175,48 @@ class FloatSlider(_BoundedFloat):
141175
continuous_update = Bool(True, help="Update the value of the widget as the user is holding the slider.").tag(sync=True)
142176
disabled = Bool(False, help="Enable or disable user changes").tag(sync=True)
143177

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

145221
style = InstanceDict(SliderStyle).tag(sync=True, **widget_serialization)
146222

packages/controls/src/widget_float.ts

+139-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,37 @@ 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: 0.1,
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: 10.,
88+
value: 1.0,
89+
});
90+
}
91+
initialize(attributes, options) {
92+
super.initialize(attributes, options);
93+
this.on('change:readout_format', this.update_readout_format, this);
94+
this.update_readout_format();
95+
}
96+
97+
update_readout_format() {
98+
this.readout_formatter = format(this.get('readout_format'));
99+
}
100+
101+
readout_formatter: any;
102+
}
103+
72104
export
73105
class FloatRangeSliderModel extends FloatSliderModel {}
74106

@@ -85,6 +117,112 @@ class FloatSliderView extends IntSliderView {
85117
_parse_value = parseFloat;
86118
}
87119

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

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) | `10.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) | `4.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) | `1.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)