plotly / plotly.py

The interactive graphing library for Python :sparkles: This project now includes Plotly Express!
https://plotly.com/python/
MIT License
16.11k stars 2.54k forks source link

Animation deletes traces #3753

Open robert-lieck opened 2 years ago

robert-lieck commented 2 years ago

Below is a minimal example, where using a slider to show different traces will delete another trace.

The example plots a grid of points and uses a slider to interactively display selected rows, however, the original grid plot disappears as soon as the slider is moved.

This looks like a bug to me. If it’s not, how can I prevent that trace from disappearing?

import numpy as np
import plotly.graph_objects as go
# generate grid
N = 10
x, y = np.meshgrid(np.linspace(0, 1, N), np.linspace(0, 1, N))
z = np.zeros((N, N))
# plot
fig = go.Figure(
    # all points (THIS DISAPPEARS!)
    data=[go.Scatter3d(x=x.flatten(), y=y.flatten(), z=z.flatten())],
    # single rows animated via slider
    frames=[go.Frame(data=[go.Scatter3d(x=x[i], y=y[i], z=z[i])], name=f"{i}") for i in range(N)],
    layout_sliders=[dict(steps=[dict(method='animate', args=[[f"{k}"]], label=f"{k}") for k in range(N)])])
fig.write_html('plot.html')
fig.show()

Two other weird things (potential bugs), which might be unrelated:

I have also posted this in the community forum but without any reactions so far.

empet commented 2 years ago

1.

import numpy as np
import plotly.graph_objects as go
# generate grid
N = 10
x, y = np.meshgrid(np.linspace(0, 1, N), np.linspace(0, 1, N))
z = np.zeros((N, N))
# plot
fig = go.Figure(

    data=[go.Scatter3d(x=[x.min(),x.max()], y=[y.min(), y.max()], z=[z.min(), z.max()], mode="markers",
                       marker_size=0.05, marker_color="white", showlegend=False),
          go.Scatter3d(x=x.flatten(), y=y.flatten(), z=z.flatten())],
    # single rows animated via slider
    frames=[go.Frame(data=[go.Scatter3d(x=x[i], y=y[i], z=z[i])], 
                     name=i,
                     traces=[1])  for i in range(N)], #traces=[1] tells plotly.js that each frame updates fig.data[1]
    layout_sliders=[dict(steps=[dict(method='animate', args=[[i]], label=f"{i}") for i in range(N)])])
fig.show()

2.

import numpy as np
import plotly.graph_objects as go
# generate grid
N = 10
x, y = np.meshgrid(np.linspace(0, 1, N), np.linspace(0, 1, N))
z = np.zeros((N, N))
# plot
fig = go.Figure(
    data=[go.Scatter3d(x=[x.min(),x.max()], y=[y.min(), y.max()], z=[z.min(), z.max()], mode="markers",
                       marker_size=0.05, marker_color="white", showlegend=False),
          go.Scatter3d(x=x.flatten(), y=y.flatten(), z=z.flatten())],
    # single rows animated via slider
    frames=[go.Frame(data=[go.Scatter3d(x=x[:i, :].flatten(), y=y[:i, :].flatten(), z=z[:i, :].flatten())], 
                     name=i,
                     traces=[1])  for i in range(N)],
    layout_sliders=[dict(steps=[dict(method='animate', args=[[i]], label=f"{i}") for i in range(N)])])
fig.show()
robert-lieck commented 2 years ago

Thanks @empet, this is really helpful!

So the behaviour for the marker size is probably intended. I wasn't aware of that inheritance, but it makes sense.

Adding auto_play=False is a good fix for the second minor issue, even though I would argue that the default behaviour should be the same in both cases, but that is definitely a minor issue.

Concerning the main issue of vanishing traces, your suggestion works, but it has to be slightly extended in more complex cases, where the animation involves multiple traces. Essentially, it seems that each animated trace "eats" one existing trace, starting at the beginning. So if you have n animated traces, you have to add n dummy traces before adding any other traces.

Still a bug, but with an acceptable workaround for the moment :slightly_smiling_face: Thanks again!

empet commented 2 years ago

No, if you have n traces, you have to add just one dummy trace, defined exactly I defined it in your case, but the drawback is that you must know the min and max coordinates among all points to be plotted in those n traces.

