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

603 graph cleanup #604

Merged
merged 4 commits into from
Aug 14, 2019
Merged
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).

## UNRELEASED
### Fixed
- 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)

## [1.1.1] - 2019-08-06
- Upgraded plotly.js to 1.49.1 [#595](https://github.com/plotly/dash-core-components/issues/595)
- Patch release [1.49.1](https://github.com/plotly/plotly.js/releases/tag/v1.49.1)
Expand Down
80 changes: 41 additions & 39 deletions src/components/Graph.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,67 +64,66 @@ const filterEventData = (gd, eventData, event) => {
return filteredEventData;
};

function generateId() {
const charAmount = 36;
const length = 7;
return (
'graph-' +
Math.random()
.toString(charAmount)
.substring(2, length)
);
}

/**
* Graph can be used to render any plotly.js-powered data visualization.
*
* You can define callbacks based on user interaction with Graphs such as
* hovering, clicking or selecting
*/
const GraphWithDefaults = props => {
const id = props.id ? props.id : generateId();
return <PlotlyGraph {...props} id={id} />;
};

class PlotlyGraph extends Component {
constructor(props) {
super(props);
this.gd = React.createRef();
this.bindEvents = this.bindEvents.bind(this);
this._hasPlotted = false;
this._prevGd = null;
this.graphResize = this.graphResize.bind(this);
}

plot(props) {
const {figure, id, animate, animation_options, config} = props;
const gd = document.getElementById(id);
const {figure, animate, animation_options, config} = props;
const gd = this.gd.current;

if (
animate &&
this._hasPlotted &&
figure.data.length === gd.data.length
) {
return Plotly.animate(id, figure, animation_options);
return Plotly.animate(gd, figure, animation_options);
}
return Plotly.react(id, {
return Plotly.react(gd, {
data: figure.data,
layout: clone(figure.layout),
frames: figure.frames,
config: config,
}).then(() => {
if (!this._hasPlotted) {
// double-check gd hasn't been unmounted
const gd = document.getElementById(id);
if (gd) {
this.bindEvents();
Plotly.Plots.resize(gd);
this._hasPlotted = true;
const gd = this.gd.current;

// double-check gd hasn't been unmounted
if (!gd) {
return;
}

// in case we've made a new DOM element, transfer events
if(this._hasPlotted && gd !== this._prevGd) {
if(this._prevGd && this._prevGd.removeAllListeners) {
this._prevGd.removeAllListeners();
Plotly.purge(this._prevGd);
}
this._hasPlotted = false;
}

if (!this._hasPlotted) {
this.bindEvents();
Plotly.Plots.resize(gd);
this._hasPlotted = true;
this._prevGd = gd;
}
});
}

extend(props) {
const {id, extendData} = props;
const {extendData} = props;
let updateData, traceIndices, maxPoints;
if (Array.isArray(extendData) && typeof extendData[0] === 'object') {
[updateData, traceIndices, maxPoints] = extendData;
Expand All @@ -143,20 +142,21 @@ class PlotlyGraph extends Component {
traceIndices = generateIndices(updateData);
}

return Plotly.extendTraces(id, updateData, traceIndices, maxPoints);
const gd = this.gd.current;
return Plotly.extendTraces(gd, updateData, traceIndices, maxPoints);
}

graphResize() {
const graphDiv = document.getElementById(this.props.id);
if (graphDiv) {
Plotly.Plots.resize(graphDiv);
const gd = this.gd.current;
if (gd) {
Plotly.Plots.resize(gd);
}
}

bindEvents() {
const {id, setProps, clear_on_unhover} = this.props;
const {setProps, clear_on_unhover} = this.props;

const gd = document.getElementById(id);
const gd = this.gd.current;

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

componentWillUnmount() {
if (this.eventEmitter) {
this.eventEmitter.removeAllListeners();
const gd = this.gd.current;
if (gd && gd.removeAllListeners) {
gd.removeAllListeners();
Plotly.purge(gd);
}
window.removeEventListener('resize', this.graphResize);
}
Expand Down Expand Up @@ -262,6 +264,7 @@ class PlotlyGraph extends Component {
<div
key={id}
id={id}
ref={this.gd}
data-dash-is-loading={
(loading_state && loading_state.is_loading) || undefined
}
Expand All @@ -279,6 +282,7 @@ const graphPropTypes = {
* components in an app.
*/
id: PropTypes.string,

/**
* Data from latest click event. Read-only.
*/
Expand Down Expand Up @@ -672,10 +676,8 @@ const graphDefaultProps = {
config: {},
};

GraphWithDefaults.propTypes = graphPropTypes;
PlotlyGraph.propTypes = graphPropTypes;

GraphWithDefaults.defaultProps = graphDefaultProps;
PlotlyGraph.defaultProps = graphDefaultProps;

export default GraphWithDefaults;
export default PlotlyGraph;
53 changes: 53 additions & 0 deletions tests/integration/graph/test_graph_purge.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import time

from selenium.webdriver.common.keys import Keys

import dash
from dash.dependencies import Input, Output
import dash_core_components as dcc
import dash_html_components as html


def test_grgp001_clean_purge(dash_duo):
app = dash.Dash(__name__)

app.layout = html.Div([
html.Button("toggle children", id="tog"),
html.Div(id="out")
])

@app.callback(
Output("out", "children"),
[Input("tog", "n_clicks")]
)
def show_output(num):
if (num or 0) % 2:
return dcc.Graph(figure={
"data": [{
"type": "scatter3d", "x": [1, 2], "y": [3, 4], "z": [5, 6]
}],
"layout": {"title": {"text": "A graph!"}}
})
else:
return "No graphs here!"

dash_duo.start_server(app)

dash_duo.wait_for_text_to_equal("#out", "No graphs here!")

tog = dash_duo.find_element("#tog")
tog.click()
dash_duo.wait_for_text_to_equal("#out .gtitle", "A graph!")

tog.click()
dash_duo.wait_for_text_to_equal("#out", "No graphs here!")

dash_duo.find_element('body').send_keys(Keys.CONTROL)

# the error with CONTROL was happening in an animation frame loop
# wait a little to ensure it has fired
time.sleep(0.5)
assert not dash_duo.get_logs()

tog.click()
dash_duo.wait_for_text_to_equal("#out .gtitle", "A graph!")