Skip to content

Commit

Permalink
Added FloatLogSlider
Browse files Browse the repository at this point in the history
a slider that uses a logarithmic scale. Closes #719
  • Loading branch information
sebasguts committed Jan 23, 2018
1 parent 7626dc8 commit ef80229
Show file tree
Hide file tree
Showing 3 changed files with 215 additions and 4 deletions.
2 changes: 1 addition & 1 deletion ipywidgets/widgets/__init__.py
Expand Up @@ -11,7 +11,7 @@
from .widget_bool import Checkbox, ToggleButton, Valid
from .widget_button import Button, ButtonStyle
from .widget_box import Box, HBox, VBox
from .widget_float import FloatText, BoundedFloatText, FloatSlider, FloatProgress, FloatRangeSlider
from .widget_float import FloatText, BoundedFloatText, FloatSlider, FloatProgress, FloatRangeSlider, FloatLogSlider
from .widget_image import Image
from .widget_int import IntText, BoundedIntText, IntSlider, IntProgress, IntRangeSlider, Play, SliderStyle
from .widget_color import ColorPicker
Expand Down
79 changes: 77 additions & 2 deletions ipywidgets/widgets/widget_float.py
Expand Up @@ -58,6 +58,39 @@ def _validate_max(self, proposal):
self.value = max
return max

class _BoundedLogFloat(_Float):
max = CFloat(10.0, help="Max value").tag(sync=True)
min = CFloat(0.0, help="Min value").tag(sync=True)
base = CFloat(2.0, help="Base of val").tag(sync=True)

@validate('value')
def _validate_value(self, proposal):
"""Cap and floor value"""
value = proposal['value']
if self.base ** self.min > value or self.base ** self.max < value:
value = min(max(value, self.base ** self.min), self.base ** self.max)
return value

@validate('min')
def _validate_min(self, proposal):
"""Enforce min <= value <= max"""
min = proposal['value']
if min > self.max:
raise TraitError('Setting min > max')
if min > self.value:
self.value = min
return min

@validate('max')
def _validate_max(self, proposal):
"""Enforce min <= value <= max"""
max = proposal['value']
if max < self.min:
raise TraitError('setting max < min')
if max < self.value:
self.value = max
return max


@register
class FloatText(_Float):
Expand Down Expand Up @@ -113,10 +146,12 @@ class FloatSlider(_BoundedFloat):
----------
value : float
position of the slider
base : float
base of the logarithmic scale. Default is 2
min : float
minimal position of the slider
minimal position of the slider in log scale, i.e., actual minimum is base ** min
max : float
maximal position of the slider
maximal position of the slider in log scale, i.e., actual maximum is base ** max
step : float
step of the trackbar
description : str
Expand All @@ -141,6 +176,46 @@ class FloatSlider(_BoundedFloat):
continuous_update = Bool(True, help="Update the value of the widget as the user is holding the slider.").tag(sync=True)
disabled = Bool(False, help="Enable or disable user changes").tag(sync=True)

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


@register
class FloatLogSlider(_BoundedLogFloat):
""" Slider/trackbar of logarithmic floating values with the specified range.
Parameters
----------
value : float
position of the slider
min : float
minimal position of the slider
max : float
maximal position of the slider
step : float
step of the trackbar
description : str
name of the slider
orientation : {'horizontal', 'vertical'}
default is 'horizontal', orientation of the slider
readout : {True, False}
default is True, display the current value of the slider next to it
readout_format : str
default is '.2f', specifier for the format function used to represent
slider value for human consumption, modeled after Python 3's format
specification mini-language (PEP 3101).
"""
_view_name = Unicode('FloatLogSliderView').tag(sync=True)
_model_name = Unicode('FloatLogSliderModel').tag(sync=True)
step = CFloat(0.1, help="Minimum step to increment the value").tag(sync=True)
orientation = CaselessStrEnum(values=['horizontal', 'vertical'],
default_value='horizontal', help="Vertical or horizontal.").tag(sync=True)
readout = Bool(True, help="Display the current value of the slider next to it.").tag(sync=True)
readout_format = NumberFormat(
'.2f', help="Format for the readout").tag(sync=True)
continuous_update = Bool(True, help="Update the value of the widget as the user is holding the slider.").tag(sync=True)
disabled = Bool(False, help="Enable or disable user changes").tag(sync=True)
base = CFloat(2., help="Base for the logarithm").tag(sync=True)


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