nicolaskruchten commented 2 years ago

The current behaviour is annoying, but is basically as designed and documented in the "current limitations and caveats" here https://plotly.com/python/animations/#current-animation-limitations-and-caveats ... Re-reading this now I should probably add a bullet explaining more explicitly that you need "the same trace" in each frame, even if some of them are "dummy" traces.

robert-lieck commented 2 years ago

@empet my issue was not the moving scene, but the disappearing traces, which is still happening in your example. But you brought up the idea of adding dummy traces. So here is my solution to the original problem (extended to animations with multiple traces):

import numpy as np
import plotly.graph_objects as go
# generate grid
N = 10
x, y = np.meshgrid(np.linspace(0, 1, N), np.linspace(0, 1, N))
z = np.zeros((N, N))
# plot
fig = go.Figure(
    data=[
        # dummy trace 1
        go.Scatter3d(x=[], y=[], z=[], showlegend=False),
        # dummy trace 2
        go.Scatter3d(x=[], y=[], z=[], showlegend=False),
        # real trace 1
        go.Scatter3d(x=x.flatten(), y=y.flatten(), z=z.flatten(),
                     mode="markers", marker=dict(size=4, color="rgb(0,200,0)"), name="Trace 1"),
        # real trace 2
        go.Scatter3d(x=x.flatten(), y=y.flatten(), z=z.flatten() + 1,
                     mode="markers", marker=dict(size=4, color="rgb(0,0,200)"), name="Trace 2")
    ],
    # animation with multiple traces in each frame, which will "eat" the dummy traces
    frames=[go.Frame(data=[go.Scatter3d(x=x[i], y=y[i], z=z[i] + j,
                                        mode="markers", marker=dict(size=6, color="rgb(200,0,0)")) for j in [0, 1]],
                     name=f"{i}") for i in range(N)],
    layout_sliders=[dict(steps=[dict(method='animate', args=[[f"{k}"]], label=f"{k}") for k in range(N)])])
fig.show()

Important: You need as many dummy traces as you have traces in the frames, otherwise, the real traces will be eaten.

robert-lieck commented 2 years ago

@nicolaskruchten thanks for your comment. I am not familiar with the internals, but the frame traces seem to "hijack" the other traces (e.g. they take their place in the legend, as can be seen when omitting showlegend=False above, etc.). So far, adding a sufficient number of dummy traces looks like a fairly robust patch, which also works with legend groups.

empet commented 2 years ago

@robert-lieck When I ran your initial code no marker was displayed from a slider step to another. I corrected that issue, and with my code each frame displays the added row.They do not disappear, as you confirmed in the first answer after my post. It wasn't quite clear from your post what are your expectations, i.e. to keep the initial markers within the plane z=0 and just recolor each line. No other marker_color has been set in your go.Frame definition. You changed the marker color in each row only after I stressed that each frame inherits the properties of the base figure. But it's important that now you get the right animation.

empet commented 2 years ago

@nicolaskruchten The same trick, in the case of a disapearing trace, has been pointed out in https://github.com/plotly/plotly.py/issues/2423. Then, such a case was inserted in a wishlist. Although no special example has been created since, now I realised that my first plotly animation has been exactly of this kind. It's this example https://plotly.com/python/animations/#moving-point-on-a-curve. The idea of inserting the base trace twice was suggested to me by Ricky Reusser. @robert-lieck's example illustrates that it is sufficiently to add an empty trace.

As a conclusion, when a frame does not update a property for all points in a scatter plot, but only for a subset of them, we must add in fig.data an empty trace of the same type as the base trace, to prevent disappearance of the points in the base trace during the animation.

robert-lieck commented 2 years ago

@empet Ah, interesting, so my initial example shows a different behaviour on your machine and on mine (on mine the grid disappears, but the frames display correctly – albeit with moving scene), good to know. Yes, your comments were very helpful and solved all issues except for the disappearing traces. Thanks for that!

Also #2423 indeed seems to be about the same issue. @nicolaskruchten your comment there about frames replacing other traces is also helpful. Maybe a bit more context and the minimal examples / workarounds from these two issues could be added to the documentation for clarification.