Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions src/lib/components/LayoutUpdater.react.js
Original file line number Diff line number Diff line change
@@ -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 = <div id={id}></div>;
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
};
30 changes: 7 additions & 23 deletions src/lib/components/TraceUpdater.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ import {
fromPairs,
mapValues,
zipObject,
isElement,
uniq
} from 'lodash';
import { getGraphDiv } from '../utils/updaterUtils';


// HELPER FUNCTIONS //
Expand Down Expand Up @@ -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 = <div id={id}></div>;
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;
}
}
Expand Down
4 changes: 3 additions & 1 deletion src/lib/index.js
Original file line number Diff line number Diff line change
@@ -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
};
46 changes: 46 additions & 0 deletions src/lib/utils/updaterUtils.js
Original file line number Diff line number Diff line change
@@ -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) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to distinguish between multiple matches versus multiple unique IDs? If I clone a graph and want the update to apply to the clone as well as the original, the clone would have the same ID.

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);
};
4 changes: 2 additions & 2 deletions usage.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down