✊🏿 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!

Prototyping 3D engineering animation with plotly in Jupyter notebook

I’m coming from a background in Mathematica 12 and could possibly need to animate upward of 23000 points of time series data. Using the main plotly python tutorial page as reference, I’m struggling now to get the animation to display correctly and could use some suggestions on how to improve my code. For some reason the path disappears upon play, and I can’t get the y or z vectors to animate. I’m left randomly nudging around code at this point or returning to mathematica, but wanted to ask one last time here for help. Thanks.


def animateDataSource(time_series_source):
    """
    Takes lists of x,y,z data and returns a list of plotly frames through the frameMaker function. 
    """
    list_of_frames = []
    for k in range(N-1):
        current_vector_data = frameMaker(k)
        list_of_frames.append(
            go.Frame(
                data=[go.Scatter3d(
                    x = [time_series_source[0][k]],
                    y = [time_series_source[1][k]],
                    z = [time_series_source[2][k]],
                    mode="markers",
                    marker=dict(color="red",size=10,opacity=0.5)),
                go.Scatter3d(
                    x=current_vector_data["x"][0],
                    y=current_vector_data["x"][1],
                    z=current_vector_data["x"][2],
                    line=dict(color='darkblue',width=2)),
                go.Scatter3d(
                    x=current_vector_data["y"][0],
                    y=current_vector_data["y"][1],
                    z=current_vector_data["y"][2],
                    line=dict(color='red',width=2)),
                go.Scatter3d(
                    x=current_vector_data["z"][0],
                    y=current_vector_data["z"][1],
                    z=current_vector_data["z"][2],
                    line=dict(color='red',width=2))],
                layout=go.Layout(
                    title = go.layout.Title(text=str([k+1,list(map(lambda x: round(x,3), time_series_source.T[k]))]))
                )
            )
        )
    return list_of_frames


def frameMaker(i):
    """
    returns x,y,z dict of currently indexed frame by vector component key
    """
    scale = 10
    list_of_lists = dict({
        "x": [[source[0][i],scale * source[0][i+1]], [source[1][i],source[1][i]], [source[2][i],source[2][i]]],
        "y": [[source[0][i],source[0][i]], [source[1][i],scale * source[1][i+1]], [source[2][i], source[2][i]]],
        "z": [[source[0][i],source[0][i]], [source[1][i],source[1][i]], [source[2][i], scale * source[2][i+1]]]
    })
    return list_of_lists


#graphics
plt = go.Figure(
    data=[go.Scatter3d(
            x=source[0],
            y=source[1],
            z=source[2],
            name="frame",
            mode="lines",
            line=dict(
                color="darkblue",
                width=2)),
        go.Scatter3d(
            x=source[0],
            y=source[1],
            z=source[2],
            name="curve",
            mode="lines",
            line=dict(
                color="darkblue",
                width=2))],
    layout = 
        go.Layout(
            title = go.layout.Title(text="Title | Total Frames: "+ str(N)),
            scene_aspectmode="cube",
            scene = dict(
                xaxis = dict(range=[-2,2], nticks=10, autorange=False),
                yaxis = dict(range=[-2,2], nticks=10, autorange=False),
                zaxis = dict(range=[-2,2], nticks=10, autorange=False)),
            updatemenus=[dict(type="buttons",
                          buttons=[dict(label="Play",
                                        method="animate",
                                        args=[None])])]),
    frames = animateDataSource(source)
)
plt.show()

@wower The basic idea behind Plotly animation is to define the initial frame as a data list of Plotly traces.
In your code above you defined two such traces, but it seems that it is the same trace, only the name is different. Am I right?

The list of frames is a list of dicts with the optional keys : 'data', 'layout', 'traces', 'name'.

  • data: is a list of dicts. Each dict contains the updated attributes of the corresponding trace in the fig.data (by your notation plt.data)
  • layout: is a dict of attributes in plt.layout, that are updated from frame to frame (in your case the title is updated)
  • traces= [0, 1] # gives the list of trace indices, included in plt.data, that are updated by the corresponding dict in each frames[k][‘data’]. If plt.data contains only one trace, then traces=[0] is the default setting.
  • name: is a string giving the name of the corresponding frame .

Hence your frames don’t meet these requirements. For each k you have:

list_of_frames.append(
            go.Frame(
                data=[go.Scatter3d(
                    x = [time_series_source[0][k]],
                    y = [time_series_source[1][k]],
                    z = [time_series_source[2][k]],
                    mode="markers",
                    marker=dict(color="red",size=10,opacity=0.5)),
                go.Scatter3d(
                    x=current_vector_data["x"][0],
                    y=current_vector_data["x"][1],
                    z=current_vector_data["x"][2],
                    line=dict(color='darkblue',width=2)),
                go.Scatter3d(
                    x=current_vector_data["y"][0],
                    y=current_vector_data["y"][1],
                    z=current_vector_data["y"][2],
                    line=dict(color='red',width=2)),
                go.Scatter3d(
                    x=current_vector_data["z"][0],
                    y=current_vector_data["z"][1],
                    z=current_vector_data["z"][2],
                    line=dict(color='red',width=2))],
                layout=go.Layout(
                    title = go.layout.Title(text=str([k+1,list(map(lambda x: round(x,3), time_series_source.T[k]))]))
                )
            )
        )

