Animated graph transitions oscillate wildly but much smoother when slider is used manually

I have created an animated graph but the transitions are very choppy and oscillate wildly. Is there a particular transition setting or tweak that I can make to rectify this? When I manually slide the slider it’s much smoother and looks much better but is still not perfect. So it seems that I first need to fix the Play button animation and then somehow smooth out the transitions. I’d appreciate any feedback on how to best do that. Here’s the section of code I use to build the frames:

frames_voronoi_team1 = [go.Frame(
                data=[go.Scatter(
                    x=group['v_vor_x'],
                    y=group['v_vor_y'],
                    mode="lines",
                    marker=dict(color='black', size=1),
                    fill="toself",
                    fillcolor=colour0,
                    opacity=.25,
                    name="Voronoi_1")
                ]) for name, group in grouped_voronoi_team1]

            frames_voronoi_team2 = [go.Frame(
                data=[go.Scatter(
                    x=group['v_vor_x'],
                    y=group['v_vor_y'],
                    mode="lines",
                    marker=dict(color='black', size=1),
                    fill="toself",
                    fillcolor=colour1,
                    opacity=.25,
                    name="Voronoi_2")
                ]) for name, group in grouped_voronoi_team2]

            # Add the frames to the figure
            fig4 = go.Figure(data=go.Scatter(mode='lines', fill='toself', line=dict(width=1), visible = "legendonly"),
                             # legendonly hides it initially
                             layout=go.Layout(
                                 xaxis=dict(range=[-6, 115], autorange=False, zeroline=False),
                                 yaxis=dict(range=[-6, 76], autorange=False, zeroline=False),
                                 hovermode="closest",
                                 transition={'duration': 0, 'easing': 'linear', 'ordering': 'traces first'},
                                 updatemenus=[dict(type='buttons',
                                                   buttons=[dict(label='Play',
                                                                 method='animate',
                                                                 args=[None,
                                                                       dict(frame=dict(duration=100,
                                                                                       redraw=False),
                                                                            transition=dict(duration=0),
                                                                            fromcurrent=True,
                                                                            mode='immediate')
                                                                       ])
                                                            ])]
                             ),
                             frames=frames_voronoi_team1
                             )

            fig5 = go.Figure(data=go.Scatter(mode='lines', fill='toself', line=dict(width=1), visible = "legendonly"),
                             # legendonly hides it initially
                             layout=go.Layout(
                                 xaxis=dict(range=[-6, 115], autorange=False, zeroline=False),
                                 yaxis=dict(range=[-6, 76], autorange=False, zeroline=False),
                                 hovermode="closest",
                                 transition={'duration': 0, 'easing': 'linear', 'ordering': 'traces first'},
                                 updatemenus=[dict(type='buttons',
                                                   buttons=[dict(label='Play',
                                                                 method='animate',
                                                                 args=[None,
                                                                       dict(frame=dict(duration=100,
                                                                                       redraw=False),
                                                                            transition=dict(duration=0),
                                                                            fromcurrent=True,
                                                                            mode='immediate')
                                                                       ])
                                                            ])]
                             ),
                             frames=frames_voronoi_team2
                             )
            # Add each figure's data to the main figure via a trace for each. 
            fig.add_trace(fig4.data[0])
            for i, frame in enumerate(fig.frames):
                fig.frames[i].data += (fig4.frames[i].data[0],)

            fig.add_trace(fig5.data[0])
            for i, frame in enumerate(fig.frames):
                fig.frames[i].data += (fig5.frames[i].data[0],)

@jack this was the issue that was blocking convex hull from being used.

Fyi: @nicolaskruchten happy to hear your thoughts

It would help to see a much simpler example with like 2 frames and 3 points or something :slight_smile:

1 Like

What I suspect is happening is that the ā€œmatchingā€ polygons in adjacent frames have differing numbers of vertices and/or the ā€œmatchingā€ vertices aren’t in the same order, and so the transition engine we have tries to transition between two polygons that aren’t particularly similar to each other, geometrically, and does its best, which is what we see here.

