From 92cc8357938c3e92cf7713e1f22334ad72decb08 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 26 Apr 2021 12:54:10 +0200 Subject: [PATCH] Refactor handling of Plotly updates --- panel/models/plotly.ts | 178 ++++++++++++++++++++++------------------- panel/pane/plotly.py | 65 +++++++-------- 2 files changed, 123 insertions(+), 120 deletions(-) diff --git a/panel/models/plotly.ts b/panel/models/plotly.ts index 97f1932327..a2bdfa5926 100644 --- a/panel/models/plotly.ts +++ b/panel/models/plotly.ts @@ -117,22 +117,29 @@ export class PlotlyPlotView extends PanelHTMLBoxView { _reacting: boolean = false _relayouting: boolean = false _layout_wrapper: PlotlyHTMLElement - _gd: any - + _watched_sources: string[] _end_relayouting = debounce(() => { this._relayouting = false - }, 2000, false) + }, 2000, false) connect_signals(): void { super.connect_signals(); - - this.connect(this.model.properties.viewport_update_policy.change, - this._updateSetViewportFunction); - this.connect(this.model.properties.viewport_update_throttle.change, - this._updateSetViewportFunction); - - this.connect(this.model.properties._render_count.change, this.plot); - this.connect(this.model.properties.viewport.change, this._updateViewportFromProperty); + const {data, data_sources, layout} = this.model.properties + this.on_change([data, data_sources, layout], () => { + const render_count = this.model._render_count + setTimeout(() => { + if (this.model._render_count === render_count) + this.model._render_count += 1; + }, 250) + }); + this.connect(this.model.properties.viewport_update_policy.change, () => { + this._updateSetViewportFunction() + }); + this.connect(this.model.properties.viewport_update_throttle.change, () => { + this._updateSetViewportFunction() + }); + this.connect(this.model.properties._render_count.change, () => this.plot()); + this.connect(this.model.properties.viewport.change, () => this._updateViewportFromProperty()); } render(): void { @@ -143,15 +150,16 @@ export class PlotlyPlotView extends PanelHTMLBoxView { this.plot() } - plot(): void { - if (!(window as any).Plotly) { return } + _trace_data(): void { const data = []; for (let i = 0; i < this.model.data.length; i++) { data.push(this._get_trace(i, false)); } + return data + } - let newLayout = deepCopy(this.model.layout); - + _layout_data(): void { + const newLayout = deepCopy(this.model.layout); if (this._relayouting) { const {layout} = this._layout_wrapper; @@ -163,78 +171,84 @@ export class PlotlyPlotView extends PanelHTMLBoxView { } }, {}); } + return newLayout + } - this._reacting = true; - (window as any).Plotly.react(this._layout_wrapper, data, newLayout, this.model.config).then(() => { - this._updateSetViewportFunction(); - this._updateViewportProperty(); - - if (!this._plotInitialized) { - // Install callbacks + _install_callbacks(): void { + // - plotly_relayout + this._layout_wrapper.on('plotly_relayout', (eventData: any) => { + if (eventData['_update_from_property'] !== true) { + this.model.relayout_data = filterEventData( + this._layout_wrapper, eventData, 'relayout'); - // - plotly_relayout - this._layout_wrapper.on('plotly_relayout', (eventData: any) => { - if (eventData['_update_from_property'] !== true) { - this.model.relayout_data = filterEventData( - this._layout_wrapper, eventData, 'relayout'); + this._updateViewportProperty(); - this._updateViewportProperty(); + this._end_relayouting(); + } + }); - this._end_relayouting(); - } - }); + // - plotly_relayouting + this._layout_wrapper.on('plotly_relayouting', () => { + if (this.model.viewport_update_policy !== 'mouseup') { + this._relayouting = true; + this._updateViewportProperty(); + } + }); + + // - plotly_restyle + this._layout_wrapper.on('plotly_restyle', (eventData: any) => { + this.model.restyle_data = filterEventData( + this._layout_wrapper, eventData, 'restyle'); + + this._updateViewportProperty(); + }); + + // - plotly_click + this._layout_wrapper.on('plotly_click', (eventData: any) => { + this.model.click_data = filterEventData( + this._layout_wrapper, eventData, 'click'); + }); + + // - plotly_hover + this._layout_wrapper.on('plotly_hover', (eventData: any) => { + this.model.hover_data = filterEventData( + this._layout_wrapper, eventData, 'hover'); + }); + + // - plotly_selected + this._layout_wrapper.on('plotly_selected', (eventData: any) => { + this.model.selected_data = filterEventData( + this._layout_wrapper, eventData, 'selected'); + }); + + // - plotly_clickannotation + this._layout_wrapper.on('plotly_clickannotation', (eventData: any) => { + delete eventData["event"]; + delete eventData["fullAnnotation"]; + this.model.clickannotation_data = eventData + }); + + // - plotly_deselect + this._layout_wrapper.on('plotly_deselect', () => { + this.model.selected_data = null; + }); + + // - plotly_unhover + this._layout_wrapper.on('plotly_unhover', () => { + this.model.hover_data = null; + }); + } - // - plotly_relayouting - this._layout_wrapper.on('plotly_relayouting', () => { - if (this.model.viewport_update_policy !== 'mouseup') { - this._relayouting = true; - this._updateViewportProperty(); - } - }); - - // - plotly_restyle - this._layout_wrapper.on('plotly_restyle', (eventData: any) => { - this.model.restyle_data = filterEventData( - this._layout_wrapper, eventData, 'restyle'); - - this._updateViewportProperty(); - }); - - // - plotly_click - this._layout_wrapper.on('plotly_click', (eventData: any) => { - this.model.click_data = filterEventData( - this._layout_wrapper, eventData, 'click'); - }); - - // - plotly_hover - this._layout_wrapper.on('plotly_hover', (eventData: any) => { - this.model.hover_data = filterEventData( - this._layout_wrapper, eventData, 'hover'); - }); - - // - plotly_selected - this._layout_wrapper.on('plotly_selected', (eventData: any) => { - this.model.selected_data = filterEventData( - this._layout_wrapper, eventData, 'selected'); - }); - - // - plotly_clickannotation - this._layout_wrapper.on('plotly_clickannotation', (eventData: any) => { - delete eventData["event"]; - delete eventData["fullAnnotation"]; - this.model.clickannotation_data = eventData - }); - - // - plotly_deselect - this._layout_wrapper.on('plotly_deselect', () => { - this.model.selected_data = null; - }); - - // - plotly_unhover - this._layout_wrapper.on('plotly_unhover', () => { - this.model.hover_data = null; - }); - } + plot(): void { + if (!(window as any).Plotly) { return } + const data = this._trace_data() + const newLayout = this._layout_data() + this._reacting = true; + (window as any).Plotly.react(this._layout_wrapper, data, newLayout, this.model.config).then(() => { + this._updateSetViewportFunction(); + this._updateViewportProperty(); + if (!this._plotInitialized) + this._install_callbacks() this._plotInitialized = true; this._reacting = false; if(!_isHidden(this._layout_wrapper)) diff --git a/panel/pane/plotly.py b/panel/pane/plotly.py index e3953024ae..9cc1b74588 100644 --- a/panel/pane/plotly.py +++ b/panel/pane/plotly.py @@ -177,15 +177,11 @@ def _plotly_json_wrapper(fig): data[idx][key] = arr return json - def _get_model(self, doc, root=None, parent=None, comm=None): - """ - Should return the bokeh model to be rendered. - """ - PlotlyPlot = lazy_load('panel.models.plotly', 'PlotlyPlot', isinstance(comm, JupyterComm)) + def _init_params(self): viewport_params = [p for p in self.param if 'viewport' in p] - params = list(Layoutable.param)+viewport_params - properties = {p : getattr(self, p) for p in params - if getattr(self, p) is not None} + parameters = list(Layoutable.param)+viewport_params + params = {p: getattr(self, p) for p in parameters + if getattr(self, p) is not None} if self.object is None: json, sources = {}, [] @@ -194,33 +190,21 @@ def _get_model(self, doc, root=None, parent=None, comm=None): json = self._plotly_json_wrapper(fig) sources = Plotly._get_sources(json) - data = json.get('data', []) - layout = json.get('layout', {}) + params['_render_count'] = self._render_count + params['config'] = self.config or {} + params['data'] = json.get('data', []) + params['data_sources'] = sources + params['layout'] = layout = json.get('layout', {}) if layout.get('autosize') and self.sizing_mode is self.param.sizing_mode.default: - properties['sizing_mode'] = 'stretch_both' - - model = PlotlyPlot( - data=data, layout=layout, config=self.config or {}, - data_sources=sources, _render_count=self._render_count, - **properties - ) - - if root is None: - root = model - - self._link_props( - model, [ - 'config', 'relayout_data', 'restyle_data', 'click_data', 'hover_data', - 'clickannotation_data', 'selected_data', 'viewport', - 'viewport_update_policy', 'viewport_update_throttle', '_render_count' - ], - doc, - root, - comm - ) + params['sizing_mode'] = 'stretch_both' + return params + def _get_model(self, doc, root=None, parent=None, comm=None): + PlotlyPlot = lazy_load('panel.models.plotly', 'PlotlyPlot', isinstance(comm, JupyterComm)) + model = PlotlyPlot(**self._init_params()) if root is None: root = model + self._link_props(model, self._linkable_params, doc, root, comm) self._models[root.ref['id']] = (model, parent) return model @@ -259,28 +243,33 @@ def _update(self, ref=None, model=None): try: update_data = ( {k: v for k, v in new.items() if k != 'uid'} != - {k: v for k, v in old.items() if k != 'uid'}) + {k: v for k, v in old.items() if k != 'uid'} + ) except Exception: update_data = True if update_data: break + updates = {} if self.sizing_mode is self.param.sizing_mode.default and 'autosize' in layout: autosize = layout.get('autosize') if autosize and model.sizing_mode != 'stretch_both': - model.sizing_mode = 'stretch_both' + updates['sizing_mode'] = 'stretch_both' elif not autosize and model.sizing_mode != 'fixed': - model.sizing_mode = 'fixed' + updates['sizing_mode'] = 'fixed' if new_sources: - model.data_sources += new_sources + updates['data_sources'] = model.data_sources + new_sources if update_data: - model.data = json.get('data') + updates['data'] = json.get('data') if update_layout: - model.layout = layout + updates['layout'] = layout + + if updates: + model.update(**updates) # Check if we should trigger rendering - if new_sources or update_sources or update_data or update_layout: + if updates or update_sources: model._render_count += 1