i.e. you insert four go.Scatter3d traces in your frames.data, but do not point out which original trace (from plt.data) is updated.

If you post a minimal data set and more details on what you intend to display with each frame, then I’l be able to give you more pointers.

I’ve finally had time to return to this problem and I deeply apologize for missing the fact I neglected to post my data source. I’ve been using a random function to generate synthetic data as I scale up from N=30 to N>23000.

#data source
N = 30
vec_x, vec_y, vec_z = [0,0,0]
list_of_lists = []
choice = [-0.2, 0.2]
for i in range(N):
    vec_x = vec_x + np.random.choice(choice)
    vec_y = vec_y + np.random.choice(choice)
    vec_z = vec_z + np.random.choice(choice)
    list_of_lists.append([vec_x, vec_y, vec_z])
points = np.array(list_of_lists)
source = points.T

I’m determine to solve the problem and have reworked the code slightly as I work to find a way through to an answer. Now the list of frames is generated in its own function. But I might try to rework it further so that it returns a dictionary?

#graphics
plt = go.Figure(
    data=[go.Scatter3d(
            x=source[0],
            y=source[1],
            z=source[2],
            name="frame",
            mode="lines",
            line=dict(
                color="darkblue",
                width=2)),
        go.Scatter3d(
            x=source[0],
            y=source[1],
            z=source[2],
            name="curve",
            mode="lines",
            line=dict(
                color="darkblue",
                width=2))],
    layout = 
        go.Layout(
            title = go.layout.Title(text="Title | Total Frames: "+ str(N)),
            scene_aspectmode="cube",
            scene = dict(
                xaxis = dict(range=[-2,2], nticks=10, autorange=False),
                yaxis = dict(range=[-2,2], nticks=10, autorange=False),
                zaxis = dict(range=[-2,2], nticks=10, autorange=False)),
            updatemenus=[dict(type="buttons",
                          buttons=[dict(label="Play",
                                        method="animate",
                                        args=[None])])]),
    frames = animateDataSource(source)
)
plt.show()
def frameMaker(i):
    """
    returns x,y,z dict of currently indexed frame by vector component key
    """
    scale = 10
    list_of_lists = dict({
        "x": [[source[0][i],scale * source[0][i+1]], [source[1][i],source[1][i]], [source[2][i],source[2][i]]],
        "y": [[source[0][i],source[0][i]], [source[1][i],scale * source[1][i+1]], [source[2][i], source[2][i]]],
        "z": [[source[0][i],source[0][i]], [source[1][i],source[1][i]], [source[2][i], scale * source[2][i+1]]]
    })
    return list_of_lists
def animateDataSource(time_series_source):
    """
    Takes lists of x,y,z data and returns a list of plotly frames through the frameMaker function. 
    """
    list_of_frames = []
    for k in range(N-1):
        current_vector_data = frameMaker(k)
        list_of_frames.append(
            go.Frame(
                data=[go.Scatter3d(
                    x = [time_series_source[0][k]],
                    y = [time_series_source[1][k]],
                    z = [time_series_source[2][k]],
                    mode="markers",
                    marker=dict(color="red",size=10,opacity=0.5)),
                go.Scatter3d(
                    x=current_vector_data["x"][0],
                    y=current_vector_data["x"][1],
                    z=current_vector_data["x"][2],
                    line=dict(color='darkblue',width=2)),
                go.Scatter3d(
                    x=current_vector_data["y"][0],
                    y=current_vector_data["y"][1],
                    z=current_vector_data["y"][2],
                    line=dict(color='red',width=2)),
                go.Scatter3d(
                    x=current_vector_data["z"][0],
                    y=current_vector_data["z"][1],
                    z=current_vector_data["z"][2],
                    line=dict(color='red',width=2))],
                layout=go.Layout(
                    title = go.layout.Title(text=str([k+1,list(map(lambda x: round(x,3), time_series_source.T[k]))]))
                )
            )
        )
    return list_of_frames

@wover Although you provided data I couldn’t deduce what you intended to animate, because the frames definition is incomplete.

To understand what I described in the previous answer as being the steps in creating an animation, I posted here https://plot.ly/~empet/15268/animation-with-more-than-one-trace/#/ an example to see effectively how the frames must be defined.

If you want to get more info, please describe in words, not code, what you want to animate.

1 Like