Skip to content

Line animations fail in some frames for some lines. #2794

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
iandanforth opened this issue Jul 9, 2018 · 13 comments · Fixed by #2814
Closed

Line animations fail in some frames for some lines. #2794

iandanforth opened this issue Jul 9, 2018 · 13 comments · Fixed by #2814
Labels
bug something broken

Comments

@iandanforth
Copy link

iandanforth commented Jul 9, 2018

OS: OSX 10.13.2
Browsers: Chrome/Firefox/Safari latest

plotly-bug

As you can see in this gif, most of the frames animate properly. Some however have the lines "jumping" to their final values rather than being smoothly transitioned.

This animated scatter is being created from the Python API, but I suspect this is more appropriate for the js side to investigate. (Correct me if I'm wrong).

Here is the script to recreate the above chart.

Notes

  • The issue seems to be largely independent of how many lines are being drawn. (You can change line_count below to see that).
  • The issue goes away if the values remain constant within each line. Comment out line 43 to see that.
import numpy as np
import colorlover as cl
from plotly.offline import plot

sim_duration = 20.0
time_inc = 0.1
line_count = 120
times = np.arange(0.0, sim_duration, time_inc)

# Setting colors for plot.
potvin_scheme = [
    'rgb(115, 0, 0)',
    'rgb(252, 33, 23)',
    'rgb(230, 185, 43)',
    'rgb(107, 211, 100)',
    'rgb(52, 211, 240)',
    'rgb(36, 81, 252)',
    'rgb(0, 6, 130)'
]
# It's hacky but also sorta cool.
c = cl.to_rgb(cl.interp(potvin_scheme, line_count))
c = [val.replace('rgb', 'rgba') for val in c]
c = [val.replace(')', ',{})') for val in c]


def get_color(trace_index: int) -> str:
    # The first and every 20th trace should be full opacity
    alpha = 0.2
    if trace_index == 0 or ((trace_index + 1) % 20 == 0):
        alpha = 1.0
    color = c[trace_index].format(alpha)
    return color

# Per Motor Unit Force

start = np.ones((line_count, len(times)))
inds = np.reshape(np.arange(1.0, 41.0, 40 / len(times)), (1, len(times)))
vals = (-np.log(inds) + 4) / 0.18
vals = np.repeat(vals, line_count, axis=0)


all_array = np.ones((line_count, len(times))) * np.reshape(np.arange(0.0, 35.0, 35 / line_count), (line_count, 1))
all_array += vals  # <---- COMMENT OUT TO REMOVE CURVE
data = []
annotations = []
anno_offsets = {
    0: 20,
    19: 30,
    39: 40,
    59: 45,
    79: 17,
    99: 56,
    119: 170
}

max_y = np.amax(all_array)
for i, t in enumerate(all_array):
    trace = dict(
        x=times[:1],
        y=t[:1],
        name=i + 1,
        marker=dict(
            color=get_color(i)
        ),
        mode='lines'
    )
    data.append(trace)

frames = []
for i in range(1, len(times), int(1 / time_inc)):
    frame_data = []
    for j, t in enumerate(all_array):
        trace = dict(
            x=times[:i],
            y=t[:i],
            name=j + 1,
            marker=dict(
                color=get_color(j)
            ),
            mode='lines'
        )
        frame_data.append(trace)

    frame = dict(
        data=frame_data
    )
    frames.append(frame)

layout = dict(
    title='Motor Unit Forces by Time',
    yaxis=dict(
        title='Motor unit force (relative to MU1 tetanus)',
        range=[0, max_y],
        autorange=False
    ),
    xaxis=dict(
        title='Time (s)',
        range=[0, sim_duration],
        autorange=False
    ),
    updatemenus=[{
        'type': 'buttons',
        'buttons': [{
            'args': [
                None,
                {'frame': {'duration': 300, 'redraw': False},
                 'fromcurrent': True,
                 'transition': {'duration': 200, 'easing': 'linear'}
                 }
            ],
            'label': 'Play',
            'method': 'animate'
        }]
    }]
)
layout['annotations'] = annotations

fig = dict(
    data=data,
    layout=layout,
    frames=frames
)

plot(fig, filename='bug.html', validate=False)
@etpinard
Copy link
Contributor

etpinard commented Jul 10, 2018

Thanks for the report!

As per our issue guidelines:

If you don't know JavaScript and still want to help us by reporting a bug, please attach the "data" and "layout" attributes that describe your graph and updates (if required to detect the bug). One way to retrieve your graph's data and layout attributes is by exporting your graph to Plotly Cloud. To do so, click on the Edit in Chart Studio mode bar button (the 2nd one from the left by default) and follow these instructions, or watch this screencast.

@etpinard
Copy link
Contributor

... and note that the same instructions work for graphs with data, layout and a set of frames like yours.

@iandanforth
Copy link
Author

@etpinard Data is generated and the layout is specified in the attached script. Thanks.

@alexcjohnson
Copy link
Collaborator

alexcjohnson commented Jul 10, 2018

@iandanforth we understand that the Python code here reproduces the bug but the first step for us to work on this issue would need to be converting it to pure JavaScript anyway, which is substantially easier for you (since you already have this graph generated) than it is for us, and can also help rule out non-JavaScript root causes. That’s why we ask reporters to provide a reproduction specifically in JavaScript. Thanks for your understanding!

@iandanforth
Copy link
Author

@alexcjohnson Thanks for the note. The output of the script is an HTML file which is 9.4M which throws an error during the export->save flow. Here is the generated file

@alexcjohnson
Copy link
Collaborator

Hmm ok, seems there are several issues here. The HTML file should help us to debug, so thanks for posting that.

@iandanforth
Copy link
Author

I should note the 'error' on export is more of a warning due to the filesize, so I can't save/share it with my free account.

@etpinard
Copy link
Contributor

Ok. I was able to reproduce the issue (issues?) in a fairly minimal codepen: https://codepen.io/etpinard/pen/pZJZJo?editors=0010

From my observations, the animation doesn't apply transitions (i.e. a linear tween) to the lines until about the 10th frame, before the animation only redraws the line with the frame data pts. That's a bug.

Now, in https://codepen.io/etpinard/pen/QBbxpV (which behaves like the python generated file @iandanforth provided, but with less traces) there seems to be a race condition where some frames transition before others. This shouldn't happen.

@etpinard etpinard added the bug something broken label Jul 12, 2018
@iandanforth
Copy link
Author

@etpinard Thanks for looking into this, I really appreciate your time and effort!

@etpinard
Copy link
Contributor

Ok, after some further investigations, I don't think there's an easy fix for this issue.

In brief, our current transition machinery (which make use of d3.js) doesn't allow to smoothly transition between line paths of different coordinate lengths.

Now, why does some frame smoothly transition in @iandanforth's example? That's because the line point decimation algo produces lines with the same number of underlying coordinates in a few cases.

Note that by setting line.simpifly to false, all frames do not smoothly transition and the "race" condition discussed previously is gone.

I found one workaround for d3's (and plotly's 😏 ) limitations in https://bocoup.com/blog/improving-d3-path-animation where we make all frames have the same number of coordinates. In your case, something like https://codepen.io/etpinard/pen/djoQXJ?editors=1010 (where all coordinates past the frame index our clamped to the "last" coord) with line.simplify: false should work. But unfortunately, this will require a patch in plotly.js where line.simplify: false trace need to through this block

if(!linear) {
addPt(clusterHighPt);
continue;
}

to not have their duplicated points taken out. The above codepen with the above patch gives:

peek 2018-07-12 16-20

@alexcjohnson do you think patching the above line in line_points.js and documenting this "equal-length" workaround is an ok solution to the problem, or should we try to find a better way to smoothly transition line paths of different coordinate lengths?

@alexcjohnson
Copy link
Collaborator

do you think patching the above line in line_points.js and documenting this "equal-length" workaround is an ok solution to the problem

Yes, seems like line.simplify: false should mean that anyway. This would be important for other animations too, even if the number of points really is constant but there's a chance that sometimes neighboring points could match.

or should we try to find a better way to smoothly transition line paths of different coordinate lengths?

That would be great, but sounds theoretically ambiguous. How can we tell if an increase in number of points is an append, prepend, insert, or something else? All of those would imply different animations.

@etpinard
Copy link
Contributor

@iandanforth #2814 along with https://gist.github.com/etpinard/c14dc5b69241586bd95c2f7bf2b0f5a7 will give you a way to smoothly animate your line traces.

@iandanforth
Copy link
Author

This is great! Again really appreciated.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug something broken
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants