Multiple ColumnDataSource or Multindex ColumnDataSource for single customJS callback

I’m creating multiple figures in a loop, that are updated with a single customJS callback.
Currently only the last set of plots are being updated because the callback only has one source for input data and once source for output data. I’m guessing it might be best to use multi-index sources and add an extra loop into my JS code?
Would I then be able to index the source when updating the plot?

for i in n:
    tc.append(figure(plot_width=300, plot_height=300))
    c.append(tc[i].circle(x='x', y='y', source=multisource[i]))

I’ve tried using multiple sources with an extra for loop in my JS callback but the html page now becomes unresponsive when trying to use the slider. I wonder if there is something more fundamental I’m missing that means trying to update multiple data sources with one widget isn’t a good idea?

It’s impossible to tell anything without a minimal reproducible example.

I think the problem was that my JS indexing was incorrect. This “simplified” example is doing what I was hoping for, in case it is useful for anyone. It would be nice to use multi-indexing for more complex cases but I think this should work for now.

from datetime import datetime, timedelta, timezone
import numpy as np
import pandas as pd
from bokeh.plotting import figure, show
from bokeh.layouts import gridplot
from bokeh.models import Legend, DateRangeSlider, ColumnDataSource, CustomJS
from bokeh.models.widgets import DataTable, TableColumn, NumberFormatter

# Generate a test date range
start_date = datetime(2019, 1, 1)
stop_date = datetime(2020, 1, 1)
numdays = (stop_date - start_date).days
date_list = [start_date + timedelta(days=x) for x in range(numdays)]
date_arr = np.array(date_list)

# Generate some test data
data1a = np.random.rand(len(date_list))
data1b = np.random.rand(len(date_list))
data2a = np.random.rand(len(date_list))
data2b = np.random.rand(len(date_list))

# Set up data sources
s1 = ColumnDataSource(data=dict(obs=data1a, mod0=data1b))
s2 = ColumnDataSource(data=dict(obs=data2a, mod0=data2b))
slist = [s1, s2]

bias = [0]
sr1 = ColumnDataSource(data=dict(mae=mae, bias=bias))  
sr2 = ColumnDataSource(data=dict(mae=mae, bias=bias))
srlist = [sr1, sr2]  

ds1 = [start_date, start_date]
ds2 = [stop_date, stop_date]
h = [0,1]
ss = ColumnDataSource(data=dict(ds1=ds1, ds2=ds2, h=h))

ts_list = [int(dt.replace(tzinfo=timezone.utc).timestamp()) for dt in date_list] 
st = ColumnDataSource(data=dict(dl=ts_list))

# Callback Javascript code for caluclating mae and bias 
# then updating sources used in plotting
callback = CustomJS(args=dict(ss=ss, st=st, s1=s1, s2=s2, sr1=sr1, sr2=sr2), code="""
    var dst =;
    var d = cb_obj.value_throttled;
    var dd = cb_obj.value;['ds1'][0] = dd[0];['ds1'][1] = dd[0];['ds2'][0] = dd[1];['ds2'][1] = dd[1];
    var d1 =;
    var d2 =;
    var start = dst['dl'].indexOf(d[0]/1000);
    var stop = dst['dl'].indexOf(d[1]/1000);
    srcd = [d1, d2];
    sro = [,];

    function quant(values, q) {
        if (values.length === 0) return 0;
        data = values.sort(function(a,b){
            return a-b;
        var pos = ((data.length) - 1) * q;
        var base = Math.floor(pos);
        var rest = pos - base;
        if ( (data[base+1]!==undefined) ) {
            return data[base] + rest * (data[base+1] - data[base]);
        } else {
            return data[base];

    function median(values) {
        return quant(values, 0.5);

    function iqr(values) {
        return quant(values, 0.75) - quant(values, 0.25);

    function target(mod, obs) {
        var medmod = median(mod);
        var medobs = median(obs);
        var iqrobs = iqr(obs);
        var iqrmod = iqr(mod);
        var modsubmed = => x - medmod);
        var obssubmed = => x - medobs);
        var normerr =, index) {
            return item - obssubmed[index];
        var abserr = => Math.abs(x));
        var s = Math.sign(iqrmod - iqrobs);
        var mae = s * (median(abserr) / iqrobs);
        var bias = (medmod - medobs) / iqrobs;
        var mae_bias = [mae, bias];
        return mae_bias;

    for (var n = 0; n <= 1; n++) { 
        var ds = srcd[n];
        var dout = sro[n];
        mod0 = ds['mod0'];
        days = [mod0];
        for (var i = 0; i <= 0; i++) {
            var obs = ds['obs'].slice(start - i, stop - i);
            var mod = days[i].slice(start - i, stop - i);
            z = target(mod, obs);
            dout['mae'][i] = z[0];
            dout['bias'][i] = z[1];


# Set up slider widget
dslider = DateRangeSlider(title="Date range", value=(start_date, stop_date),
        start=start_date, end=stop_date, step=86400000, callback_policy="mouseup")
dslider.js_on_change('value_throttled', callback)
dslider.js_on_change('value', callback)

# Format for datatable
formatter = NumberFormatter(format='0.00')
columns = [TableColumn(field="mae", title="MAE", formatter=formatter),
        TableColumn(field="bias", title="Bias", formatter=formatter)]   

# First set of plots
datatable1=(DataTable(source=sr1, columns=columns, width=110, height=300 ))
tc1 = figure(plot_width=300, plot_height=300, title='Target', x_axis_label='MAE',
   y_axis_label='bias', x_range=(-1.1, 1.1), y_range=(-1.1, 1.1))
tc1.axis[0].fixed_location = 0
tc1.axis[1].fixed_location = 0'mae', y='bias', source=sr1, size=7)
pl1 = figure(plot_width=1000, plot_height=300, x_axis_type="datetime", 
pl1.line(date_arr, data1a, line_width=2, alpha=0.8)
pl1.line(date_arr, data1b, line_width=2, color="red", alpha=0.8)
pl1.harea(x1='ds1', x2='ds2', y='h', source=ss, fill_color='blue', fill_alpha=0.1)

# Second set of plots
datatable2=(DataTable(source=sr2, columns=columns, width=110, height=300 ))
tc2 = figure(plot_width=300, plot_height=300, title='Target', x_axis_label='MAE',
   y_axis_label='bias', x_range=(-1.1, 1.1), y_range=(-1.1, 1.1))
tc2.axis[0].fixed_location = 0
tc2.axis[1].fixed_location = 0'mae', y='bias', source=sr2, size=7)    
pl2 = figure(plot_width=1000, plot_height=300, x_axis_type="datetime", 
pl2.line(date_arr, data2a, line_width=2, alpha=0.8)
pl2.line(date_arr, data2b, line_width=2, color="red", alpha=0.8)
pl2.harea(x1='ds1', x2='ds2', y='h', source=ss, fill_color='blue', fill_alpha=0.1)
show(gridplot([[dslider, None, None],
                [pl1, tc1, datatable1],
                [pl2, tc2, datatable2]]))

Are you sure it’s a minimal example? Right now it spans 159 lines and has some repeated blocks. There also seem to be some domain-specific logic in that JS callback which makes it hard to reason about.

No this isn’t a minimal case, just a simplified version of the solution I found. If I get time to work out a multi-index approach I will try to share an example.