Plotly can animate a chart in the browser with no extra code on the page. A figure may carry frames: a list of named snapshots, each of which overrides some traces' data. The browser steps through the frames when you press a play button or drag a slider, and that machinery is also just data, updatemenus for the buttons and sliders for the control, so an animated figure is still a plain figure you can build and check.
Two things make animations honest. The axis ranges must be fixed across frames: otherwise the axes rescale at every step and a point can look as if it did not move when the scale moved instead. And the frame names must match the slider's steps exactly, or the slider jumps to nothing. This question builds an animated scatter with Express, builds the same thing by hand from frames, and reads a frame back.
Implement animated_scatter(df, x, y, frame), figure_from_frames(frames) and last_frame_summary(fig). The signatures and docstrings are already in the editor.
animated_scatter(df, x, y, frame) returns a Plotly Express scatter with animation_frame=frame, and fixed axis ranges: the x axis range is [min(x), max(x)] and the y axis range [min(y), max(y)] over the whole frame (all animation frames), exactly those numbers.figure_from_frames(frames) takes a dict mapping a frame name to an (x, y) pair of equal-length sequences (iterated in insertion order, at least one entry) and returns a go.Figure whose initial data is one Scatter trace (markers only) of the first entry, with one go.Frame per entry (same name, each holding one Scatter of that entry's points), and a slider with one step per frame, in order. The slider step for a frame has method="animate", label equal to the frame name and args[0] == [frame_name].last_frame_summary(fig) describes the last frame of fig as a dict {"name": frame name, "n_points": number of x values of its first trace, "x_max": largest x value of its first trace} (x_max as a Python float).fig.show().px.scatter(..., animation_frame="col", range_x=[lo, hi], range_y=[lo, hi]) fixes the axes for the whole animation.
go.Frame(data=[go.Scatter(...)], name="2020") is one snapshot. go.Figure(data=[...], frames=[...]) attaches them.
A slider step is dict(method="animate", label=name, args=[[name], {...}]); fig.update_layout(sliders=[dict(steps=[...])]) adds the slider.
No results yet
Run your code to check it against the examples, or submit to run every test.