From bdacdad3cf0af6e670b6e78275f758f79c6b5f95 Mon Sep 17 00:00:00 2001 From: Trung Le Date: Wed, 4 Aug 2021 15:35:11 +0200 Subject: [PATCH 01/10] Add new toolbar to figure --- .gitignore | 3 ++ bqplot/figure.py | 3 +- js/less/bqplot.less | 7 ++++ js/src/Figure.ts | 77 +++++++++++++++++++++++++++++++++++++ js/src/FigureModel.ts | 89 ++++++++++++++++++++++++++++++++++++++++++- js/src/Toolbar.ts | 2 +- 6 files changed, 177 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 32aee2c71..172cd59ea 100644 --- a/.gitignore +++ b/.gitignore @@ -64,3 +64,6 @@ node_modules *.swp *.tsbuildinfo + +# VS Code settings +.vscode/* \ No newline at end of file diff --git a/bqplot/figure.py b/bqplot/figure.py index 3baee6bee..e98ed401f 100644 --- a/bqplot/figure.py +++ b/bqplot/figure.py @@ -27,7 +27,7 @@ """ from traitlets import ( - Unicode, Instance, List, Dict, Enum, Float, Int, TraitError, default, + Bool, Unicode, Instance, List, Dict, Enum, Float, Int, TraitError, default, validate ) from ipywidgets import DOMWidget, register, widget_serialization @@ -152,6 +152,7 @@ class Figure(DOMWidget): .tag(sync=True, display_name='Legend position') animation_duration = Int().tag(sync=True, display_name='Animation duration') + show_toolbar = Bool(default_value=True).tag(sync=True) @default('scale_x') def _default_scale_x(self): diff --git a/js/less/bqplot.less b/js/less/bqplot.less index 9f4b4f7d7..4831bbdb5 100644 --- a/js/less/bqplot.less +++ b/js/less/bqplot.less @@ -325,6 +325,13 @@ image-rendering: -moz-crisp-edges; /* this is guaranteed to work for firefox */ } } + +.bqplot .toolbar_div { + position:absolute; + transition: visibility 0.5s linear, opacity 0.5s linear; + +} + .tooltip_div { z-index: 1001; } diff --git a/js/src/Figure.ts b/js/src/Figure.ts index be0c10206..ea83bf9c7 100644 --- a/js/src/Figure.ts +++ b/js/src/Figure.ts @@ -28,6 +28,7 @@ import { AxisModel } from './AxisModel'; import { Mark } from './Mark'; import { MarkModel } from './MarkModel'; import { Interaction } from './Interaction'; +import { FigureModel } from './FigureModel'; THREE.ShaderChunk['scales'] = require('raw-loader!../shaders/scales.glsl').default; @@ -102,6 +103,7 @@ export class Figure extends widgets.DOMWidgetView { protected async renderImpl() { const figureSize = this.getFigureSize(); + this.width = figureSize.width; this.height = figureSize.height; @@ -327,6 +329,10 @@ export class Figure extends widgets.DOMWidgetView { window.removeEventListener('resize', this.debouncedRelayout); }); + if (this.model.get('show_toolbar')) { + this.toolbar_div = this.create_toolbar(); + } + return Promise.all([mark_views_updated, axis_views_updated]); } @@ -1249,6 +1255,76 @@ export class Figure extends widgets.DOMWidgetView { this.el.classList.add(this.model.get('theme')); } + create_toolbar() { + const toolbar = d3 + .select(document.createElement('div')) + .attr('class', 'toolbar_div'); + + const _panzoom = document.createElement('button'); + _panzoom.classList.add('jupyter-widgets'); // @jupyter-widgets/controls css + _panzoom.classList.add('jupyter-button'); // @jupyter-widgets/controls css + _panzoom.setAttribute('data-toggle', 'tooltip'); + _panzoom.setAttribute('title', 'PanZoom'); + const panzoomicon = document.createElement('i'); + panzoomicon.style.marginRight = '0px'; + panzoomicon.className = 'fa fa-arrows'; + _panzoom.appendChild(panzoomicon); + _panzoom.onclick = (e) => { + e.preventDefault(); + (this.model as FigureModel).panzoom(); + }; + + const _reset = document.createElement('button'); + _reset.classList.add('jupyter-widgets'); // @jupyter-widgets/controls css + _reset.classList.add('jupyter-button'); // @jupyter-widgets/controls css + _reset.setAttribute('data-toggle', 'tooltip'); + _reset.setAttribute('title', 'Reset'); + const refreshicon = document.createElement('i'); + refreshicon.style.marginRight = '0px'; + refreshicon.className = 'fa fa-refresh'; + _reset.appendChild(refreshicon); + _reset.onclick = (e) => { + e.preventDefault(); + (this.model as FigureModel).reset(); + }; + + + const _save = document.createElement('button'); + _save.classList.add('jupyter-widgets'); // @jupyter-widgets/controls css + _save.classList.add('jupyter-button'); // @jupyter-widgets/controls css + _save.setAttribute('data-toggle', 'tooltip'); + _save.setAttribute('title', 'Save'); + const saveicon = document.createElement('i'); + saveicon.style.marginRight = '0px' + saveicon.className = 'fa fa-save'; + _save.appendChild(saveicon); + _save.onclick = (e) => { + e.preventDefault(); + this.save_png(undefined, undefined); + }; + + toolbar.node().appendChild(_panzoom); + toolbar.node().appendChild(_reset); + toolbar.node().appendChild(_save); + + this.el.appendChild(toolbar.node()); + toolbar.node().style.top = `${this.margin.top / 2.0}px`; + toolbar.node().style.right = `${this.margin.right}px`; + toolbar.node().style.visibility = 'hidden'; + toolbar.node().style.opacity = '0'; + this.el.addEventListener('mouseenter', () => { + toolbar.node().style.visibility = 'visible'; + toolbar.node().style.opacity = '1'; + }); + this.el.addEventListener('mouseleave', () => { + toolbar.node().style.visibility = 'hidden'; + toolbar.node().style.opacity = '0'; + }); + return toolbar; + } + + + axis_views: widgets.ViewList; bg: d3.Selection; bg_events: d3.Selection; @@ -1278,6 +1354,7 @@ export class Figure extends widgets.DOMWidgetView { svg_background: d3.Selection; title: d3.Selection; tooltip_div: d3.Selection; + toolbar_div: d3.Selection; width: number; x_pad_dict: { [id: string]: number }; xPaddingArr: { [id: string]: number }; diff --git a/js/src/FigureModel.ts b/js/src/FigureModel.ts index a76e89f0d..135e6882e 100644 --- a/js/src/FigureModel.ts +++ b/js/src/FigureModel.ts @@ -15,7 +15,9 @@ import * as widgets from '@jupyter-widgets/base'; import { semver_range } from './version'; - +import { Interaction } from './Interaction'; +import {PanZoomModel} from './PanZoomModel' +import * as _ from 'underscore'; export class FigureModel extends widgets.DOMWidgetModel { defaults() { return { @@ -54,6 +56,7 @@ export class FigureModel extends widgets.DOMWidgetModel { padding_y: 0.025, legend_location: 'top-right', animation_duration: 0, + show_toolbar: true, }; } @@ -70,12 +73,91 @@ export class FigureModel extends widgets.DOMWidgetModel { } } - save_png() { + save_png() { // TODO: Any view of this Figure model will pick up this event // and render a png. Remove this eventually. this.trigger('save_png'); } + panzoom() { + if (this._panzoomData._panning) { + this.set('interaction', this._panzoomData.cached_interaction); + this._panzoomData._panning = false; + this.save_changes(); + } else { + this._panzoomData.cached_interaction = this.get('interaction'); + const panzoom = this._panzoomData._panzoom; + if (panzoom) { + this.set('interaction', panzoom); + this.save_changes(); + } else { + this._create_panzoom_model().then((model) => { + this.set('interaction', model); + this._panzoomData._panzoom = model; + this.save_changes(); + }); + } + this._panzoomData._panning = true; + } + } + + _create_panzoom_model(): Promise { + /** + * Creates a panzoom interaction widget for the specified figure. + * + * It will discover the relevant scales for the specified figure. + */ + return this.widget_manager + .new_widget({ + model_name: 'PanZoomModel', + model_module: 'bqplot', + model_module_version: this.get('_model_module_version'), + view_name: 'PanZoom', + view_module: 'bqplot', + view_module_version: this.get('_view_module_version'), + }) + .then((model: PanZoomModel) => { + return Promise.all(this.get('marks')).then((marks: any[]) => { + const x_scales = [], + y_scales = []; + for (let i = 0; i < marks.length; ++i) { + const preserve_domain = marks[i].get('preserve_domain'); + const scales = marks[i].get('scales'); + _.each(scales, (v, k) => { + const dimension = marks[i].get('scales_metadata')[k]['dimension']; + if (dimension === 'x' && !preserve_domain[k]) { + x_scales.push(scales[k]); + } + if (dimension === 'y' && !preserve_domain[k]) { + y_scales.push(scales[k]); + } + }); + } + model.set('scales', { + x: x_scales, + y: y_scales, + }); + model.save_changes(); + return model; + }); + }); + } + + /** + * Reset the scales, delete the PanZoom widget, set the figure + * interaction back to its previous value. + */ + reset() { + this.set('interaction', this._panzoomData.cached_interaction); + const panzoom = this._panzoomData._panzoom; + panzoom.reset_scales(); + panzoom.close(); + this._panzoomData._panzoom = null; + this._panzoomData._panning = false; + this.save_changes(); + } + + static serializers = { ...widgets.DOMWidgetModel.serializers, marks: { deserialize: widgets.unpack_models }, @@ -85,4 +167,7 @@ export class FigureModel extends widgets.DOMWidgetModel { scale_y: { deserialize: widgets.unpack_models }, layout: { deserialize: widgets.unpack_models }, }; + + private _panzoomData: { _panning: boolean; cached_interaction: Interaction, _panzoom: PanZoomModel|undefined } = + { _panning: false, cached_interaction: null, _panzoom: undefined }; } diff --git a/js/src/Toolbar.ts b/js/src/Toolbar.ts index 11e9de286..0983a6085 100644 --- a/js/src/Toolbar.ts +++ b/js/src/Toolbar.ts @@ -57,7 +57,7 @@ export class ToolbarModel extends widgets.DOMWidgetModel { } else { if (figure) { this.cached_interaction = figure.get('interaction'); - const panzoom = this.get('_panzoom'); + const panzoom = this.get('_panzoom'); if (panzoom) { figure.set('interaction', panzoom); figure.save_changes(); From 1dae44e816fcd972970a98caa1b0737a48e6789c Mon Sep 17 00:00:00 2001 From: Trung Le Date: Wed, 4 Aug 2021 15:39:22 +0200 Subject: [PATCH 02/10] Lint the code --- js/less/bqplot.less | 3 +-- js/src/Figure.ts | 7 ++----- js/src/FigureModel.ts | 14 ++++++++------ js/src/Graph.ts | 14 ++++++++++---- js/src/GridHeatMap.ts | 2 +- js/src/HeatMap.ts | 8 ++++++-- js/src/HeatMapModel.ts | 4 +++- js/src/HistModel.ts | 4 +++- js/src/PanZoom.ts | 4 ++-- js/src/ScatterGL.ts | 32 ++++++++++++++++---------------- js/src/Toolbar.ts | 2 +- 11 files changed, 53 insertions(+), 41 deletions(-) diff --git a/js/less/bqplot.less b/js/less/bqplot.less index 4831bbdb5..ed1ae174e 100644 --- a/js/less/bqplot.less +++ b/js/less/bqplot.less @@ -327,9 +327,8 @@ } .bqplot .toolbar_div { - position:absolute; + position: absolute; transition: visibility 0.5s linear, opacity 0.5s linear; - } .tooltip_div { diff --git a/js/src/Figure.ts b/js/src/Figure.ts index ea83bf9c7..9f4aa775f 100644 --- a/js/src/Figure.ts +++ b/js/src/Figure.ts @@ -1288,14 +1288,13 @@ export class Figure extends widgets.DOMWidgetView { (this.model as FigureModel).reset(); }; - const _save = document.createElement('button'); _save.classList.add('jupyter-widgets'); // @jupyter-widgets/controls css _save.classList.add('jupyter-button'); // @jupyter-widgets/controls css _save.setAttribute('data-toggle', 'tooltip'); _save.setAttribute('title', 'Save'); const saveicon = document.createElement('i'); - saveicon.style.marginRight = '0px' + saveicon.style.marginRight = '0px'; saveicon.className = 'fa fa-save'; _save.appendChild(saveicon); _save.onclick = (e) => { @@ -1323,8 +1322,6 @@ export class Figure extends widgets.DOMWidgetView { return toolbar; } - - axis_views: widgets.ViewList; bg: d3.Selection; bg_events: d3.Selection; @@ -1364,7 +1361,7 @@ export class Figure extends widgets.DOMWidgetView { private dummyNodes: Dict = {}; private _update_requested: boolean; - private relayoutRequested: boolean = false; + private relayoutRequested = false; // this is public for the test framework, but considered a private API public _initial_marks_created: Promise; diff --git a/js/src/FigureModel.ts b/js/src/FigureModel.ts index 135e6882e..a4a8ec28a 100644 --- a/js/src/FigureModel.ts +++ b/js/src/FigureModel.ts @@ -16,7 +16,7 @@ import * as widgets from '@jupyter-widgets/base'; import { semver_range } from './version'; import { Interaction } from './Interaction'; -import {PanZoomModel} from './PanZoomModel' +import { PanZoomModel } from './PanZoomModel'; import * as _ from 'underscore'; export class FigureModel extends widgets.DOMWidgetModel { defaults() { @@ -73,7 +73,7 @@ export class FigureModel extends widgets.DOMWidgetModel { } } - save_png() { + save_png() { // TODO: Any view of this Figure model will pick up this event // and render a png. Remove this eventually. this.trigger('save_png'); @@ -82,7 +82,7 @@ export class FigureModel extends widgets.DOMWidgetModel { panzoom() { if (this._panzoomData._panning) { this.set('interaction', this._panzoomData.cached_interaction); - this._panzoomData._panning = false; + this._panzoomData._panning = false; this.save_changes(); } else { this._panzoomData.cached_interaction = this.get('interaction'); @@ -157,7 +157,6 @@ export class FigureModel extends widgets.DOMWidgetModel { this.save_changes(); } - static serializers = { ...widgets.DOMWidgetModel.serializers, marks: { deserialize: widgets.unpack_models }, @@ -168,6 +167,9 @@ export class FigureModel extends widgets.DOMWidgetModel { layout: { deserialize: widgets.unpack_models }, }; - private _panzoomData: { _panning: boolean; cached_interaction: Interaction, _panzoom: PanZoomModel|undefined } = - { _panning: false, cached_interaction: null, _panzoom: undefined }; + private _panzoomData: { + _panning: boolean; + cached_interaction: Interaction; + _panzoom: PanZoomModel | undefined; + } = { _panning: false, cached_interaction: null, _panzoom: undefined }; } diff --git a/js/src/Graph.ts b/js/src/Graph.ts index 74f930fae..c69537e66 100644 --- a/js/src/Graph.ts +++ b/js/src/Graph.ts @@ -370,7 +370,9 @@ export class Graph extends Mark { } private dragstarted(d: NodeData) { - if (this.model.static) return; + if (this.model.static) { + return; + } if (!d3GetEvent().active) { this.force_layout.alphaTarget(0.4).restart(); } @@ -379,13 +381,17 @@ export class Graph extends Mark { } private dragged(d: NodeData) { - if (this.model.static) return; + if (this.model.static) { + return; + } d.fx = d3GetEvent().x; d.fy = d3GetEvent().y; } private dragended(d: NodeData) { - if (this.model.static) return; + if (this.model.static) { + return; + } if (!d3GetEvent().active) { this.force_layout.alphaTarget(0.4); } @@ -573,7 +579,7 @@ export class Graph extends Mark { compute_view_padding() { const xPadding = d3.max( - this.model.mark_data.map(function (d) { + this.model.mark_data.map((d) => { return ( (d.shape_attrs.r || d.shape_attrs.width / 2 || d.shape_attrs.rx) + 1.0 ); diff --git a/js/src/GridHeatMap.ts b/js/src/GridHeatMap.ts index 6e1ea0056..a5c06088b 100644 --- a/js/src/GridHeatMap.ts +++ b/js/src/GridHeatMap.ts @@ -317,7 +317,7 @@ export class GridHeatMap extends Mark { } const clearing_style = {}; - for (let key in style_dict) { + for (const key in style_dict) { clearing_style[key] = null; } applyStyles(elements, clearing_style); diff --git a/js/src/HeatMap.ts b/js/src/HeatMap.ts index e12c60261..d9c024655 100644 --- a/js/src/HeatMap.ts +++ b/js/src/HeatMap.ts @@ -60,10 +60,14 @@ export class HeatMap extends Mark { set_positional_scales() { this.listenTo(this.scales.x, 'domain_changed', () => { - if (!this.model.dirty) this.draw(); + if (!this.model.dirty) { + this.draw(); + } }); this.listenTo(this.scales.y, 'domain_changed', () => { - if (!this.model.dirty) this.draw(); + if (!this.model.dirty) { + this.draw(); + } }); } diff --git a/js/src/HeatMapModel.ts b/js/src/HeatMapModel.ts index dea10736f..7099fb72c 100644 --- a/js/src/HeatMapModel.ts +++ b/js/src/HeatMapModel.ts @@ -63,7 +63,9 @@ export class HeatMapModel extends MarkModel { } update_domains() { - if (!this.mark_data) return; + if (!this.mark_data) { + return; + } const scales = this.get('scales'); const flat_colors = [].concat.apply( diff --git a/js/src/HistModel.ts b/js/src/HistModel.ts index 92835080c..59fa7f744 100644 --- a/js/src/HistModel.ts +++ b/js/src/HistModel.ts @@ -162,7 +162,9 @@ export class HistModel extends MarkModel { } update_domains() { - if (!this.mark_data) return; + if (!this.mark_data) { + return; + } // For histogram, changing the x-scale domain changes a lot of // things including the data which is to be plotted. So the x-domain diff --git a/js/src/PanZoom.ts b/js/src/PanZoom.ts index 205ab5f6a..e9495954b 100644 --- a/js/src/PanZoom.ts +++ b/js/src/PanZoom.ts @@ -27,7 +27,7 @@ export class PanZoom extends interaction.Interaction { const that = this; // chrome bug that requires a listener on the parent svg node // https://github.com/d3/d3-zoom/issues/231#issuecomment-802713799 - this.parent.svg.node().addEventListener(`wheel`, nop, { passive: false }); + this.parent.svg.node().addEventListener('wheel', nop, { passive: false }); this.d3el .style('cursor', 'move') .call( @@ -56,7 +56,7 @@ export class PanZoom extends interaction.Interaction { } remove() { - this.parent.svg.node().removeEventListener(`wheel`, nop); + this.parent.svg.node().removeEventListener('wheel', nop); super.remove(); } diff --git a/js/src/ScatterGL.ts b/js/src/ScatterGL.ts index 44f56abba..35b783ccc 100644 --- a/js/src/ScatterGL.ts +++ b/js/src/ScatterGL.ts @@ -114,10 +114,10 @@ class AttributeParameters { class ColorAttributeParameters extends AttributeParameters { constructor( array: TypedArray, - item_size: number = 1, - mesh_per_attribute: number = 1, - normalized: boolean = false, - use_colormap: boolean = true + item_size = 1, + mesh_per_attribute = 1, + normalized = false, + use_colormap = true ) { super(array, item_size, mesh_per_attribute, normalized); this.use_colormap = use_colormap; @@ -129,10 +129,10 @@ class ColorAttributeParameters extends AttributeParameters { class SelectionAttributeParameters extends AttributeParameters { constructor( array: TypedArray, - item_size: number = 1, - mesh_per_attribute: number = 1, - normalized: boolean = false, - use_selection: boolean = true + item_size = 1, + mesh_per_attribute = 1, + normalized = false, + use_selection = true ) { super(array, item_size, mesh_per_attribute, normalized); this.use_selection = use_selection; @@ -677,7 +677,7 @@ export class ScatterGL extends Mark { value: THREE.InstancedBufferAttribute, value_previous: THREE.InstancedBufferAttribute, new_parameters: AttributeParameters, - animate: boolean = true, + animate = true, after_animation: Function = () => {} ) { if (animate) { @@ -752,7 +752,7 @@ export class ScatterGL extends Mark { this.transition(set, after_animation, this); } - update_x(rerender: boolean = true) { + update_x(rerender = true) { const x_array = to_float_array(this.model.get('x')); const new_markers_number = Math.min(x_array.length, this.y.array.length); @@ -774,7 +774,7 @@ export class ScatterGL extends Mark { } } - update_y(rerender: boolean = true) { + update_y(rerender = true) { const y_array = to_float_array(this.model.get('y')); const new_markers_number = Math.min(this.x.array.length, y_array.length); @@ -816,7 +816,7 @@ export class ScatterGL extends Mark { } } - update_color(rerender: boolean = true) { + update_color(rerender = true) { const color_parameters = this.get_color_attribute_parameters(); this.color = this.update_attribute('color', this.color, color_parameters); this.color.normalized = color_parameters.normalized; @@ -830,7 +830,7 @@ export class ScatterGL extends Mark { } } - update_opacity(rerender: boolean = true) { + update_opacity(rerender = true) { const opacity_parameters = this.get_opacity_attribute_parameters(); [this.opacity, this.opacity_previous] = this.update_attributes( 'opacity', @@ -844,7 +844,7 @@ export class ScatterGL extends Mark { } } - update_size(rerender: boolean = true) { + update_size(rerender = true) { const size_parameters = this.get_size_attribute_parameters(); [this.size, this.size_previous] = this.update_attributes( 'size', @@ -858,7 +858,7 @@ export class ScatterGL extends Mark { } } - update_rotation(rerender: boolean = true) { + update_rotation(rerender = true) { const rotation_parameters = this.get_rotation_attribute_parameters(); [this.rotation, this.rotation_previous] = this.update_attributes( 'rotation', @@ -872,7 +872,7 @@ export class ScatterGL extends Mark { } } - update_selected(rerender: boolean = true) { + update_selected(rerender = true) { const selected_parameters = this.get_selected_attribute_parameters(); this.selected = this.update_attribute( 'selected', diff --git a/js/src/Toolbar.ts b/js/src/Toolbar.ts index 0983a6085..11e9de286 100644 --- a/js/src/Toolbar.ts +++ b/js/src/Toolbar.ts @@ -57,7 +57,7 @@ export class ToolbarModel extends widgets.DOMWidgetModel { } else { if (figure) { this.cached_interaction = figure.get('interaction'); - const panzoom = this.get('_panzoom'); + const panzoom = this.get('_panzoom'); if (panzoom) { figure.set('interaction', panzoom); figure.save_changes(); From 2df2b684e14bbca341e11e50f9b1af591f0c18c9 Mon Sep 17 00:00:00 2001 From: Trung Le Date: Fri, 6 Aug 2021 13:58:29 +0200 Subject: [PATCH 03/10] Remove unused property in `Figure` --- bqplot/figure.py | 2 ++ bqplot/pyplot.py | 2 +- js/src/Figure.ts | 11 +++++++---- js/src/FigureModel.ts | 40 ++++++++++++++++++++++------------------ 4 files changed, 32 insertions(+), 23 deletions(-) diff --git a/bqplot/figure.py b/bqplot/figure.py index e98ed401f..39caab381 100644 --- a/bqplot/figure.py +++ b/bqplot/figure.py @@ -87,6 +87,8 @@ class Figure(DOMWidget): pixel_ratio: Pixel ratio of the WebGL canvas (2 on retina screens). Set to 1 for better performance, but less crisp edges. If set to None it will use the browser's window.devicePixelRatio. + show_toolbar: boolean (default: True) + Show or hide the integrated toolbar. Layout Attributes diff --git a/bqplot/pyplot.py b/bqplot/pyplot.py index 48422857f..760339eec 100644 --- a/bqplot/pyplot.py +++ b/bqplot/pyplot.py @@ -118,7 +118,7 @@ def hashable(data, v): return True -def show(key=None, display_toolbar=True): +def show(key=None, display_toolbar=False): """Shows the current context figure in the output area. Parameters diff --git a/js/src/Figure.ts b/js/src/Figure.ts index 9f4aa775f..38b55c065 100644 --- a/js/src/Figure.ts +++ b/js/src/Figure.ts @@ -330,7 +330,7 @@ export class Figure extends widgets.DOMWidgetView { }); if (this.model.get('show_toolbar')) { - this.toolbar_div = this.create_toolbar(); + this.create_toolbar(); } return Promise.all([mark_views_updated, axis_views_updated]); @@ -1255,7 +1255,12 @@ export class Figure extends widgets.DOMWidgetView { this.el.classList.add(this.model.get('theme')); } - create_toolbar() { + /** + * Generate an integrated toolbar which is shown on mouse over + * for this figure. + * + */ + create_toolbar(): void { const toolbar = d3 .select(document.createElement('div')) .attr('class', 'toolbar_div'); @@ -1319,7 +1324,6 @@ export class Figure extends widgets.DOMWidgetView { toolbar.node().style.visibility = 'hidden'; toolbar.node().style.opacity = '0'; }); - return toolbar; } axis_views: widgets.ViewList; @@ -1351,7 +1355,6 @@ export class Figure extends widgets.DOMWidgetView { svg_background: d3.Selection; title: d3.Selection; tooltip_div: d3.Selection; - toolbar_div: d3.Selection; width: number; x_pad_dict: { [id: string]: number }; xPaddingArr: { [id: string]: number }; diff --git a/js/src/FigureModel.ts b/js/src/FigureModel.ts index a4a8ec28a..9434e2491 100644 --- a/js/src/FigureModel.ts +++ b/js/src/FigureModel.ts @@ -79,34 +79,38 @@ export class FigureModel extends widgets.DOMWidgetModel { this.trigger('save_png'); } - panzoom() { - if (this._panzoomData._panning) { + /** + * Start or stop pan zoom mode + * + */ + panzoom(): void { + if (this._panzoomData.panning) { this.set('interaction', this._panzoomData.cached_interaction); - this._panzoomData._panning = false; + this._panzoomData.panning = false; this.save_changes(); } else { this._panzoomData.cached_interaction = this.get('interaction'); - const panzoom = this._panzoomData._panzoom; + const panzoom = this._panzoomData.panzoom; if (panzoom) { this.set('interaction', panzoom); this.save_changes(); } else { this._create_panzoom_model().then((model) => { this.set('interaction', model); - this._panzoomData._panzoom = model; + this._panzoomData.panzoom = model; this.save_changes(); }); } - this._panzoomData._panning = true; + this._panzoomData.panning = true; } } + /** + * Creates a panzoom interaction widget for the this model. + * + * It will discover the relevant scales of this model. + */ _create_panzoom_model(): Promise { - /** - * Creates a panzoom interaction widget for the specified figure. - * - * It will discover the relevant scales for the specified figure. - */ return this.widget_manager .new_widget({ model_name: 'PanZoomModel', @@ -147,13 +151,13 @@ export class FigureModel extends widgets.DOMWidgetModel { * Reset the scales, delete the PanZoom widget, set the figure * interaction back to its previous value. */ - reset() { + reset(): void { this.set('interaction', this._panzoomData.cached_interaction); - const panzoom = this._panzoomData._panzoom; + const panzoom = this._panzoomData.panzoom; panzoom.reset_scales(); panzoom.close(); - this._panzoomData._panzoom = null; - this._panzoomData._panning = false; + this._panzoomData.panzoom = null; + this._panzoomData.panning = false; this.save_changes(); } @@ -168,8 +172,8 @@ export class FigureModel extends widgets.DOMWidgetModel { }; private _panzoomData: { - _panning: boolean; + panning: boolean; cached_interaction: Interaction; - _panzoom: PanZoomModel | undefined; - } = { _panning: false, cached_interaction: null, _panzoom: undefined }; + panzoom: PanZoomModel | undefined; + } = { panning: false, cached_interaction: null, panzoom: undefined }; } From 9ed729a0b5b8bbe9cb659a1583b4b58dd21bb6e6 Mon Sep 17 00:00:00 2001 From: Trung Le Date: Mon, 16 Aug 2021 10:35:44 +0200 Subject: [PATCH 04/10] Handle `show_toolbar` dynamically --- js/src/Figure.ts | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/js/src/Figure.ts b/js/src/Figure.ts index 38b55c065..5c3f5de04 100644 --- a/js/src/Figure.ts +++ b/js/src/Figure.ts @@ -329,10 +329,20 @@ export class Figure extends widgets.DOMWidgetView { window.removeEventListener('resize', this.debouncedRelayout); }); + this.toolbar_div = this.create_toolbar(); if (this.model.get('show_toolbar')) { - this.create_toolbar(); + this.toolbar_div.node().style.display = 'unset' } + this.model.on('change:show_toolbar', (_, show_toolbar) => { + const toolbar = this.toolbar_div.node() + if(show_toolbar){ + toolbar.style.display = 'unset' + } else { + toolbar.style.display = 'none' + } + }) + return Promise.all([mark_views_updated, axis_views_updated]); } @@ -1260,7 +1270,7 @@ export class Figure extends widgets.DOMWidgetView { * for this figure. * */ - create_toolbar(): void { + create_toolbar(): d3.Selection { const toolbar = d3 .select(document.createElement('div')) .attr('class', 'toolbar_div'); @@ -1324,6 +1334,8 @@ export class Figure extends widgets.DOMWidgetView { toolbar.node().style.visibility = 'hidden'; toolbar.node().style.opacity = '0'; }); + toolbar.node().style.display = 'none' + return toolbar; } axis_views: widgets.ViewList; @@ -1355,6 +1367,7 @@ export class Figure extends widgets.DOMWidgetView { svg_background: d3.Selection; title: d3.Selection; tooltip_div: d3.Selection; + toolbar_div: d3.Selection; width: number; x_pad_dict: { [id: string]: number }; xPaddingArr: { [id: string]: number }; From c258dfb243712e86ef3eafcc4ff9de4497a35c90 Mon Sep 17 00:00:00 2001 From: Trung Le Date: Mon, 16 Aug 2021 10:42:08 +0200 Subject: [PATCH 05/10] Remove unnecessary `_` symbols --- .gitignore | 3 --- js/src/Figure.ts | 48 +++++++++++++++++++++---------------------- js/src/FigureModel.ts | 28 ++++++++++++------------- 3 files changed, 38 insertions(+), 41 deletions(-) diff --git a/.gitignore b/.gitignore index 172cd59ea..32aee2c71 100644 --- a/.gitignore +++ b/.gitignore @@ -64,6 +64,3 @@ node_modules *.swp *.tsbuildinfo - -# VS Code settings -.vscode/* \ No newline at end of file diff --git a/js/src/Figure.ts b/js/src/Figure.ts index 5c3f5de04..c44ee506d 100644 --- a/js/src/Figure.ts +++ b/js/src/Figure.ts @@ -1275,51 +1275,51 @@ export class Figure extends widgets.DOMWidgetView { .select(document.createElement('div')) .attr('class', 'toolbar_div'); - const _panzoom = document.createElement('button'); - _panzoom.classList.add('jupyter-widgets'); // @jupyter-widgets/controls css - _panzoom.classList.add('jupyter-button'); // @jupyter-widgets/controls css - _panzoom.setAttribute('data-toggle', 'tooltip'); - _panzoom.setAttribute('title', 'PanZoom'); + const panzoom = document.createElement('button'); + panzoom.classList.add('jupyter-widgets'); // @jupyter-widgets/controls css + panzoom.classList.add('jupyter-button'); // @jupyter-widgets/controls css + panzoom.setAttribute('data-toggle', 'tooltip'); + panzoom.setAttribute('title', 'PanZoom'); const panzoomicon = document.createElement('i'); panzoomicon.style.marginRight = '0px'; panzoomicon.className = 'fa fa-arrows'; - _panzoom.appendChild(panzoomicon); - _panzoom.onclick = (e) => { + panzoom.appendChild(panzoomicon); + panzoom.onclick = (e) => { e.preventDefault(); (this.model as FigureModel).panzoom(); }; - const _reset = document.createElement('button'); - _reset.classList.add('jupyter-widgets'); // @jupyter-widgets/controls css - _reset.classList.add('jupyter-button'); // @jupyter-widgets/controls css - _reset.setAttribute('data-toggle', 'tooltip'); - _reset.setAttribute('title', 'Reset'); + const reset = document.createElement('button'); + reset.classList.add('jupyter-widgets'); // @jupyter-widgets/controls css + reset.classList.add('jupyter-button'); // @jupyter-widgets/controls css + reset.setAttribute('data-toggle', 'tooltip'); + reset.setAttribute('title', 'Reset'); const refreshicon = document.createElement('i'); refreshicon.style.marginRight = '0px'; refreshicon.className = 'fa fa-refresh'; - _reset.appendChild(refreshicon); - _reset.onclick = (e) => { + reset.appendChild(refreshicon); + reset.onclick = (e) => { e.preventDefault(); (this.model as FigureModel).reset(); }; - const _save = document.createElement('button'); - _save.classList.add('jupyter-widgets'); // @jupyter-widgets/controls css - _save.classList.add('jupyter-button'); // @jupyter-widgets/controls css - _save.setAttribute('data-toggle', 'tooltip'); - _save.setAttribute('title', 'Save'); + const save = document.createElement('button'); + save.classList.add('jupyter-widgets'); // @jupyter-widgets/controls css + save.classList.add('jupyter-button'); // @jupyter-widgets/controls css + save.setAttribute('data-toggle', 'tooltip'); + save.setAttribute('title', 'Save'); const saveicon = document.createElement('i'); saveicon.style.marginRight = '0px'; saveicon.className = 'fa fa-save'; - _save.appendChild(saveicon); - _save.onclick = (e) => { + save.appendChild(saveicon); + save.onclick = (e) => { e.preventDefault(); this.save_png(undefined, undefined); }; - toolbar.node().appendChild(_panzoom); - toolbar.node().appendChild(_reset); - toolbar.node().appendChild(_save); + toolbar.node().appendChild(panzoom); + toolbar.node().appendChild(reset); + toolbar.node().appendChild(save); this.el.appendChild(toolbar.node()); toolbar.node().style.top = `${this.margin.top / 2.0}px`; diff --git a/js/src/FigureModel.ts b/js/src/FigureModel.ts index 9434e2491..cbe634e93 100644 --- a/js/src/FigureModel.ts +++ b/js/src/FigureModel.ts @@ -84,24 +84,24 @@ export class FigureModel extends widgets.DOMWidgetModel { * */ panzoom(): void { - if (this._panzoomData.panning) { - this.set('interaction', this._panzoomData.cached_interaction); - this._panzoomData.panning = false; + if (this.panzoomData.panning) { + this.set('interaction', this.panzoomData.cached_interaction); + this.panzoomData.panning = false; this.save_changes(); } else { - this._panzoomData.cached_interaction = this.get('interaction'); - const panzoom = this._panzoomData.panzoom; + this.panzoomData.cached_interaction = this.get('interaction'); + const panzoom = this.panzoomData.panzoom; if (panzoom) { this.set('interaction', panzoom); this.save_changes(); } else { - this._create_panzoom_model().then((model) => { + this.create_panzoom_model().then((model) => { this.set('interaction', model); - this._panzoomData.panzoom = model; + this.panzoomData.panzoom = model; this.save_changes(); }); } - this._panzoomData.panning = true; + this.panzoomData.panning = true; } } @@ -110,7 +110,7 @@ export class FigureModel extends widgets.DOMWidgetModel { * * It will discover the relevant scales of this model. */ - _create_panzoom_model(): Promise { + private create_panzoom_model(): Promise { return this.widget_manager .new_widget({ model_name: 'PanZoomModel', @@ -152,12 +152,12 @@ export class FigureModel extends widgets.DOMWidgetModel { * interaction back to its previous value. */ reset(): void { - this.set('interaction', this._panzoomData.cached_interaction); - const panzoom = this._panzoomData.panzoom; + this.set('interaction', this.panzoomData.cached_interaction); + const panzoom = this.panzoomData.panzoom; panzoom.reset_scales(); panzoom.close(); - this._panzoomData.panzoom = null; - this._panzoomData.panning = false; + this.panzoomData.panzoom = null; + this.panzoomData.panning = false; this.save_changes(); } @@ -171,7 +171,7 @@ export class FigureModel extends widgets.DOMWidgetModel { layout: { deserialize: widgets.unpack_models }, }; - private _panzoomData: { + private panzoomData: { panning: boolean; cached_interaction: Interaction; panzoom: PanZoomModel | undefined; From cfb4ec7316b047fcc2cc9349012747976564268d Mon Sep 17 00:00:00 2001 From: Trung Le Date: Mon, 16 Aug 2021 11:00:21 +0200 Subject: [PATCH 06/10] Rename `show_toolbar` into `display_toolbar` --- bqplot/figure.py | 4 ++-- js/src/Figure.ts | 6 +++--- js/src/FigureModel.ts | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/bqplot/figure.py b/bqplot/figure.py index 39caab381..af143fea5 100644 --- a/bqplot/figure.py +++ b/bqplot/figure.py @@ -87,7 +87,7 @@ class Figure(DOMWidget): pixel_ratio: Pixel ratio of the WebGL canvas (2 on retina screens). Set to 1 for better performance, but less crisp edges. If set to None it will use the browser's window.devicePixelRatio. - show_toolbar: boolean (default: True) + display_toolbar: boolean (default: True) Show or hide the integrated toolbar. Layout Attributes @@ -154,7 +154,7 @@ class Figure(DOMWidget): .tag(sync=True, display_name='Legend position') animation_duration = Int().tag(sync=True, display_name='Animation duration') - show_toolbar = Bool(default_value=True).tag(sync=True) + display_toolbar = Bool(default_value=True).tag(sync=True) @default('scale_x') def _default_scale_x(self): diff --git a/js/src/Figure.ts b/js/src/Figure.ts index c44ee506d..46ef09d1d 100644 --- a/js/src/Figure.ts +++ b/js/src/Figure.ts @@ -330,13 +330,13 @@ export class Figure extends widgets.DOMWidgetView { }); this.toolbar_div = this.create_toolbar(); - if (this.model.get('show_toolbar')) { + if (this.model.get('display_toolbar')) { this.toolbar_div.node().style.display = 'unset' } - this.model.on('change:show_toolbar', (_, show_toolbar) => { + this.model.on('change:display_toolbar', (_, display_toolbar) => { const toolbar = this.toolbar_div.node() - if(show_toolbar){ + if(display_toolbar){ toolbar.style.display = 'unset' } else { toolbar.style.display = 'none' diff --git a/js/src/FigureModel.ts b/js/src/FigureModel.ts index cbe634e93..4a6f4fc1a 100644 --- a/js/src/FigureModel.ts +++ b/js/src/FigureModel.ts @@ -56,7 +56,7 @@ export class FigureModel extends widgets.DOMWidgetModel { padding_y: 0.025, legend_location: 'top-right', animation_duration: 0, - show_toolbar: true, + display_toolbar: true, }; } From 38a3b3e731d185a1563fb3b73d5d08c20c9f4db9 Mon Sep 17 00:00:00 2001 From: Trung Le Date: Mon, 16 Aug 2021 22:40:26 +0200 Subject: [PATCH 07/10] Remove old toolbar from pyplot figure --- bqplot/pyplot.py | 11 +++-------- js/src/Figure.ts | 5 +++-- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/bqplot/pyplot.py b/bqplot/pyplot.py index 760339eec..3f9bdc8b6 100644 --- a/bqplot/pyplot.py +++ b/bqplot/pyplot.py @@ -118,7 +118,7 @@ def hashable(data, v): return True -def show(key=None, display_toolbar=False): +def show(key=None, display_toolbar=True): """Shows the current context figure in the output area. Parameters @@ -152,13 +152,8 @@ def show(key=None, display_toolbar=False): figure = current_figure() else: figure = _context['figure_registry'][key] - if display_toolbar: - if not hasattr(figure, 'pyplot'): - figure.pyplot = Toolbar(figure=figure) - figure.pyplot_vbox = VBox([figure, figure.pyplot]) - display(figure.pyplot_vbox) - else: - display(figure) + figure.display_toolbar = display_toolbar + display(figure) def figure(key=None, fig=None, **kwargs): diff --git a/js/src/Figure.ts b/js/src/Figure.ts index 46ef09d1d..7ba1ecc3b 100644 --- a/js/src/Figure.ts +++ b/js/src/Figure.ts @@ -335,6 +335,7 @@ export class Figure extends widgets.DOMWidgetView { } this.model.on('change:display_toolbar', (_, display_toolbar) => { + const toolbar = this.toolbar_div.node() if(display_toolbar){ toolbar.style.display = 'unset' @@ -1324,8 +1325,8 @@ export class Figure extends widgets.DOMWidgetView { this.el.appendChild(toolbar.node()); toolbar.node().style.top = `${this.margin.top / 2.0}px`; toolbar.node().style.right = `${this.margin.right}px`; - toolbar.node().style.visibility = 'hidden'; - toolbar.node().style.opacity = '0'; + toolbar.node().style.visibility = 'visible'; + toolbar.node().style.opacity = '1'; this.el.addEventListener('mouseenter', () => { toolbar.node().style.visibility = 'visible'; toolbar.node().style.opacity = '1'; From 001e52c0642f0694e30f54a3d2aae70ea0261324 Mon Sep 17 00:00:00 2001 From: Trung Le Date: Tue, 17 Aug 2021 13:44:09 +0200 Subject: [PATCH 08/10] Remove unused imports --- bqplot/pyplot.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/bqplot/pyplot.py b/bqplot/pyplot.py index 3f9bdc8b6..fcbd943ce 100644 --- a/bqplot/pyplot.py +++ b/bqplot/pyplot.py @@ -50,7 +50,6 @@ import sys from collections import OrderedDict from IPython.display import display -from ipywidgets import VBox from ipywidgets import Image as ipyImage from numpy import arange, issubdtype, array, column_stack, shape from .figure import Figure @@ -58,7 +57,6 @@ from .axes import Axis from .marks import (Lines, Scatter, ScatterGL, Hist, Bars, OHLC, Pie, Map, Image, Label, HeatMap, GridHeatMap, topo_load, Boxplot, Bins) -from .toolbar import Toolbar from .interacts import (BrushIntervalSelector, FastIntervalSelector, BrushSelector, IndexSelector, MultiSelector, LassoSelector) From 247ec971d1d9d7bc1774f3554c3d87d7e19153f6 Mon Sep 17 00:00:00 2001 From: Trung Le Date: Tue, 17 Aug 2021 14:50:13 +0200 Subject: [PATCH 09/10] Update code style --- js/src/Figure.ts | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/js/src/Figure.ts b/js/src/Figure.ts index 7ba1ecc3b..ab6dc0557 100644 --- a/js/src/Figure.ts +++ b/js/src/Figure.ts @@ -331,18 +331,17 @@ export class Figure extends widgets.DOMWidgetView { this.toolbar_div = this.create_toolbar(); if (this.model.get('display_toolbar')) { - this.toolbar_div.node().style.display = 'unset' + this.toolbar_div.node().style.display = 'unset'; } this.model.on('change:display_toolbar', (_, display_toolbar) => { - - const toolbar = this.toolbar_div.node() - if(display_toolbar){ - toolbar.style.display = 'unset' + const toolbar = this.toolbar_div.node(); + if (display_toolbar) { + toolbar.style.display = 'unset'; } else { - toolbar.style.display = 'none' + toolbar.style.display = 'none'; } - }) + }); return Promise.all([mark_views_updated, axis_views_updated]); } @@ -1335,7 +1334,7 @@ export class Figure extends widgets.DOMWidgetView { toolbar.node().style.visibility = 'hidden'; toolbar.node().style.opacity = '0'; }); - toolbar.node().style.display = 'none' + toolbar.node().style.display = 'none'; return toolbar; } @@ -1368,7 +1367,7 @@ export class Figure extends widgets.DOMWidgetView { svg_background: d3.Selection; title: d3.Selection; tooltip_div: d3.Selection; - toolbar_div: d3.Selection; + toolbar_div: d3.Selection; width: number; x_pad_dict: { [id: string]: number }; xPaddingArr: { [id: string]: number }; From a23edd22ce9e3974d7305d57703a9e2fe03e012f Mon Sep 17 00:00:00 2001 From: Trung Le Date: Wed, 18 Aug 2021 09:17:09 +0200 Subject: [PATCH 10/10] Hide toolbar by default --- js/src/Figure.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/js/src/Figure.ts b/js/src/Figure.ts index ab6dc0557..5bd55c1ec 100644 --- a/js/src/Figure.ts +++ b/js/src/Figure.ts @@ -1324,8 +1324,8 @@ export class Figure extends widgets.DOMWidgetView { this.el.appendChild(toolbar.node()); toolbar.node().style.top = `${this.margin.top / 2.0}px`; toolbar.node().style.right = `${this.margin.right}px`; - toolbar.node().style.visibility = 'visible'; - toolbar.node().style.opacity = '1'; + toolbar.node().style.visibility = 'hidden'; + toolbar.node().style.opacity = '0'; this.el.addEventListener('mouseenter', () => { toolbar.node().style.visibility = 'visible'; toolbar.node().style.opacity = '1';