diff --git a/ipywidgets/widgets/__init__.py b/ipywidgets/widgets/__init__.py index 520b78222d..b2bc206515 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 98d9ea8f43..1a0430d50e 100644 --- a/ipywidgets/widgets/widget_float.py +++ b/ipywidgets/widgets/widget_float.py @@ -58,6 +58,40 @@ def _validate_max(self, proposal): self.value = max return max +class _BoundedLogFloat(_Float): + max = CFloat(4.0, help="Max value for the exponent").tag(sync=True) + min = CFloat(0.0, help="Min value for the exponent").tag(sync=True) + base = CFloat(10.0, help="Base of value").tag(sync=True) + value = CFloat(1.0, help="Float value").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 base ** min <= value <= base ** max""" + min = proposal['value'] + if min > self.max: + raise TraitError('Setting min > max') + if self.base ** min > self.value: + self.value = self.base ** min + return min + + @validate('max') + def _validate_max(self, proposal): + """Enforce base ** min <= value <= base ** max""" + max = proposal['value'] + if max < self.min: + raise TraitError('setting max < min') + if self.base ** max < self.value: + self.value = self.base ** max + return max + @register class FloatText(_Float): @@ -141,6 +175,48 @@ 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 + base : float + base of the logarithmic scale. Default is 10 + min : float + minimal position of the slider in log scale, i.e., actual minimum is base ** min + max : float + maximal position of the slider in log scale, i.e., actual maximum is base ** max + step : float + step of the trackbar, denotes steps for the exponent, not the actual value + 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 in the exponent 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(10., 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 29f9c237f9..dc80665515 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,39 @@ class FloatSliderModel extends BoundedFloatModel { readout_formatter: any; } +export +class FloatLogSliderModel extends BoundedFloatModel { + defaults() { + return _.extend(super.defaults(), { + _model_name: 'FloatLogSliderModel', + _view_name: 'FloatLogSliderView', + step: 0.1, + orientation: 'horizontal', + _range: false, + readout: true, + readout_format: '.2f', + slider_color: null, + continuous_update: true, + disabled: false, + base: 10., + value: 1.0, + min: 0, + max: 4 + }); + } + 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 +119,112 @@ 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'); + let base = this.model.get('base'); + + if (isNaN(value)) { + this.readout.textContent = this.valueToString(this.model.get('value')); + } else { + value = Math.max(Math.min(value, Math.pow(base,vmax)), Math.pow(base,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 { /** diff --git a/packages/schema/jupyterwidgetmodels.latest.md b/packages/schema/jupyterwidgetmodels.latest.md index 886402dfd8..2b099a4910 100644 --- a/packages/schema/jupyterwidgetmodels.latest.md +++ b/packages/schema/jupyterwidgetmodels.latest.md @@ -296,6 +296,31 @@ Attribute | Type | Default | Help `layout` | reference to Layout widget | reference to new instance | `style` | reference to DescriptionStyle widget | reference to new instance | Styling customizations +### FloatLogSliderModel (@jupyter-widgets/controls, 1.1.0); FloatLogSliderView (@jupyter-widgets/controls, 1.1.0) + +Attribute | Type | Default | Help +-----------------|------------------|------------------|---- +`_dom_classes` | array | `[]` | CSS classes applied to widget DOM element +`_model_module` | string | `'@jupyter-widgets/controls'` | +`_model_module_version` | string | `'1.1.0'` | +`_model_name` | string | `'FloatLogSliderModel'` | +`_view_module` | string | `'@jupyter-widgets/controls'` | +`_view_module_version` | string | `'1.1.0'` | +`_view_name` | string | `'FloatLogSliderView'` | +`base` | number (float) | `10.0` | Base for the logarithm +`continuous_update` | boolean | `true` | Update the value of the widget as the user is holding the slider. +`description` | string | `''` | Description of the control. +`disabled` | boolean | `false` | Enable or disable user changes +`layout` | reference to Layout widget | reference to new instance | +`max` | number (float) | `4.0` | Max value for the exponent +`min` | number (float) | `0.0` | Min value for the exponent +`orientation` | string (one of `'horizontal'`, `'vertical'`) | `'horizontal'` | Vertical or horizontal. +`readout` | boolean | `true` | Display the current value of the slider next to it. +`readout_format` | string | `'.2f'` | Format for the readout +`step` | number (float) | `0.1` | Minimum step in the exponent to increment the value +`style` | reference to SliderStyle widget | reference to new instance | +`value` | number (float) | `1.0` | Float value + ### FloatProgressModel (@jupyter-widgets/controls, 1.1.0); ProgressView (@jupyter-widgets/controls, 1.1.0) Attribute | Type | Default | Help