From ef8022932979d11b4ac31a67eb0868e0ed3ee7ad Mon Sep 17 00:00:00 2001 From: Sebastian Gutsche Date: Tue, 23 Jan 2018 22:29:17 +0100 Subject: [PATCH] Added FloatLogSlider a slider that uses a logarithmic scale. Closes #719 --- ipywidgets/widgets/__init__.py | 2 +- ipywidgets/widgets/widget_float.py | 79 ++++++++++++++- packages/controls/src/widget_float.ts | 138 +++++++++++++++++++++++++- 3 files changed, 215 insertions(+), 4 deletions(-) diff --git a/ipywidgets/widgets/__init__.py b/ipywidgets/widgets/__init__.py index 520b78222df..b2bc2065156 100644 --- a/ipywidgets/widgets/__init__.py +++ b/ipywidgets/widgets/__init__.py @@ -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 diff --git a/ipywidgets/widgets/widget_float.py b/ipywidgets/widgets/widget_float.py index 98d9ea8f43d..b216c29eaba 100644 --- a/ipywidgets/widgets/widget_float.py +++ b/ipywidgets/widgets/widget_float.py @@ -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): @@ -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 @@ -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) diff --git a/packages/controls/src/widget_float.ts b/packages/controls/src/widget_float.ts index 29f9c237f9d..ea309fd9c29 100644 --- a/packages/controls/src/widget_float.ts +++ b/packages/controls/src/widget_float.ts @@ -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() { @@ -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 {} @@ -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 { /**