Controlling animation speed using graph_objects in python

I am trying to generate an animated plot using following code.

trace1 = go.Scatter(y=[1], name='Testing Points')
layout =go.Layout(title='Animation Test',
                  title_x=0.5,
                  xaxis_title='Time', 
                  yaxis_title='Test Points',
                  updatemenus=[dict(type="buttons",
                                    buttons=[dict(label="Play",
                                                  method="animate",
                                                  args=[None])])]
                           )
frames= []
for i in range(0,100):
    trace1 = go.Scatter(y=list(range(i)))
    frames.append(go.Frame(data=[trace1]))
    
fig = go.Figure(data=[trace1], 
          layout=layout,
          frames=frames
         )

fig.show()

Can someone help me figure out how to control the animation speed of this? I have seen ā€˜transition/durationā€™ property, but not really sure how to use this in the above setting.

Hi @rahulrajpl,

This a modified version of your code that works:

y=np.arange(100)     
fig=go.Figure(go.Scatter(y=y[:1], mode='lines', name='Testing Points'))
fig.update_layout(title='Animation Test',
                  title_x=0.5,
                  width=600, height=600, 
                  xaxis_title='Time', 
                  yaxis_title='Test Points',
                  yaxis_range=(0,99),
                  xaxis_range=(0,99), #you generate y-values for i =0, ...99, 
                                      #that are assigned, by default, to x-values 0, 1, ..., 99
                  
                  updatemenus=[dict(buttons = [dict(
                                               args = [None, {"frame": {"duration": 50, 
                                                                        "redraw": False},
                                                              "fromcurrent": True, 
                                                              "transition": {"duration": 0}}],
                                               label = "Play",
                                               method = "animate")],
                                type='buttons',
                                showactive=False,
                                y=1,
                                x=1.12,
                                xanchor='right',
                                yanchor='top')])
                                          
                    
frames= [go.Frame(data=[go.Scatter(y=y[:i])]) for i in range(1, 100)]
fig.update(frames=frames)

fig.show()

The transition duration defines the amount of time spent interpolating a trace from one state to another (currently limited to scatter traces), while the frame duration defines the total time spent in that state, including time spent transitioning.

2 Likes

Thank you. That worked.

Hi @empet,

I am reviving this thread because I am integrating your previous answer about rotations with the present one.



def rotate_z(x, y, z, theta):
    w = x+1j*y
    return np.real(np.exp(1j*theta)*w), np.imag(np.exp(1j*theta)*w), z

x_eye = -1.25
y_eye = 2
z_eye = 0.5
frames=[]

fig = go.Figure(data=[go.Surface(z=Z[0:200,0:100], x=X[0:200,0:100], y=Y[0:200,0:100], surfacecolor=d_matrix, connectgaps=True)])


layout = go.Layout(
         title='Animation Test',
         width=600,
         height=600,
         scene=dict(camera=dict(eye=dict(x=x_eye, y=y_eye, z=z_eye))),

updatemenus=[dict(buttons = [dict(args = [None, {"frame": {"duration": 1, 
                                                           "redraw": False},
                                                 "fromcurrent": True, 
                                                 "transition": {"duration": 100}}],
                                  label = "Play",
                                 method = "animate")],
             type='buttons',
             showactive=False,
             y=1,
             x=1.12,
             xanchor='right',
             yanchor='top')])

for t in np.arange(0, 6.26, 0.1):
    xe, ye, ze = rotate_z(x_eye, y_eye, z_eye, -t)
    frames.append(go.Frame(layout=dict(scene_camera_eye=dict(x=xe, y=ye, z=ze))))
    
fig.update(frames=frames)

fig.show()
plotly.offline.plot(fig, filename=r'\test.html')

My goal is a smoother and faster transition between different frames. You can see the final output in html format here.

Using the code you provided in this thread, I have noticed that the parameters ā€œdurationā€ for both frame and transition are not affecting the output

@giammi56

The animation of a Mesh3d rotation is sufficiently smooth when are rotated its vertices,
not the camera eye: https://chart-studio.plotly.com/~empet/15684
For surfaces the smoothnes depends on the shape of x,y,z and the number of frames:

import numpy as np
from numpy import sin, cos, pi
import plotly.graph_objects as go


def rot(alpha):  #planar rotation of alpha radians
    return np.array([[cos(alpha), -sin(alpha)], 
                     [sin(alpha), cos(alpha)]])

### HERE you define the x, y, z arrays for go.Surface

fig = go.Figure(go.Surface(x=x,
                           y=y,
                           z=z, 
                           colorscale='balance',
                           colorbar=dict(thickness=20,  len=0.6)))
                            
fig.update_layout(width=800,
                  height=800, 
                  scene=dict(camera=dict(eye=dict(x=1.65, y=1.65, z=0.8)),
                             aspectratio=dict(x=1,
                                              y=1,
                                              z=0.35))                        
                );
                                      

frames = []
T = np.arange(0,  2, 0.125)
xy = np.stack((x, y))
for t in T:
    xr, yr  =  np.einsum('ik, kjm -> ijm', rot(t), xy)#  batch rotation of all points (x,y) on the surface
    frames.append(go.Frame(data=[go.Surface(x=xr, y=yr)])) #z is the same in a 3d rotation about zaxis
fig.update(frames=frames);    


fig.update_layout(updatemenus=[dict(type='buttons',
                  showactive=False,
                  y=0.7,
                  x=-0.15,
                  xanchor='left',
                  yanchor='bottom',
                  buttons=[dict(label='Play',
                                 method='animate',
                                 args=[None, dict(frame=dict(duration=5, redraw=True), 
                                                             transition=dict(duration=4),
                                                             fromcurrent=True,
                                                             mode='immediate'
                                                            )]
                                            )
                                      ]
                              )
                        ])

Hi @empet,

