Skip to content
This repository was archived by the owner on Jun 3, 2024. It is now read-only.

Commit a92ac35

Browse files
committed
stop making random graph ID, and purge correctly on unmount
1 parent 17e4886 commit a92ac35

File tree

2 files changed

+89
-38
lines changed

2 files changed

+89
-38
lines changed

src/components/Graph.react.js

Lines changed: 36 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -64,67 +64,63 @@ const filterEventData = (gd, eventData, event) => {
6464
return filteredEventData;
6565
};
6666

67-
function generateId() {
68-
const charAmount = 36;
69-
const length = 7;
70-
return (
71-
'graph-' +
72-
Math.random()
73-
.toString(charAmount)
74-
.substring(2, length)
75-
);
76-
}
77-
7867
/**
7968
* Graph can be used to render any plotly.js-powered data visualization.
8069
*
8170
* You can define callbacks based on user interaction with Graphs such as
8271
* hovering, clicking or selecting
8372
*/
84-
const GraphWithDefaults = props => {
85-
const id = props.id ? props.id : generateId();
86-
return <PlotlyGraph {...props} id={id} />;
87-
};
88-
8973
class PlotlyGraph extends Component {
9074
constructor(props) {
9175
super(props);
76+
this.gd = React.createRef();
9277
this.bindEvents = this.bindEvents.bind(this);
9378
this._hasPlotted = false;
79+
this._prevGd = null;
9480
this.graphResize = this.graphResize.bind(this);
9581
}
9682

9783
plot(props) {
98-
const {figure, id, animate, animation_options, config} = props;
99-
const gd = document.getElementById(id);
84+
const {figure, animate, animation_options, config} = props;
85+
const gd = this.gd.current;
10086

10187
if (
10288
animate &&
10389
this._hasPlotted &&
10490
figure.data.length === gd.data.length
10591
) {
106-
return Plotly.animate(id, figure, animation_options);
92+
return Plotly.animate(gd, figure, animation_options);
10793
}
108-
return Plotly.react(id, {
94+
return Plotly.react(gd, {
10995
data: figure.data,
11096
layout: clone(figure.layout),
11197
frames: figure.frames,
11298
config: config,
11399
}).then(() => {
100+
const gd = this.gd.current;
101+
102+
// double-check gd hasn't been unmounted
103+
if (!gd) {
104+
return;
105+
}
106+
107+
// in case we've made a new DOM element, transfer events
108+
if(this._hasPlotted && gd !== this._prevGd) {
109+
this._prevGd.removeAllListeners();
110+
this._hasPlotted = false;
111+
}
112+
114113
if (!this._hasPlotted) {
115-
// double-check gd hasn't been unmounted
116-
const gd = document.getElementById(id);
117-
if (gd) {
118-
this.bindEvents();
119-
Plotly.Plots.resize(gd);
120-
this._hasPlotted = true;
121-
}
114+
this.bindEvents();
115+
Plotly.Plots.resize(gd);
116+
this._hasPlotted = true;
117+
this._prevGd = gd;
122118
}
123119
});
124120
}
125121

126122
extend(props) {
127-
const {id, extendData} = props;
123+
const {extendData} = props;
128124
let updateData, traceIndices, maxPoints;
129125
if (Array.isArray(extendData) && typeof extendData[0] === 'object') {
130126
[updateData, traceIndices, maxPoints] = extendData;
@@ -143,20 +139,21 @@ class PlotlyGraph extends Component {
143139
traceIndices = generateIndices(updateData);
144140
}
145141

146-
return Plotly.extendTraces(id, updateData, traceIndices, maxPoints);
142+
const gd = this.gd.current;
143+
return Plotly.extendTraces(gd, updateData, traceIndices, maxPoints);
147144
}
148145

149146
graphResize() {
150-
const graphDiv = document.getElementById(this.props.id);
151-
if (graphDiv) {
152-
Plotly.Plots.resize(graphDiv);
147+
const gd = this.gd.current;
148+
if (gd) {
149+
Plotly.Plots.resize(gd);
153150
}
154151
}
155152

156153
bindEvents() {
157-
const {id, setProps, clear_on_unhover} = this.props;
154+
const {setProps, clear_on_unhover} = this.props;
158155

159-
const gd = document.getElementById(id);
156+
const gd = this.gd.current;
160157

161158
gd.on('plotly_click', eventData => {
162159
const clickData = filterEventData(gd, eventData, 'click');
@@ -212,9 +209,10 @@ class PlotlyGraph extends Component {
212209
}
213210

214211
componentWillUnmount() {
215-
const gd = document.getElementById(this.props.id);
212+
const gd = this.gd.current;
216213
if (gd && gd.removeAllListeners) {
217214
gd.removeAllListeners();
215+
Plotly.purge(gd);
218216
}
219217
window.removeEventListener('resize', this.graphResize);
220218
}
@@ -263,6 +261,7 @@ class PlotlyGraph extends Component {
263261
<div
264262
key={id}
265263
id={id}
264+
ref={this.gd}
266265
data-dash-is-loading={
267266
(loading_state && loading_state.is_loading) || undefined
268267
}
@@ -280,6 +279,7 @@ const graphPropTypes = {
280279
* components in an app.
281280
*/
282281
id: PropTypes.string,
282+
283283
/**
284284
* Data from latest click event. Read-only.
285285
*/
@@ -673,10 +673,8 @@ const graphDefaultProps = {
673673
config: {},
674674
};
675675

676-
GraphWithDefaults.propTypes = graphPropTypes;
677676
PlotlyGraph.propTypes = graphPropTypes;
678677

679-
GraphWithDefaults.defaultProps = graphDefaultProps;
680678
PlotlyGraph.defaultProps = graphDefaultProps;
681679

682-
export default GraphWithDefaults;
680+
export default PlotlyGraph;
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import time
2+
3+
from selenium.webdriver.common.keys import Keys
4+
5+
import dash
6+
from dash.dependencies import Input, Output
7+
import dash_core_components as dcc
8+
import dash_html_components as html
9+
10+
11+
def test_grgp001_clean_purge(dash_duo):
12+
app = dash.Dash(__name__)
13+
14+
app.layout = html.Div([
15+
html.Button("toggle children", id="tog"),
16+
html.Div(id="out")
17+
])
18+
19+
@app.callback(
20+
Output("out", "children"),
21+
[Input("tog", "n_clicks")]
22+
)
23+
def show_output(num):
24+
if (num or 0) % 2:
25+
return dcc.Graph(figure={
26+
"data": [{
27+
"type": "scatter3d", "x": [1, 2], "y": [3, 4], "z": [5, 6]
28+
}],
29+
"layout": {"title": {"text": "A graph!"}}
30+
})
31+
else:
32+
return "No graphs here!"
33+
34+
dash_duo.start_server(app)
35+
36+
dash_duo.wait_for_text_to_equal("#out", "No graphs here!")
37+
38+
tog = dash_duo.find_element("#tog")
39+
tog.click()
40+
dash_duo.wait_for_text_to_equal("#out .gtitle", "A graph!")
41+
42+
tog.click()
43+
dash_duo.wait_for_text_to_equal("#out", "No graphs here!")
44+
45+
dash_duo.find_element('body').send_keys(Keys.CONTROL)
46+
47+
# the error with CONTROL was happening in an animation frame loop
48+
# wait a little to ensure it has fired
49+
time.sleep(0.5)
50+
assert not dash_duo.get_logs()
51+
52+
tog.click()
53+
dash_duo.wait_for_text_to_equal("#out .gtitle", "A graph!")

0 commit comments

Comments
 (0)