Skip to content

Commit

Permalink
Merge pull request #1915 from nickmelnikov82/fix-graph-animate-frames
Browse files Browse the repository at this point in the history
Fix graph animate frames
  • Loading branch information
alexcjohnson authored Mar 9, 2022
2 parents 58bec06 + 7e78702 commit da43568
Show file tree
Hide file tree
Showing 3 changed files with 141 additions and 12 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
29 changes: 18 additions & 11 deletions components/dash-core-components/src/fragments/Graph.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -147,29 +147,36 @@ 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);

const figureClone = {
data: figure.data,
layout: this.getLayout(figure.layout, responsive),
frames: figure.frames,
config: this.getConfig(config, responsive),
};

if (
animate &&
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
if (figure.frames) {
return Plotly.deleteFrames(gd)
.then(() => Plotly.addFrames(gd, figure.frames))
.then(() =>
Plotly.animate(gd, figureClone, 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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -162,3 +163,121 @@ 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_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_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_text_to_equal(
"#relayout-data", "[0, -1, -2]"
), "graph data must contain frame [0,-1,-2]"

0 comments on commit da43568

Please sign in to comment.