Search code examples
pythonplotlyscatter-plotplotly-dash

Python plotly animate marker styles or points in Scatter3d over time


I have a plotly & Dash based application with a 3D Scatterplot. The data head looks like this:

'Classname'    'Date'     '0'     '1'     '2'
 B              1542       0.95   0.98     0.80
 B              1725       1.00   1.00     0.75
 C              1620       0.74   0.36     0.85

I got 26 classes. not every class is represented in every year. Now I want to animate the datapoints by their Date variable but the rest of the data should always be visible so I can see where in the point cloud the data point is. Example: All data points are grey and only the points of the current data frame are highlighted with a color. Since I don't know if that´s possible, I just tried to add the animation frames as additional points with different style. But the animation does not work and I don´t know why. I can see the 'data', a slider and the buttons but no animated frames. If I click on PLAY nothing happens. Sometimes if I click arount in the graph, there suddenly appear some of the additional 'diamond' points but that´s very buggy and the animation still doesnt work. Looks like this: some 'diamond' shaped points appear but they don´t make sense and the animation doesn´t work

I was sticking to this documentation and also tried the advice in this question.

Here´s my code to create the figure:

def animate_time_tsne(dff):
    # make figure
    fig_dict = {
        "data": [],
        "layout": {},
        "frames": []
    }

    years = np.unique(dff['Date'])
    styles = np.unique(dff['Classname'])

    fig_dict["layout"]["hovermode"] = "closest"
    fig_dict["layout"]["updatemenus"] = [
        {
            "buttons": [
                {
                    "args": [None, {"frame": {"duration": 500, "redraw": False},
                                    "fromcurrent": True, "transition": {"duration": 300,
                                                                        "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"
        }
    ]

    sliders_dict = {
        "active": 0,
        "yanchor": "top",
        "xanchor": "left",
        "currentvalue": {
            "font": {"size": 20},
            "prefix": "Year:",
            "visible": True,
            "xanchor": "right"
        },
        "transition": {"duration": 300, "easing": "cubic-in-out"},
        "pad": {"b": 10, "t": 50},
        "len": 0.9,
        "x": 0.1,
        "y": 0,
        "steps": []
    }

    # create data
    colors = px.colors.qualitative.Dark24 + px.colors.qualitative.Light24,
    for i, style in enumerate(styles):
        data_by_style = dff[dff['Classname'] == style]
        data_dict = go.Scatter3d(
            x=data_by_style['0'], y=data_by_style['1'], z=data_by_style['2'],
            mode='markers', marker={'color': colors[0][i], 'size':5},
            name=style,
            #customdata=[data_by_style['Filename'], data_by_style['Classname']]
        )
        fig_dict['data'].append(data_dict)
    fig_dict['data'] = fig_dict['data']*2

    # create frames
    for year in years:
        if not np.isnan(year):
            frame = {"data": [], "name": str(year), "traces":[1]}
            data_by_year = dff[dff['Date'] == year]
            for style in styles:
                data_by_year_style = data_by_year[data_by_year['Classname'] == style]
                data_dict = go.Scatter3d(
                    x=data_by_year_style['0'], y=data_by_year_style['1'],
                    z=data_by_year_style['2'],
                    mode='markers',
                    marker={'size': 15, 'symbol': 'diamond', 'color':colors[0][-1]},
                    name=style
                )
                frame['data'].append(data_dict)

            fig_dict['frames'].append(frame)
            slider_step = {"args": [
                [year],
                {"frame": {"duration": 300, "redraw": False},
                 "mode": "immediate",
                 "transition": {"duration": 300}}
            ],
                "label": year,
                "method": "animate"}
            sliders_dict["steps"].append(slider_step)

    fig_dict["layout"]["sliders"] = [sliders_dict]

    return go.Figure(fig_dict)

Solution

  • I couldn´t find it out with go.figure but I found a working solution with plotly express.

    dff.dropna(subset=['Date'], inplace=True)  # drop nan values
        dff = dff.sort_values('Date', ascending=True)
        x_range = [dff['0'].min(), dff['0'].max()]
        y_range = [dff['1'].min(), dff['1'].max()]
        z_range = [dff['2'].min(), dff['2'].max()]
        colors = px.colors.qualitative.Dark24 + px.colors.qualitative.Light24
    
        fig = px.scatter_3d(
            dff, x='0', y='1', z='2',
            animation_frame='Date',
            range_x=x_range, range_y=y_range, range_z=z_range,
            height=900, width=1200
        )
    
        fig.layout.updatemenus[0].buttons[0].args[1]['frame']['duration'] = 300
        fig.layout.updatemenus[0].buttons[0].args[1]['transition']['duration'] = 100
        for x in fig.frames:
            x.data[0]['marker']['color'] = '#ff00c2'
            x.data[0]['marker']['symbol'] = 'diamond'
            x.data[0]['marker']['size'] = 20
    
        styles = np.unique(dff['Classname'])
    
        for i, style in enumerate(styles):
            data_by_style = dff[dff['Classname'] == style]
            fig.add_trace(go.Scatter3d(
                x=data_by_style['0'], y=data_by_style['1'], z=data_by_style['2'],
                mode='markers',
                marker={'color': colors[i], 'size': 5},
                name=style,
                opacity=0.1
            ))
    
        return fig