Thank you for your kind reply. I am wondering if the code is gonna work since xy = np.stack((x, y)) is dim = 2 but np.einsum('ik, kjm -> ijm', requires dim=3 for the operand number 1 (i.e. rot(t) ). In the first example here the rotation is a 3x3 matrix composed of three rotations around the three cartesian axes, but here is a plane rotation around z-axis.

The code you posted is working with some modifications:



import numpy as np
from numpy import sin, cos, pi
import plotly.graph_objects as go


def rot(alpha):  #planar rotation of alpha radians
    return np.array([[cos(alpha), -sin(alpha)], 
                     [sin(alpha), cos(alpha)]])

### HERE you define the x, y, z arrays for go.Surface

fig = go.Figure(go.Surface(x=x,
                           y=y,
                           z=z, 
                           colorscale='balance',
                           colorbar=dict(thickness=20,  len=0.6)))
                            
fig.update_layout(width=800,
                  height=800, 
                  scene=dict(camera=dict(eye=dict(x=1.65, y=1.65, z=0.8)),
                             aspectratio=dict(x=1,
                                              y=1,
                                              z=0.35))                        
                );
                                      

frames = []
T = np.arange(0,  2*np.pi, 0.125) # full rotation around z axis
xy = np.stack((x, y))
for t in T:
    xr, yr  =  np.einsum('ik, kj -> ij', rot(t), xy)#  batch rotation of all points (x,y) on the surface
    frames.append(go.Frame(data=[go.Surface(x=xr, y=yr, z=z)])) #z is the same in a 3d rotation about zaxis
fig.update(frames=frames);    


fig.update_layout(updatemenus=[dict(type='buttons',
                  showactive=False,
                  y=0.7,
                  x=-0.15,
                  xanchor='left',
                  yanchor='bottom',
                  buttons=[dict(label='Play',
                                 method='animate',
                                 args=[None, dict(frame=dict(duration=5, redraw=True), 
                                                             transition=dict(duration=4),
                                                             fromcurrent=True,
                                                             mode='immediate'
                                                            )]
                                            )
                                      ]
                              )
                        ])

What about implementing a full 3d rotation? Iā€™ll need to compose the R matrix of the three Ralpha x Rbeta x R gamma matrixes, but how to do that in a neat way?

Hi @giammi56,

your x=X[0:200,0:100] has the shape (200, 100)
y, as well , (200, 100)

xy = np.stack((x,y)) has the shape (2, 200, 100)
The rotation matrix has the shape (2,2). Hence the definition is correct!!!
np.einsum performs ā€œa multiplicationā€ of the rotation matrix with each column vector of elements x[k, m], y[k, m], but a batch multiplication.

If you are familiar with tensors, np.einsum, above, defines the tensor product of the involved tensors.

LE: If you define a 3d rotation about z, rot3d(alpha), the code is similar, but unnecessary:

frames = []
T = np.arange(0,  2*np.pi, 0.125) # full rotation around z axis
xyz = np.stack((x, y, z))  # shape (3, 200, 100)
for t in T:
    xr, yr , zr =  np.einsum('ik, kjm -> ijm', rot3d(t), xyz)#  batch rotation of all points (x,y,z) on the surface
    frames.append(go.Frame(data=[go.Surface(x=xr, y=yr, z=zr)]))

I was reshaping the X and Y to (20000,) thinking that your code was working for dim=1 vectors. Now I get it!

What about implementing an R as a composition of the Ra x Rb x Rc ? How would you design the rot() function to take all of them in account? Would you proceed as in your example keeping them separated?

As always, thank you so much for your assistance!

Do you intend with this rotation composition to define a 3d rotation about an arbitrary axis, of direction
v=[a, b, c]?

@giammi56

In this line of code

 frames.append(go.Frame(data=[go.Surface(x=xr, y=yr)])

I didnā€™t omit z=z . Plotly frames record ONLY the data or attributes that change from frame to frame. Adding z=z overloads the frames and makes a less smooth animation

Thatā€™s very powerfull of Plotly. I am learning a lot everytime!

That would be optimal!

Using x,y,z.shape=(20000,) , I am defining the rot3D as

def rot3d(alpha,beta,gamma):  #planar rotation of alpha radians
    return np.array([[np.cos(alpha)*np.cos(beta), np.cos(alpha)*np.sin(beta)*np.sin(gamma)-np.sin(alpha)*np.cos(gamma), np.cos(alpha)*np.sin(beta)*np.cos(gamma)+np.sin(alpha)*np.sin(gamma)], 
                      [np.sin(alpha)*np.cos(beta), np.sin(alpha)*np.sin(beta)*np.sin(gamma)+np.cos(alpha)*np.cos(gamma), np.sin(alpha)*np.sin(beta)*np.cos(gamma)-np.cos(alpha)*np.sin(gamma)],
                      [-np.sin(beta),              np.cos(beta)*np.sin(gamma),                                           np.cos(beta)*np.cos(gamma)]])

with the matrix product:

xr, yr , zr =  np.einsum('ik, kj -> ij', rot3d(t,0,0), xyz) #by now just a fixed angle

but as you can see here it is wobbling. How do I find the right centre of my mesh?

@giammi56

Without the complete code it is difficult to express an opinion. What shape has xyz? Why did you flatten the x, y, z arrays to get ones of shape (20000,)!!!

The full project is a two layers comparison of theoretical x,y,z (20000,) and experimental x,y,z (836,) data. They should rotate the same amount synchronized. It is easier to work with the data processed in the same way and then reshape them just for the surface data (i.e. the theoretical data)

the shape is (3,20000)

The full code is the following. It is not working because I donā€™t know how to update layouts from different supblots (there is not so much literature).

def rot3d(alpha,beta,gamma):  #planar rotation of alpha radians
    return np.array([[np.cos(alpha)*np.cos(beta), np.cos(alpha)*np.sin(beta)*np.sin(gamma)-np.sin(alpha)*np.cos(gamma), np.cos(alpha)*np.sin(beta)*np.cos(gamma)+np.sin(alpha)*np.sin(gamma)], 
                      [np.sin(alpha)*np.cos(beta), np.sin(alpha)*np.sin(beta)*np.sin(gamma)+np.cos(alpha)*np.cos(gamma), np.sin(alpha)*np.sin(beta)*np.cos(gamma)-np.cos(alpha)*np.sin(gamma)],
                      [-np.sin(beta),              np.cos(beta)*np.sin(gamma),                                           np.cos(beta)*np.cos(gamma)]])

framesexp=[]
framesth=[]

fig9 = go.Figure()

fig9 = make_subplots(rows=1, cols=2,
                     subplot_titles=('Experiment', 'Theory'),
                     specs=[[{"type": "mesh3d"}, {"type": "surface"}]],)


fig9.add_trace(go.Mesh3d(x=xgo, y=ygo, z=zgo,i=tigo, j=tjgo, k=tkgo, intensity=dgo,
                           colorscale="Viridis",
                           colorbar_len=0.75,
                           flatshading=True,
                           lighting=dict(ambient=0.5,diffuse=1,fresnel=4,specular=0.5,roughness=0.05,facenormalsepsilon=0,vertexnormalsepsilon=0),lightposition=dict(x=100,y=100,z=1000)),1,1)
        
fig9.add_trace(go.Surface(x=X, y=Y, z=Z, surfacecolor=d_matrix, connectgaps=True),1,2)

T = np.arange(0,  2*np.pi, 0.125)
xyzm = np.stack((xgo, ygo, zgo))
xyzs = np.stack((X.reshape(-1), Y.reshape(-1), Z.reshape(-1)))
for alpha in T:
    xrm, yrm, zrm  =  np.einsum('ik, kj -> ij', rot3d(alpha,0.25*np.pi,0.), xyzm)#  batch rotation of all points (x,y,z) on the Mesh
    xrs, yrs, zrs  =  np.einsum('ik, kj -> ij', rot3d(alpha,0.25*np.pi,0.), xyzs)#  batch rotation of all points (x,y,z) on the Mesh
    framesexp.append(go.Frame(data=[go.Mesh3d(x=xrm, y=yrm, z=zrm)])) #z is the same in a 3d rotation about zaxis
    framesth.append(go.Frame(data=[go.Surface(x=xrs.reshape(100,200), y=yrs.reshape(100,200), z=zrs.reshape(100,200))])) #z is the same in a 3d rotation about zaxis
fig9.update((frames=framesexp),1,1); 


fig9.update_layout(updatemenus=[dict(type='buttons',
                  showactive=False,
                  y=0.7,
                  x=-0.15,
                  xanchor='left',
                  yanchor='bottom',
                  buttons=[dict(label='Play',
                                 method='animate',
                                 args=[None, dict(frame=dict(duration=5, redraw=True), 
                                                             transition=dict(duration=4),
                                                             fromcurrent=True,
                                                             mode='immediate'
                                                            )]
                                            )
                                      ]
                              )
                        ])

# fig9.show()
plotly.offline.plot(fig9, filename=r'\OUTPUTS\test_double.html')

in particulat this is poorly written and just a wild (wrong) guess: fig9.update((frames=framesexp),1,1);

I tried with something that makes more sense like

fig9.update_scenes(frames=framesexp,row=1,col=1) 
fig9.update_scenes(frames=framesth,row=1,col=2)

But frames is not a valid property

 Valid properties:
 annotations
        A tuple of
        :class:`plotly.graph_objects.layout.scene.Annotation`
        instances or dicts with compatible properties
    annotationdefaults
        When used in a template (as
        layout.template.layout.scene.annotationdefaults), sets
        the default property values to use for elements of
        layout.scene.annotations
    aspectmode
        If "cube", this scene's axes are drawn as a cube,
        regardless of the axes' ranges. If "data", this scene's
        axes are drawn in proportion with the axes' ranges. If
        "manual", this scene's axes are drawn in proportion
        with the input of "aspectratio" (the default behavior
        if "aspectratio" is provided). If "auto", this scene's
        axes are drawn using the results of "data" except when
        one axis is more than four times the size of the two
        others, where in that case the results of "cube" are
        used.
    aspectratio
        Sets this scene's axis aspectratio.
    bgcolor

    camera
        :class:`plotly.graph_objects.layout.scene.Camera`
        instance or dict with compatible properties
    domain
        :class:`plotly.graph_objects.layout.scene.Domain`
        instance or dict with compatible properties
    dragmode
        Determines the mode of drag interactions for this
        scene.
    hovermode
        Determines the mode of hover interactions for this
        scene.
    uirevision
        Controls persistence of user-driven changes in camera
        attributes. Defaults to `layout.uirevision`.
    xaxis
        :class:`plotly.graph_objects.layout.scene.XAxis`
        instance or dict with compatible properties
    yaxis
        :class:`plotly.graph_objects.layout.scene.YAxis`
        instance or dict with compatible properties
    zaxis
        :class:`plotly.graph_objects.layout.scene.ZAxis`
        instance or dict with compatible properties

Hi @giammi56,

When you are animating traces in two subplots, you donā€™t need two frame lists but only one:

frames = []
.
.
.
for alpha in T:
    xrm, yrm, zrm  =  np.einsum('ik, kj -> ij', rot3d(alpha,0.25*np.pi,0.), xyzm)
    xrs, yrs, zrs  =  np.einsum('ik, kj -> ij', rot3d(alpha,0.25*np.pi,0.), xyzs)
    frames.append(go.Frame(data=[go.Mesh3d(x=xrm, y=yrm, z=zrm),
                                 go.Surface(x=xrs.reshape(100,200), 
                                            y=yrs.reshape(100,200),
                                            z=zrs.reshape(100,200))],
                           traces=[0,1])) 
fig9.update(frames=frames)

Since in your fig.data[0] is defined the Mesh3d, while fig.data[1] contains data for the Surface, when you are defining each go.Frame.data, data[0] contains updates from the Mesh3d, and data[1] for the Surface.
Moreover to be sure that these updates are performed like that, set traces =[0,1], to inform plotly.js that fig.data[0], and fig.data[1] are updated by the frame data[0], respectively by the frame data[1].

Perfect, a clever solution. It works good!

I will try to increase the number of frames, although that will make the output very heavy (it is already 77 Mb).

I am just wondering now why the surface is striped. I restarted the kernel, cleaned the variables and I have run it from different IDEs

@giammi56
Itā€™s surfacecolor that created the surface texture. But it looks interesting :slight_smile:

It is indeed very interesting but nevertheless not welcome :slight_smile: :joy:
The surface should map the distance of each point from the origin (0,0,0)

d_matrix = np.sqrt(X**2+Y**2+Z**2)

fig9.add_trace(go.Surface(x=X, y=Y, z=Z, surfacecolor=d_matrix, connectgaps=True),1,2)

There is the first frame before rotation where it is displayed correctly. Then suddenly it changes. Should d_matrix be recalculated every time a rotation is performed? For Mesh3d is defined in the same way, but it is displayed correctly and even when in the former attempt the rotation of the second trace was displayed in the first one, the surface was correctly displayed.

Without d_matrix the texture is working fine. I tried to change the type of plot from Surface to Mesh3D (therefore surfacecolor to intensity) and the problem in not there anymore, but I need a Surface!

@giammi56

As you rotate the surface, the distance to origin is changing, but it seems that you kept dmatrix constant in each frame. To remove this bug consider the x, y coords on your surface, and z= dmatrix, and rotate the associated points (x,y,z) the same way you rotated the surface. After each rotation, set in each frame the surfacecolor=z_after_rotation.

The rotation of the surface should be around the origin and the distance of each point shouldnā€™t change! Why in Mesh3D is instead working they way it should (i.e. each point maintains the same distance during the rotation hence the same colour) ? When I recalculate the d_matrix with the new rotated coordinates the value of the scale is also changing at each iteration (just for the Surface) and this is simply wrong. Is it shifting?