Skip to content

Commit da43568

Browse files
authored
Merge pull request #1915 from nickmelnikov82/fix-graph-animate-frames
Fix graph animate frames
2 parents 58bec06 + 7e78702 commit da43568

File tree

3 files changed

+141
-12
lines changed

3 files changed

+141
-12
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ This project adheres to [Semantic Versioning](https://semver.org/).
44

55
## [Unreleased]
66

7+
### Fixed
8+
- [#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.
9+
710
### Fixed
811
- [#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.
912

components/dash-core-components/src/fragments/Graph.react.js

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -147,29 +147,36 @@ class PlotlyGraph extends Component {
147147
const {animate, animation_options, responsive} = props;
148148

149149
const gd = this.gd.current;
150-
151150
figure = props._dashprivate_transformFigure(figure, gd);
152151
config = props._dashprivate_transformConfig(config, gd);
153152

153+
const figureClone = {
154+
data: figure.data,
155+
layout: this.getLayout(figure.layout, responsive),
156+
frames: figure.frames,
157+
config: this.getConfig(config, responsive),
158+
};
159+
154160
if (
155161
animate &&
156162
this._hasPlotted &&
157163
figure.data.length === gd.data.length
158164
) {
159-
return Plotly.animate(gd, figure, animation_options);
165+
// in case we've have figure frames,
166+
// we need to recreate frames before animation
167+
if (figure.frames) {
168+
return Plotly.deleteFrames(gd)
169+
.then(() => Plotly.addFrames(gd, figure.frames))
170+
.then(() =>
171+
Plotly.animate(gd, figureClone, animation_options)
172+
);
173+
}
174+
return Plotly.animate(gd, figureClone, animation_options);
160175
}
161176

162-
const configClone = this.getConfig(config, responsive);
163-
const layoutClone = this.getLayout(figure.layout, responsive);
164-
165177
gd.classList.add('dash-graph--pending');
166178

167-
return Plotly.react(gd, {
168-
data: figure.data,
169-
layout: layoutClone,
170-
frames: figure.frames,
171-
config: configClone,
172-
}).then(() => {
179+
return Plotly.react(gd, figureClone).then(() => {
173180
const gd = this.gd.current;
174181

175182
// double-check gd hasn't been unmounted

components/dash-core-components/tests/integration/graph/test_graph_basics.py

Lines changed: 120 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1+
import json
12
import pytest
23
import pandas as pd
34
from multiprocessing import Value, Lock
45
import numpy as np
56
from time import sleep
6-
7+
import plotly.graph_objects as go
78
from dash import Dash, Input, Output, dcc, html
89

910
import dash.testing.wait as wait
@@ -162,3 +163,121 @@ def update_graph(n_clicks):
162163
dash_dcc.wait_for_element("#my-graph:not([data-dash-is-loading])")
163164

164165
assert dash_dcc.get_logs() == []
166+
167+
168+
def test_grbs005_graph_update_frames(dash_dcc):
169+
app = Dash(__name__)
170+
171+
def get_scatter(multiplier, offset):
172+
return go.Scatter(
173+
x=list(map(lambda n: n * multiplier, [0, 1, 2])),
174+
y=list(map(lambda n: n + offset, [0, 1, 2])),
175+
mode="markers",
176+
)
177+
178+
def get_figure(data, frames, title):
179+
return go.Figure(
180+
data=data,
181+
layout=go.Layout(
182+
title=title,
183+
yaxis=dict(range=[-1, 5]),
184+
xaxis=dict(range=[-3, 3]),
185+
updatemenus=[
186+
dict(
187+
type="buttons",
188+
buttons=[
189+
dict(
190+
label="Play",
191+
method="animate",
192+
args=[
193+
None,
194+
{
195+
"frame": {"duration": 100, "redraw": True},
196+
"fromcurrent": False,
197+
"transition": {
198+
"duration": 500,
199+
"easing": "quadratic-in-out",
200+
},
201+
},
202+
],
203+
)
204+
],
205+
)
206+
],
207+
),
208+
frames=frames,
209+
)
210+
211+
app.layout = html.Div(
212+
[
213+
html.Label("Choose dataset"),
214+
dcc.RadioItems(
215+
id="change-data",
216+
options=[
217+
{"label": "No data", "value": 0},
218+
{"label": "Data A", "value": 1},
219+
{"label": "Data B", "value": 2},
220+
],
221+
value=0,
222+
),
223+
dcc.Graph(
224+
id="test-change",
225+
animate=True,
226+
animation_options={"frame": {"redraw": True}},
227+
),
228+
html.Div(id="relayout-data"),
229+
]
230+
)
231+
232+
@app.callback(
233+
Output("relayout-data", "children"),
234+
[Input("test-change", "figure")],
235+
)
236+
def show_relayout_data(data):
237+
frames = data.get("frames", [])
238+
if frames:
239+
return json.dumps(frames[0]["data"][0]["x"])
240+
return ""
241+
242+
@app.callback(
243+
Output("test-change", "figure"),
244+
Input("change-data", "value"),
245+
)
246+
def set_data(dataset):
247+
if dataset == 1:
248+
title = "Dataset A"
249+
data = get_scatter(1, 0)
250+
frames = [
251+
go.Frame(data=get_scatter(1, 1)),
252+
]
253+
elif dataset == 2:
254+
title = "Dataset B"
255+
data = get_scatter(-1, 0)
256+
frames = [
257+
go.Frame(data=get_scatter(-1, 1)),
258+
]
259+
else:
260+
title = "Select a dataset"
261+
data = []
262+
frames = []
263+
264+
fig = get_figure(data, frames, title)
265+
return fig
266+
267+
dash_dcc.start_server(app)
268+
dash_dcc.wait_for_element("#test-change")
269+
270+
dash_dcc.find_elements('input[type="radio"]')[0].click()
271+
assert dash_dcc.wait_for_text_to_equal(
272+
"#relayout-data", ""
273+
), "initial graph data must contain empty string"
274+
275+
dash_dcc.find_elements('input[type="radio"]')[1].click()
276+
assert dash_dcc.wait_for_text_to_equal(
277+
"#relayout-data", "[0, 1, 2]"
278+
), "graph data must contain frame [0,1,2]"
279+
280+
dash_dcc.find_elements('input[type="radio"]')[2].click()
281+
assert dash_dcc.wait_for_text_to_equal(
282+
"#relayout-data", "[0, -1, -2]"
283+
), "graph data must contain frame [0,-1,-2]"

0 commit comments

Comments
 (0)