diff --git a/src/lib/components/LayoutUpdater.react.js b/src/lib/components/LayoutUpdater.react.js new file mode 100644 index 0000000..6255863 --- /dev/null +++ b/src/lib/components/LayoutUpdater.react.js @@ -0,0 +1,84 @@ +import { Component } from 'react'; +import PropTypes from 'prop-types'; +import { getGraphDiv, removeApplyRelayoutEvent } from '../utils/updaterUtils'; + + + +/** + * LayoutUpdater is a component which updates the layout of a plotly graph. + */ +export default class LayoutUpdater extends Component { + static #previousLayout = {}; + + shouldComponentUpdate({ updateData }) { + return typeof updateData == 'object' && LayoutUpdater.#previousLayout !== updateData; + } + + render() { + const { id, gdID, updateData, triggerRelayout } = this.props; + const idDiv =
; + if (!this.shouldComponentUpdate(this.props)) { + console.log("LayoutUpdater " + gdID + ": no update required"); + console.log("LayoutUpdater no update: updateData: " + updateData); + return idDiv; + } + console.log("LayoutUpdater " + gdID + ": update required"); + console.log("LayoutUpdater update: updateData: " + updateData); + + LayoutUpdater.#previousLayout = updateData; + let trgrRelayout = triggerRelayout; + if (updateData.hasOwnProperty('triggerRelayout')) { + trgrRelayout =updateData.triggerRelayout; + delete updateData.triggerRelayout; + } + + let graphDiv = getGraphDiv(gdID); + if (trgrRelayout === true) { Plotly.relayout(graphDiv, updateData); } + else { removeApplyRelayoutEvent(graphDiv, updateData); } + return idDiv; + } +} + +LayoutUpdater.defaultProps = { + triggerRelayout: false, +}; + +LayoutUpdater.propTypes = { + /** + * The ID used to identify this component in Dash callbacks. + */ + id: PropTypes.string, + + /** + * The id of the graph-div whose layout will be updated. + * + * .. Note: + * + * * if you use multiple graphs; each graph MUST have a unique id; otherwise we + * cannot guarantee that resampling will work correctly. + * * LayoutUpdater will determine the html-graph-div by performing partial matching + * on the "id" property (using `gdID`) of all divs with classname="dash-graph". + * It will select the first item of that match list; so if multiple same + * graph-div IDs are used, or one graph-div-ID is a subset of the other (partial + * matching) there is no guarantee that the correct div will be selected. + */ + gdID: PropTypes.string.isRequired, + + /** + * Whether a Relayout will be triggered after the update or not. + */ + triggerRelayout: PropTypes.bool, + + /** + * The layout update data dict. + * If this contains the `triggerRelayout` key, it will be used to determine whether + * a relayout-event will be triggered or not. + */ + updateData: PropTypes.object, + + /** + * Dash-assigned callback that should be called to report property changes + * to Dash, to make them available for callbacks. + */ + setProps: PropTypes.func +}; diff --git a/src/lib/components/TraceUpdater.react.js b/src/lib/components/TraceUpdater.react.js index f28bbe2..3ab5486 100644 --- a/src/lib/components/TraceUpdater.react.js +++ b/src/lib/components/TraceUpdater.react.js @@ -12,9 +12,9 @@ import { fromPairs, mapValues, zipObject, - isElement, uniq } from 'lodash'; +import { getGraphDiv } from '../utils/updaterUtils'; // HELPER FUNCTIONS // @@ -68,41 +68,25 @@ export default class TraceUpdater extends Component { static #previousLayout = null; - shouldComponentUpdate({updateData}) { - return isArray(updateData) && TraceUpdater.#previousLayout !== head(updateData); + shouldComponentUpdate({ updateData }) { + return isArray(updateData) && updateData.length > 1 && TraceUpdater.#previousLayout !== head(updateData); } render() { // VALIDATION // const {id, gdID, sequentialUpdate, updateData} = this.props; const idDiv =
; - if (!this.shouldComponentUpdate(this.props)) { - return idDiv; - } - - // see this link for more information https://stackoverflow.com/a/34002028 - let graphDiv = document?.querySelectorAll('div[id*="' + gdID + '"][class*="dash-graph"]'); - if (graphDiv.length > 1) { - throw new SyntaxError("TraceUpdater: multiple graphs with ID=\"" + gdID + "\" found; n=" + graphDiv.length + " \n(either multiple graphs with same ID's or current ID is a str-subset of other graph IDs)"); - } else if (graphDiv.length < 1) { - throw new SyntaxError("TraceUpdater: no graphs with ID=\"" + gdID + "\" found"); - } - - graphDiv = graphDiv?.[0]?.getElementsByClassName('js-plotly-plot')?.[0]; - if (!isElement(graphDiv)) { - throw new Error(`Invalid gdID '${gdID}'`); - } + if (!this.shouldComponentUpdate(this.props)) { return idDiv; } // EXECUTION // TraceUpdater.#previousLayout = head(updateData); + let graphDiv = getGraphDiv(gdID); const traces = filterTraces(tail(updateData)); - if (sequentialUpdate) { formatTraces(traces).forEach(trace => plotlyRestyle(graphDiv, trace)); - } else { - plotlyRestyle(graphDiv, mergeTraces(traces)); - } + } else { plotlyRestyle(graphDiv, mergeTraces(traces)); } + // RETURN // return idDiv; } } diff --git a/src/lib/index.js b/src/lib/index.js index 9f54856..fbe43d5 100644 --- a/src/lib/index.js +++ b/src/lib/index.js @@ -1,6 +1,8 @@ /* eslint-disable import/prefer-default-export */ import TraceUpdater from './components/TraceUpdater.react'; +import LayoutUpdater from './components/LayoutUpdater.react'; export { - TraceUpdater + TraceUpdater, + LayoutUpdater }; diff --git a/src/lib/utils/updaterUtils.js b/src/lib/utils/updaterUtils.js new file mode 100644 index 0000000..fdb0fc5 --- /dev/null +++ b/src/lib/utils/updaterUtils.js @@ -0,0 +1,46 @@ +import { isElement } from "lodash"; + +export function getGraphDiv(gdID) { + // see this link for more information https://stackoverflow.com/a/34002028 + let graphDiv = document?.querySelectorAll('div[id*="' + gdID + '"][class*="dash-graph"]'); + if (graphDiv.length > 1) { + throw new SyntaxError("LayoutUpdater: multiple graphs with ID=\"" + gdID + "\" found; n=" + graphDiv.length + " \n(either multiple graphs with same ID's or current ID is a str-subset of other graph IDs)"); + } else if (graphDiv.length < 1) { + throw new SyntaxError("LayoutUpdater: no graphs with ID=\"" + gdID + "\" found"); + } + graphDiv = graphDiv?.[0]?.getElementsByClassName('js-plotly-plot')?.[0]; + if (!isElement(graphDiv)) { + throw new Error(`Invalid gdID '${gdID}'`); + } + return graphDiv; +} + + +Function.prototype.clone = function () { + let cloneObj = this; + if (this.__isClone) { + cloneObj = this.__clonedFrom; + } + let temp = function () { return cloneObj.apply(this, arguments); }; + for (let key in this) { + temp[key] = this[key]; + } + temp.__isClone = true; + temp.__clonedFrom = cloneObj; + return temp; +}; + +export async function removeApplyRelayoutEvent(graphDiv, updateData) { + let relayout_func = graphDiv._ev._events.plotly_relayout; + if (Array.isArray(relayout_func)) { relayout_func = relayout_func[0]; }; + if (relayout_func == undefined) { + console.log('no relayout_func found for ' + graphDiv.parentNode.id); + return idDiv; + } + relayout_func = relayout_func.clone(); + // console.log("relayout_func get: ", graphDiv.parentNode.id, relayout_func); + await graphDiv.removeAllListeners('plotly_relayout'); + await Plotly.relayout(graphDiv, updateData); + console.log("relayout_func set: ", graphDiv.parentNode.id, " ", relayout_func); + await graphDiv.on('plotly_relayout', relayout_func); +}; \ No newline at end of file diff --git a/usage.py b/usage.py index 938d0f3..75e408a 100644 --- a/usage.py +++ b/usage.py @@ -1,10 +1,10 @@ import trace_updater import dash -from dash.dependencies import Input, Output +# from dash.dependencies import Input, Output import plotly.graph_objs as go from dash import html, dcc from plotly_resampler import FigureResampler -from plotly_resampler.downsamplers import LTTB +# from plotly_resampler.aggregation import LTTB, EveryNthPoint import numpy as np # Construct a high-frequency signal