☰Read-only archive⌕
Looking for help with Bokeh?

This is a read-only archive. Ask questions and join the conversation on GitHub.

Visit Community Support on GitHub →
Archived discussions · Captured October 28, 2026

Multiple ColumnDataSource or Multindex ColumnDataSource for single customJS callback

Community Support
RobM
RobM

Hello,
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?
e.g.

tc=[]
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]))
⌁   Link to post
RobM
RobM

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?

⌁   Link to post
RobM
RobM

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]

mae=[0]
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 = st.data;
    var d = cb_obj.value_throttled;
    var dd = cb_obj.value;
    ss.data['ds1'][0] = dd[0];
    ss.data['ds1'][1] = dd[0];
    ss.data['ds2'][0] = dd[1];
    ss.data['ds2'][1] = dd[1];
    var d1 = s1.data;
    var d2 = s2.data;
    var start = dst['dl'].indexOf(d[0]/1000);
    var stop = dst['dl'].indexOf(d[1]/1000);
    
    srcd = [d1, d2];
    sro = [sr1.data, sr2.data];

    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 = mod.map(x => x - medmod);
        var obssubmed = obs.map(x => x - medobs);
        var normerr = modsubmed.map(function(item, index) {
            return item - obssubmed[index];
        })
        var abserr = normerr.map(x => 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];
        }
    }

    sr1.change.emit();
    sr2.change.emit();
    ss.change.emit();
""")  

# 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
tc1.circle(x='mae', y='bias', source=sr1, size=7)
pl1 = figure(plot_width=1000, plot_height=300, x_axis_type="datetime", 
        x_range=(start_date,stop_date))
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
tc2.circle(x='mae', y='bias', source=sr2, size=7)    
pl2 = figure(plot_width=1000, plot_height=300, x_axis_type="datetime", 
        x_range=(start_date,stop_date))
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]]))
⌁   Link to post
p-himik
p-himik

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.

⌁   Link to post

End of archived discussion · Replies are disabled