For example if I have a triangle ABC transitioning to a square DEFG (assuming that geometrically A is ā€œnearā€ G and B is ā€œnearā€ D and C is ā€œnearā€ E without necessarily being in the same place, like maybe the square and triangle are slightly translated/rotated):

 
A       GF
BC      DE

then the ā€œmatchingā€ points A and D aren’t in the same place, and there’s a new point G that has to come from somewhere, so you’ll see a really rough transition. In general there’s not a straightforward algo that I know of for arranging these things with minimal ugliness.

The problem is compounded by the fact that you’ve got a covering set of polygons, but the transition oddities between adjacent polygons aren’t correlated, so you get this odd effect :slight_smile:

So if I understand correctly, we have pre-generated frames of voronoi grouping (let’s call them A, B, and C). Then with Plotly you want to generate an animation such that we have A → A’ → B → B’ → C, where A’ and B’ are inferred by Plotly, and it struggles to infer A’ and B’ due to the matching problems described by @nicolaskruchten ? Let me know if this is correct.

I think we’re saying the same thing, yes. It’s also a poorly-specified problem if the polygons don’t have the same number of vertices across frames, which is not at all unlikely when the polygons come out of a voronoi algo like this.

You may have more luck representing each of the line segments between the polygons as individual traces, in which case there’s a more deterministic mapping between vertices across frames, but I can’t guarantee this won’t bump up against additional limitations in our cross-frame matching system.

The following solution worked for me:

fig.layout.updatemenus[0].buttons[0].args[1]["frame"]["duration"] = 250 # buttons
fig.layout.updatemenus[0].buttons[0].args[1]["transition"]["duration"] = 0
fig.layout.updatemenus[0].buttons[1].args[1]["frame"]["duration"] = 250
fig.layout.updatemenus[0].buttons[1].args[1]["transition"]["duration"] = 0
fig.layout.sliders[0].steps[0].args[1]["frame"]["duration"] = 0 # slider
fig.layout.updatemenus[0].buttons[0].args[1]["visible"] = False

# cut every other step out so it transitions smoothly (if you don't do this it will be a bit 
# choppy, but otherwise ok)
steps = []
i = 0
for step in fig.layout.sliders[0].steps:
if (i % 2) == 0:
      steps.append(step)
      i += 1

@nicolaskruchten does Plotly Animation only support scatter and bar? Any chance to support Sankey diagram by chance?

Sankeys do support some animated transitions (as do Treemaps and Sunbursts) but there is no helper to create them like with plotly.express.

@nicolaskruchten Thanks for fast response.
Let me know if you have a quick example of go.Sankey animation. I am very close to crack this through and it will look awesome. I am following this tutorial

