From 26f0e7afb8bdcff6fb6cc12859cb16ac7068fac2 Mon Sep 17 00:00:00 2001 From: nickmelnikov82 Date: Tue, 25 Jan 2022 16:00:56 +0200 Subject: [PATCH 1/6] Fixed graph animate frames. --- .../src/fragments/Graph.react.js | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/components/dash-core-components/src/fragments/Graph.react.js b/components/dash-core-components/src/fragments/Graph.react.js index 7cb415fd13..8487e1897c 100644 --- a/components/dash-core-components/src/fragments/Graph.react.js +++ b/components/dash-core-components/src/fragments/Graph.react.js @@ -146,7 +146,6 @@ class PlotlyGraph extends Component { const {animate, animation_options, responsive} = props; const gd = this.gd.current; - figure = props._dashprivate_transformFigure(figure, gd); config = props._dashprivate_transformConfig(config, gd); @@ -155,7 +154,19 @@ class PlotlyGraph extends Component { this._hasPlotted && figure.data.length === gd.data.length ) { - return Plotly.animate(gd, figure, animation_options); + // in case we've have figure frames, + // we need to recreate frames before animation + let result; + if (figure.frames) { + result = Plotly.deleteFrames(gd).then(() => { + return Plotly.addFrames(gd, figure.frames).then(() => { + return Plotly.animate(gd, figure, animation_options); + }); + }); + } else { + result = Plotly.animate(gd, figure, animation_options); + } + return result; } const configClone = this.getConfig(config, responsive); From 91a7a7f67bc515f33818f595567d0c870f6ef19f Mon Sep 17 00:00:00 2001 From: nickmelnikov82 Date: Thu, 10 Feb 2022 14:48:10 +0200 Subject: [PATCH 2/6] Added graph frame update test. --- .../integration/graph/test_graph_basics.py | 123 +++++++++++++++++- 1 file changed, 122 insertions(+), 1 deletion(-) diff --git a/components/dash-core-components/tests/integration/graph/test_graph_basics.py b/components/dash-core-components/tests/integration/graph/test_graph_basics.py index b612f5e152..d5e4b46609 100644 --- a/components/dash-core-components/tests/integration/graph/test_graph_basics.py +++ b/components/dash-core-components/tests/integration/graph/test_graph_basics.py @@ -1,9 +1,10 @@ +import json import pytest import pandas as pd from multiprocessing import Value, Lock import numpy as np from time import sleep - +import plotly.graph_objects as go from dash import Dash, Input, Output, dcc, html import dash.testing.wait as wait @@ -162,3 +163,123 @@ def update_graph(n_clicks): dash_dcc.wait_for_element("#my-graph:not([data-dash-is-loading])") assert dash_dcc.get_logs() == [] + + +def test_grbs005_graph_update_frames(dash_dcc): + app = Dash(__name__) + + def get_scatter(multiplier, offset): + return go.Scatter( + x=list(map(lambda n: n * multiplier, [0, 1, 2])), + y=list(map(lambda n: n + offset, [0, 1, 2])), + mode="markers", + ) + + def get_figure(data, frames, title): + return go.Figure( + data=data, + layout=go.Layout( + title=title, + yaxis=dict(range=[-1, 5]), + xaxis=dict(range=[-3, 3]), + updatemenus=[ + dict( + type="buttons", + buttons=[ + dict( + label="Play", + method="animate", + args=[ + None, + { + "frame": {"duration": 100, "redraw": True}, + "fromcurrent": False, + "transition": { + "duration": 500, + "easing": "quadratic-in-out", + }, + }, + ], + ) + ], + ) + ], + ), + frames=frames, + ) + + app.layout = html.Div( + [ + html.Label("Choose dataset"), + dcc.RadioItems( + id="change-data", + options=[ + {"label": "No data", "value": 0}, + {"label": "Data A", "value": 1}, + {"label": "Data B", "value": 2}, + ], + value=0, + ), + dcc.Graph( + id="test-change", + animate=True, + animation_options={"frame": {"redraw": True}}, + ), + html.Div(id="relayout-data"), + ] + ) + + @app.callback( + Output("relayout-data", "children"), + [Input("test-change", "figure")], + ) + def show_relayout_data(data): + frames = data.get("frames", []) + if frames: + return json.dumps(frames[0]["data"][0]["x"]) + return "" + + @app.callback( + Output("test-change", "figure"), + Input("change-data", "value"), + ) + def set_data(dataset): + if dataset == 1: + title = "Dataset A" + data = get_scatter(1, 0) + frames = [ + go.Frame(data=get_scatter(1, 1)), + ] + elif dataset == 2: + title = "Dataset B" + data = get_scatter(-1, 0) + frames = [ + go.Frame(data=get_scatter(-1, 1)), + ] + else: + title = "Select a dataset" + data = [] + frames = [] + + fig = get_figure(data, frames, title) + return fig + + dash_dcc.start_server(app) + dash_dcc.wait_for_element("#test-change") + + dash_dcc.find_elements('input[type="radio"]')[0].click() + + assert ( + dash_dcc.wait_for_element("#relayout-data").get_attribute("innerHTML") == "" + ), "initial graph data must contain empty string" + + dash_dcc.find_elements('input[type="radio"]')[1].click() + assert ( + dash_dcc.wait_for_element("#relayout-data").get_attribute("innerHTML") + == "[0, 1, 2]" + ), "graph data must contain frame [0,1,2]" + dash_dcc.find_elements('input[type="radio"]')[2].click() + assert ( + dash_dcc.wait_for_element("#relayout-data").get_attribute("innerHTML") + == "[0, -1, -2]" + ), "graph data must contain frame [0,-1,-2]" From 58ff2abbb36c61b1a44700345eb25d7f3f3c8e83 Mon Sep 17 00:00:00 2001 From: nickmelnikov82 Date: Thu, 10 Feb 2022 15:39:37 +0200 Subject: [PATCH 3/6] Fixed graph test. --- .../tests/integration/graph/test_graph_basics.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/components/dash-core-components/tests/integration/graph/test_graph_basics.py b/components/dash-core-components/tests/integration/graph/test_graph_basics.py index d5e4b46609..8823922f09 100644 --- a/components/dash-core-components/tests/integration/graph/test_graph_basics.py +++ b/components/dash-core-components/tests/integration/graph/test_graph_basics.py @@ -268,18 +268,16 @@ def set_data(dataset): dash_dcc.wait_for_element("#test-change") dash_dcc.find_elements('input[type="radio"]')[0].click() - - assert ( - dash_dcc.wait_for_element("#relayout-data").get_attribute("innerHTML") == "" + assert dash_dcc.wait_for_text_to_equal( + "#relayout-data", "" ), "initial graph data must contain empty string" dash_dcc.find_elements('input[type="radio"]')[1].click() - assert ( - dash_dcc.wait_for_element("#relayout-data").get_attribute("innerHTML") - == "[0, 1, 2]" + assert dash_dcc.wait_for_text_to_equal( + "#relayout-data", "[0, 1, 2]" ), "graph data must contain frame [0,1,2]" + dash_dcc.find_elements('input[type="radio"]')[2].click() - assert ( - dash_dcc.wait_for_element("#relayout-data").get_attribute("innerHTML") - == "[0, -1, -2]" + assert dash_dcc.wait_for_text_to_equal( + "#relayout-data", "[0, -1, -2]" ), "graph data must contain frame [0,-1,-2]" From 24e5acc1062efcb246d282b013bb5a922d19a8ad Mon Sep 17 00:00:00 2001 From: Nick Melnikov Date: Fri, 4 Mar 2022 09:35:39 +0100 Subject: [PATCH 4/6] Simplifying the plotly update frames code. Co-authored-by: Alex Johnson --- .../src/fragments/Graph.react.js | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/components/dash-core-components/src/fragments/Graph.react.js b/components/dash-core-components/src/fragments/Graph.react.js index 8487e1897c..85f9bb0410 100644 --- a/components/dash-core-components/src/fragments/Graph.react.js +++ b/components/dash-core-components/src/fragments/Graph.react.js @@ -156,17 +156,12 @@ class PlotlyGraph extends Component { ) { // in case we've have figure frames, // we need to recreate frames before animation - let result; if (figure.frames) { - result = Plotly.deleteFrames(gd).then(() => { - return Plotly.addFrames(gd, figure.frames).then(() => { - return Plotly.animate(gd, figure, animation_options); - }); - }); - } else { - result = Plotly.animate(gd, figure, animation_options); + return Plotly.deleteFrames(gd) + .then(() => Plotly.addFrames(gd, figure.frames)) + .then(() => Plotly.animate(gd, figure, animation_options)); } - return result; + return Plotly.animate(gd, figure, animation_options); } const configClone = this.getConfig(config, responsive); From ecd4e9c07eaf0f55c648a690f76dbc5d204f1d4e Mon Sep 17 00:00:00 2001 From: nickmelnikov82 Date: Fri, 4 Mar 2022 12:36:23 +0200 Subject: [PATCH 5/6] Graph plot code refactoring. --- .../src/fragments/Graph.react.js | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/components/dash-core-components/src/fragments/Graph.react.js b/components/dash-core-components/src/fragments/Graph.react.js index 85f9bb0410..a382176ec8 100644 --- a/components/dash-core-components/src/fragments/Graph.react.js +++ b/components/dash-core-components/src/fragments/Graph.react.js @@ -149,6 +149,13 @@ class PlotlyGraph extends Component { figure = props._dashprivate_transformFigure(figure, gd); config = props._dashprivate_transformConfig(config, gd); + const figureClone = { + data: figure.data, + layout: this.getLayout(figure.layout, responsive), + frames: figure.frames, + config: this.getConfig(config, responsive), + }; + if ( animate && this._hasPlotted && @@ -159,22 +166,16 @@ class PlotlyGraph extends Component { if (figure.frames) { return Plotly.deleteFrames(gd) .then(() => Plotly.addFrames(gd, figure.frames)) - .then(() => Plotly.animate(gd, figure, animation_options)); + .then(() => + Plotly.animate(gd, figureClone, animation_options) + ); } - return Plotly.animate(gd, figure, animation_options); + return Plotly.animate(gd, figureClone, animation_options); } - const configClone = this.getConfig(config, responsive); - const layoutClone = this.getLayout(figure.layout, responsive); - gd.classList.add('dash-graph--pending'); - return Plotly.react(gd, { - data: figure.data, - layout: layoutClone, - frames: figure.frames, - config: configClone, - }).then(() => { + return Plotly.react(gd, figureClone).then(() => { const gd = this.gd.current; // double-check gd hasn't been unmounted From 7e78702b484dcdc2cb4975fd0398f09fdb786598 Mon Sep 17 00:00:00 2001 From: nickmelnikov82 Date: Wed, 9 Mar 2022 12:25:36 +0200 Subject: [PATCH 6/6] Added an entry to the changelog. --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ce89761e4a..6a5fa0e200 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ This project adheres to [Semantic Versioning](https://semver.org/). ## [Unreleased] +### Fixed +- [#1915](https://github.com/plotly/dash/pull/1915) Fix bug [#1474](https://github.com/plotly/dash/issues/1474) when both dcc.Graph and go.Figure have animation, and when the second animation in Figure is executed, the Frames from the first animation are played instead of the second one. + ### Fixed - [#1953](https://github.com/plotly/dash/pull/1953) Fix bug [#1783](https://github.com/plotly/dash/issues/1783) in which a failed hot reloader blocks the UI with alerts.