diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fd2628a3..3b3cae4b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/src/components/Graph.react.js b/src/components/Graph.react.js index 1086c9010..c97cb8267 100644 --- a/src/components/Graph.react.js +++ b/src/components/Graph.react.js @@ -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) => { @@ -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 && @@ -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; @@ -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; @@ -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', () => { @@ -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); } } diff --git a/tests/integration/graph/test_graph_basics.py b/tests/integration/graph/test_graph_basics.py index 960e22ee3..304d6ef99 100644 --- a/tests/integration/graph/test_graph_basics.py +++ b/tests/integration/graph/test_graph_basics.py @@ -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): @@ -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..."