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

Graph inifinite loop #608 #621

Merged
merged 16 commits into from
Aug 28, 2019
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).

## Unreleased
### Fixed

- Fixed an infinite loop problem when `Graph` is wrapped by `Loading` component [#608](https://github.com/plotly/dash-core-components/issues/608)

## [1.1.2] - 2019-08-27
### 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)
Expand Down
47 changes: 23 additions & 24 deletions src/components/Graph.react.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {contains, filter, clone, has, isNil, type, omit} from 'ramda';
import {contains, filter, clone, has, isNil, type, omit, equals} from 'ramda';
/* global Plotly:true */

const filterEventData = (gd, eventData, event) => {
Expand Down Expand Up @@ -83,7 +83,6 @@ class PlotlyGraph extends Component {
plot(props) {
const {figure, animate, animation_options, config} = props;
const gd = this.gd.current;

if (
animate &&
this._hasPlotted &&
Expand All @@ -98,7 +97,6 @@ class PlotlyGraph extends Component {
config: config,
}).then(() => {
const gd = this.gd.current;

// double-check gd hasn't been unmounted
if (!gd) {
return;
Expand Down Expand Up @@ -154,7 +152,14 @@ class PlotlyGraph extends Component {
}

bindEvents() {
const {setProps, clear_on_unhover} = this.props;
const {
setProps,
clear_on_unhover,
relayoutData,
restyleData,
hoverData,
selectedData,
} = this.props;

const gd = this.gd.current;

Expand All @@ -172,30 +177,30 @@ class PlotlyGraph extends Component {
setProps({clickAnnotationData});
});
gd.on('plotly_hover', eventData => {
const hoverData = filterEventData(gd, eventData, 'hover');
if (!isNil(hoverData)) {
setProps({hoverData});
const hover = filterEventData(gd, eventData, 'hover');
if (!isNil(hover) && !equals(hover, hoverData)) {
setProps({hoverData: hover});
}
});
gd.on('plotly_selected', eventData => {
const selectedData = filterEventData(gd, eventData, 'selected');
if (!isNil(selectedData)) {
setProps({selectedData});
const selected = filterEventData(gd, eventData, 'selected');
if (!isNil(selected) && !equals(selected, selectedData)) {
setProps({selectedData: selected});
}
});
gd.on('plotly_deselect', () => {
setProps({selectedData: null});
});
gd.on('plotly_relayout', eventData => {
const relayoutData = filterEventData(gd, eventData, 'relayout');
if (!isNil(relayoutData)) {
setProps({relayoutData});
const relayout = filterEventData(gd, eventData, 'relayout');
if (!isNil(relayout) && !equals(relayout, relayoutData)) {
setProps({relayoutData: relayout});
}
});
gd.on('plotly_restyle', eventData => {
const restyleData = filterEventData(gd, eventData, 'restyle');
if (!isNil(restyleData)) {
setProps({restyleData});
const restyle = filterEventData(gd, eventData, 'restyle');
if (!isNil(restyle) && !equals(restyle, restyleData)) {
setProps({restyleData: restyle});
}
});
gd.on('plotly_unhover', () => {
Expand Down Expand Up @@ -236,17 +241,11 @@ class PlotlyGraph extends Component {
*/
return;
}

const figureChanged = this.props.figure !== nextProps.figure;

if (figureChanged) {
if (this.props.figure !== nextProps.figure) {
this.plot(nextProps);
}

const extendDataChanged =
this.props.extendData !== nextProps.extendData;

if (extendDataChanged) {
if (this.props.extendData !== nextProps.extendData) {
this.extend(nextProps);
}
}
Expand Down
61 changes: 61 additions & 0 deletions tests/integration/graph/test_graph_basics.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import pytest
import pandas as pd
import numpy as np
import dash
import dash_html_components as html
import dash_core_components as dcc
from dash.dependencies import Input, Output
import dash.testing.wait as wait


def test_grbs001_graph_without_ids(dash_duo):
Expand All @@ -20,3 +25,59 @@ def test_grbs001_graph_without_ids(dash_duo):
assert not dash_duo.wait_for_element(".graph-no-id-2").get_attribute(
"id"
), "the graph should contain no more auto-generated id"


@pytest.mark.DCC608
def test_grbs002_wrapped_graph_has_no_infinite_loop(dash_duo):

df = pd.DataFrame(np.random.randn(50, 50))
figure = {
"data": [
{"x": df.columns, "y": df.index, "z": df.values, "type": "heatmap"}
],
"layout": {"xaxis": {"scaleanchor": "y"}},
}

app = dash.Dash(__name__)
app.layout = html.Div(
style={
"backgroundColor": "red",
"height": "100vmin",
"width": "100vmin",
"overflow": "hidden",
"position": "relative",
},
children=[
dcc.Loading(
children=[
dcc.Graph(
id="graph",
figure=figure,
style={
"position": "absolute",
"top": 0,
"left": 0,
"backgroundColor": "blue",
"width": "100%",
"height": "100%",
"overflow": "hidden",
},
)
]
)
],
)

@app.callback(Output("graph", "figure"), [Input("graph", "relayoutData")])
def selected_df_figure(selection):
figure["data"][0]["x"] = df.columns
figure["data"][0]["y"] = df.index
figure["data"][0]["z"] = df.values
return figure

dash_duo.start_server(app)

wait.until(lambda: dash_duo.driver.title == "Dash", timeout=2)
assert (
len({dash_duo.driver.title for _ in range(20)}) == 1
), "after the first update, there should contain no extra Updating..."