Workarounds for smooth transitions of data points between animation frames?

I am interested in smooth transitions of data points along a path when animating / changing frames of a scatter plots via a slider. I saw a few open GH issues on smooth animations, so I understand that the functionality is not available in Bokeh via the Python interface. However, since there was a few years since the latest update in those issues, I am wondering if there is any recent workaround that could provide this functionality?

Below is a sample scatter plot, where I would like to smoothly animate transitions between frames.

from bokeh.plotting import figure
from bokeh.models import ColumnDataSource, Slider
from bokeh.layouts import layout
from bokeh.io import curdoc


values_over_time = {'x': [1], 'y': [1]}
src = ColumnDataSource(values_over_time)
p = figure(x_range=(0, 5), y_range=(0, 5))
p.circle('x', 'y', source=src, size=20)

def scatter_update(attr, old, new):
    src.data = {'x': [new], 'y': [new]}

slider = Slider(start=1, end=4, step=1, value=1, title="Frame")
slider.on_change('value', scatter_update)

doc = curdoc()
curdoc().add_root(layout([[slider], [p]]))

In addition to looking nice, I think smooth transitions are useful for visually linking data points between frames (possibly also achievable via a faint background line). In the example above, I could just increase the resolution of the slider, but in my actual data this is not possible since frames are far apart. The amount of data I am interested in plotting is hundreds of dots and a few tens of frames.

I realize that this transition behavior is possible in plotly and I might end up using that instead, but would like to know if it is at all possible in Bokeh first. One of the GH issues mentioned Coffee script and I guess that this behavior might possibly be achieved via custom JS? I don’t have experience in either but would be ready to try some things out if it is possible to received a few pointers of where to start. I appreciate any tips for how to get started with this, or letting me know that it is not possible to achieve currently.

Recently answered a similar question here: https://stackoverflow.com/a/61673679/564509

Thanks for your quick reply @p-himik!

I read your image example and it is quite difficult for me to deduce what is necessary to go from there to a more simple example such as the scatter plot above. Do you have an example at hands with a simpler glyph or would you have time to create one for a similar situation as the scatter plot sample I posted above? I totally understand if you don’t, but thought I would ask just in case.

Conceptually, I would like the dots in frame one to transition linearly along the hypotenuse to the corresponding dot in frame two. Since there would be more than one data point in each frame (in my actual data), I am thinking that keeping the data points in the same order in my data source would enable me to use the index as a key to figure out which dots transitions where in the next frame.

I should also say that my eventual goal is to run this animation in a JupyterLab Notebook using Bokeh plots displayed via the Panel library, and the slider widget would be created using Panel (which I believe this uses Bokeh sliders under the hood).

@joelostblom I think the issues were maybe misleading in focussing on “smooth” transitions. Really they are about making scripted and automatic transitions be first-class “things” in their own right. You can have “smooth” transitions now, but you would have to compute and update the coordinate data for the “between” frames yourself. Does that make sense?

Thanks @Bryan, that’s encouraging to know! Is there an example somewhere of how to do this in Bokeh with a simple glyph such as a scatter plot? I have looked through the gallery online as well as downloaded and run the example server apps, but all the examples I have seen with a slider (such as gapminder), include instant transitions between frames rather than smooth animated ones.

You can have “smooth” transitions now, but you would have to compute and update the coordinate data for the “between” frames yourself.

I have a high level idea of what this could look like in Python in the sense that I could precompute the entire table at a finer resolution than my original data (but I believe that this would also introduce these synthetic transition points on the slider, which I would like to avoid). Do you mean it would be possible to calculate only the transition between two frames on the go versus precomputing the entire table? And would this be done on the javascript or python end?

I’m not sure if this is asking currently or prospectively.

Currently, a JS callback could potentially do something like:

  • compute a bunch of intermediate states between the last value and the current value
  • push those on to some kind of global list of changes to be applied
  • start a setInterval or similar to apply those updates

But there are lots of corner cases and potential complications, e.g what if more that one thing tries to make updates to the same thing in an overlapping way? Maybe not an issue at all in this case but definitely for a general solution. Or maybe this should not happen on regular slider updates, but only in some dedicated “animation” mode (You’ll notice the plotly version is like this: it does not update “smoothly” when manually moving the slider, only when the “play” button pressed)

Prospectively we’d like to provide some nice abstractions to make this much simpler to express from the user side. In principle any numeric properties should be able to be “scripted” by giving a start value, end value, transfer function, and time interval.

Thanks @Bryan and sorry for the ambiguous language, I meant currently (although the prospective part you described sounds exciting!).

  • compute a bunch of intermediate states between the last value and the current value
  • push those on to some kind of global list of changes to be applied
  • start a setInterval or similar to apply those updates

This sounds exactly like what I want to do! Calculate intermediate states for each scatter point when switching frames and have the dots transition through their intermediate states to the next “real” state (the value in the next frame). Are you aware of an example somewhere showing a basic implementation of this? Or would you have time to share one with me (maybe as an addition to the Bokeh gallery if this is something you think would be generally useful for Bokeh users to have access to?). I don’t think the global collisions you mentioned would be a problem in my specific scenario (scatterplot transition paths).