Expand Down
138 changes: 137 additions & 1 deletion packages/controls/src/widget_float.ts
Expand Up @@ -12,13 +12,14 @@ import {
import * as _ from 'underscore';

import {
IntSliderView, IntRangeSliderView, IntTextView
IntSliderView, IntRangeSliderView, IntTextView, BaseIntSliderView
} from './widget_int';

import {
format
} from 'd3-format';


export
class FloatModel extends CoreDescriptionModel {
defaults() {
Expand Down Expand Up @@ -69,6 +70,36 @@ class FloatSliderModel extends BoundedFloatModel {
readout_formatter: any;
}

export
class FloatLogSliderModel extends BoundedFloatModel {
defaults() {
return _.extend(super.defaults(), {
_model_name: 'FloatLogSliderModel',
_view_name: 'FloatLogSliderView',
step: 1.0,
orientation: 'horizontal',
_range: false,
readout: true,
readout_format: '.2f',
slider_color: null,
continuous_update: true,
disabled: false,
base: 2.,
});
}
initialize(attributes, options) {
super.initialize(attributes, options);
this.on('change:readout_format', this.update_readout_format, this);
this.update_readout_format();
}

update_readout_format() {
this.readout_formatter = format(this.get('readout_format'));
}

readout_formatter: any;
}

export
class FloatRangeSliderModel extends FloatSliderModel {}

Expand All @@ -85,6 +116,111 @@ class FloatSliderView extends IntSliderView {
_parse_value = parseFloat;
}


export
class FloatLogSliderView extends BaseIntSliderView {

update(options?) {
super.update(options);
let min = this.model.get('min');
let max = this.model.get('max');
let value = this.model.get('value');
let base = this.model.get('base');

let log_value = Math.log( value ) / Math.log( base );

if(log_value > max) {
log_value = max;
} else if(log_value < min) {
log_value = min;
}
this.$slider.slider('option', 'value', log_value);
this.readout.textContent = this.valueToString(value);
if(this.model.get('value') !== value) {
this.model.set('value', value, {updated_view: this});
this.touch();
}
}

/**
* Write value to a string
*/
valueToString(value: number): string {
let format = this.model.readout_formatter;
return format(value);
}

/**
* Parse value from a string
*/
stringToValue(text: string): number {
return this._parse_value(text);
}

/**
* this handles the entry of text into the contentEditable label first, the
* value is checked if it contains a parseable value then it is clamped
* within the min-max range of the slider finally, the model is updated if
* the value is to be changed
*
* if any of these conditions are not met, the text is reset
*/
handleTextChange() {
let value = this.stringToValue(this.readout.textContent);
let vmin = this.model.get('min');
let vmax = this.model.get('max');

if (isNaN(value as number)) {
this.readout.textContent = this.valueToString(this.model.get('value'));
} else {
value = Math.max(Math.min(value as number, vmax), vmin);

if (value !== this.model.get('value')) {
this.readout.textContent = this.valueToString(value);
this.model.set('value', value, {updated_view: this});
this.touch();
} else {
this.readout.textContent = this.valueToString(this.model.get('value'));
}
}
}
/**
* Called when the slider value is changing.
*/
handleSliderChange(e, ui) {
let base = this.model.get('base');
let actual_value = Math.pow(base,this._validate_slide_value(ui.value));
this.readout.textContent = this.valueToString(actual_value);

// Only persist the value while sliding if the continuous_update
// trait is set to true.
if (this.model.get('continuous_update')) {
this.handleSliderChanged(e, ui);
}
}

/**
* Called when the slider value has changed.
*
* Calling model.set will trigger all of the other views of the
* model to update.
*/
handleSliderChanged(e, ui) {
let base = this.model.get('base');
let actual_value = Math.pow(base,this._validate_slide_value(ui.value));
this.model.set('value', actual_value, {updated_view: this});
this.touch();
}

_validate_slide_value(x) {
return x;
}

_parse_value = parseFloat;

}


export
class FloatRangeSliderView extends IntRangeSliderView {
/**
Expand Down

0 comments on commit ef80229

Please sign in to comment.