Search code examples
pythonanimation3dplotlyjupyter

How to create a rotating animation of a Scatter3D plot with plotly and save it as gif?


With plotly in jupyter I am creating a Scatter3D plot as follows:

# Configure the trace.
trace = go.Scatter3d(
    x=x,
    y=y,
    z=z,
    mode='markers',
    marker=dict(color=colors, size=1)
)

# Configure the layout.
layout = go.Layout(
    margin={'l': 0, 'r': 0, 'b': 0, 't': 0},
    height = 1000,
    width = 1000
)

data = [trace]

plot_figure = go.Figure(data=data, layout=layout)

# Render the plot.
plotly.offline.iplot(plot_figure)

How can I rotate a plot generated like this in order to create a gif video out of it i.e. stored as a gif file like rotate.gif which shows an animation of the plot rotated?

Based on the comments given I created this code (complete, working example):

import plotly.graph_objects as go
import numpy as np
import plotly.io as pio


# Helix equation
t = np.linspace(0, 10, 50)
x, y, z = np.cos(t), np.sin(t), t

fig= go.Figure(go.Scatter3d(x=x, y=y, z=z, mode='markers'))

x_eye = -1.25
y_eye = 2
z_eye = 0.5

fig.update_layout(
         title='Animation Test',
         width=600,
         height=600,
         scene_camera_eye=dict(x=x_eye, y=y_eye, z=z_eye),
         updatemenus=[dict(type='buttons',
                  showactive=False,
                  y=1,
                  x=0.8,
                  xanchor='left',
                  yanchor='bottom',
                  pad=dict(t=45, r=10),
                  buttons=[dict(label='Play',
                                 method='animate',
                                 args=[None, dict(frame=dict(duration=5, redraw=True), 
                                                             transition=dict(duration=1),
                                                             fromcurrent=True,
                                                             mode='immediate'
                                                            )]
                                            )
                                      ]
                              )
                        ]
)


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

frames=[]
for k, t in enumerate(np.arange(0, 6.26, 0.1)):
    xe, ye, ze = rotate_z(x_eye, y_eye, z_eye, -t)
    newframe = go.Frame(layout=dict(scene_camera_eye=dict(x=xe, y=ye, z=ze)))
    frames.append(newframe)
    pio.write_image(newframe, f"images/images_{k+1:03d}.png", width=400, height=400, scale=1)
fig.frames=frames

fig.show()

which runs without an error and does rotate the scenery when I press on Play, however the image that is saved just shows an empty 2D coordinate system:

enter image description here

but not what I actually see rotating. It also seems those image are created when I execute the cell in the jupyter notebook and not after I press "Play". Seems that there are two figures, one that I can see rotating, and the image of an empty 2D coordinate system that gets saved to a file ...


Solution

  • Note pio.write_image() expects a Figure object or dict representing a figure as the 1st argument (we can't just pass an update object or a frame). The idea is precisely to apply the changes from each animation frame to the figure and export it at each point sequentially :

    frames=[]
    for k, t in enumerate(np.arange(0, 6.26, 0.1)):
        xe, ye, ze = rotate_z(x_eye, y_eye, z_eye, -t)
        newframe = go.Frame(layout=dict(scene_camera_eye=dict(x=xe, y=ye, z=ze)))
        frames.append(newframe)
        fig.update_layout(scene_camera_eye=dict(x=xe, y=ye, z=ze))
        pio.write_image(fig, f"images/images_{k+1:03d}.png", width=400, height=400, scale=1)
    

    The animation frames describe only the changes to be applied at some point to the figure, asynchronously, using Plotly.js animate method (js runtime), but the image export is done synchronously (python runtime), before the figure is eventually rendered (because before fig.show()).