From 89ed4e8e843ad0080c84a5109a5e50ae8ea73c74 Mon Sep 17 00:00:00 2001 From: Vidar Tonaas Fauske Date: Fri, 10 Jan 2020 11:27:54 +0100 Subject: [PATCH 1/9] Add TimePicker, DatetimePicker Also adds some attributes to DatePicker for consistency. --- ipywidgets/widgets/__init__.py | 2 + ipywidgets/widgets/trait_types.py | 103 ++++++ ipywidgets/widgets/widget_date.py | 43 ++- ipywidgets/widgets/widget_datetime.py | 86 +++++ ipywidgets/widgets/widget_time.py | 93 ++++++ packages/controls/src/index.ts | 2 + packages/controls/src/widget_datetime.ts | 307 ++++++++++++++++++ packages/controls/src/widget_time.ts | 173 ++++++++++ packages/controls/test/src/dummy-manager.ts | 1 + packages/controls/test/src/index.ts | 2 + packages/controls/test/src/utils.ts | 110 +++++++ .../controls/test/src/widget_datetime_test.ts | 115 +++++++ .../controls/test/src/widget_time_test.ts | 137 ++++++++ 13 files changed, 1171 insertions(+), 3 deletions(-) create mode 100644 ipywidgets/widgets/widget_datetime.py create mode 100644 ipywidgets/widgets/widget_time.py create mode 100644 packages/controls/src/widget_datetime.ts create mode 100644 packages/controls/src/widget_time.ts create mode 100644 packages/controls/test/src/utils.ts create mode 100644 packages/controls/test/src/widget_datetime_test.ts create mode 100644 packages/controls/test/src/widget_time_test.ts diff --git a/ipywidgets/widgets/__init__.py b/ipywidgets/widgets/__init__.py index cd91177e11..2f5c5c9a30 100644 --- a/ipywidgets/widgets/__init__.py +++ b/ipywidgets/widgets/__init__.py @@ -15,6 +15,8 @@ from .widget_int import IntText, BoundedIntText, IntSlider, IntProgress, IntRangeSlider, Play, SliderStyle from .widget_color import ColorPicker from .widget_date import DatePicker +from .widget_datetime import DatetimePicker +from .widget_time import TimePicker from .widget_output import Output from .widget_selection import RadioButtons, ToggleButtons, ToggleButtonsStyle, Dropdown, Select, SelectionSlider, SelectMultiple, SelectionRangeSlider from .widget_selectioncontainer import Tab, Accordion, Stacked diff --git a/ipywidgets/widgets/trait_types.py b/ipywidgets/widgets/trait_types.py index 1cf69fb755..b89ec13c93 100644 --- a/ipywidgets/widgets/trait_types.py +++ b/ipywidgets/widgets/trait_types.py @@ -64,6 +64,13 @@ class Date(traitlets.TraitType): default_value = dt.date(1900, 1, 1) +class Time(traitlets.TraitType): + """A trait type holding a Python time object""" + + klass = dt.date + default_value = dt.time() + + def datetime_to_json(pydt, manager): """Serialize a Python datetime object to json. @@ -175,6 +182,102 @@ def validate(self, obj, value): except Exception: self.error(obj, value) +def time_to_json(pyt, manager): + """Serialize a Python time object to json.""" + if pyt is None: + return None + else: + return dict( + hours=pyt.hour, # Hours, Minutes, Seconds and Milliseconds + minutes=pyt.minute, # are plural in JS + seconds=pyt.second, + milliseconds=pyt.microsecond / 1000, + ) + + +def time_from_json(js, manager): + """Deserialize a Python time object from json.""" + if js is None: + return None + else: + return dt.time( + js["hours"], js["minutes"], js["seconds"], js["milliseconds"] * 1000 + ) + + +time_serialization = {"from_json": time_from_json, "to_json": time_to_json} + + +def datetime_to_json(pydt, manager): + """Serialize a Python datetime object to json. + + Instantiating a JavaScript Date object with a string assumes that the + string is a UTC string, while instantiating it with constructor arguments + assumes that it's in local time: + + >>> cdate = new Date('2015-05-12') + Mon May 11 2015 20:00:00 GMT-0400 (Eastern Daylight Time) + >>> cdate = new Date(2015, 4, 12) // Months are 0-based indices in JS + Tue May 12 2015 00:00:00 GMT-0400 (Eastern Daylight Time) + + Attributes of this dictionary are to be passed to the JavaScript Date + constructor. + """ + if pydt is None: + return None + else: + try: + utcdt = pydt.astimezone(dt.timezone.utc) + except (ValueError, OSError): + # If year is outside valid range for conversion, + # use it as-is + utcdt = pydt + return dict( + year=utcdt.year, + month=utcdt.month - 1, # Months are 0-based indices in JS + date=utcdt.day, + hours=utcdt.hour, # Hours, Minutes, Seconds and Milliseconds + minutes=utcdt.minute, # are plural in JS + seconds=utcdt.second, + milliseconds=utcdt.microsecond / 1000, + ) + + +def datetime_from_json(js, manager): + """Deserialize a Python datetime object from json.""" + if js is None: + return None + else: + try: + return dt.datetime( + js["year"], + js["month"] + 1, # Months are 1-based in Python + js["date"], + js["hours"], + js["minutes"], + js["seconds"], + js["milliseconds"] * 1000, + dt.timezone.utc, + ).astimezone() + except (ValueError, OSError): + # If year is outside valid range for conversion, + # return naive datetime + return dt.datetime( + js["year"], + js["month"] + 1, # Months are 1-based in Python + js["date"], + js["hours"], + js["minutes"], + js["seconds"], + js["milliseconds"] * 1000, + dt.timezone.utc, + ) + + +datetime_serialization = {"from_json": datetime_from_json, "to_json": datetime_to_json} + + + class InstanceDict(traitlets.Instance): """An instance trait which coerces a dict to an instance. diff --git a/ipywidgets/widgets/widget_date.py b/ipywidgets/widgets/widget_date.py index b0e9a00597..2088dd971b 100644 --- a/ipywidgets/widgets/widget_date.py +++ b/ipywidgets/widgets/widget_date.py @@ -11,7 +11,7 @@ from .widget import register from .widget_core import CoreWidget from .trait_types import Date, date_serialization -from traitlets import Unicode, Bool +from traitlets import Unicode, Bool, Union, CInt, CaselessStrEnum, TraitError, validate @register @@ -36,9 +36,46 @@ class DatePicker(DescriptionWidget, ValueWidget, CoreWidget): >>> date_pick = widgets.DatePicker() >>> date_pick.value = datetime.date(2019, 7, 9) """ + + _view_name = Unicode('DatePickerView').tag(sync=True) + _model_name = Unicode('DatePickerModel').tag(sync=True) + value = Date(None, allow_none=True).tag(sync=True, **date_serialization) disabled = Bool(False, help="Enable or disable user changes.").tag(sync=True) + min = Date(None, allow_none=True).tag(sync=True, **date_serialization) + max = Date(None, allow_none=True).tag(sync=True, **date_serialization) + step = Union( + (CInt(1), CaselessStrEnum(["any"])), + help='The date step to use for the picker, in days, or "any".', + ).tag(sync=True) - _view_name = Unicode('DatePickerView').tag(sync=True) - _model_name = Unicode('DatePickerModel').tag(sync=True) + @validate("value") + def _validate_value(self, proposal): + """Cap and floor value""" + value = proposal["value"] + if self.min and self.min > value: + value = max(value, self.min) + if self.max and self.max < value: + value = min(value, self.max) + return value + + @validate("min") + def _validate_min(self, proposal): + """Enforce min <= value <= max""" + min = proposal["value"] + if self.max and min > self.max: + raise TraitError("Setting min > max") + if self.value and min > self.value: + self.value = min + return min + + @validate("max") + def _validate_max(self, proposal): + """Enforce min <= value <= max""" + max = proposal["value"] + if self.min and max < self.min: + raise TraitError("setting max < min") + if self.value and max < self.value: + self.value = max + return max diff --git a/ipywidgets/widgets/widget_datetime.py b/ipywidgets/widgets/widget_datetime.py new file mode 100644 index 0000000000..12e450de73 --- /dev/null +++ b/ipywidgets/widgets/widget_datetime.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python +# coding: utf-8 + +# Copyright (c) Vidar Tonaas Fauske. +# Distributed under the terms of the Modified BSD License. + +""" +Time and datetime picker widgets +""" + +from traitlets import Unicode, Bool, validate, TraitError + +from .trait_types import datetime_serialization, Datetime +from .valuewidget import ValueWidget +from .widget import register +from .widget_core import CoreWidget +from .widget_description import DescriptionWidget + + +@register +class DatetimePicker(DescriptionWidget, ValueWidget, CoreWidget): + """ + Display a widget for picking times. + + Parameters + ---------- + + value: datetime.datetime + The current value of the widget. + + disabled: bool + Whether to disable user changes. + + min: datetime.datetime + The lower allowed datetime bound + + max: datetime.datetime + The upper allowed datetime bound + + Examples + -------- + + >>> import datetime + >>> import ipydatetime + >>> datetime_pick = ipydatetime.TimePicker() + >>> datetime_pick.value = datetime.datetime(2018, 09, 5, 12, 34, 3) + """ + + _view_name = Unicode("DatetimeView").tag(sync=True) + _model_name = Unicode("DatetimeModel").tag(sync=True) + + value = Datetime(None, allow_none=True).tag(sync=True, **datetime_serialization) + disabled = Bool(False, help="Enable or disable user changes.").tag(sync=True) + + min = Datetime(None, allow_none=True).tag(sync=True, **datetime_serialization) + max = Datetime(None, allow_none=True).tag(sync=True, **datetime_serialization) + + @validate("value") + def _validate_value(self, proposal): + """Cap and floor value""" + value = proposal["value"] + if self.min and self.min > value: + value = max(value, self.min) + if self.max and self.max < value: + value = min(value, self.max) + return value + + @validate("min") + def _validate_min(self, proposal): + """Enforce min <= value <= max""" + min = proposal["value"] + if self.max and min > self.max: + raise TraitError("Setting min > max") + if self.value and min > self.value: + self.value = min + return min + + @validate("max") + def _validate_max(self, proposal): + """Enforce min <= value <= max""" + max = proposal["value"] + if self.min and max < self.min: + raise TraitError("setting max < min") + if self.value and max < self.value: + self.value = max + return max diff --git a/ipywidgets/widgets/widget_time.py b/ipywidgets/widgets/widget_time.py new file mode 100644 index 0000000000..646e9bbe57 --- /dev/null +++ b/ipywidgets/widgets/widget_time.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python +# coding: utf-8 + +# Copyright (c) Vidar Tonaas Fauske. +# Distributed under the terms of the Modified BSD License. + +""" +Time picker widget +""" + +from traitlets import Unicode, Bool, Union, CaselessStrEnum, CFloat, validate, TraitError + +from .trait_types import Time, time_serialization +from .valuewidget import ValueWidget +from .widget import register +from .widget_core import CoreWidget +from .widget_description import DescriptionWidget + + +@register +class TimePicker(DescriptionWidget, ValueWidget, CoreWidget): + """ + Display a widget for picking times. + + Parameters + ---------- + + value: datetime.time + The current value of the widget. + + disabled: bool + Whether to disable user changes. + + min: datetime.time + The lower allowed time bound + + max: datetime.time + The upper allowed time bound + + step: float | 'any' + The time step to use for the picker, in seconds, or "any" + + Examples + -------- + + >>> import datetime + >>> import ipydatetime + >>> time_pick = ipydatetime.TimePicker() + >>> time_pick.value = datetime.time(12, 34, 3) + """ + + _view_name = Unicode("TimeView").tag(sync=True) + _model_name = Unicode("TimeModel").tag(sync=True) + + value = Time(None, allow_none=True).tag(sync=True, **time_serialization) + disabled = Bool(False, help="Enable or disable user changes.").tag(sync=True) + + min = Time(None, allow_none=True).tag(sync=True, **time_serialization) + max = Time(None, allow_none=True).tag(sync=True, **time_serialization) + step = Union( + (CFloat(60), CaselessStrEnum(["any"])), + help='The time step to use for the picker, in seconds, or "any".', + ).tag(sync=True) + + @validate("value") + def _validate_value(self, proposal): + """Cap and floor value""" + value = proposal["value"] + if self.min and self.min > value: + value = max(value, self.min) + if self.max and self.max < value: + value = min(value, self.max) + return value + + @validate("min") + def _validate_min(self, proposal): + """Enforce min <= value <= max""" + min = proposal["value"] + if self.max and min > self.max: + raise TraitError("Setting min > max") + if self.value and min > self.value: + self.value = min + return min + + @validate("max") + def _validate_max(self, proposal): + """Enforce min <= value <= max""" + max = proposal["value"] + if self.min and max < self.min: + raise TraitError("setting max < min") + if self.value and max < self.value: + self.value = max + return max diff --git a/packages/controls/src/index.ts b/packages/controls/src/index.ts index d7134b92c7..912458d981 100644 --- a/packages/controls/src/index.ts +++ b/packages/controls/src/index.ts @@ -12,6 +12,8 @@ export * from './widget_video'; export * from './widget_audio'; export * from './widget_color'; export * from './widget_date'; +export * from './widget_datetime'; +export * from './widget_time'; export * from './widget_int'; export * from './widget_float'; export * from './widget_controller'; diff --git a/packages/controls/src/widget_datetime.ts b/packages/controls/src/widget_datetime.ts new file mode 100644 index 0000000000..a422c0d40a --- /dev/null +++ b/packages/controls/src/widget_datetime.ts @@ -0,0 +1,307 @@ +// Copyright (c) Vidar Tonaas Fauske +// Distributed under the terms of the Modified BSD License. + +import { ISerializers } from '@jupyter-widgets/base'; + +import { uuid } from './utils'; + +import { DescriptionView } from './widget_description'; + +import { CoreDescriptionModel } from './widget_core'; + +import { serialize_time } from './widget_time'; + +export interface ISerializedDatetime { + /** + * UTC full year + */ + year: number; + + /** + * UTC zero-based month (0 means January, 11 means December) + */ + month: number; + + /** + * UTC day of month + */ + date: number; + + /** + * UTC hour (24H format) + */ + hours: number; + + /** + * UTC minutes + */ + minutes: number; + + /** + * UTC seconds + */ + seconds: number; + + /** + * UTC millisconds + */ + milliseconds: number; +} + +export function serialize_datetime(value: Date): ISerializedDatetime | null { + if (value === null) { + return null; + } else { + return { + year: value.getUTCFullYear(), + month: value.getUTCMonth(), + date: value.getUTCDate(), + hours: value.getUTCHours(), + minutes: value.getUTCMinutes(), + seconds: value.getUTCSeconds(), + milliseconds: value.getUTCMilliseconds() + }; + } +} + +export function deserialize_datetime(value: ISerializedDatetime): Date | null { + if (value === null) { + return null; + } else { + const date = new Date(); + date.setUTCFullYear(value.year, value.month, value.date); + date.setUTCHours( + value.hours, + value.minutes, + value.seconds, + value.milliseconds + ); + return date; + } +} + +export const datetime_serializers = { + serialize: serialize_datetime, + deserialize: deserialize_datetime +}; + +export class DatetimeModel extends CoreDescriptionModel { + defaults(): Backbone.ObjectHash { + return { + ...super.defaults(), + _model_name: 'DatetimeModel', + _view_name: 'DatetimeView', + value: null, + disabled: false, + min: null, + max: null + }; + } + + static serializers: ISerializers = { + ...CoreDescriptionModel.serializers, + value: datetime_serializers, + min: datetime_serializers, + max: datetime_serializers + }; +} + +export class DatetimeView extends DescriptionView { + render(): void { + super.render(); + this.el.classList.add('jupyter-widgets'); + this.el.classList.add('widget-inline-hbox'); + this.el.classList.add('widget-datetimepicker'); + + const test = document.createElement('input'); + test.type = 'datetime-local'; + if (test.type === 'text') { + // No native support, split into date and time input: + this._datepicker = document.createElement('input'); + this._datepicker.setAttribute('type', 'date'); + this._datepicker.id = this.label.htmlFor = uuid(); + + this._timepicker = document.createElement('input'); + this._timepicker.setAttribute('type', 'time'); + this._timepicker.id = uuid(); + + this.el.appendChild(this._datepicker); + this.el.appendChild(this._timepicker); + } else { + this._datetimepicker = test; + this._datetimepicker.id = this.label.htmlFor = uuid(); + this.el.appendChild(this._datetimepicker); + } + + this.listenTo(this.model, 'change:value', this._update_value); + this.listenTo(this.model, 'change', this.update2); + this._update_value(); + this.update2(); + } + + /** + * Update the contents of this view + * + * Called when the model is changed. The model may have been + * changed by another view or by a state update from the back-end. + */ + update2(model?: Backbone.Model, options?: any): void { + if (options === undefined || options.updated_view !== this) { + const min = this.model.get('min') as Date | null; + const max = this.model.get('max') as Date | null; + if (this._datetimepicker) { + this._datetimepicker.disabled = this.model.get('disabled'); + this._datetimepicker!.min = Private.dt_as_dt_string(min); + this._datetimepicker!.max = Private.dt_as_dt_string(max); + } else { + this._datepicker!.disabled = this.model.get('disabled'); + this._datepicker!.min = Private.dt_as_date_string(min); + this._datepicker!.max = Private.dt_as_date_string(max); + this._timepicker!.disabled = this.model.get('disabled'); + // Don't support min/max time here. + // It could be added by enabling min time if value is min date, + // and enabling max time if value is max date, but leave as + // TODO for now. + } + } + } + + events(): { [e: string]: string } { + // Typescript doesn't understand that these functions are called, so we + // specifically use them here so it knows they are being used. + void this._picker_change; + void this._picker_focusout; + return { + 'change [type="date"]': '_picker_change', + 'change [type="time"]': '_picker_change', + 'change [type="datetime-local"]': '_picker_change', + 'focusout [type="date"]': '_picker_focusout', + 'focusout [type="datetime-local"]': '_picker_focusout', + 'focusout [type="time"]': '_picker_focusout' + }; + } + + private _update_value( + model?: Backbone.Model, + newValue?: any, + options?: any + ): void { + if (options === undefined || options.updated_view !== this) { + const value = this.model.get('value') as Date | null; + if (this._datetimepicker) { + this._datetimepicker.value = Private.dt_as_dt_string(value); + } else { + this._datepicker!.valueAsDate = value; + this._timepicker!.value = Private.dt_as_time_string(value); + } + } + } + + private _picker_change(): void { + if (this._datetimepicker) { + if (!this._datetimepicker.validity.badInput) { + const v = this._datetimepicker.value; + let date = v ? new Date(v) : null; + if (date && isNaN(date.valueOf())) { + date = null; + } + this.model.set('value', date, { updated_view: this }); + this.touch(); + } + } else { + if ( + !this._datepicker!.validity.badInput && + !this._timepicker!.validity.badInput + ) { + const date = this._datepicker!.valueAsDate; + const time = serialize_time(this._timepicker!.value); + if (date !== null && time !== null) { + // * Use local time * + date.setHours( + time.hours, + time.minutes, + time.seconds, + time.milliseconds + ); + } + this.model.set('value', time !== null && date, { updated_view: this }); + this.touch(); + } + } + } + + private _picker_focusout(): void { + const pickers = [this._datetimepicker, this._datepicker, this._timepicker]; + if (pickers.some(p => p && p.validity.badInput)) { + this.model.set('value', null); + this.touch(); + } + } + + private _datetimepicker: HTMLInputElement | undefined; + private _timepicker: HTMLInputElement | undefined; + private _datepicker: HTMLInputElement | undefined; +} + +namespace Private { + // eslint-disable-next-line no-inner-declarations + export function dt_as_dt_string(value: Date | null): string { + if (value === null) { + return ''; + } + // Replicate `toISOString()` but in local time zone: + const parts = []; + parts.push( + `${value + .getFullYear() + .toString() + .padStart(4, '0')}` + ); + parts.push(`-${(value.getMonth() + 1).toString().padStart(2, '0')}`); + parts.push( + `-${value + .getDate() + .toString() + .padStart(2, '0')}` + ); + parts.push( + `T${value + .getHours() + .toString() + .padStart(2, '0')}` + ); + parts.push( + `:${value + .getMinutes() + .toString() + .padStart(2, '0')}` + ); + if (value.getSeconds() > 0 || value.getMilliseconds() > 0) { + parts.push( + `:${value + .getSeconds() + .toString() + .padStart(2, '0')}` + ); + if (value.getMilliseconds() > 0) { + parts.push( + `.${value + .getMilliseconds() + .toString() + .padStart(3, '0')}` + ); + } + } + return parts.join(''); + } + + // eslint-disable-next-line no-inner-declarations + export function dt_as_date_string(value: Date | null): string { + return value ? dt_as_dt_string(value).split('T', 2)[0] : ''; + } + + // eslint-disable-next-line no-inner-declarations + export function dt_as_time_string(value: Date | null): string { + return value ? dt_as_dt_string(value).split('T', 2)[1] : ''; + } +} diff --git a/packages/controls/src/widget_time.ts b/packages/controls/src/widget_time.ts new file mode 100644 index 0000000000..eb899377ec --- /dev/null +++ b/packages/controls/src/widget_time.ts @@ -0,0 +1,173 @@ +// Copyright (c) Vidar Tonaas Fauske +// Distributed under the terms of the Modified BSD License. + +import { ISerializers } from '@jupyter-widgets/base'; + +import { uuid } from './utils'; + +import { DescriptionView } from './widget_description'; + +import { CoreDescriptionModel } from './widget_core'; + +const PARSER = /(\d\d):(\d\d)(:(\d\d)(.(\d{1,3})\d*)?)?/; + +export interface ISerializedTime { + /** + * Integer hour (24H format) + */ + hours: number; + + /** + * Integer minutes + */ + minutes: number; + + /** + * Integer seconds + */ + seconds: number; + + /** + * Millisconds + */ + milliseconds: number; +} + +export function serialize_time(value: string): ISerializedTime | null { + if (value === null) { + return null; + } else { + const res = PARSER.exec(value); + if (res === null) { + return null; + } + return { + hours: Math.min(23, parseInt(res[1], 10)), + minutes: Math.min(59, parseInt(res[2], 10)), + seconds: res[4] ? Math.min(59, parseInt(res[4], 10)) : 0, + milliseconds: res[6] ? parseInt(res[6], 10) : 0 + }; + } +} + +export function deserialize_time(value: ISerializedTime): string | null { + if (value === null) { + return null; + } else { + const parts = [ + `${value.hours + .toString() + .padStart(2, '0')}:${value.minutes.toString().padStart(2, '0')}` + ]; + if (value.seconds > 0 || value.milliseconds > 0) { + parts.push(`:${value.seconds.toString().padStart(2, '0')}`); + if (value.milliseconds > 0) { + parts.push(`.${value.milliseconds.toString().padStart(3, '0')}`); + } + } + return parts.join(''); + } +} + +export const time_serializers = { + serialize: serialize_time, + deserialize: deserialize_time +}; + +export class TimeModel extends CoreDescriptionModel { + defaults(): Backbone.ObjectHash { + return { + ...super.defaults(), + _model_name: TimeModel.model_name, + _view_name: TimeModel.view_name, + value: null, + disabled: false, + min: null, + max: null, + step: 60 + }; + } + + static serializers: ISerializers = { + ...CoreDescriptionModel.serializers, + value: time_serializers, + min: time_serializers, + max: time_serializers + }; + + static model_name = 'TimeModel'; + static view_name = 'TimeView'; +} + +export class TimeView extends DescriptionView { + render(): void { + super.render(); + this.el.classList.add('jupyter-widgets'); + this.el.classList.add('widget-inline-hbox'); + this.el.classList.add('widget-timepicker'); + + this._timepicker = document.createElement('input'); + this._timepicker.setAttribute('type', 'time'); + this._timepicker.id = this.label.htmlFor = uuid(); + + this.el.appendChild(this._timepicker); + + this.listenTo(this.model, 'change:value', this._update_value); + this.listenTo(this.model, 'change', this.update2); + this._update_value(); + this.update2(); + } + + /** + * Update the contents of this view + * + * Called when the model is changed. The model may have been + * changed by another view or by a state update from the back-end. + */ + update2(model?: Backbone.Model, options?: any): void { + if (options === undefined || options.updated_view !== this) { + this._timepicker!.disabled = this.model.get('disabled'); + this._timepicker!.min = this.model.get('min'); + this._timepicker!.max = this.model.get('max'); + this._timepicker!.step = this.model.get('step'); + } + return super.update(); + } + + events(): { [e: string]: string } { + // Typescript doesn't understand that these functions are called, so we + // specifically use them here so it knows they are being used. + void this._picker_change; + void this._picker_focusout; + return { + 'change [type="time"]': '_picker_change', + 'focusout [type="time"]': '_picker_focusout' + }; + } + + private _update_value( + model?: Backbone.Model, + newValue?: any, + options?: any + ): void { + if (options === undefined || options.updated_view !== this) { + this._timepicker!.value = this.model.get('value'); + } + } + + private _picker_change(): void { + if (!this._timepicker!.validity.badInput) { + this.model.set('value', this._timepicker!.value, { updated_view: this }); + this.touch(); + } + } + + private _picker_focusout(): void { + if (this._timepicker!.validity.badInput) { + this.model.set('value', null, { updated_view: this }); + this.touch(); + } + } + + private _timepicker: HTMLInputElement | undefined; +} diff --git a/packages/controls/test/src/dummy-manager.ts b/packages/controls/test/src/dummy-manager.ts index b59313fb4f..4e84438cad 100644 --- a/packages/controls/test/src/dummy-manager.ts +++ b/packages/controls/test/src/dummy-manager.ts @@ -119,4 +119,5 @@ export class DummyManager extends ManagerBase { } el: HTMLElement; + testClasses: { [key: string]: any } = testWidgets; } diff --git a/packages/controls/test/src/index.ts b/packages/controls/test/src/index.ts index 29f9816b35..36de567d2c 100644 --- a/packages/controls/test/src/index.ts +++ b/packages/controls/test/src/index.ts @@ -2,6 +2,8 @@ // Distributed under the terms of the Modified BSD License. import './widget_date_test'; +import './widget_datetime_test'; +import './widget_time_test'; import './widget_string_test'; import './widget_upload_test'; import './lumino/currentselection_test'; diff --git a/packages/controls/test/src/utils.ts b/packages/controls/test/src/utils.ts new file mode 100644 index 0000000000..9d41fd6cb4 --- /dev/null +++ b/packages/controls/test/src/utils.ts @@ -0,0 +1,110 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + +import * as widgets from '@jupyter-widgets/base'; +import * as services from '@jupyterlab/services'; +import { DummyManager } from './dummy-manager'; + +let numComms = 0; + +export class MockComm implements widgets.IClassicComm { + constructor() { + this.comm_id = `mock-comm-id-${numComms}`; + numComms += 1; + } + + on_close(fn: Function | null): void { + this._on_close = fn; + } + + on_msg(fn: Function | null): void { + this._on_msg = fn; + } + + _process_msg(msg: services.KernelMessage.ICommMsgMsg): void | Promise { + if (this._on_msg) { + return this._on_msg(msg); + } else { + return Promise.resolve(); + } + } + + open( + data?: any, + metadata?: any, + buffers?: ArrayBuffer[] | ArrayBufferView[] + ): string { + if (this._on_open) { + this._on_open(); + } + return ''; + } + + close( + data?: any, + metadata?: any, + buffers?: ArrayBuffer[] | ArrayBufferView[] + ): string { + if (this._on_close) { + this._on_close(); + } + return ''; + } + + send( + data?: any, + metadata?: any, + buffers?: ArrayBuffer[] | ArrayBufferView[] + ): string { + return ''; + } + + comm_id: string; + target_name: string; + _on_msg: Function | null = null; + _on_close: Function | null = null; + _on_open: Function | null = null; +} + +export interface IConstructor { + new (attributes?: any, options?: any): T; +} + +export function createTestModel( + constructor: IConstructor, + attributes?: any, + widget_manager?: widgets.WidgetModel['widget_manager'] +): T { + const id = widgets.uuid(); + const modelOptions = { + widget_manager: widget_manager || new DummyManager(), + model_id: id + }; + + return new constructor(attributes, modelOptions); +} + +export async function createTestModelFromSerialized< + T extends widgets.WidgetModel +>( + constructor: IConstructor, + state?: any, + widget_manager?: widgets.WidgetModel['widget_manager'] +): Promise { + widget_manager = widget_manager || new DummyManager(); + const attributes = await (constructor as any)._deserialize_state( + state, + widget_manager + ); + + return createTestModel(constructor, attributes, widget_manager); +} + +export function createTestView( + model: widgets.WidgetModel, + viewCtor: IConstructor +): Promise { + const mgr = model.widget_manager as DummyManager; + mgr.testClasses[model.get('_view_name')] = viewCtor; + return model.widget_manager.create_view(model, undefined) as any; +} diff --git a/packages/controls/test/src/widget_datetime_test.ts b/packages/controls/test/src/widget_datetime_test.ts new file mode 100644 index 0000000000..b3864cd050 --- /dev/null +++ b/packages/controls/test/src/widget_datetime_test.ts @@ -0,0 +1,115 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + +import { expect } from 'chai'; + +import { DummyManager } from './dummy-manager'; + +import { + createTestModel, + createTestView, + createTestModelFromSerialized +} from './utils'; + +import { DatetimeModel, DatetimeView } from '../../lib'; + +describe('Datetime', () => { + const date = new Date(); + + describe('DatetimeModel', () => { + it('should be createable', () => { + const model = createTestModel(DatetimeModel); + expect(model).to.be.an.instanceof(DatetimeModel); + expect(model.get('value')).to.be.a('null'); + }); + + it('should be createable with a value', () => { + const state = { value: date }; + const model = createTestModel(DatetimeModel, state); + expect(model).to.be.instanceof(DatetimeModel); + expect(model.get('value')).to.equal(date); + }); + + it('should serialize as expected', async () => { + const state_in = { + value: { + year: 2002, + month: 2, + date: 20, + hours: 20, + minutes: 2, + seconds: 20, + milliseconds: 2 + } + }; + + const model = await createTestModelFromSerialized( + DatetimeModel, + state_in + ); + model.widget_manager.register_model( + model.model_id, + Promise.resolve(model) + ); + + const state_out = await (model.widget_manager as DummyManager).get_state(); + const models = Object.keys(state_out.state).map( + k => state_out.state[k].state + ); + expect(models.length).to.equal(1); + expect(models[0]._model_name).to.equal('DatetimeModel'); + expect(models[0].value).to.eql(state_in.value); + }); + + it('should deserialize to Date object', async () => { + const state_in = { + value: { + year: 2002, + month: 2, + date: 20, + hours: 20, + minutes: 2, + seconds: 20, + milliseconds: 2 + } + }; + + const model = await createTestModelFromSerialized( + DatetimeModel, + state_in + ); + expect(model.get('value')).to.eql( + new Date(Date.UTC(2002, 2, 20, 20, 2, 20, 2)) + ); + }); + + it('should deserialize null', async () => { + const state_in = { value: null }; + + const model = await createTestModelFromSerialized( + DatetimeModel, + state_in + ); + expect(model.get('value')).to.be.a('null'); + }); + + it('should deserialize undefined', async () => { + const state_in = {}; + const model = await createTestModelFromSerialized( + DatetimeModel, + state_in + ); + expect(model.get('value')).to.be.a('null'); + }); + }); + + describe('DatetimeView', () => { + it('should be createable', async () => { + const state = {}; + const model = createTestModel(DatetimeModel, state); + const view = await createTestView(model, DatetimeView); + expect(view).to.be.an.instanceof(DatetimeView); + expect(view.model).to.equal(model); + }); + }); +}); diff --git a/packages/controls/test/src/widget_time_test.ts b/packages/controls/test/src/widget_time_test.ts new file mode 100644 index 0000000000..19d3d68de3 --- /dev/null +++ b/packages/controls/test/src/widget_time_test.ts @@ -0,0 +1,137 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + +import { expect } from 'chai'; + +import { + createTestModel, + createTestView, + createTestModelFromSerialized +} from './utils'; + +import { TimeModel, TimeView } from '../../lib'; +import { DummyManager } from './dummy-manager'; + +describe('Time', () => { + const timeString = new Date() + .toISOString() + .split('T', 2)[1] + .slice(0, -1); + + describe('TimeModel', () => { + it('should be createable', () => { + const model = createTestModel(TimeModel); + expect(model).to.be.an.instanceof(TimeModel); + expect(model.get('value')).to.be.a('null'); + }); + + it('should be createable with a value', () => { + const state = { value: timeString }; + const model = createTestModel(TimeModel, state); + expect(model).to.be.an.instanceof(TimeModel); + expect(model.get('value')).to.equal(timeString); + }); + + it('should serialize as expected', async () => { + const state_in = { + value: { + hours: 13, + minutes: 37, + seconds: 42, + milliseconds: 333 + } + }; + + const model = await createTestModelFromSerialized(TimeModel, state_in); + model.widget_manager.register_model( + model.model_id, + Promise.resolve(model) + ); + + const state_out = await (model.widget_manager as DummyManager).get_state(); + const models = Object.keys(state_out.state).map( + k => state_out.state[k].state + ); + expect(models.length).to.equal(1); + expect(models[0]._model_name).to.equal('TimeModel'); + expect(models[0].value).to.eql(state_in.value); + }); + + it('should deserialize to short form', async () => { + const state_in = { + value: { + hours: 13, + minutes: 37, + seconds: 0, + milliseconds: 0 + } + }; + + const model = await createTestModelFromSerialized(TimeModel, state_in); + expect(model.get('value')).to.equal('13:37'); + }); + + it('should deserialize to medium form', async () => { + const state_in = { + value: { + hours: 13, + minutes: 37, + seconds: 42, + milliseconds: 0 + } + }; + + const model = await createTestModelFromSerialized(TimeModel, state_in); + expect(model.get('value')).to.equal('13:37:42'); + }); + + it('should not be thrown off by irrelevant zeroes', async () => { + const state_in = { + value: { + hours: 0, + minutes: 0, + seconds: 0, + milliseconds: 7 + } + }; + + const model = await createTestModelFromSerialized(TimeModel, state_in); + expect(model.get('value')).to.equal('00:00:00.007'); + }); + + it('should deserialize null', async () => { + const state_in = { value: null }; + + const model = await createTestModelFromSerialized(TimeModel, state_in); + expect(model.get('value')).to.be.a('null'); + }); + + it('should deserialize undefined', async () => { + const state_in = {}; + const model = await createTestModelFromSerialized(TimeModel, state_in); + expect(model.get('value')).to.be.a('null'); + }); + }); + + describe('TimeView', () => { + it('should be createable', async () => { + const state = {}; + const model = createTestModel(TimeModel, state); + const view = await createTestView(model, TimeView); + expect(view).to.be.an.instanceof(TimeView); + expect(view.model).to.equal(model); + }); + + it('should be updated when the value changes', async () => { + const state = {}; + const model = createTestModel(TimeModel, state); + const view = await createTestView(model, TimeView); + + model.set('value', timeString); + const picker = view.el.querySelector( + 'input[type="time"]' + ) as HTMLInputElement; + expect(picker.value).to.equal(timeString); + }); + }); +}); From 10e76838a93bbfff54c101b4e343caff8c89637f Mon Sep 17 00:00:00 2001 From: Vidar Tonaas Fauske Date: Fri, 17 Jan 2020 16:57:28 +0000 Subject: [PATCH 2/9] Fix generator for unions --- packages/schema/generate-spec.py | 35 +++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/packages/schema/generate-spec.py b/packages/schema/generate-spec.py index 29b422ba62..8d3908911e 100644 --- a/packages/schema/generate-spec.py +++ b/packages/schema/generate-spec.py @@ -5,8 +5,22 @@ import json from operator import itemgetter -from traitlets import (CaselessStrEnum, Unicode, Tuple, List, Bool, CFloat, - Float, CInt, Int, Instance, Dict, Bytes, Any) +from traitlets import ( + CaselessStrEnum, + Unicode, + Tuple, + List, + Bool, + CFloat, + Float, + CInt, + Int, + Instance, + Dict, + Bytes, + Any, + Union, +) import ipywidgets as widgets from ipywidgets import Color @@ -62,7 +76,16 @@ def trait_type(trait, widget_list): w_type = 'color' elif isinstance(trait, Dict): w_type = 'object' - elif isinstance(trait, Bytes) or isinstance(trait, ByteMemoryView): + elif isinstance(trait, Union): + union_attributes = [] + union_types = [] + for ut in trait.trait_types: + ua = trait_type(ut, widget_list) + union_attributes.append(ua) + union_types.append(ua['type']) + w_type = union_types + attributes['union_attributes'] = union_attributes + elif isinstance(trait, (Bytes, ByteMemoryView)): w_type = 'bytes' elif isinstance(trait, Instance) and issubclass(trait.klass, widgets.Widget): @@ -89,6 +112,8 @@ def jsdefault(trait): default = trait.make_dynamic_default() if issubclass(trait.klass, widgets.Widget): return 'reference to new instance' + elif isinstance(trait, Union): + default = trait.make_dynamic_default() else: default = trait.default_value if isinstance(default, bytes) or isinstance(default, memoryview): @@ -112,6 +137,10 @@ def mddefault(attribute): def mdtype(attribute): md_type = attribute['type'] + if 'union_attributes' in attribute and isinstance(md_type, (list, tuple)): + md_type = ' or '.join( + mdtype(ua) for ua in attribute['union_attributes'] + ) if md_type in NUMBER_MAP: md_type = NUMBER_MAP[md_type] if attribute.get('allow_none'): From 2df57a7c0fd6b3894710b43e10857496f0c8702a Mon Sep 17 00:00:00 2001 From: Vidar Tonaas Fauske Date: Fri, 17 Jan 2020 16:57:44 +0000 Subject: [PATCH 3/9] Regen schemas --- .../schema/jupyterwidgetmodels.latest.json | 823 +++++++++++++++++- packages/schema/jupyterwidgetmodels.latest.md | 46 + 2 files changed, 852 insertions(+), 17 deletions(-) diff --git a/packages/schema/jupyterwidgetmodels.latest.json b/packages/schema/jupyterwidgetmodels.latest.json index 0d437cfd8d..28966e09c9 100644 --- a/packages/schema/jupyterwidgetmodels.latest.json +++ b/packages/schema/jupyterwidgetmodels.latest.json @@ -1360,6 +1360,117 @@ "version": "2.0.0" } }, + { + "attributes": [ + { + "default": [], + "help": "CSS classes applied to widget DOM element", + "items": { + "type": "string" + }, + "name": "_dom_classes", + "type": "array" + }, + { + "default": "@jupyter-widgets/controls", + "help": "", + "name": "_model_module", + "type": "string" + }, + { + "default": "2.0.0", + "help": "", + "name": "_model_module_version", + "type": "string" + }, + { + "default": "ColorsInputModel", + "help": "", + "name": "_model_name", + "type": "string" + }, + { + "default": "@jupyter-widgets/controls", + "help": "", + "name": "_view_module", + "type": "string" + }, + { + "default": "2.0.0", + "help": "", + "name": "_view_module_version", + "type": "string" + }, + { + "default": "ColorsInputView", + "help": "", + "name": "_view_name", + "type": "string" + }, + { + "default": true, + "help": "", + "name": "allow_duplicates", + "type": "bool" + }, + { + "default": [], + "help": "", + "name": "allowed_tags", + "type": "array" + }, + { + "default": "", + "help": "Description of the control.", + "name": "description", + "type": "string" + }, + { + "default": "reference to new instance", + "help": "", + "name": "layout", + "type": "reference", + "widget": "Layout" + }, + { + "default": "reference to new instance", + "help": "Styling customizations", + "name": "style", + "type": "reference", + "widget": "DescriptionStyle" + }, + { + "allow_none": true, + "default": null, + "help": "Is widget tabbable?", + "name": "tabbable", + "type": "bool" + }, + { + "allow_none": true, + "default": null, + "help": "A tooltip caption.", + "name": "tooltip", + "type": "string" + }, + { + "default": [], + "help": "List of string tags", + "name": "value", + "type": "array" + } + ], + "model": { + "module": "@jupyter-widgets/controls", + "name": "ColorsInputModel", + "version": "2.0.0" + }, + "view": { + "module": "@jupyter-widgets/controls", + "name": "ColorsInputView", + "version": "2.0.0" + } + }, { "attributes": [ { @@ -1953,6 +2064,35 @@ "type": "reference", "widget": "Layout" }, + { + "allow_none": true, + "default": null, + "help": "", + "name": "max", + "type": "Date" + }, + { + "allow_none": true, + "default": null, + "help": "", + "name": "min", + "type": "Date" + }, + { + "default": 1, + "help": "The date step to use for the picker, in days, or \"any\".", + "name": "step", + "type": ["int", "string"], + "union_attributes": [ + { + "type": "int" + }, + { + "enum": ["any"], + "type": "string" + } + ] + }, { "default": "reference to new instance", "help": "Styling customizations", @@ -1993,6 +2133,126 @@ "version": "2.0.0" } }, + { + "attributes": [ + { + "default": [], + "help": "CSS classes applied to widget DOM element", + "items": { + "type": "string" + }, + "name": "_dom_classes", + "type": "array" + }, + { + "default": "@jupyter-widgets/controls", + "help": "", + "name": "_model_module", + "type": "string" + }, + { + "default": "2.0.0", + "help": "", + "name": "_model_module_version", + "type": "string" + }, + { + "default": "DatetimeModel", + "help": "", + "name": "_model_name", + "type": "string" + }, + { + "default": "@jupyter-widgets/controls", + "help": "", + "name": "_view_module", + "type": "string" + }, + { + "default": "2.0.0", + "help": "", + "name": "_view_module_version", + "type": "string" + }, + { + "default": "DatetimeView", + "help": "", + "name": "_view_name", + "type": "string" + }, + { + "default": "", + "help": "Description of the control.", + "name": "description", + "type": "string" + }, + { + "default": false, + "help": "Enable or disable user changes.", + "name": "disabled", + "type": "bool" + }, + { + "default": "reference to new instance", + "help": "", + "name": "layout", + "type": "reference", + "widget": "Layout" + }, + { + "allow_none": true, + "default": null, + "help": "", + "name": "max", + "type": "Datetime" + }, + { + "allow_none": true, + "default": null, + "help": "", + "name": "min", + "type": "Datetime" + }, + { + "default": "reference to new instance", + "help": "Styling customizations", + "name": "style", + "type": "reference", + "widget": "DescriptionStyle" + }, + { + "allow_none": true, + "default": null, + "help": "Is widget tabbable?", + "name": "tabbable", + "type": "bool" + }, + { + "allow_none": true, + "default": null, + "help": "A tooltip caption.", + "name": "tooltip", + "type": "string" + }, + { + "allow_none": true, + "default": null, + "help": "", + "name": "value", + "type": "Datetime" + } + ], + "model": { + "module": "@jupyter-widgets/controls", + "name": "DatetimeModel", + "version": "2.0.0" + }, + "view": { + "module": "@jupyter-widgets/controls", + "name": "DatetimeView", + "version": "2.0.0" + } + }, { "attributes": [ { @@ -3087,7 +3347,7 @@ "type": "string" }, { - "default": "GridBoxModel", + "default": "FloatsInputModel", "help": "", "name": "_model_name", "type": "string" @@ -3105,33 +3365,171 @@ "type": "string" }, { - "default": "GridBoxView", + "default": "FloatsInputView", "help": "", "name": "_view_name", "type": "string" }, { - "default": "", - "enum": ["success", "info", "warning", "danger", ""], - "help": "Use a predefined styling for the box.", - "name": "box_style", - "type": "string" + "default": true, + "help": "", + "name": "allow_duplicates", + "type": "bool" }, { "default": [], - "help": "List of widget children", - "items": { - "type": "reference", - "widget": "Widget" - }, - "name": "children", + "help": "", + "name": "allowed_tags", "type": "array" }, { - "default": "reference to new instance", - "help": "", - "name": "layout", - "type": "reference", + "default": "", + "help": "Description of the control.", + "name": "description", + "type": "string" + }, + { + "default": ".1f", + "help": "", + "name": "format", + "type": "string" + }, + { + "default": "reference to new instance", + "help": "", + "name": "layout", + "type": "reference", + "widget": "Layout" + }, + { + "allow_none": true, + "default": null, + "help": "", + "name": "max", + "type": "float" + }, + { + "allow_none": true, + "default": null, + "help": "", + "name": "min", + "type": "float" + }, + { + "default": "reference to new instance", + "help": "Styling customizations", + "name": "style", + "type": "reference", + "widget": "DescriptionStyle" + }, + { + "allow_none": true, + "default": null, + "help": "Is widget tabbable?", + "name": "tabbable", + "type": "bool" + }, + { + "default": "", + "enum": ["primary", "success", "info", "warning", "danger", ""], + "help": "Use a predefined styling for the tags.", + "name": "tag_style", + "type": "string" + }, + { + "allow_none": true, + "default": null, + "help": "A tooltip caption.", + "name": "tooltip", + "type": "string" + }, + { + "default": [], + "help": "List of float tags", + "name": "value", + "type": "array" + } + ], + "model": { + "module": "@jupyter-widgets/controls", + "name": "FloatsInputModel", + "version": "2.0.0" + }, + "view": { + "module": "@jupyter-widgets/controls", + "name": "FloatsInputView", + "version": "2.0.0" + } + }, + { + "attributes": [ + { + "default": [], + "help": "CSS classes applied to widget DOM element", + "items": { + "type": "string" + }, + "name": "_dom_classes", + "type": "array" + }, + { + "default": "@jupyter-widgets/controls", + "help": "", + "name": "_model_module", + "type": "string" + }, + { + "default": "2.0.0", + "help": "", + "name": "_model_module_version", + "type": "string" + }, + { + "default": "GridBoxModel", + "help": "", + "name": "_model_name", + "type": "string" + }, + { + "default": "@jupyter-widgets/controls", + "help": "", + "name": "_view_module", + "type": "string" + }, + { + "default": "2.0.0", + "help": "", + "name": "_view_module_version", + "type": "string" + }, + { + "default": "GridBoxView", + "help": "", + "name": "_view_name", + "type": "string" + }, + { + "default": "", + "enum": ["success", "info", "warning", "danger", ""], + "help": "Use a predefined styling for the box.", + "name": "box_style", + "type": "string" + }, + { + "default": [], + "help": "List of widget children", + "items": { + "type": "reference", + "widget": "Widget" + }, + "name": "children", + "type": "array" + }, + { + "default": "reference to new instance", + "help": "", + "name": "layout", + "type": "reference", "widget": "Layout" }, { @@ -4109,6 +4507,144 @@ "version": "2.0.0" } }, + { + "attributes": [ + { + "default": [], + "help": "CSS classes applied to widget DOM element", + "items": { + "type": "string" + }, + "name": "_dom_classes", + "type": "array" + }, + { + "default": "@jupyter-widgets/controls", + "help": "", + "name": "_model_module", + "type": "string" + }, + { + "default": "2.0.0", + "help": "", + "name": "_model_module_version", + "type": "string" + }, + { + "default": "IntsInputModel", + "help": "", + "name": "_model_name", + "type": "string" + }, + { + "default": "@jupyter-widgets/controls", + "help": "", + "name": "_view_module", + "type": "string" + }, + { + "default": "2.0.0", + "help": "", + "name": "_view_module_version", + "type": "string" + }, + { + "default": "IntsInputView", + "help": "", + "name": "_view_name", + "type": "string" + }, + { + "default": true, + "help": "", + "name": "allow_duplicates", + "type": "bool" + }, + { + "default": [], + "help": "", + "name": "allowed_tags", + "type": "array" + }, + { + "default": "", + "help": "Description of the control.", + "name": "description", + "type": "string" + }, + { + "default": ".3g", + "help": "", + "name": "format", + "type": "string" + }, + { + "default": "reference to new instance", + "help": "", + "name": "layout", + "type": "reference", + "widget": "Layout" + }, + { + "allow_none": true, + "default": null, + "help": "", + "name": "max", + "type": "int" + }, + { + "allow_none": true, + "default": null, + "help": "", + "name": "min", + "type": "int" + }, + { + "default": "reference to new instance", + "help": "Styling customizations", + "name": "style", + "type": "reference", + "widget": "DescriptionStyle" + }, + { + "allow_none": true, + "default": null, + "help": "Is widget tabbable?", + "name": "tabbable", + "type": "bool" + }, + { + "default": "", + "enum": ["primary", "success", "info", "warning", "danger", ""], + "help": "Use a predefined styling for the tags.", + "name": "tag_style", + "type": "string" + }, + { + "allow_none": true, + "default": null, + "help": "A tooltip caption.", + "name": "tooltip", + "type": "string" + }, + { + "default": [], + "help": "List of int tags", + "name": "value", + "type": "array" + } + ], + "model": { + "module": "@jupyter-widgets/controls", + "name": "IntsInputModel", + "version": "2.0.0" + }, + "view": { + "module": "@jupyter-widgets/controls", + "name": "IntsInputView", + "version": "2.0.0" + } + }, { "attributes": [ { @@ -5518,6 +6054,124 @@ "version": "2.0.0" } }, + { + "attributes": [ + { + "default": [], + "help": "CSS classes applied to widget DOM element", + "items": { + "type": "string" + }, + "name": "_dom_classes", + "type": "array" + }, + { + "default": "@jupyter-widgets/controls", + "help": "", + "name": "_model_module", + "type": "string" + }, + { + "default": "2.0.0", + "help": "", + "name": "_model_module_version", + "type": "string" + }, + { + "default": "TagsInputModel", + "help": "", + "name": "_model_name", + "type": "string" + }, + { + "default": "@jupyter-widgets/controls", + "help": "", + "name": "_view_module", + "type": "string" + }, + { + "default": "2.0.0", + "help": "", + "name": "_view_module_version", + "type": "string" + }, + { + "default": "TagsInputView", + "help": "", + "name": "_view_name", + "type": "string" + }, + { + "default": true, + "help": "", + "name": "allow_duplicates", + "type": "bool" + }, + { + "default": [], + "help": "", + "name": "allowed_tags", + "type": "array" + }, + { + "default": "", + "help": "Description of the control.", + "name": "description", + "type": "string" + }, + { + "default": "reference to new instance", + "help": "", + "name": "layout", + "type": "reference", + "widget": "Layout" + }, + { + "default": "reference to new instance", + "help": "Styling customizations", + "name": "style", + "type": "reference", + "widget": "DescriptionStyle" + }, + { + "allow_none": true, + "default": null, + "help": "Is widget tabbable?", + "name": "tabbable", + "type": "bool" + }, + { + "default": "", + "enum": ["primary", "success", "info", "warning", "danger", ""], + "help": "Use a predefined styling for the tags.", + "name": "tag_style", + "type": "string" + }, + { + "allow_none": true, + "default": null, + "help": "A tooltip caption.", + "name": "tooltip", + "type": "string" + }, + { + "default": [], + "help": "List of string tags", + "name": "value", + "type": "array" + } + ], + "model": { + "module": "@jupyter-widgets/controls", + "name": "TagsInputModel", + "version": "2.0.0" + }, + "view": { + "module": "@jupyter-widgets/controls", + "name": "TagsInputView", + "version": "2.0.0" + } + }, { "attributes": [ { @@ -5759,6 +6413,141 @@ "version": "2.0.0" } }, + { + "attributes": [ + { + "default": [], + "help": "CSS classes applied to widget DOM element", + "items": { + "type": "string" + }, + "name": "_dom_classes", + "type": "array" + }, + { + "default": "@jupyter-widgets/controls", + "help": "", + "name": "_model_module", + "type": "string" + }, + { + "default": "2.0.0", + "help": "", + "name": "_model_module_version", + "type": "string" + }, + { + "default": "TimeModel", + "help": "", + "name": "_model_name", + "type": "string" + }, + { + "default": "@jupyter-widgets/controls", + "help": "", + "name": "_view_module", + "type": "string" + }, + { + "default": "2.0.0", + "help": "", + "name": "_view_module_version", + "type": "string" + }, + { + "default": "TimeView", + "help": "", + "name": "_view_name", + "type": "string" + }, + { + "default": "", + "help": "Description of the control.", + "name": "description", + "type": "string" + }, + { + "default": false, + "help": "Enable or disable user changes.", + "name": "disabled", + "type": "bool" + }, + { + "default": "reference to new instance", + "help": "", + "name": "layout", + "type": "reference", + "widget": "Layout" + }, + { + "allow_none": true, + "default": null, + "help": "", + "name": "max", + "type": "Time" + }, + { + "allow_none": true, + "default": null, + "help": "", + "name": "min", + "type": "Time" + }, + { + "default": 60, + "help": "The time step to use for the picker, in seconds, or \"any\".", + "name": "step", + "type": ["float", "string"], + "union_attributes": [ + { + "type": "float" + }, + { + "enum": ["any"], + "type": "string" + } + ] + }, + { + "default": "reference to new instance", + "help": "Styling customizations", + "name": "style", + "type": "reference", + "widget": "DescriptionStyle" + }, + { + "allow_none": true, + "default": null, + "help": "Is widget tabbable?", + "name": "tabbable", + "type": "bool" + }, + { + "allow_none": true, + "default": null, + "help": "A tooltip caption.", + "name": "tooltip", + "type": "string" + }, + { + "allow_none": true, + "default": null, + "help": "", + "name": "value", + "type": "Time" + } + ], + "model": { + "module": "@jupyter-widgets/controls", + "name": "TimeModel", + "version": "2.0.0" + }, + "view": { + "module": "@jupyter-widgets/controls", + "name": "TimeView", + "version": "2.0.0" + } + }, { "attributes": [ { diff --git a/packages/schema/jupyterwidgetmodels.latest.md b/packages/schema/jupyterwidgetmodels.latest.md index 873fc33ede..904945d691 100644 --- a/packages/schema/jupyterwidgetmodels.latest.md +++ b/packages/schema/jupyterwidgetmodels.latest.md @@ -369,11 +369,35 @@ Attribute | Type | Default | Help `description` | string | `''` | Description of the control. `disabled` | boolean | `false` | Enable or disable user changes. `layout` | reference to Layout widget | reference to new instance | +`max` | `null` or Date | `null` | +`min` | `null` or Date | `null` | +`step` | number (integer) or string (one of `'any'`) | `1` | The date step to use for the picker, in days, or "any". `style` | reference to DescriptionStyle widget | reference to new instance | Styling customizations `tabbable` | `null` or boolean | `null` | Is widget tabbable? `tooltip` | `null` or string | `null` | A tooltip caption. `value` | `null` or Date | `null` | +### DatetimeModel (@jupyter-widgets/controls, 2.0.0); DatetimeView (@jupyter-widgets/controls, 2.0.0) + +Attribute | Type | Default | Help +-----------------|------------------|------------------|---- +`_dom_classes` | array of string | `[]` | CSS classes applied to widget DOM element +`_model_module` | string | `'@jupyter-widgets/controls'` | +`_model_module_version` | string | `'2.0.0'` | +`_model_name` | string | `'DatetimeModel'` | +`_view_module` | string | `'@jupyter-widgets/controls'` | +`_view_module_version` | string | `'2.0.0'` | +`_view_name` | string | `'DatetimeView'` | +`description` | string | `''` | Description of the control. +`disabled` | boolean | `false` | Enable or disable user changes. +`layout` | reference to Layout widget | reference to new instance | +`max` | `null` or Datetime | `null` | +`min` | `null` or Datetime | `null` | +`style` | reference to DescriptionStyle widget | reference to new instance | Styling customizations +`tabbable` | `null` or boolean | `null` | Is widget tabbable? +`tooltip` | `null` or string | `null` | A tooltip caption. +`value` | `null` or Datetime | `null` | + ### DescriptionStyleModel (@jupyter-widgets/controls, 2.0.0); StyleView (@jupyter-widgets/base, 2.0.0) Attribute | Type | Default | Help @@ -1114,6 +1138,28 @@ Attribute | Type | Default | Help `tooltip` | `null` or string | `null` | A tooltip caption. `value` | string | `''` | String value +### TimeModel (@jupyter-widgets/controls, 2.0.0); TimeView (@jupyter-widgets/controls, 2.0.0) + +Attribute | Type | Default | Help +-----------------|------------------|------------------|---- +`_dom_classes` | array of string | `[]` | CSS classes applied to widget DOM element +`_model_module` | string | `'@jupyter-widgets/controls'` | +`_model_module_version` | string | `'2.0.0'` | +`_model_name` | string | `'TimeModel'` | +`_view_module` | string | `'@jupyter-widgets/controls'` | +`_view_module_version` | string | `'2.0.0'` | +`_view_name` | string | `'TimeView'` | +`description` | string | `''` | Description of the control. +`disabled` | boolean | `false` | Enable or disable user changes. +`layout` | reference to Layout widget | reference to new instance | +`max` | `null` or Time | `null` | +`min` | `null` or Time | `null` | +`step` | number (float) or string (one of `'any'`) | `60` | The time step to use for the picker, in seconds, or "any". +`style` | reference to DescriptionStyle widget | reference to new instance | Styling customizations +`tabbable` | `null` or boolean | `null` | Is widget tabbable? +`tooltip` | `null` or string | `null` | A tooltip caption. +`value` | `null` or Time | `null` | + ### ToggleButtonModel (@jupyter-widgets/controls, 2.0.0); ToggleButtonView (@jupyter-widgets/controls, 2.0.0) Attribute | Type | Default | Help From d29affc92ae86cf7cc4d8bf003ad81758b8f9d5b Mon Sep 17 00:00:00 2001 From: Vidar Tonaas Fauske Date: Mon, 20 Jan 2020 15:48:15 +0000 Subject: [PATCH 4/9] Add time + datetime pickers to Widget list example --- docs/source/examples/Widget List.ipynb | 2870 ++++++++++++------------ 1 file changed, 1460 insertions(+), 1410 deletions(-) diff --git a/docs/source/examples/Widget List.ipynb b/docs/source/examples/Widget List.ipynb index b4e19ecdca..3a46bc955b 100644 --- a/docs/source/examples/Widget List.ipynb +++ b/docs/source/examples/Widget List.ipynb @@ -1,1410 +1,1460 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "nbsphinx": "hidden" - }, - "source": [ - "[Index](Index.ipynb) - [Back](Widget Basics.ipynb) - [Next](Output Widget.ipynb)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Widget List" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import ipywidgets as widgets" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "slideshow": { - "slide_type": "slide" - } - }, - "source": [ - "## Numeric widgets" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "There are many widgets distributed with ipywidgets that are designed to display numeric values. Widgets exist for displaying integers and floats, both bounded and unbounded. The integer widgets share a similar naming scheme to their floating point counterparts. By replacing `Float` with `Int` in the widget name, you can find the Integer equivalent." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### IntSlider \n", - "- The slider is displayed with a specified, initial `value`. Lower and upper bounds are defined by `min` and `max`, and the value can be incremented according to the `step` parameter.\n", - "- The slider's label is defined by `description` parameter \n", - "- The slider's `orientation` is either 'horizontal' (default) or 'vertical'\n", - "- `readout` displays the current value of the slider next to it. The options are **True** (default) or **False** \n", - " - `readout_format` specifies the format function used to represent slider value. The default is '.2f'\n", - " " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "widgets.IntSlider(\n", - " value=7,\n", - " min=0,\n", - " max=10,\n", - " step=1,\n", - " description='Test:',\n", - " disabled=False,\n", - " continuous_update=False,\n", - " orientation='horizontal',\n", - " readout=True,\n", - " readout_format='d'\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "slideshow": { - "slide_type": "slide" - } - }, - "source": [ - "### FloatSlider " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "widgets.FloatSlider(\n", - " value=7.5,\n", - " min=0,\n", - " max=10.0,\n", - " step=0.1,\n", - " description='Test:',\n", - " disabled=False,\n", - " continuous_update=False,\n", - " orientation='horizontal',\n", - " readout=True,\n", - " readout_format='.1f',\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "An example of sliders **displayed vertically**." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "widgets.FloatSlider(\n", - " value=7.5,\n", - " min=0,\n", - " max=10.0,\n", - " step=0.1,\n", - " description='Test:',\n", - " disabled=False,\n", - " continuous_update=False,\n", - " orientation='vertical',\n", - " readout=True,\n", - " readout_format='.1f',\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### FloatLogSlider" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The `FloatLogSlider` has a log scale, which makes it easy to have a slider that covers a wide range of positive magnitudes. The `min` and `max` refer to the minimum and maximum exponents of the `base`, and the `value` refers to the actual value of the slider." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "widgets.FloatLogSlider(\n", - " value=10,\n", - " base=10,\n", - " min=-10, # max exponent of base\n", - " max=10, # min exponent of base\n", - " step=0.2, # exponent step\n", - " description='Log Slider'\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### IntRangeSlider" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "widgets.IntRangeSlider(\n", - " value=[5, 7],\n", - " min=0,\n", - " max=10,\n", - " step=1,\n", - " description='Test:',\n", - " disabled=False,\n", - " continuous_update=False,\n", - " orientation='horizontal',\n", - " readout=True,\n", - " readout_format='d',\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### FloatRangeSlider" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "widgets.FloatRangeSlider(\n", - " value=[5, 7.5],\n", - " min=0,\n", - " max=10.0,\n", - " step=0.1,\n", - " description='Test:',\n", - " disabled=False,\n", - " continuous_update=False,\n", - " orientation='horizontal',\n", - " readout=True,\n", - " readout_format='.1f',\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### IntProgress" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "widgets.IntProgress(\n", - " value=7,\n", - " min=0,\n", - " max=10,\n", - " description='Loading:',\n", - " bar_style='', # 'success', 'info', 'warning', 'danger' or ''\n", - " style={'bar_color': 'maroon'},\n", - " orientation='horizontal'\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "slideshow": { - "slide_type": "slide" - } - }, - "source": [ - "### FloatProgress" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "widgets.FloatProgress(\n", - " value=7.5,\n", - " min=0,\n", - " max=10.0,\n", - " description='Loading:',\n", - " bar_style='info',\n", - " style={'bar_color': '#ffff00'},\n", - " orientation='horizontal'\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The numerical text boxes that impose some limit on the data (range, integer-only) impose that restriction when the user presses enter.\n", - "\n", - "### BoundedIntText" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "widgets.BoundedIntText(\n", - " value=7,\n", - " min=0,\n", - " max=10,\n", - " step=1,\n", - " description='Text:',\n", - " disabled=False\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "slideshow": { - "slide_type": "slide" - } - }, - "source": [ - "### BoundedFloatText" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "widgets.BoundedFloatText(\n", - " value=7.5,\n", - " min=0,\n", - " max=10.0,\n", - " step=0.1,\n", - " description='Text:',\n", - " disabled=False\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### IntText" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "widgets.IntText(\n", - " value=7,\n", - " description='Any:',\n", - " disabled=False\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "slideshow": { - "slide_type": "slide" - } - }, - "source": [ - "### FloatText" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "widgets.FloatText(\n", - " value=7.5,\n", - " description='Any:',\n", - " disabled=False\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "slideshow": { - "slide_type": "slide" - } - }, - "source": [ - "## Boolean widgets" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "There are three widgets that are designed to display a boolean value." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### ToggleButton" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "widgets.ToggleButton(\n", - " value=False,\n", - " description='Click me',\n", - " disabled=False,\n", - " button_style='', # 'success', 'info', 'warning', 'danger' or ''\n", - " tooltip='Description',\n", - " icon='check' # (FontAwesome names without the `fa-` prefix)\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "slideshow": { - "slide_type": "slide" - } - }, - "source": [ - "### Checkbox \n", - "- `value` specifies the value of the checkbox\n", - "- `indent` parameter places an indented checkbox, aligned with other controls. Options are **True** (default) or **False** \n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "widgets.Checkbox(\n", - " value=False,\n", - " description='Check me',\n", - " disabled=False,\n", - " indent=False\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Valid\n", - "\n", - "The valid widget provides a read-only indicator." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "widgets.Valid(\n", - " value=False,\n", - " description='Valid!',\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "slideshow": { - "slide_type": "slide" - } - }, - "source": [ - "## Selection widgets" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "There are several widgets that can be used to display single selection lists, and two that can be used to select multiple values. All inherit from the same base class. You can specify the **enumeration of selectable options by passing a list** (options are either (label, value) pairs, or simply values for which the labels are derived by calling `str`).\n", - "\n", - "
\n", - "Changes in *ipywidgets 8*:\n", - " \n", - "Selection widgets no longer accept a dictionary of options. Pass a list of key-value pairs instead.\n", - "
" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "slideshow": { - "slide_type": "slide" - } - }, - "source": [ - "### Dropdown" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "widgets.Dropdown(\n", - " options=['1', '2', '3'],\n", - " value='2',\n", - " description='Number:',\n", - " disabled=False,\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The following is also valid, displaying the words `'One', 'Two', 'Three'` as the dropdown choices but returning the values `1, 2, 3`." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "widgets.Dropdown(\n", - " options=[('One', 1), ('Two', 2), ('Three', 3)],\n", - " value=2,\n", - " description='Number:',\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "slideshow": { - "slide_type": "slide" - } - }, - "source": [ - "### RadioButtons" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "widgets.RadioButtons(\n", - " options=['pepperoni', 'pineapple', 'anchovies'],\n", - "# value='pineapple', # Defaults to 'pineapple'\n", - "# layout={'width': 'max-content'}, # If the items' names are long\n", - " description='Pizza topping:',\n", - " disabled=False\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### With dynamic layout and very long labels" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "widgets.Box(\n", - " [\n", - " widgets.Label(value='Pizza topping with a very long label:'), \n", - " widgets.RadioButtons(\n", - " options=[\n", - " 'pepperoni', \n", - " 'pineapple', \n", - " 'anchovies', \n", - " 'and the long name that will fit fine and the long name that will fit fine and the long name that will fit fine '\n", - " ],\n", - " layout={'width': 'max-content'}\n", - " )\n", - " ]\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "slideshow": { - "slide_type": "slide" - } - }, - "source": [ - "### Select" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "widgets.Select(\n", - " options=['Linux', 'Windows', 'OSX'],\n", - " value='OSX',\n", - " # rows=10,\n", - " description='OS:',\n", - " disabled=False\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### SelectionSlider" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "widgets.SelectionSlider(\n", - " options=['scrambled', 'sunny side up', 'poached', 'over easy'],\n", - " value='sunny side up',\n", - " description='I like my eggs ...',\n", - " disabled=False,\n", - " continuous_update=False,\n", - " orientation='horizontal',\n", - " readout=True\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### SelectionRangeSlider\n", - "\n", - "The value, index, and label keys are 2-tuples of the min and max values selected. The options must be nonempty." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import datetime\n", - "dates = [datetime.date(2015, i, 1) for i in range(1, 13)]\n", - "options = [(i.strftime('%b'), i) for i in dates]\n", - "widgets.SelectionRangeSlider(\n", - " options=options,\n", - " index=(0, 11),\n", - " description='Months (2015)',\n", - " disabled=False\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "slideshow": { - "slide_type": "slide" - } - }, - "source": [ - "### ToggleButtons" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "widgets.ToggleButtons(\n", - " options=['Slow', 'Regular', 'Fast'],\n", - " description='Speed:',\n", - " disabled=False,\n", - " button_style='', # 'success', 'info', 'warning', 'danger' or ''\n", - " tooltips=['Description of slow', 'Description of regular', 'Description of fast'],\n", - "# icons=['check'] * 3\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### SelectMultiple\n", - "Multiple values can be selected with shift and/or ctrl (or command) pressed and mouse clicks or arrow keys." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "widgets.SelectMultiple(\n", - " options=['Apples', 'Oranges', 'Pears'],\n", - " value=['Oranges'],\n", - " #rows=10,\n", - " description='Fruits',\n", - " disabled=False\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "slideshow": { - "slide_type": "slide" - } - }, - "source": [ - "## String widgets" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "There are several widgets that can be used to display a string value. The `Text`, `Textarea`, and `Combobox` widgets accept input. The `HTML` and `HTMLMath` widgets display a string as HTML (`HTMLMath` also renders math). The `Label` widget can be used to construct a custom control label." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "slideshow": { - "slide_type": "slide" - } - }, - "source": [ - "### Text" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "widgets.Text(\n", - " value='Hello World',\n", - " placeholder='Type something',\n", - " description='String:',\n", - " disabled=False \n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Textarea" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "widgets.Textarea(\n", - " value='Hello World',\n", - " placeholder='Type something',\n", - " description='String:',\n", - " disabled=False\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Combobox" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "widgets.Combobox(\n", - " # value='John',\n", - " placeholder='Choose Someone',\n", - " options=['Paul', 'John', 'George', 'Ringo'],\n", - " description='Combobox:',\n", - " ensure_option=True,\n", - " disabled=False\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Password\n", - "\n", - "The `Password` widget hides user input on the screen. **This widget is not a secure way to collect sensitive information because:**\n", - "\n", - "+ The contents of the `Password` widget are transmitted unencrypted.\n", - "+ If the widget state is saved in the notebook the contents of the `Password` widget is stored as plain text." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "widgets.Password(\n", - " value='password',\n", - " placeholder='Enter password',\n", - " description='Password:',\n", - " disabled=False\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "slideshow": { - "slide_type": "slide" - } - }, - "source": [ - "### Label\n", - "\n", - "The `Label` widget is useful if you need to build a custom description next to a control using similar styling to the built-in control descriptions." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "widgets.HBox([widgets.Label(value=\"The $m$ in $E=mc^2$:\"), widgets.FloatSlider()])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### HTML" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "widgets.HTML(\n", - " value=\"Hello World\",\n", - " placeholder='Some HTML',\n", - " description='Some HTML',\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### HTML Math" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "widgets.HTMLMath(\n", - " value=r\"Some math and HTML: \\(x^2\\) and $$\\frac{x+1}{x-1}$$\",\n", - " placeholder='Some HTML',\n", - " description='Some HTML',\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Image" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "file = open(\"images/WidgetArch.png\", \"rb\")\n", - "image = file.read()\n", - "widgets.Image(\n", - " value=image,\n", - " format='png',\n", - " width=300,\n", - " height=400,\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "slideshow": { - "slide_type": "slide" - } - }, - "source": [ - "## Button" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "button = widgets.Button(\n", - " description='Click me',\n", - " disabled=False,\n", - " button_style='', # 'success', 'info', 'warning', 'danger' or ''\n", - " tooltip='Click me',\n", - " icon='check' # (FontAwesome names without the `fa-` prefix)\n", - ")\n", - "button" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The `icon` attribute can be used to define an icon; see the [fontawesome](https://fontawesome.com/icons) page for available icons. \n", - "A callback function `foo` can be registered using `button.on_click(foo)`. The function `foo` will be called when the button is clicked with the button instance as its single argument." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Output\n", - "\n", - "The `Output` widget can capture and display stdout, stderr and [rich output generated by IPython](http://ipython.readthedocs.io/en/stable/api/generated/IPython.display.html#module-IPython.display). For detailed documentation, see the [output widget examples](https://ipywidgets.readthedocs.io/en/latest/examples/Output Widget.html)." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Play (Animation) widget" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The `Play` widget is useful to perform animations by iterating on a sequence of integers with a certain speed. The value of the slider below is linked to the player." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "play = widgets.Play(\n", - " value=50,\n", - " min=0,\n", - " max=100,\n", - " step=1,\n", - " interval=500,\n", - " description=\"Press play\",\n", - " disabled=False\n", - ")\n", - "slider = widgets.IntSlider()\n", - "widgets.jslink((play, 'value'), (slider, 'value'))\n", - "widgets.HBox([play, slider])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Date picker\n", - "\n", - "The date picker widget works in Chrome, Firefox and IE Edge, but does not currently work in Safari because it does not support the HTML date input field." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "widgets.DatePicker(\n", - " description='Pick a Date',\n", - " disabled=False\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Color picker" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "widgets.ColorPicker(\n", - " concise=False,\n", - " description='Pick a color',\n", - " value='blue',\n", - " disabled=False\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## File Upload\n", - "\n", - "The `FileUpload` allows to upload any type of file(s) into memory in the kernel." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "widgets.FileUpload(\n", - " accept='', # Accepted file extension e.g. '.txt', '.pdf', 'image/*', 'image/*,.pdf'\n", - " multiple=False # True to accept multiple files upload else False\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The upload widget exposes a `value` attribute that contains the files uploaded. The value attribute is a tuple with a dictionary for each uploaded file. For instance:\n", - "\n", - "```python\n", - "uploader = widgets.FileUpload()\n", - "display(uploader)\n", - "\n", - "# upload something...\n", - "\n", - "# once a file is uploaded, use the `.value` attribute to retrieve the content:\n", - "uploader.value\n", - "#=> (\n", - "#=> {\n", - "#=> 'name': 'example.txt',\n", - "#=> 'type': 'text/plain',\n", - "#=> 'size': 36,\n", - "#=> 'last_modified': datetime.datetime(2020, 1, 9, 15, 58, 43, 321000, tzinfo=datetime.timezone.utc), \n", - "#=> 'content': \n", - "#=> },\n", - "#=> )\n", - "```\n", - "\n", - "Entries in the dictionary can be accessed either as items, as one would any dictionary, or as attributes:\n", - "\n", - "```\n", - "uploaded_file = uploader.value[0]\n", - "uploaded_file[\"size\"]\n", - "#=> 36\n", - "uploaded_file.size\n", - "#=> 36\n", - "```\n", - "\n", - "The contents of the file uploaded are in the value of the `content` key. They are a [memory view](https://docs.python.org/3/library/stdtypes.html#memory-views):\n", - "\n", - "```python\n", - "uploaded_file.content\n", - "#=> \n", - "```\n", - "\n", - "You can extract the content to bytes:\n", - "\n", - "```python\n", - "uploaded_file.content.tobytes()\n", - "#=> b'This is the content of example.txt.\\n'\n", - "```\n", - "\n", - "If the file is a text file, you can get the contents as a string by [decoding it](https://docs.python.org/3/library/codecs.html):\n", - "\n", - "```python\n", - "import codecs\n", - "codecs.decode(uploaded_file.content, encoding=\"utf-8\")\n", - "#=> 'This is the content of example.txt.\\n'\n", - "```\n", - "\n", - "You can save the uploaded file to the filesystem from the kernel:\n", - "\n", - "```python\n", - "with open(\"./saved-output.txt\", \"wb\") as fp:\n", - " fp.write(uploaded_file.content)\n", - "```\n", - "\n", - "To convert the uploaded file into a Pandas dataframe, you can use a [BytesIO object](https://docs.python.org/3/library/io.html#binary-i-o):\n", - "\n", - "```python\n", - "import io\n", - "import pandas as pd\n", - "pd.read_csv(io.BytesIO(uploaded_file.content))\n", - "```\n", - "\n", - "If the uploaded file is an image, you can visualize it with an [image](#Image) widget:\n", - "\n", - "```python\n", - "widgets.Image(value=uploaded_file.content.tobytes())\n", - "```\n", - "\n", - "
\n", - "Changes in *ipywidgets 8*:\n", - " \n", - "The `FileUpload` changed significantly in ipywidgets 8:\n", - " \n", - "- The `.value` traitlet is now a list of dictionaries, rather than a dictionary mapping the uploaded name to the content. To retrieve the original form, use `{f[\"name\"]: f.content.tobytes() for f in uploader.value}`.\n", - "- The `.data` traitlet has been removed. To retrieve it, use `[f.content.tobytes() for f in uploader.value]`.\n", - "- The `.metadata` traitlet has been removed. To retrieve it, use `[{k: v for k, v in f.items() if k != \"content\"} for f in w.value]`.\n", - "
\n", - "\n", - "
\n", - "Warning: When using the `FileUpload` Widget, uploaded file content might be saved in the notebook if widget state is saved.\n", - "
" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Controller\n", - "\n", - "The `Controller` allows a game controller to be used as an input device." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "widgets.Controller(\n", - " index=0,\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Container/Layout widgets\n", - "\n", - "These widgets are used to hold other widgets, called children. Each has a `children` property that may be set either when the widget is created or later." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Box" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "items = [widgets.Label(str(i)) for i in range(4)]\n", - "widgets.Box(items)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### HBox" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "items = [widgets.Label(str(i)) for i in range(4)]\n", - "widgets.HBox(items)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### VBox" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "items = [widgets.Label(str(i)) for i in range(4)]\n", - "left_box = widgets.VBox([items[0], items[1]])\n", - "right_box = widgets.VBox([items[2], items[3]])\n", - "widgets.HBox([left_box, right_box])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### GridBox\n", - "\n", - "This box uses the HTML Grid specification to lay out its children in two dimensional grid. The example below lays out the 8 items inside in 3 columns and as many rows as needed to accommodate the items." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "items = [widgets.Label(str(i)) for i in range(8)]\n", - "widgets.GridBox(items, layout=widgets.Layout(grid_template_columns=\"repeat(3, 100px)\"))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Accordion" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "accordion = widgets.Accordion(children=[widgets.IntSlider(), widgets.Text()], titles=('Slider', 'Text'))\n", - "accordion" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Tabs\n", - "\n", - "In this example the children are set after the tab is created. Titles for the tabs are set in the same way they are for `Accordion`." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "tab_contents = ['P0', 'P1', 'P2', 'P3', 'P4']\n", - "children = [widgets.Text(description=name) for name in tab_contents]\n", - "tab = widgets.Tab()\n", - "tab.children = children\n", - "tab.titles = [str(i) for i in range(len(children))]\n", - "tab" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Stacked\n", - "\n", - "The `Stacked` widget can have multiple children widgets as for `Tab` and `Accordion`, but only shows one at a time depending on the value of ``selected_index``:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "button = widgets.Button(description='Click here')\n", - "slider = widgets.IntSlider()\n", - "stacked = widgets.Stacked([button, slider])\n", - "stacked # will show only the button" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "This can be used in combination with another selection-based widget to show different widgets depending on the selection:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "dropdown = widgets.Dropdown(options=['button', 'slider'])\n", - "widgets.jslink((dropdown, 'index'), (stacked, 'selected_index'))\n", - "widgets.VBox([dropdown, stacked])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Accordion, Tab, and Stacked use `selected_index`, not value\n", - "\n", - "Unlike the rest of the widgets discussed earlier, the container widgets `Accordion` and `Tab` update their `selected_index` attribute when the user changes which accordion or tab is selected. That means that you can both see what the user is doing *and* programmatically set what the user sees by setting the value of `selected_index`.\n", - "\n", - "Setting `selected_index = None` closes all of the accordions or deselects all tabs." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "In the cells below try displaying or setting the `selected_index` of the `tab` and/or `accordion`." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "tab.selected_index = 3" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "accordion.selected_index = None" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Nesting tabs and accordions\n", - "\n", - "Tabs and accordions can be nested as deeply as you want. If you have a few minutes, try nesting a few accordions or putting an accordion inside a tab or a tab inside an accordion. \n", - "\n", - "The example below makes a couple of tabs with an accordion children in one of them" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "tab_nest = widgets.Tab()\n", - "tab_nest.children = [accordion, accordion]\n", - "tab_nest.titles = ('An accordion', 'Copy of the accordion')\n", - "tab_nest" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "nbsphinx": "hidden" - }, - "source": [ - "[Index](Index.ipynb) - [Back](Widget Basics.ipynb) - [Next](Output Widget.ipynb)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.6.8" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "nbsphinx": "hidden" + }, + "source": [ + "[Index](Index.ipynb) - [Back](Widget Basics.ipynb) - [Next](Output Widget.ipynb)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Widget List" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import ipywidgets as widgets" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Numeric widgets" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "There are many widgets distributed with ipywidgets that are designed to display numeric values. Widgets exist for displaying integers and floats, both bounded and unbounded. The integer widgets share a similar naming scheme to their floating point counterparts. By replacing `Float` with `Int` in the widget name, you can find the Integer equivalent." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### IntSlider \n", + "- The slider is displayed with a specified, initial `value`. Lower and upper bounds are defined by `min` and `max`, and the value can be incremented according to the `step` parameter.\n", + "- The slider's label is defined by `description` parameter \n", + "- The slider's `orientation` is either 'horizontal' (default) or 'vertical'\n", + "- `readout` displays the current value of the slider next to it. The options are **True** (default) or **False** \n", + " - `readout_format` specifies the format function used to represent slider value. The default is '.2f'\n", + " " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "widgets.IntSlider(\n", + " value=7,\n", + " min=0,\n", + " max=10,\n", + " step=1,\n", + " description='Test:',\n", + " disabled=False,\n", + " continuous_update=False,\n", + " orientation='horizontal',\n", + " readout=True,\n", + " readout_format='d'\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### FloatSlider " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "widgets.FloatSlider(\n", + " value=7.5,\n", + " min=0,\n", + " max=10.0,\n", + " step=0.1,\n", + " description='Test:',\n", + " disabled=False,\n", + " continuous_update=False,\n", + " orientation='horizontal',\n", + " readout=True,\n", + " readout_format='.1f',\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "An example of sliders **displayed vertically**." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "widgets.FloatSlider(\n", + " value=7.5,\n", + " min=0,\n", + " max=10.0,\n", + " step=0.1,\n", + " description='Test:',\n", + " disabled=False,\n", + " continuous_update=False,\n", + " orientation='vertical',\n", + " readout=True,\n", + " readout_format='.1f',\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### FloatLogSlider" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The `FloatLogSlider` has a log scale, which makes it easy to have a slider that covers a wide range of positive magnitudes. The `min` and `max` refer to the minimum and maximum exponents of the `base`, and the `value` refers to the actual value of the slider." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "widgets.FloatLogSlider(\n", + " value=10,\n", + " base=10,\n", + " min=-10, # max exponent of base\n", + " max=10, # min exponent of base\n", + " step=0.2, # exponent step\n", + " description='Log Slider'\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### IntRangeSlider" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "widgets.IntRangeSlider(\n", + " value=[5, 7],\n", + " min=0,\n", + " max=10,\n", + " step=1,\n", + " description='Test:',\n", + " disabled=False,\n", + " continuous_update=False,\n", + " orientation='horizontal',\n", + " readout=True,\n", + " readout_format='d',\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### FloatRangeSlider" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "widgets.FloatRangeSlider(\n", + " value=[5, 7.5],\n", + " min=0,\n", + " max=10.0,\n", + " step=0.1,\n", + " description='Test:',\n", + " disabled=False,\n", + " continuous_update=False,\n", + " orientation='horizontal',\n", + " readout=True,\n", + " readout_format='.1f',\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### IntProgress" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "widgets.IntProgress(\n", + " value=7,\n", + " min=0,\n", + " max=10,\n", + " description='Loading:',\n", + " bar_style='', # 'success', 'info', 'warning', 'danger' or ''\n", + " style={'bar_color': 'maroon'},\n", + " orientation='horizontal'\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### FloatProgress" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "widgets.FloatProgress(\n", + " value=7.5,\n", + " min=0,\n", + " max=10.0,\n", + " description='Loading:',\n", + " bar_style='info',\n", + " style={'bar_color': '#ffff00'},\n", + " orientation='horizontal'\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The numerical text boxes that impose some limit on the data (range, integer-only) impose that restriction when the user presses enter.\n", + "\n", + "### BoundedIntText" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "widgets.BoundedIntText(\n", + " value=7,\n", + " min=0,\n", + " max=10,\n", + " step=1,\n", + " description='Text:',\n", + " disabled=False\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### BoundedFloatText" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "widgets.BoundedFloatText(\n", + " value=7.5,\n", + " min=0,\n", + " max=10.0,\n", + " step=0.1,\n", + " description='Text:',\n", + " disabled=False\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### IntText" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "widgets.IntText(\n", + " value=7,\n", + " description='Any:',\n", + " disabled=False\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### FloatText" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "widgets.FloatText(\n", + " value=7.5,\n", + " description='Any:',\n", + " disabled=False\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Boolean widgets" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "There are three widgets that are designed to display a boolean value." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### ToggleButton" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "widgets.ToggleButton(\n", + " value=False,\n", + " description='Click me',\n", + " disabled=False,\n", + " button_style='', # 'success', 'info', 'warning', 'danger' or ''\n", + " tooltip='Description',\n", + " icon='check' # (FontAwesome names without the `fa-` prefix)\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Checkbox \n", + "- `value` specifies the value of the checkbox\n", + "- `indent` parameter places an indented checkbox, aligned with other controls. Options are **True** (default) or **False** \n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "widgets.Checkbox(\n", + " value=False,\n", + " description='Check me',\n", + " disabled=False,\n", + " indent=False\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Valid\n", + "\n", + "The valid widget provides a read-only indicator." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "widgets.Valid(\n", + " value=False,\n", + " description='Valid!',\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Selection widgets" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "There are several widgets that can be used to display single selection lists, and two that can be used to select multiple values. All inherit from the same base class. You can specify the **enumeration of selectable options by passing a list** (options are either (label, value) pairs, or simply values for which the labels are derived by calling `str`).\n", + "\n", + "
\n", + "Changes in *ipywidgets 8*:\n", + " \n", + "Selection widgets no longer accept a dictionary of options. Pass a list of key-value pairs instead.\n", + "
" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Dropdown" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "widgets.Dropdown(\n", + " options=['1', '2', '3'],\n", + " value='2',\n", + " description='Number:',\n", + " disabled=False,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The following is also valid, displaying the words `'One', 'Two', 'Three'` as the dropdown choices but returning the values `1, 2, 3`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "widgets.Dropdown(\n", + " options=[('One', 1), ('Two', 2), ('Three', 3)],\n", + " value=2,\n", + " description='Number:',\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### RadioButtons" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "widgets.RadioButtons(\n", + " options=['pepperoni', 'pineapple', 'anchovies'],\n", + "# value='pineapple', # Defaults to 'pineapple'\n", + "# layout={'width': 'max-content'}, # If the items' names are long\n", + " description='Pizza topping:',\n", + " disabled=False\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### With dynamic layout and very long labels" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "widgets.Box(\n", + " [\n", + " widgets.Label(value='Pizza topping with a very long label:'), \n", + " widgets.RadioButtons(\n", + " options=[\n", + " 'pepperoni', \n", + " 'pineapple', \n", + " 'anchovies', \n", + " 'and the long name that will fit fine and the long name that will fit fine and the long name that will fit fine '\n", + " ],\n", + " layout={'width': 'max-content'}\n", + " )\n", + " ]\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Select" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "widgets.Select(\n", + " options=['Linux', 'Windows', 'OSX'],\n", + " value='OSX',\n", + " # rows=10,\n", + " description='OS:',\n", + " disabled=False\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### SelectionSlider" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "widgets.SelectionSlider(\n", + " options=['scrambled', 'sunny side up', 'poached', 'over easy'],\n", + " value='sunny side up',\n", + " description='I like my eggs ...',\n", + " disabled=False,\n", + " continuous_update=False,\n", + " orientation='horizontal',\n", + " readout=True\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### SelectionRangeSlider\n", + "\n", + "The value, index, and label keys are 2-tuples of the min and max values selected. The options must be nonempty." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import datetime\n", + "dates = [datetime.date(2015, i, 1) for i in range(1, 13)]\n", + "options = [(i.strftime('%b'), i) for i in dates]\n", + "widgets.SelectionRangeSlider(\n", + " options=options,\n", + " index=(0, 11),\n", + " description='Months (2015)',\n", + " disabled=False\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### ToggleButtons" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "widgets.ToggleButtons(\n", + " options=['Slow', 'Regular', 'Fast'],\n", + " description='Speed:',\n", + " disabled=False,\n", + " button_style='', # 'success', 'info', 'warning', 'danger' or ''\n", + " tooltips=['Description of slow', 'Description of regular', 'Description of fast'],\n", + "# icons=['check'] * 3\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### SelectMultiple\n", + "Multiple values can be selected with shift and/or ctrl (or command) pressed and mouse clicks or arrow keys." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "widgets.SelectMultiple(\n", + " options=['Apples', 'Oranges', 'Pears'],\n", + " value=['Oranges'],\n", + " #rows=10,\n", + " description='Fruits',\n", + " disabled=False\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## String widgets" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "There are several widgets that can be used to display a string value. The `Text`, `Textarea`, and `Combobox` widgets accept input. The `HTML` and `HTMLMath` widgets display a string as HTML (`HTMLMath` also renders math). The `Label` widget can be used to construct a custom control label." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Text" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "widgets.Text(\n", + " value='Hello World',\n", + " placeholder='Type something',\n", + " description='String:',\n", + " disabled=False \n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Textarea" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "widgets.Textarea(\n", + " value='Hello World',\n", + " placeholder='Type something',\n", + " description='String:',\n", + " disabled=False\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Combobox" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "widgets.Combobox(\n", + " # value='John',\n", + " placeholder='Choose Someone',\n", + " options=['Paul', 'John', 'George', 'Ringo'],\n", + " description='Combobox:',\n", + " ensure_option=True,\n", + " disabled=False\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Password\n", + "\n", + "The `Password` widget hides user input on the screen. **This widget is not a secure way to collect sensitive information because:**\n", + "\n", + "+ The contents of the `Password` widget are transmitted unencrypted.\n", + "+ If the widget state is saved in the notebook the contents of the `Password` widget is stored as plain text." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "widgets.Password(\n", + " value='password',\n", + " placeholder='Enter password',\n", + " description='Password:',\n", + " disabled=False\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Label\n", + "\n", + "The `Label` widget is useful if you need to build a custom description next to a control using similar styling to the built-in control descriptions." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "widgets.HBox([widgets.Label(value=\"The $m$ in $E=mc^2$:\"), widgets.FloatSlider()])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### HTML" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "widgets.HTML(\n", + " value=\"Hello World\",\n", + " placeholder='Some HTML',\n", + " description='Some HTML',\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### HTML Math" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "widgets.HTMLMath(\n", + " value=r\"Some math and HTML: \\(x^2\\) and $$\\frac{x+1}{x-1}$$\",\n", + " placeholder='Some HTML',\n", + " description='Some HTML',\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Image" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "file = open(\"images/WidgetArch.png\", \"rb\")\n", + "image = file.read()\n", + "widgets.Image(\n", + " value=image,\n", + " format='png',\n", + " width=300,\n", + " height=400,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Button" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "button = widgets.Button(\n", + " description='Click me',\n", + " disabled=False,\n", + " button_style='', # 'success', 'info', 'warning', 'danger' or ''\n", + " tooltip='Click me',\n", + " icon='check' # (FontAwesome names without the `fa-` prefix)\n", + ")\n", + "button" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The `icon` attribute can be used to define an icon; see the [fontawesome](https://fontawesome.com/icons) page for available icons. \n", + "A callback function `foo` can be registered using `button.on_click(foo)`. The function `foo` will be called when the button is clicked with the button instance as its single argument." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Output\n", + "\n", + "The `Output` widget can capture and display stdout, stderr and [rich output generated by IPython](http://ipython.readthedocs.io/en/stable/api/generated/IPython.display.html#module-IPython.display). For detailed documentation, see the [output widget examples](https://ipywidgets.readthedocs.io/en/latest/examples/Output Widget.html)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Play (Animation) widget" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The `Play` widget is useful to perform animations by iterating on a sequence of integers with a certain speed. The value of the slider below is linked to the player." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "play = widgets.Play(\n", + " value=50,\n", + " min=0,\n", + " max=100,\n", + " step=1,\n", + " interval=500,\n", + " description=\"Press play\",\n", + " disabled=False\n", + ")\n", + "slider = widgets.IntSlider()\n", + "widgets.jslink((play, 'value'), (slider, 'value'))\n", + "widgets.HBox([play, slider])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Date picker\n", + "\n", + "For a list of browsers that support the date picker widget, see the [MDN article for the HTML date input field](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/date#Browser_compatibility)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "widgets.DatePicker(\n", + " description='Pick a Date',\n", + " disabled=False\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Time picker\n", + "\n", + "For a list of browsers that support the time picker widget, see the [MDN article for the HTML time input field](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/time#Browser_compatibility)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "widgets.TimePicker(\n", + " description='Pick a Time',\n", + " disabled=False\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Datetime picker\n", + "\n", + "For a list of browsers that support the datetime picker widget, see the [MDN article for the HTML datetime-local input field](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/datetime-local#Browser_compatibility). For the browsers that do not support the datetime-local input, we try to fall back on displaying separate date and time inputs.\n", + "\n", + "### Time zones\n", + "\n", + "There are two points worth to note with regards to timezones for datetimes:\n", + "- The browser always picks datetimes using *its* timezone.\n", + "- The kernel always gets the datetimes in the default system timezone of the kernel (see https://docs.python.org/3/library/datetime.html#datetime.datetime.astimezone with `None` as the argument).\n", + "\n", + "This means that if the kernel and browser have different timezones, the default string serialization of the timezones might differ, but they will still represent the same point in time." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "widgets.DatetimePicker(\n", + " description='Pick a Time',\n", + " disabled=False\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Color picker" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "widgets.ColorPicker(\n", + " concise=False,\n", + " description='Pick a color',\n", + " value='blue',\n", + " disabled=False\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## File Upload\n", + "\n", + "The `FileUpload` allows to upload any type of file(s) into memory in the kernel." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "widgets.FileUpload(\n", + " accept='', # Accepted file extension e.g. '.txt', '.pdf', 'image/*', 'image/*,.pdf'\n", + " multiple=False # True to accept multiple files upload else False\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The upload widget exposes a `value` attribute that contains the files uploaded. The value attribute is a tuple with a dictionary for each uploaded file. For instance:\n", + "\n", + "```python\n", + "uploader = widgets.FileUpload()\n", + "display(uploader)\n", + "\n", + "# upload something...\n", + "\n", + "# once a file is uploaded, use the `.value` attribute to retrieve the content:\n", + "uploader.value\n", + "#=> (\n", + "#=> {\n", + "#=> 'name': 'example.txt',\n", + "#=> 'type': 'text/plain',\n", + "#=> 'size': 36,\n", + "#=> 'last_modified': datetime.datetime(2020, 1, 9, 15, 58, 43, 321000, tzinfo=datetime.timezone.utc), \n", + "#=> 'content': \n", + "#=> },\n", + "#=> )\n", + "```\n", + "\n", + "Entries in the dictionary can be accessed either as items, as one would any dictionary, or as attributes:\n", + "\n", + "```\n", + "uploaded_file = uploader.value[0]\n", + "uploaded_file[\"size\"]\n", + "#=> 36\n", + "uploaded_file.size\n", + "#=> 36\n", + "```\n", + "\n", + "The contents of the file uploaded are in the value of the `content` key. They are a [memory view](https://docs.python.org/3/library/stdtypes.html#memory-views):\n", + "\n", + "```python\n", + "uploaded_file.content\n", + "#=> \n", + "```\n", + "\n", + "You can extract the content to bytes:\n", + "\n", + "```python\n", + "uploaded_file.content.tobytes()\n", + "#=> b'This is the content of example.txt.\\n'\n", + "```\n", + "\n", + "If the file is a text file, you can get the contents as a string by [decoding it](https://docs.python.org/3/library/codecs.html):\n", + "\n", + "```python\n", + "import codecs\n", + "codecs.decode(uploaded_file.content, encoding=\"utf-8\")\n", + "#=> 'This is the content of example.txt.\\n'\n", + "```\n", + "\n", + "You can save the uploaded file to the filesystem from the kernel:\n", + "\n", + "```python\n", + "with open(\"./saved-output.txt\", \"wb\") as fp:\n", + " fp.write(uploaded_file.content)\n", + "```\n", + "\n", + "To convert the uploaded file into a Pandas dataframe, you can use a [BytesIO object](https://docs.python.org/3/library/io.html#binary-i-o):\n", + "\n", + "```python\n", + "import io\n", + "import pandas as pd\n", + "pd.read_csv(io.BytesIO(uploaded_file.content))\n", + "```\n", + "\n", + "If the uploaded file is an image, you can visualize it with an [image](#Image) widget:\n", + "\n", + "```python\n", + "widgets.Image(value=uploaded_file.content.tobytes())\n", + "```\n", + "\n", + "
\n", + "Changes in *ipywidgets 8*:\n", + " \n", + "The `FileUpload` changed significantly in ipywidgets 8:\n", + " \n", + "- The `.value` traitlet is now a list of dictionaries, rather than a dictionary mapping the uploaded name to the content. To retrieve the original form, use `{f[\"name\"]: f.content.tobytes() for f in uploader.value}`.\n", + "- The `.data` traitlet has been removed. To retrieve it, use `[f.content.tobytes() for f in uploader.value]`.\n", + "- The `.metadata` traitlet has been removed. To retrieve it, use `[{k: v for k, v in f.items() if k != \"content\"} for f in w.value]`.\n", + "
\n", + "\n", + "
\n", + "Warning: When using the `FileUpload` Widget, uploaded file content might be saved in the notebook if widget state is saved.\n", + "
" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Controller\n", + "\n", + "The `Controller` allows a game controller to be used as an input device." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "widgets.Controller(\n", + " index=0,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Container/Layout widgets\n", + "\n", + "These widgets are used to hold other widgets, called children. Each has a `children` property that may be set either when the widget is created or later." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Box" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "items = [widgets.Label(str(i)) for i in range(4)]\n", + "widgets.Box(items)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### HBox" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "items = [widgets.Label(str(i)) for i in range(4)]\n", + "widgets.HBox(items)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### VBox" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "items = [widgets.Label(str(i)) for i in range(4)]\n", + "left_box = widgets.VBox([items[0], items[1]])\n", + "right_box = widgets.VBox([items[2], items[3]])\n", + "widgets.HBox([left_box, right_box])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### GridBox\n", + "\n", + "This box uses the HTML Grid specification to lay out its children in two dimensional grid. The example below lays out the 8 items inside in 3 columns and as many rows as needed to accommodate the items." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "items = [widgets.Label(str(i)) for i in range(8)]\n", + "widgets.GridBox(items, layout=widgets.Layout(grid_template_columns=\"repeat(3, 100px)\"))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Accordion" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "accordion = widgets.Accordion(children=[widgets.IntSlider(), widgets.Text()], titles=('Slider', 'Text'))\n", + "accordion" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Tabs\n", + "\n", + "In this example the children are set after the tab is created. Titles for the tabs are set in the same way they are for `Accordion`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "tab_contents = ['P0', 'P1', 'P2', 'P3', 'P4']\n", + "children = [widgets.Text(description=name) for name in tab_contents]\n", + "tab = widgets.Tab()\n", + "tab.children = children\n", + "tab.titles = [str(i) for i in range(len(children))]\n", + "tab" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Stacked\n", + "\n", + "The `Stacked` widget can have multiple children widgets as for `Tab` and `Accordion`, but only shows one at a time depending on the value of ``selected_index``:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "button = widgets.Button(description='Click here')\n", + "slider = widgets.IntSlider()\n", + "stacked = widgets.Stacked([button, slider])\n", + "stacked # will show only the button" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This can be used in combination with another selection-based widget to show different widgets depending on the selection:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "dropdown = widgets.Dropdown(options=['button', 'slider'])\n", + "widgets.jslink((dropdown, 'index'), (stacked, 'selected_index'))\n", + "widgets.VBox([dropdown, stacked])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Accordion, Tab, and Stacked use `selected_index`, not value\n", + "\n", + "Unlike the rest of the widgets discussed earlier, the container widgets `Accordion` and `Tab` update their `selected_index` attribute when the user changes which accordion or tab is selected. That means that you can both see what the user is doing *and* programmatically set what the user sees by setting the value of `selected_index`.\n", + "\n", + "Setting `selected_index = None` closes all of the accordions or deselects all tabs." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In the cells below try displaying or setting the `selected_index` of the `tab` and/or `accordion`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "tab.selected_index = 3" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "accordion.selected_index = None" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Nesting tabs and accordions\n", + "\n", + "Tabs and accordions can be nested as deeply as you want. If you have a few minutes, try nesting a few accordions or putting an accordion inside a tab or a tab inside an accordion. \n", + "\n", + "The example below makes a couple of tabs with an accordion children in one of them" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "tab_nest = widgets.Tab()\n", + "tab_nest.children = [accordion, accordion]\n", + "tab_nest.titles = ('An accordion', 'Copy of the accordion')\n", + "tab_nest" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "nbsphinx": "hidden" + }, + "source": [ + "[Index](Index.ipynb) - [Back](Widget Basics.ipynb) - [Next](Output Widget.ipynb)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.8" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} From 6d6a756f0d5499c24c0ca0ce6f40f7d79f48dbf9 Mon Sep 17 00:00:00 2001 From: Vidar Tonaas Fauske Date: Tue, 4 Feb 2020 18:38:10 +0000 Subject: [PATCH 5/9] Separate naive/aware datetime pickers --- ipywidgets/widgets/trait_types.py | 48 +++ ipywidgets/widgets/widget_datetime.py | 58 ++- packages/controls/src/widget_datetime.ts | 91 +++++ .../controls/test/src/widget_datetime_test.ts | 87 ++++- .../schema/jupyterwidgetmodels.latest.json | 349 ++++++++++++++++-- packages/schema/jupyterwidgetmodels.latest.md | 21 ++ 6 files changed, 616 insertions(+), 38 deletions(-) diff --git a/ipywidgets/widgets/trait_types.py b/ipywidgets/widgets/trait_types.py index b89ec13c93..f1a453457c 100644 --- a/ipywidgets/widgets/trait_types.py +++ b/ipywidgets/widgets/trait_types.py @@ -121,6 +121,54 @@ def datetime_from_json(js, manager): } +def naive_to_json(pydt, manager): + """Serialize a naive Python datetime object to json. + + Instantiating a JavaScript Date object with a string assumes that the + string is a UTC string, while instantiating it with constructor arguments + assumes that it's in local time: + + >>> cdate = new Date('2015-05-12') + Mon May 11 2015 20:00:00 GMT-0400 (Eastern Daylight Time) + >>> cdate = new Date(2015, 4, 12) // Months are 0-based indices in JS + Tue May 12 2015 00:00:00 GMT-0400 (Eastern Daylight Time) + + Attributes of this dictionary are to be passed to the JavaScript Date + constructor. + """ + if pydt is None: + return None + else: + naivedt = pydt.replace(tzinfo=None) + return dict( + year=naivedt.year, + month=naivedt.month - 1, # Months are 0-based indices in JS + date=naivedt.day, + hours=naivedt.hour, # Hours, Minutes, Seconds and Milliseconds + minutes=naivedt.minute, # are plural in JS + seconds=naivedt.second, + milliseconds=naivedt.microsecond / 1000, + ) + + +def naive_from_json(js, manager): + """Deserialize a naive Python datetime object from json.""" + if js is None: + return None + else: + return dt.datetime( + js["year"], + js["month"] + 1, # Months are 1-based in Python + js["date"], + js["hours"], + js["minutes"], + js["seconds"], + js["milliseconds"] * 1000, + ) + +naive_serialization = {"from_json": naive_from_json, "to_json": naive_to_json} + + def date_to_json(pydate, manager): """Serialize a Python date object. diff --git a/ipywidgets/widgets/widget_datetime.py b/ipywidgets/widgets/widget_datetime.py index 12e450de73..0a1ccab6ac 100644 --- a/ipywidgets/widgets/widget_datetime.py +++ b/ipywidgets/widgets/widget_datetime.py @@ -10,7 +10,7 @@ from traitlets import Unicode, Bool, validate, TraitError -from .trait_types import datetime_serialization, Datetime +from .trait_types import datetime_serialization, Datetime, naive_serialization from .valuewidget import ValueWidget from .widget import register from .widget_core import CoreWidget @@ -20,7 +20,7 @@ @register class DatetimePicker(DescriptionWidget, ValueWidget, CoreWidget): """ - Display a widget for picking times. + Display a widget for picking datetimes. Parameters ---------- @@ -42,7 +42,7 @@ class DatetimePicker(DescriptionWidget, ValueWidget, CoreWidget): >>> import datetime >>> import ipydatetime - >>> datetime_pick = ipydatetime.TimePicker() + >>> datetime_pick = ipydatetime.DatetimePicker() >>> datetime_pick.value = datetime.datetime(2018, 09, 5, 12, 34, 3) """ @@ -55,10 +55,16 @@ class DatetimePicker(DescriptionWidget, ValueWidget, CoreWidget): min = Datetime(None, allow_none=True).tag(sync=True, **datetime_serialization) max = Datetime(None, allow_none=True).tag(sync=True, **datetime_serialization) + def _validate_tz(self, value): + if value.tzinfo is None: + raise TraitError('%s values needs to be timezone aware' % (self.__class__.__name__,)) + return value + @validate("value") def _validate_value(self, proposal): """Cap and floor value""" value = proposal["value"] + value = self._validate_tz(value) if self.min and self.min > value: value = max(value, self.min) if self.max and self.max < value: @@ -69,6 +75,7 @@ def _validate_value(self, proposal): def _validate_min(self, proposal): """Enforce min <= value <= max""" min = proposal["value"] + min = self._validate_tz(min) if self.max and min > self.max: raise TraitError("Setting min > max") if self.value and min > self.value: @@ -79,8 +86,53 @@ def _validate_min(self, proposal): def _validate_max(self, proposal): """Enforce min <= value <= max""" max = proposal["value"] + max = self._validate_tz(max) if self.min and max < self.min: raise TraitError("setting max < min") if self.value and max < self.value: self.value = max return max + + +@register +class NaiveDatetimePicker(DatetimePicker): + """ + Display a widget for picking naive datetimes (i.e. timezone unaware). + + Parameters + ---------- + + value: datetime.datetime + The current value of the widget. + + disabled: bool + Whether to disable user changes. + + min: datetime.datetime + The lower allowed datetime bound + + max: datetime.datetime + The upper allowed datetime bound + + Examples + -------- + + >>> import datetime + >>> import ipydatetime + >>> datetime_pick = ipydatetime.NaiveDatetimePicker() + >>> datetime_pick.value = datetime.datetime(2018, 09, 5, 12, 34, 3) + """ + + # Replace the serializers and model names: + + _model_name = Unicode("NaiveDatetimeModel").tag(sync=True) + + value = Datetime(None, allow_none=True).tag(sync=True, **naive_serialization) + + min = Datetime(None, allow_none=True).tag(sync=True, **naive_serialization) + max = Datetime(None, allow_none=True).tag(sync=True, **naive_serialization) + + def _validate_tz(self, value): + if value.tzinfo is not None: + raise TraitError('%s values needs to be timezone unaware' % (self.__class__.__name__,)) + return value diff --git a/packages/controls/src/widget_datetime.ts b/packages/controls/src/widget_datetime.ts index a422c0d40a..9fc43f8aa2 100644 --- a/packages/controls/src/widget_datetime.ts +++ b/packages/controls/src/widget_datetime.ts @@ -305,3 +305,94 @@ namespace Private { return value ? dt_as_dt_string(value).split('T', 2)[1] : ''; } } + +export interface ISerializedNaiveDatetime { + /** + * full year + */ + year: number; + + /** + * zero-based month (0 means January, 11 means December) + */ + month: number; + + /** + * day of month + */ + date: number; + + /** + * hour (24H format) + */ + hours: number; + + /** + * minutes + */ + minutes: number; + + /** + * seconds + */ + seconds: number; + + /** + * millisconds + */ + milliseconds: number; +} + +export function serialize_naive( + value: Date | null +): ISerializedNaiveDatetime | null { + if (value === null) { + return null; + } else { + return { + year: value.getFullYear(), + month: value.getMonth(), + date: value.getDate(), + hours: value.getHours(), + minutes: value.getMinutes(), + seconds: value.getSeconds(), + milliseconds: value.getMilliseconds() + }; + } +} + +export function deserialize_naive( + value: ISerializedNaiveDatetime +): Date | null { + if (value === null) { + return null; + } else { + const date = new Date(); + date.setFullYear(value.year, value.month, value.date); + date.setHours( + value.hours, + value.minutes, + value.seconds, + value.milliseconds + ); + return date; + } +} + +export const naive_serializers = { + serialize: serialize_naive, + deserialize: deserialize_naive +}; + +export class NaiveDatetimeModel extends DatetimeModel { + defaults(): Backbone.ObjectHash { + return { ...super.defaults(), _model_name: 'NaiveDatetimeModel' }; + } + + static serializers: ISerializers = { + ...CoreDescriptionModel.serializers, + value: naive_serializers, + min: naive_serializers, + max: naive_serializers + }; +} diff --git a/packages/controls/test/src/widget_datetime_test.ts b/packages/controls/test/src/widget_datetime_test.ts index b3864cd050..607567889e 100644 --- a/packages/controls/test/src/widget_datetime_test.ts +++ b/packages/controls/test/src/widget_datetime_test.ts @@ -11,7 +11,7 @@ import { createTestModelFromSerialized } from './utils'; -import { DatetimeModel, DatetimeView } from '../../lib'; +import { DatetimeModel, DatetimeView, NaiveDatetimeModel } from '../../lib'; describe('Datetime', () => { const date = new Date(); @@ -112,4 +112,89 @@ describe('Datetime', () => { expect(view.model).to.equal(model); }); }); + + describe('NaiveDatetimeModel', () => { + it('should be createable', () => { + const model = createTestModel(NaiveDatetimeModel); + expect(model).to.be.an.instanceof(NaiveDatetimeModel); + expect(model.get('value')).to.be.a('null'); + }); + + it('should be createable with a value', () => { + const state = { value: date }; + const model = createTestModel(NaiveDatetimeModel, state); + expect(model).to.be.an.instanceof(NaiveDatetimeModel); + expect(model.get('value')).to.eql(date); + }); + + it('should serialize as expected', async () => { + const state_in = { + value: { + year: 2002, + month: 2, + date: 20, + hours: 20, + minutes: 2, + seconds: 20, + milliseconds: 2 + } + }; + + const model = await createTestModelFromSerialized( + NaiveDatetimeModel, + state_in + ); + model.widget_manager.register_model( + model.model_id, + Promise.resolve(model) + ); + + const state_out = await (model.widget_manager as DummyManager).get_state(); + const models = Object.keys(state_out.state).map( + k => state_out.state[k].state + ); + expect(models.length).to.eql(1); + expect(models[0]._model_name).to.be('NaiveDatetimeModel'); + expect(models[0].value).to.eql(state_in.value); + }); + + it('should deserialize to Date object', async () => { + const state_in = { + value: { + year: 2002, + month: 2, + date: 20, + hours: 20, + minutes: 2, + seconds: 20, + milliseconds: 2 + } + }; + + const model = await createTestModelFromSerialized( + NaiveDatetimeModel, + state_in + ); + expect(model.get('value')).to.eql(new Date(2002, 2, 20, 20, 2, 20, 2)); + }); + + it('should deserialize null', async () => { + const state_in = { value: null }; + + const model = await createTestModelFromSerialized( + NaiveDatetimeModel, + state_in + ); + expect(model.get('value')).to.be.a('null'); + }); + + it('should deserialize undefined', async () => { + const state_in = {}; + const model = await createTestModelFromSerialized( + NaiveDatetimeModel, + state_in + ); + expect(model.get('value')).to.be.a('null'); + }); + }); }); diff --git a/packages/schema/jupyterwidgetmodels.latest.json b/packages/schema/jupyterwidgetmodels.latest.json index 28966e09c9..a64b33beda 100644 --- a/packages/schema/jupyterwidgetmodels.latest.json +++ b/packages/schema/jupyterwidgetmodels.latest.json @@ -310,7 +310,13 @@ { "allow_none": true, "default": null, - "enum": ["contain", "cover", "fill", "scale-down", "none"], + "enum": [ + "contain", + "cover", + "fill", + "scale-down", + "none" + ], "help": "The object-fit CSS attribute.", "name": "object_fit", "type": "string" @@ -360,7 +366,13 @@ { "allow_none": true, "default": null, - "enum": ["visible", "hidden", "inherit", "initial", "unset"], + "enum": [ + "visible", + "hidden", + "inherit", + "initial", + "unset" + ], "help": "The visibility CSS attribute.", "name": "visibility", "type": "string" @@ -433,7 +445,13 @@ }, { "default": "", - "enum": ["success", "info", "warning", "danger", ""], + "enum": [ + "success", + "info", + "warning", + "danger", + "" + ], "help": "Use a predefined styling for the box.", "name": "box_style", "type": "string" @@ -915,7 +933,13 @@ }, { "default": "", - "enum": ["success", "info", "warning", "danger", ""], + "enum": [ + "success", + "info", + "warning", + "danger", + "" + ], "help": "Use a predefined styling for the box.", "name": "box_style", "type": "string" @@ -1012,7 +1036,14 @@ }, { "default": "", - "enum": ["primary", "success", "info", "warning", "danger", ""], + "enum": [ + "primary", + "success", + "info", + "warning", + "danger", + "" + ], "help": "Use a predefined styling for the button.", "name": "button_style", "type": "string" @@ -2082,13 +2113,18 @@ "default": 1, "help": "The date step to use for the picker, in days, or \"any\".", "name": "step", - "type": ["int", "string"], + "type": [ + "int", + "string" + ], "union_attributes": [ { "type": "int" }, { - "enum": ["any"], + "enum": [ + "any" + ], "type": "string" } ] @@ -2542,7 +2578,14 @@ }, { "default": "", - "enum": ["primary", "success", "info", "warning", "danger", ""], + "enum": [ + "primary", + "success", + "info", + "warning", + "danger", + "" + ], "help": "Use a predefined styling for the button.", "name": "button_style", "type": "string" @@ -2718,7 +2761,10 @@ }, { "default": "horizontal", - "enum": ["horizontal", "vertical"], + "enum": [ + "horizontal", + "vertical" + ], "help": "Vertical or horizontal.", "name": "orientation", "type": "string" @@ -2831,7 +2877,13 @@ { "allow_none": true, "default": "", - "enum": ["success", "info", "warning", "danger", ""], + "enum": [ + "success", + "info", + "warning", + "danger", + "" + ], "help": "Use a predefined styling for the progess bar.", "name": "bar_style", "type": "string" @@ -2863,7 +2915,10 @@ }, { "default": "horizontal", - "enum": ["horizontal", "vertical"], + "enum": [ + "horizontal", + "vertical" + ], "help": "Vertical or horizontal.", "name": "orientation", "type": "string" @@ -2993,7 +3048,10 @@ }, { "default": "horizontal", - "enum": ["horizontal", "vertical"], + "enum": [ + "horizontal", + "vertical" + ], "help": "Vertical or horizontal.", "name": "orientation", "type": "string" @@ -3039,7 +3097,10 @@ "type": "string" }, { - "default": [0.0, 1.0], + "default": [ + 0.0, + 1.0 + ], "help": "Tuple of (lower, upper) bounds", "name": "value", "type": "array" @@ -3142,7 +3203,10 @@ }, { "default": "horizontal", - "enum": ["horizontal", "vertical"], + "enum": [ + "horizontal", + "vertical" + ], "help": "Vertical or horizontal.", "name": "orientation", "type": "string" @@ -3431,7 +3495,14 @@ }, { "default": "", - "enum": ["primary", "success", "info", "warning", "danger", ""], + "enum": [ + "primary", + "success", + "info", + "warning", + "danger", + "" + ], "help": "Use a predefined styling for the tags.", "name": "tag_style", "type": "string" @@ -3510,7 +3581,13 @@ }, { "default": "", - "enum": ["success", "info", "warning", "danger", ""], + "enum": [ + "success", + "info", + "warning", + "danger", + "" + ], "help": "Use a predefined styling for the box.", "name": "box_style", "type": "string" @@ -3607,7 +3684,13 @@ }, { "default": "", - "enum": ["success", "info", "warning", "danger", ""], + "enum": [ + "success", + "info", + "warning", + "danger", + "" + ], "help": "Use a predefined styling for the box.", "name": "box_style", "type": "string" @@ -4018,7 +4101,13 @@ }, { "default": "", - "enum": ["success", "info", "warning", "danger", ""], + "enum": [ + "success", + "info", + "warning", + "danger", + "" + ], "help": "Use a predefined styling for the progess bar.", "name": "bar_style", "type": "string" @@ -4050,7 +4139,10 @@ }, { "default": "horizontal", - "enum": ["horizontal", "vertical"], + "enum": [ + "horizontal", + "vertical" + ], "help": "Vertical or horizontal.", "name": "orientation", "type": "string" @@ -4180,7 +4272,10 @@ }, { "default": "horizontal", - "enum": ["horizontal", "vertical"], + "enum": [ + "horizontal", + "vertical" + ], "help": "Vertical or horizontal.", "name": "orientation", "type": "string" @@ -4225,7 +4320,10 @@ "type": "string" }, { - "default": [0, 1], + "default": [ + 0, + 1 + ], "help": "Tuple of (lower, upper) bounds", "name": "value", "type": "array" @@ -4328,7 +4426,10 @@ }, { "default": "horizontal", - "enum": ["horizontal", "vertical"], + "enum": [ + "horizontal", + "vertical" + ], "help": "Vertical or horizontal.", "name": "orientation", "type": "string" @@ -4615,7 +4716,14 @@ }, { "default": "", - "enum": ["primary", "success", "info", "warning", "danger", ""], + "enum": [ + "primary", + "success", + "info", + "warning", + "danger", + "" + ], "help": "Use a predefined styling for the tags.", "name": "tag_style", "type": "string" @@ -4813,6 +4921,126 @@ "version": "2.0.0" } }, + { + "attributes": [ + { + "default": [], + "help": "CSS classes applied to widget DOM element", + "items": { + "type": "string" + }, + "name": "_dom_classes", + "type": "array" + }, + { + "default": "@jupyter-widgets/controls", + "help": "", + "name": "_model_module", + "type": "string" + }, + { + "default": "2.0.0", + "help": "", + "name": "_model_module_version", + "type": "string" + }, + { + "default": "NaiveDatetimeModel", + "help": "", + "name": "_model_name", + "type": "string" + }, + { + "default": "@jupyter-widgets/controls", + "help": "", + "name": "_view_module", + "type": "string" + }, + { + "default": "2.0.0", + "help": "", + "name": "_view_module_version", + "type": "string" + }, + { + "default": "DatetimeView", + "help": "", + "name": "_view_name", + "type": "string" + }, + { + "default": "", + "help": "Description of the control.", + "name": "description", + "type": "string" + }, + { + "default": false, + "help": "Enable or disable user changes.", + "name": "disabled", + "type": "bool" + }, + { + "default": "reference to new instance", + "help": "", + "name": "layout", + "type": "reference", + "widget": "Layout" + }, + { + "allow_none": true, + "default": null, + "help": "", + "name": "max", + "type": "Datetime" + }, + { + "allow_none": true, + "default": null, + "help": "", + "name": "min", + "type": "Datetime" + }, + { + "default": "reference to new instance", + "help": "Styling customizations", + "name": "style", + "type": "reference", + "widget": "DescriptionStyle" + }, + { + "allow_none": true, + "default": null, + "help": "Is widget tabbable?", + "name": "tabbable", + "type": "bool" + }, + { + "allow_none": true, + "default": null, + "help": "A tooltip caption.", + "name": "tooltip", + "type": "string" + }, + { + "allow_none": true, + "default": null, + "help": "", + "name": "value", + "type": "Datetime" + } + ], + "model": { + "module": "@jupyter-widgets/controls", + "name": "NaiveDatetimeModel", + "version": "2.0.0" + }, + "view": { + "module": "@jupyter-widgets/controls", + "name": "DatetimeView", + "version": "2.0.0" + } + }, { "attributes": [ { @@ -5574,7 +5802,10 @@ "type": "bool" }, { - "default": [0, 0], + "default": [ + 0, + 0 + ], "help": "Min and max selected indices", "name": "index", "type": "array" @@ -5588,7 +5819,10 @@ }, { "default": "horizontal", - "enum": ["horizontal", "vertical"], + "enum": [ + "horizontal", + "vertical" + ], "help": "Vertical or horizontal.", "name": "orientation", "type": "string" @@ -5721,7 +5955,10 @@ }, { "default": "horizontal", - "enum": ["horizontal", "vertical"], + "enum": [ + "horizontal", + "vertical" + ], "help": "Vertical or horizontal.", "name": "orientation", "type": "string" @@ -5877,7 +6114,13 @@ }, { "default": "", - "enum": ["success", "info", "warning", "danger", ""], + "enum": [ + "success", + "info", + "warning", + "danger", + "" + ], "help": "Use a predefined styling for the box.", "name": "box_style", "type": "string" @@ -5990,7 +6233,13 @@ }, { "default": "", - "enum": ["success", "info", "warning", "danger", ""], + "enum": [ + "success", + "info", + "warning", + "danger", + "" + ], "help": "Use a predefined styling for the box.", "name": "box_style", "type": "string" @@ -6142,7 +6391,14 @@ }, { "default": "", - "enum": ["primary", "success", "info", "warning", "danger", ""], + "enum": [ + "primary", + "success", + "info", + "warning", + "danger", + "" + ], "help": "Use a predefined styling for the tags.", "name": "tag_style", "type": "string" @@ -6497,13 +6753,18 @@ "default": 60, "help": "The time step to use for the picker, in seconds, or \"any\".", "name": "step", - "type": ["float", "string"], + "type": [ + "float", + "string" + ], "union_attributes": [ { "type": "float" }, { - "enum": ["any"], + "enum": [ + "any" + ], "type": "string" } ] @@ -6597,7 +6858,14 @@ }, { "default": "", - "enum": ["primary", "success", "info", "warning", "danger", ""], + "enum": [ + "primary", + "success", + "info", + "warning", + "danger", + "" + ], "help": "Use a predefined styling for the button.", "name": "button_style", "type": "string" @@ -6725,7 +6993,14 @@ { "allow_none": true, "default": "", - "enum": ["primary", "success", "info", "warning", "danger", ""], + "enum": [ + "primary", + "success", + "info", + "warning", + "danger", + "" + ], "help": "Use a predefined styling for the buttons.", "name": "button_style", "type": "string" @@ -6924,7 +7199,13 @@ }, { "default": "", - "enum": ["success", "info", "warning", "danger", ""], + "enum": [ + "success", + "info", + "warning", + "danger", + "" + ], "help": "Use a predefined styling for the box.", "name": "box_style", "type": "string" diff --git a/packages/schema/jupyterwidgetmodels.latest.md b/packages/schema/jupyterwidgetmodels.latest.md index 904945d691..c213b9d2b6 100644 --- a/packages/schema/jupyterwidgetmodels.latest.md +++ b/packages/schema/jupyterwidgetmodels.latest.md @@ -855,6 +855,27 @@ Attribute | Type | Default | Help `source` | array | `[]` | The source (widget, 'trait_name') pair `target` | array | `[]` | The target (widget, 'trait_name') pair +### NaiveDatetimeModel (@jupyter-widgets/controls, 2.0.0); DatetimeView (@jupyter-widgets/controls, 2.0.0) + +Attribute | Type | Default | Help +-----------------|------------------|------------------|---- +`_dom_classes` | array of string | `[]` | CSS classes applied to widget DOM element +`_model_module` | string | `'@jupyter-widgets/controls'` | +`_model_module_version` | string | `'2.0.0'` | +`_model_name` | string | `'NaiveDatetimeModel'` | +`_view_module` | string | `'@jupyter-widgets/controls'` | +`_view_module_version` | string | `'2.0.0'` | +`_view_name` | string | `'DatetimeView'` | +`description` | string | `''` | Description of the control. +`disabled` | boolean | `false` | Enable or disable user changes. +`layout` | reference to Layout widget | reference to new instance | +`max` | `null` or Datetime | `null` | +`min` | `null` or Datetime | `null` | +`style` | reference to DescriptionStyle widget | reference to new instance | Styling customizations +`tabbable` | `null` or boolean | `null` | Is widget tabbable? +`tooltip` | `null` or string | `null` | A tooltip caption. +`value` | `null` or Datetime | `null` | + ### PasswordModel (@jupyter-widgets/controls, 2.0.0); PasswordView (@jupyter-widgets/controls, 2.0.0) Attribute | Type | Default | Help From 35503f4aae0f6004d11d5e0505d36b1bb98596f4 Mon Sep 17 00:00:00 2001 From: Vidar Tonaas Fauske Date: Tue, 18 Feb 2020 12:54:18 +0000 Subject: [PATCH 6/9] Fix test --- packages/controls/test/src/widget_datetime_test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/controls/test/src/widget_datetime_test.ts b/packages/controls/test/src/widget_datetime_test.ts index 607567889e..961af33fcc 100644 --- a/packages/controls/test/src/widget_datetime_test.ts +++ b/packages/controls/test/src/widget_datetime_test.ts @@ -154,7 +154,7 @@ describe('Datetime', () => { k => state_out.state[k].state ); expect(models.length).to.eql(1); - expect(models[0]._model_name).to.be('NaiveDatetimeModel'); + expect(models[0]._model_name).to.be.a('NaiveDatetimeModel'); expect(models[0].value).to.eql(state_in.value); }); From 3549968d54e8ce5ce5d13c0538152155cf052ea8 Mon Sep 17 00:00:00 2001 From: Vidar Tonaas Fauske Date: Tue, 18 Feb 2020 13:00:23 +0000 Subject: [PATCH 7/9] Actually fix test --- packages/controls/test/src/widget_datetime_test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/controls/test/src/widget_datetime_test.ts b/packages/controls/test/src/widget_datetime_test.ts index 961af33fcc..967d5bfc6e 100644 --- a/packages/controls/test/src/widget_datetime_test.ts +++ b/packages/controls/test/src/widget_datetime_test.ts @@ -154,7 +154,7 @@ describe('Datetime', () => { k => state_out.state[k].state ); expect(models.length).to.eql(1); - expect(models[0]._model_name).to.be.a('NaiveDatetimeModel'); + expect(models[0]._model_name).to.eql('NaiveDatetimeModel'); expect(models[0].value).to.eql(state_in.value); }); From 22a8ae345b298d5beb1f345a2610a17da16b8adb Mon Sep 17 00:00:00 2001 From: Vidar Tonaas Fauske Date: Mon, 15 Feb 2021 12:01:30 +0000 Subject: [PATCH 8/9] Fixes plus tests from ipydatetime --- .../tests/test_datetime_serializers.py | 91 ++++++++++++++ .../widgets/tests/test_widget_datetime.py | 111 ++++++++++++++++++ .../tests/test_widget_naive_datetime.py | 95 +++++++++++++++ ipywidgets/widgets/tests/test_widget_time.py | 87 ++++++++++++++ ipywidgets/widgets/trait_types.py | 52 +++++--- ipywidgets/widgets/widget_datetime.py | 5 +- ipywidgets/widgets/widget_time.py | 5 +- setup.cfg | 1 + 8 files changed, 423 insertions(+), 24 deletions(-) create mode 100644 ipywidgets/widgets/tests/test_datetime_serializers.py create mode 100644 ipywidgets/widgets/tests/test_widget_datetime.py create mode 100644 ipywidgets/widgets/tests/test_widget_naive_datetime.py create mode 100644 ipywidgets/widgets/tests/test_widget_time.py diff --git a/ipywidgets/widgets/tests/test_datetime_serializers.py b/ipywidgets/widgets/tests/test_datetime_serializers.py new file mode 100644 index 0000000000..adfdef3615 --- /dev/null +++ b/ipywidgets/widgets/tests/test_datetime_serializers.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python +# coding: utf-8 + +# Copyright (c) Vidar Tonaas Fauske. +# Distributed under the terms of the Modified BSD License. + +import pytest + +import datetime +import pytz + +from traitlets import TraitError + +from ..trait_types import ( + time_to_json, + time_from_json, + datetime_to_json, + datetime_from_json, +) + + +def test_time_serialize_none(): + assert time_to_json(None, None) == None + + +def test_time_serialize_value(): + t = datetime.time(13, 37, 42, 7000) + assert time_to_json(t, None) == dict( + hours=13, minutes=37, seconds=42, milliseconds=7 + ) + + +def test_time_deserialize_none(): + assert time_from_json(None, None) == None + + +def test_time_deserialize_value(): + v = dict(hours=13, minutes=37, seconds=42, milliseconds=7) + assert time_from_json(v, None) == datetime.time(13, 37, 42, 7000) + + +def test_datetime_serialize_none(): + assert datetime_to_json(None, None) == None + + +def test_datetime_serialize_value(): + t = datetime.datetime(2002, 2, 20, 13, 37, 42, 7000, pytz.utc) + assert datetime_to_json(t, None) == dict( + year=2002, + month=1, # Months are 0-based indices in JS + date=20, + hours=13, + minutes=37, + seconds=42, + milliseconds=7, + ) + + +def test_datetime_serialize_non_utz(): + # Non-existant timezone, so it wil never be the local one: + tz = pytz.FixedOffset(42) + t = datetime.datetime(2002, 2, 20, 13, 37, 42, 7000, tz) + assert datetime_to_json(t, None) == dict( + year=2002, + month=1, # Months are 0-based indices in JS + date=20, + hours=12, + minutes=55, + seconds=42, + milliseconds=7, + ) + + +def test_datetime_deserialize_none(): + assert datetime_from_json(None, None) == None + + +def test_datetime_deserialize_value(): + tz = pytz.FixedOffset(42) + v = dict( + year=2002, + month=1, # Months are 0-based indices in JS + date=20, + hours=13, + minutes=37, + seconds=42, + milliseconds=7, + ) + assert datetime_from_json(v, None) == datetime.datetime( + 2002, 2, 20, 14, 19, 42, 7000, tz + ) diff --git a/ipywidgets/widgets/tests/test_widget_datetime.py b/ipywidgets/widgets/tests/test_widget_datetime.py new file mode 100644 index 0000000000..9e66c68846 --- /dev/null +++ b/ipywidgets/widgets/tests/test_widget_datetime.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python +# coding: utf-8 + +# Copyright (c) Vidar Tonaas Fauske. +# Distributed under the terms of the Modified BSD License. + +import pytest + +import datetime + +import pytz +from traitlets import TraitError + +from ..widget_datetime import DatetimePicker + + +def test_time_creation_blank(): + w = DatetimePicker() + assert w.value is None + + +def test_time_creation_value(): + t = datetime.datetime.now(pytz.utc) + w = DatetimePicker(value=t) + assert w.value is t + + +def test_time_validate_value_none(): + t = datetime.datetime(2002, 2, 20, 13, 37, 42, 7, tzinfo=pytz.utc) + t_min = datetime.datetime(1442, 1, 1, tzinfo=pytz.utc) + t_max = datetime.datetime(2056, 1, 1, tzinfo=pytz.utc) + w = DatetimePicker(value=t, min=t_min, max=t_max) + w.value = None + assert w.value is None + + +def test_time_validate_value_vs_min(): + t = datetime.datetime(2002, 2, 20, 13, 37, 42, 7, tzinfo=pytz.utc) + t_min = datetime.datetime(2019, 1, 1, tzinfo=pytz.utc) + t_max = datetime.datetime(2056, 1, 1, tzinfo=pytz.utc) + w = DatetimePicker(min=t_min, max=t_max) + w.value = t + assert w.value.year == 2019 + + +def test_time_validate_value_vs_max(): + t = datetime.datetime(2002, 2, 20, 13, 37, 42, 7, tzinfo=pytz.utc) + t_min = datetime.datetime(1664, 1, 1, tzinfo=pytz.utc) + t_max = datetime.datetime(1994, 1, 1, tzinfo=pytz.utc) + w = DatetimePicker(min=t_min, max=t_max) + w.value = t + assert w.value.year == 1994 + + +def test_time_validate_min_vs_value(): + t = datetime.datetime(2002, 2, 20, 13, 37, 42, 7, tzinfo=pytz.utc) + t_min = datetime.datetime(2019, 1, 1, tzinfo=pytz.utc) + t_max = datetime.datetime(2056, 1, 1, tzinfo=pytz.utc) + w = DatetimePicker(value=t, max=t_max) + w.min = t_min + assert w.value.year == 2019 + + +def test_time_validate_min_vs_max(): + t = datetime.datetime(2002, 2, 20, 13, 37, 42, 7, tzinfo=pytz.utc) + t_min = datetime.datetime(2112, 1, 1, tzinfo=pytz.utc) + t_max = datetime.datetime(2056, 1, 1, tzinfo=pytz.utc) + w = DatetimePicker(value=t, max=t_max) + with pytest.raises(TraitError): + w.min = t_min + + +def test_time_validate_max_vs_value(): + t = datetime.datetime(2002, 2, 20, 13, 37, 42, 7, tzinfo=pytz.utc) + t_min = datetime.datetime(1664, 1, 1, tzinfo=pytz.utc) + t_max = datetime.datetime(1994, 1, 1, tzinfo=pytz.utc) + w = DatetimePicker(value=t, min=t_min) + w.max = t_max + assert w.value.year == 1994 + + +def test_time_validate_max_vs_min(): + t = datetime.datetime(2002, 2, 20, 13, 37, 42, 7, tzinfo=pytz.utc) + t_min = datetime.datetime(1664, 1, 1, tzinfo=pytz.utc) + t_max = datetime.datetime(1337, 1, 1, tzinfo=pytz.utc) + w = DatetimePicker(value=t, min=t_min) + with pytest.raises(TraitError): + w.max = t_max + + +def test_time_validate_naive(): + t = datetime.datetime(2002, 2, 20, 13, 37, 42, 7, tzinfo=pytz.utc) + t_min = datetime.datetime(1442, 1, 1, tzinfo=pytz.utc) + t_max = datetime.datetime(2056, 1, 1, tzinfo=pytz.utc) + + w = DatetimePicker(value=t, min=t_min, max=t_max) + with pytest.raises(TraitError): + w.max = t_max.replace(tzinfo=None) + with pytest.raises(TraitError): + w.min = t_min.replace(tzinfo=None) + with pytest.raises(TraitError): + w.value = t.replace(tzinfo=None) + + +def test_datetime_tzinfo(): + tz = pytz.timezone('Australia/Sydney') + t = datetime.datetime(2002, 2, 20, 13, 37, 42, 7, tzinfo=tz) + w = DatetimePicker(value=t) + assert w.value == t + # tzinfo only changes upon input from user + assert w.value.tzinfo == tz diff --git a/ipywidgets/widgets/tests/test_widget_naive_datetime.py b/ipywidgets/widgets/tests/test_widget_naive_datetime.py new file mode 100644 index 0000000000..212673d5db --- /dev/null +++ b/ipywidgets/widgets/tests/test_widget_naive_datetime.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python +# coding: utf-8 + +# Copyright (c) Vidar Tonaas Fauske. +# Distributed under the terms of the Modified BSD License. + +import pytest + +import datetime + +import pytz +from traitlets import TraitError + +from ..widget_datetime import NaiveDatetimePicker + + +def test_time_creation_blank(): + w = NaiveDatetimePicker() + assert w.value is None + + +def test_time_creation_value(): + t = datetime.datetime.today() + w = NaiveDatetimePicker(value=t) + assert w.value is t + + +def test_time_validate_value_none(): + t = datetime.datetime(2002, 2, 20, 13, 37, 42, 7) + t_min = datetime.datetime(1442, 1, 1) + t_max = datetime.datetime(2056, 1, 1) + w = NaiveDatetimePicker(value=t, min=t_min, max=t_max) + w.value = None + assert w.value is None + + +def test_time_validate_value_vs_min(): + t = datetime.datetime(2002, 2, 20, 13, 37, 42, 7) + t_min = datetime.datetime(2019, 1, 1) + t_max = datetime.datetime(2056, 1, 1) + w = NaiveDatetimePicker(min=t_min, max=t_max) + w.value = t + assert w.value.year == 2019 + + +def test_time_validate_value_vs_max(): + t = datetime.datetime(2002, 2, 20, 13, 37, 42, 7) + t_min = datetime.datetime(1664, 1, 1) + t_max = datetime.datetime(1994, 1, 1) + w = NaiveDatetimePicker(min=t_min, max=t_max) + w.value = t + assert w.value.year == 1994 + + +def test_time_validate_min_vs_value(): + t = datetime.datetime(2002, 2, 20, 13, 37, 42, 7) + t_min = datetime.datetime(2019, 1, 1) + t_max = datetime.datetime(2056, 1, 1) + w = NaiveDatetimePicker(value=t, max=t_max) + w.min = t_min + assert w.value.year == 2019 + + +def test_time_validate_min_vs_max(): + t = datetime.datetime(2002, 2, 20, 13, 37, 42, 7) + t_min = datetime.datetime(2112, 1, 1) + t_max = datetime.datetime(2056, 1, 1) + w = NaiveDatetimePicker(value=t, max=t_max) + with pytest.raises(TraitError): + w.min = t_min + + +def test_time_validate_max_vs_value(): + t = datetime.datetime(2002, 2, 20, 13, 37, 42, 7) + t_min = datetime.datetime(1664, 1, 1) + t_max = datetime.datetime(1994, 1, 1) + w = NaiveDatetimePicker(value=t, min=t_min) + w.max = t_max + assert w.value.year == 1994 + + +def test_time_validate_max_vs_min(): + t = datetime.datetime(2002, 2, 20, 13, 37, 42, 7) + t_min = datetime.datetime(1664, 1, 1) + t_max = datetime.datetime(1337, 1, 1) + w = NaiveDatetimePicker(value=t, min=t_min) + with pytest.raises(TraitError): + w.max = t_max + + +def test_datetime_tzinfo(): + tz = pytz.timezone('Australia/Sydney') + t = datetime.datetime(2002, 2, 20, 13, 37, 42, 7, tzinfo=tz) + with pytest.raises(TraitError): + w = NaiveDatetimePicker(value=t) diff --git a/ipywidgets/widgets/tests/test_widget_time.py b/ipywidgets/widgets/tests/test_widget_time.py new file mode 100644 index 0000000000..25df8b7c42 --- /dev/null +++ b/ipywidgets/widgets/tests/test_widget_time.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python +# coding: utf-8 + +# Copyright (c) Vidar Tonaas Fauske. +# Distributed under the terms of the Modified BSD License. + +import pytest + +import datetime + +from traitlets import TraitError + +from ..widget_time import TimePicker + + +def test_time_creation_blank(): + w = TimePicker() + assert w.value is None + + +def test_time_creation_value(): + t = datetime.time() + w = TimePicker(value=t) + assert w.value is t + + +def test_time_validate_value_none(): + t = datetime.time(13, 37, 42, 7) + t_min = datetime.time(2) + t_max = datetime.time(22) + w = TimePicker(value=t, min=t_min, max=t_max) + w.value = None + assert w.value is None + + +def test_time_validate_value_vs_min(): + t = datetime.time(13, 37, 42, 7) + t_min = datetime.time(14) + t_max = datetime.time(22) + w = TimePicker(min=t_min, max=t_max) + w.value = t + assert w.value.hour == 14 + + +def test_time_validate_value_vs_max(): + t = datetime.time(13, 37, 42, 7) + t_min = datetime.time(2) + t_max = datetime.time(12) + w = TimePicker(min=t_min, max=t_max) + w.value = t + assert w.value.hour == 12 + + +def test_time_validate_min_vs_value(): + t = datetime.time(13, 37, 42, 7) + t_min = datetime.time(14) + t_max = datetime.time(22) + w = TimePicker(value=t, max=t_max) + w.min = t_min + assert w.value.hour == 14 + + +def test_time_validate_min_vs_max(): + t = datetime.time(13, 37, 42, 7) + t_min = datetime.time(14) + t_max = datetime.time(12) + w = TimePicker(value=t, max=t_max) + with pytest.raises(TraitError): + w.min = t_min + + +def test_time_validate_max_vs_value(): + t = datetime.time(13, 37, 42, 7) + t_min = datetime.time(2) + t_max = datetime.time(12) + w = TimePicker(value=t, min=t_min) + w.max = t_max + assert w.value.hour == 12 + + +def test_time_validate_max_vs_min(): + t = datetime.time(13, 37, 42, 7) + t_min = datetime.time(2) + t_max = datetime.time(1) + w = TimePicker(value=t, min=t_min) + with pytest.raises(TraitError): + w.max = t_max diff --git a/ipywidgets/widgets/trait_types.py b/ipywidgets/widgets/trait_types.py index f1a453457c..591f2a8fb0 100644 --- a/ipywidgets/widgets/trait_types.py +++ b/ipywidgets/widgets/trait_types.py @@ -89,14 +89,20 @@ def datetime_to_json(pydt, manager): if pydt is None: return None else: + try: + utcdt = pydt.astimezone(dt.timezone.utc) + except (ValueError, OSError): + # If year is outside valid range for conversion, + # use it as-is + utcdt = pydt return dict( - year=pydt.year, - month=pydt.month - 1, # Months are 0-based indices in JS - date=pydt.day, - hours=pydt.hour, # Hours, Minutes, Seconds and Milliseconds - minutes=pydt.minute, # are plural in JS - seconds=pydt.second, - milliseconds=pydt.microsecond / 1000 + year=utcdt.year, + month=utcdt.month - 1, # Months are 0-based indices in JS + date=utcdt.day, + hours=utcdt.hour, # Hours, Minutes, Seconds and Milliseconds + minutes=utcdt.minute, # are plural in JS + seconds=utcdt.second, + milliseconds=utcdt.microsecond / 1000, ) @@ -105,15 +111,29 @@ def datetime_from_json(js, manager): if js is None: return None else: - return dt.datetime( - js['year'], - js['month'] + 1, # Months are 1-based in Python - js['date'], - js['hours'], - js['minutes'], - js['seconds'], - js['milliseconds'] * 1000 - ) + try: + return dt.datetime( + js["year"], + js["month"] + 1, # Months are 1-based in Python + js["date"], + js["hours"], + js["minutes"], + js["seconds"], + js["milliseconds"] * 1000, + ).astimezone() + except (ValueError, OSError): + # If year is outside valid range for conversion, + # return UTC datetime + return dt.datetime( + js["year"], + js["month"] + 1, # Months are 1-based in Python + js["date"], + js["hours"], + js["minutes"], + js["seconds"], + js["milliseconds"] * 1000, + dt.timezone.utc, + ) datetime_serialization = { 'from_json': datetime_from_json, diff --git a/ipywidgets/widgets/widget_datetime.py b/ipywidgets/widgets/widget_datetime.py index 0a1ccab6ac..c248c5125c 100644 --- a/ipywidgets/widgets/widget_datetime.py +++ b/ipywidgets/widgets/widget_datetime.py @@ -1,7 +1,4 @@ -#!/usr/bin/env python -# coding: utf-8 - -# Copyright (c) Vidar Tonaas Fauske. +# Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. """ diff --git a/ipywidgets/widgets/widget_time.py b/ipywidgets/widgets/widget_time.py index 646e9bbe57..8aa820edc8 100644 --- a/ipywidgets/widgets/widget_time.py +++ b/ipywidgets/widgets/widget_time.py @@ -1,7 +1,4 @@ -#!/usr/bin/env python -# coding: utf-8 - -# Copyright (c) Vidar Tonaas Fauske. +# Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. """ diff --git a/setup.cfg b/setup.cfg index 04741e02b0..56e1b6784b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -46,6 +46,7 @@ install_requires = test = pytest>=3.6.0 pytest-cov + pytz [options.package_data] ipywidgets = From d6681a6ff85fa693bb579de446186e9ca6982ffa Mon Sep 17 00:00:00 2001 From: Vidar Tonaas Fauske Date: Fri, 26 Mar 2021 17:26:31 +0000 Subject: [PATCH 9/9] Update ipywidgets/widgets/tests/test_widget_datetime.py Co-authored-by: Jeremy Tuloup --- ipywidgets/widgets/tests/test_widget_datetime.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ipywidgets/widgets/tests/test_widget_datetime.py b/ipywidgets/widgets/tests/test_widget_datetime.py index 9e66c68846..7f46b05474 100644 --- a/ipywidgets/widgets/tests/test_widget_datetime.py +++ b/ipywidgets/widgets/tests/test_widget_datetime.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # coding: utf-8 -# Copyright (c) Vidar Tonaas Fauske. +# Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. import pytest