Or maybe this should not happen on regular slider updates, but only in some dedicated “animation” mode (You’ll notice the plotly version is like this: it does not update “smoothly” when manually moving the slider, only when the “play” button pressed)

Interesting, I didn’t know plotly worked like this. For my own purposes, I would ideally use the slider, but I would be more than happy using whichever solution is easier to implement of those two.

Hi @joelostblom

The following is a skeleton of an approach for the basic example outlined that can be run via a bokeh server.

The idea is to use the slider on-change to register a periodic callback that animates what you want and a separate counter is used to unregister that callback. Everything is encapsulated in a class to have easy access to all the quantities of interest.

bokeh serve animex.py

animex.py

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
"""

from bokeh.plotting import figure

from bokeh.models import ColumnDataSource, Slider
from bokeh.layouts import layout

from bokeh.io import curdoc


class UI(object):
    def __init__(self):
        self.doc = curdoc()
        
        self.data = {'x': [1], 'y': [1]}
        self.source = ColumnDataSource(self.data)

        self.p = figure(x_range=(0, 5), y_range=(0, 5))
        self.p.circle('x', 'y', source=self.source, size=20)

        self.slider = Slider(start=1, end=4, step=1, value=1, title='Frame')
        self.slider.on_change('value', self._init_cb)

        self.periodic_cb = None

    def go(self):
        self.doc.add_root(layout([[self.slider],[self.p]]))

    def _init_cb(self, attr, old, new):
        self.counter = 10
        self._delta = float(new - old)/self.counter
        self.periodic_cb = self.doc.add_periodic_callback(self._animate_cb, 100)

    def _animate_cb(self):
        print("_animate_cb() INFO active")
        if self.counter:
            self.counter -= 1

            df = self.source.to_df()
            df.x += self._delta
            df.y += self._delta
            
            self.source.data = dict(df)
    
        else:
            self.doc.remove_periodic_callback(self.periodic_cb)
            

ui = UI()
ui.go()

Thank you so much for providing an example @_jm!

It’s neat that the animation can be done on the Python side the way you shows, and I like that clicking at the end of the slider mimics what a play button would do. I am going to experiment with this approach on more data later today, but a couple of observations I noticed when trying your example:

  1. The transitions seem to stutter a bit, even when I tweak the values of the counter, the second argument to the periodic callback, and remove the print statement (they are not as smooth as this d3 example). Is this an inherent limitation of doing the animation on the Python side compared to the JS side or could I get around it somehow?

  2. If I click the slider, it works as I would expect, but when I drag the slider while holding the mouse button down, there is an error message continuously printing in the terminal saying ValueError: callback already ran or was already removed, cannot be removed again and the position of the data point will no longer correspond to the value of the sldier (see the animation below in the end). Any idea why this happens?

(The transitions stutter more in the video due to the fps when recording, but there is still stutter in the app itself)

Thanks again!

@joelostblom

I don’t think the relatively simple example with minimal data being pushed around should cause much of an issue with using python for the callback in this case.

It’s admittedly subjective, but things seem reasonably smooth in my environment if I tinker with the parameters (number of intermediate steps via the counter and / or the frequency of the periodic callback). I am running both server and client via localhost on a fairly new laptop.

The error you observe is due to the system trying to remove a callback after it is already been disposed. I guess this one of those possible corner cases alluded to when trying to have a quick workaround. I did not go through the stack trace to resolve, but the added conditional logic to the periodic callback _animate_cb() addresses the problem in my quick testing.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
"""

from bokeh.plotting import figure

from bokeh.models import ColumnDataSource, Slider
from bokeh.layouts import layout

from bokeh.io import curdoc


class UI(object):
    def __init__(self):
        self.doc = curdoc()
        
        self.data = {'x': [1], 'y': [1]}
        self.source = ColumnDataSource(self.data)

        self.p = figure(x_range=(0, 5), y_range=(0, 5))
        self.p.circle('x', 'y', source=self.source, size=20)

        self.slider = Slider(start=1, end=4, step=1, value=1, title='Frame')
        self.slider.on_change('value', self._init_cb)

        self.periodic_cb = None

    def go(self):
        self.doc.add_root(layout([[self.slider],[self.p]]))

    def _init_cb(self, attr, old, new):
        self.counter = 50
        self._delta = float(new - old)/self.counter
        self.periodic_cb = self.doc.add_periodic_callback(self._animate_cb, 10)

    def _animate_cb(self):
        if self.counter:
            self.counter -= 1

            df = self.source.to_df()
            df.x += self._delta
            df.y += self._delta
            
            self.source.data = dict(df)
    
        elif self.periodic_cb is not None:
            self.doc.remove_periodic_callback(self.periodic_cb)
            self.periodic_cb = None
            

ui = UI()
ui.go()

An additional point to consider re: optimization, is that I used pandas dataframes in the animation callback. Working with these can be generally more expensive than dicts, and are not really buying anything here. I just like the convenience of the syntax. So you could simplify in that area to see if it helps with the lack of smoothness you experience.