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]]))