[Plotly animation with GO object](https://stackoverflow.com/questions/63328589/animated-plot-with-plotly)

I am trying to create the animation using go.Sankey, the frame appending seems working but it stopped at the following

     fig = go.Figure(fig_dict)

showing an error message of ā€œValueError: Invalid property specified for object of type plotly.graph_objs.Scatter: ā€˜dataā€™ā€

how to tell plotly it is a sankey not a scatter?

snippet of the code:

frame = {"data": [], "name": str(date)}
    data_dict = go.Sankey(
        valueformat = ".2f",
        valuesuffix = "M",
        # Define nodes
        node = dict(
        pad = 15,
        thickness = 15,
        line = dict(color = "white", width = 0),
        label =  node_df['node_label'],
        color =  node_color
        ),
        # Add links
        link = dict(
        source = link_df['source'],
        target = link_df['target'],
        value =  link_df['value'],
        label =  link_df['label'],
        color =  link_color
        ),
    )
    frame["data"].append(data_dict)
    # data will be the first frame
    if date==date_vec[0]:
        fig_dict["data"].append(frame)
    fig_dict["frames"].append(frame)

@nicolaskruchten so if I try to animate sankey here, it becomes a deadend?

I’m sorry I wasn’t able to get to answering your question… It would be easier for me if you provided a complete code listing showing a minimal example of what you’re trying to do.

Here’s a quick and dirty example of a Sankey diagram with a single extra frame and an animation button:


import plotly.graph_objects as go

fig = go.Figure(
    data=[
        go.Sankey(
            node=dict(
                label=["A1", "A2", "B1", "B2", "C1", "C2"]
            ),
            link=dict(
                source=[0, 1, 0, 2, 3, 3],
                target=[2, 3, 3, 4, 4, 5],
                value=[8, 4, 2, 8, 4, 2],
            ),
        )
    ],
    frames=[
        dict(
            data=[
                go.Sankey(
                    node=dict(
                        label=["A1", "A2", "B1", "B2", "C1", "C2"]
                    ),
                    link=dict(
                        source=[0, 1, 0, 2, 3, 3],
                        target=[2, 3, 3, 4, 4, 5],
                        value=[8, 4, 2, 18, 4, 2],
                    ),
                )
            ]
        )
    ],
    layout=go.Layout(
        updatemenus=[
            dict(
                type="buttons",
                buttons=[dict(label="Play", method="animate", args=[None])],
            )
        ]
    ),
)

fig.show()
1 Like

Thank you, Nicolas. This is awesome and I will try this out.
FYI, I was searching other solutions to animate Sankey diagram. Some other libraries are trying to animate the Sankey diagram but I think the way Plotly handles it is still the favorite. If you like to know what others do, here are a few links.

@nicolaskruchten Following your example, I am trying to add more controls to the buttons as shown in

https://plotly.com/python/animations/#adding-control-buttons-to-animations

No error messages but the button does not work/respond. Is there a way to fix this? Please let me know and thanks.

# 
import plotly.graph_objects as go

fig = go.Figure(
    data=[
        go.Sankey(
            node=dict(
                label=["A1", "A2", "B1", "B2", "C1", "C2"]
            ),
            link=dict(
                source=[0, 1, 0, 2, 3, 3],
                target=[2, 3, 3, 4, 4, 5],
                value=[8, 4, 2, 8, 4, 2],
            ),
        )
    ],
    frames=[
        dict(
            data=[
                go.Sankey(
                    node=dict(
                        label=["A1", "A2", "B1", "B2", "C1", "C2"]
                    ),
                    link=dict(
                        source=[0, 1, 0, 2, 3, 3],
                        target=[2, 3, 3, 4, 4, 5],
                        value=[8, 4, 2, 18, 4, 2],
                    ),
                )
            ]
        ),
        dict(
            data=[
                go.Sankey(
                    node=dict(
                        label=["A1", "A2", "B1", "B2", "C1", "C2"]
                    ),
                    link=dict(
                        source=[0, 1, 0, 2, 3, 3],
                        target=[2, 3, 3, 4, 4, 5],
                        value=[14, 10, 2, 5, 4, 12],
                    ),
                )
            ]
        )
    ],
    # layout=go.Layout(
    #     updatemenus=[
    #         dict(
    #             type="buttons",
    #             buttons=[dict(label="Play", method="animate", args=[None])],
    #         )
    #     ]
    # ),
)
fig["layout"]["updatemenus"] = [
    {
        "buttons": [
            {
                "args": [None, {"frame": {"duration": 100, "redraw": False},
                                "fromcurrent": True, "transition": {"duration": 0, "easing": "quadratic-in-out"}}],
                "label": "Play",
                "method": "animate"
            },
            {
                "args": [[None], {"frame": {"duration": 0, "redraw": False},
                                  "mode": "immediate", "transition": {"duration": 0}}],
                "label": "Pause",
                "method": "animate"
            }
        ],
        "direction": "left",
        "pad": {"r": 10, "t": 87},
        "showactive": False,
        "type": "buttons",
        "x": 0.1,
        "xanchor": "right",
        "y": 0,
        "yanchor": "top"
    }
]

fig.show()
print('sankey is animated.')

@nicolaskruchten basically, two questions:

  1. can we add frame transition and duration control to sankey animation? The following control for args in button, does not respond when trying this out.
{"frame": {"duration": 100, "redraw": False},
                                "fromcurrent": True, "transition": {"duration": 0, "easing": "quadratic-in-out"}
  1. will slider module work with sankey animation?