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

Commit 5ed92d7

Browse files
authored
Merge pull request #604 from plotly/603-graph-cleanup
603 graph cleanup
2 parents aeed80f + a788b4e commit 5ed92d7

File tree

3 files changed

+98
-39
lines changed

3 files changed

+98
-39
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22
All notable changes to this project will be documented in this file.
33
This project adheres to [Semantic Versioning](http://semver.org/).
44

5+
## UNRELEASED
6+
### Fixed
7+
- Fixed problems with `Graph` components leaking events and being recreated multiple times if declared with no ID [#604](https://github.com/plotly/dash-core-components/pull/604)
8+
59
## [1.1.1] - 2019-08-06
610
- Upgraded plotly.js to 1.49.1 [#595](https://github.com/plotly/dash-core-components/issues/595)
711
- Patch release [1.49.1](https://github.com/plotly/plotly.js/releases/tag/v1.49.1)

src/components/Graph.react.js

Lines changed: 41 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -64,67 +64,66 @@ 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(() => {
114-
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;
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+
if(this._prevGd && this._prevGd.removeAllListeners) {
110+
this._prevGd.removeAllListeners();
111+
Plotly.purge(this._prevGd);
121112
}
113+
this._hasPlotted = false;
114+
}
115+
116+
if (!this._hasPlotted) {
117+
this.bindEvents();
118+
Plotly.Plots.resize(gd);
119+
this._hasPlotted = true;
120+
this._prevGd = gd;
122121
}
123122
});
124123
}
125124

126125
extend(props) {
127-
const {id, extendData} = props;
126+
const {extendData} = props;
128127
let updateData, traceIndices, maxPoints;
129128
if (Array.isArray(extendData) && typeof extendData[0] === 'object') {
130129
[updateData, traceIndices, maxPoints] = extendData;
@@ -143,20 +142,21 @@ class PlotlyGraph extends Component {
143142
traceIndices = generateIndices(updateData);
144143
}
145144

146-
return Plotly.extendTraces(id, updateData, traceIndices, maxPoints);
145+
const gd = this.gd.current;
146+
return Plotly.extendTraces(gd, updateData, traceIndices, maxPoints);
147147
}
148148

149149
graphResize() {
150-
const graphDiv = document.getElementById(this.props.id);
151-
if (graphDiv) {
152-
Plotly.Plots.resize(graphDiv);
150+
const gd = this.gd.current;
151+
if (gd) {
152+
Plotly.Plots.resize(gd);
153153
}
154154
}
155155

156156
bindEvents() {
157-
const {id, setProps, clear_on_unhover} = this.props;
157+
const {setProps, clear_on_unhover} = this.props;
158158

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

161161
gd.on('plotly_click', eventData => {
162162
const clickData = filterEventData(gd, eventData, 'click');
@@ -212,8 +212,10 @@ class PlotlyGraph extends Component {
212212
}
213213

214214
componentWillUnmount() {
215-
if (this.eventEmitter) {
216-
this.eventEmitter.removeAllListeners();
215+
const gd = this.gd.current;
216+
if (gd && gd.removeAllListeners) {
217+
gd.removeAllListeners();
218+
Plotly.purge(gd);
217219
}
218220
window.removeEventListener('resize', this.graphResize);
219221
}
@@ -262,6 +264,7 @@ class PlotlyGraph extends Component {
262264
<div
263265
key={id}
264266
id={id}
267+
ref={this.gd}
265268
data-dash-is-loading={
266269
(loading_state && loading_state.is_loading) || undefined
267270
}
@@ -279,6 +282,7 @@ const graphPropTypes = {
279282
* components in an app.
280283
*/
281284
id: PropTypes.string,
285+
282286
/**
283287
* Data from latest click event. Read-only.
284288
*/
@@ -672,10 +676,8 @@ const graphDefaultProps = {
672676
config: {},
673677
};
674678

675-
GraphWithDefaults.propTypes = graphPropTypes;
676679
PlotlyGraph.propTypes = graphPropTypes;
677680

678-
GraphWithDefaults.defaultProps = graphDefaultProps;
679681
PlotlyGraph.defaultProps = graphDefaultProps;
680682

681-
export default GraphWithDefaults;
683+
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)