Selectively updating one plot in an animation

I created an animation in Bokeh that consists of a set of line plots, a slider to highlight one of the plots, and a dot that moves along the selected line plot as time progresses. I have implemented this using CustomJS callbacks.

In the first case I tried plotting only the plot that’s selected by the slider. Every time the slider value is changed, the plot source changes, which leads to the plot being re-rendered. The animation in this case is very snappy.

In the second case, I wished to go a step further and have all the line plots displayed with low opacity in the background, along with one additional plot with high opacity that corresponds to the line plot selected by the slider. In this case, the animation is almost 10 times slower, despite the fact that the slider is only really changing one line plot, as the low opacity plots are static.

This leads me to believe that when the source is updated by the slider, all the plots are re-rendered, including the static ones. I would thus like to know if there is some way to only refresh the line plot linked to the slider?

I’m working with Bokeh version 1.4. The code for the second case is given below. The code for the first case is identical except that the following two lines of code need to be commented out:

for n in range(N):
    plot.line(x=x, y=y[n], color=colors[n], alpha=0.1)

Code for the second case:

import numpy as np
import time
from bokeh.models import ColumnDataSource, CustomJS, Slider, Toggle, Dropdown
from bokeh.io import push_notebook, show, output_notebook
from bokeh.layouts import row, column, gridplot, grid
from bokeh.plotting import figure, show, output_file
from bokeh.palettes import all_palettes
from bokeh.palettes import viridis as colormap
output_notebook() # render animation inline

dt = 5e-4
ds = 100 # downsampling factor
N = 20 # number of inputs
x = np.arange(0,2*np.pi,dt)
f = np.arange(1,N+1)
y = np.sin(np.outer(f,x))

xi = 0 # initial index for time slider
fi = 0 # initial index for freq slider

# store in dictionary to access in JS
signals = {}
for i in np.arange(len(f)):
    signals['y{}'.format(i)] = y[i]

x_t = [x[xi]]
y_t = [y[fi][xi]]

# load data in Bokeh
signals = ColumnDataSource(signals)
graphs = ColumnDataSource(dict(x=x,y=y[0]))
nodes = ColumnDataSource(dict(x_t=x_t,y_t=y_t))
index_t = ColumnDataSource(dict(index=x))
index_f = ColumnDataSource(dict(index=f))

# construct bokeh plots
TOOLS = "pan, box_zoom, wheel_zoom, box_select, reset"
colors = colormap(N)

plot = figure(tools=TOOLS, plot_width=200, plot_height=30, title="Signal vs Time")
plot.title.align='center'
plot.line(x='x', y='y', source=graphs, color='black')
plot.circle(x='x_t', y='y_t', source=nodes, size=15, color='grey')
for n in range(N):
    plot.line(x=x, y=y[n], color=colors[n], alpha=0.1)

# time slider
Timecallback = CustomJS(args=dict(graphs=graphs, nodes=nodes, dt=dt*ds),
                    code="""
    const arrayPos = (array) => Math.abs(array - cb_obj.value) < 0.1*dt;
    const new_value = graphs.data['x'].findIndex(arrayPos);
    
    nodes.data['x_t']  = [graphs.data['x'][new_value]];
    nodes.data['y_t']  = [graphs.data['y'][new_value]];
    nodes.change.emit();
""")

t_slider = Slider(start=x[0], end=x[-1], value=x[xi], step=dt*ds, title="Time")
t_slider.js_on_change('value', Timecallback)

# freq slider
Freqcallback = CustomJS(args=dict(signals=signals, graphs=graphs, nodes=nodes, t_slider=t_slider, indexCDS=index_f, indexT=index_t),
                    code="""   
    const new_value = indexCDS.data['index'].indexOf(cb_obj.value);
    const t_id = indexT.data['index'].indexOf(t_slider.value);    

    graphs.data['y'] = signals.data['y' + new_value.toString()];
    graphs.change.emit();
    
    const y = graphs.data['y'];
    nodes.data['y_t'] = [y[t_id]];
    nodes.change.emit();
""")

f_slider = Slider(start=f[0], end=f[-1], value=f[fi], step=f[1]-f[0], title="Frequency")
f_slider.js_on_change('value', Freqcallback)

# play/pause
toggle_js = CustomJS(args=dict(slider=t_slider,indexCDS=index_t, dt=dt*ds),code="""
    var check_and_iterate = function(index){
        var slider_val = slider.value;
        var toggle_val = cb_obj.active;
        if(toggle_val == false) {
            cb_obj.label = '► Play';
            clearInterval(looop);
            }
        else if(slider_val >= index[index.length - 1]) {
            cb_obj.label = '► Play';
            slider.value = index[0];
            cb_obj.active = false;
            clearInterval(looop);
            }
        else if(slider_val !== index[index.length - 1]){
            slider.value = slider.value + dt;
            }
        else {
        clearInterval(looop);
            }
    }
    if(cb_obj.active == false){
        cb_obj.label = '► Play';
        clearInterval(looop);
    }
    else {
        cb_obj.label = '❚❚ Pause';
        var looop = setInterval(check_and_iterate, 10, indexCDS.data['index']);
    };
""")

toggle = Toggle(label='► Play',active=False)
toggle.js_on_change('active',toggle_js)

# render interactive plot
c = column(f_slider, t_slider)
layout = grid([[plot], [[None,toggle,None],c]], sizing_mode = 'scale_width')
show(layout)