✊🏿 Black Lives Matter. Please consider donating to Black Girls Code today.
⚾️ It's finally Baseball season! Root for the home team... & Register for our Sports Analytics Webinar!

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)