Resample ColumnDataSource in CustomJS?

Alright, because this was of interest to me and because I’m likely gonna have to flesh this kind of thing out for my job sooner or later, here’s a working example of upsampling on the fly. It assumes the data you’re passing in has consistent time steps (so an initial pandas resample would be required on the data before passing it in here), and it only upsamples (by binning and taking the mean). But it’s a start I think.

Also see this thread where I asked about the little function I hacked together to save bokeh with additional JS resources: Importing additional JS resources to standalone html document - I use this function here as you’ll see…

I added as much commenting as possible but strongly suggest console.logging the crap out the callback to see what’s going on.

# -*- coding: utf-8 -*-
"""
Created on Sat Nov 20 19:11:45 2021
@author: gmerritt
"""
import numpy as np
import pandas as pd
from bokeh.models import Line, ColumnDataSource, CustomJS, Slider
from bokeh.plotting import figure
from bokeh.layouts import layout
from bokeh.sampledata import sea_surface_temperature

from bokeh.embed import components
from bokeh.resources import Resources

def save_html_wJSResources(bk_obj,fname,resources_list,html_title='Bokeh Plot'):
    '''function to save a bokeh figure/layout/widget but with additional JS resources imported at the top of the html
    resources_list is a list input of where to import additional JS libs so they can be utilized into CustomJS etc in bokeh work
    e.g. ['http://d3js.org/d3.v6.js']
    '''
    script, div = components(bk_obj)
    resources = Resources()
    # print(resources)
    
    tpl = '''<!DOCTYPE html>
    <html lang="en">
        <head>
            <meta charset="utf-8">
            <title>'''+html_title+'''</title>
            '''
    tpl = tpl+resources.render_js()            
    for r in resources_list:
        tpl = tpl +'''\n<script src="'''+r+'''"></script>'''
      
    tpl = tpl+script+\
        '''
            </head>
            <body>'''\
                +div+\
            '''</body>
        </html>'''
    
    with open(fname,'w') as f:
        f.write(tpl)
        

df = sea_surface_temperature.sea_surface_temperature

data = {'time':np.array(df.index),'temperature':np.array(df['temperature'])}

src = ColumnDataSource(data=data)
glyph = Line(x='time',y='temperature')

f = figure(height=400,width=800,x_axis_type='datetime')
rend = f.add_glyph(src,glyph)

#slider_dict, each value on the slider corresponds to a specific time bin size
slider_dict = {0:'Raw',1:'12 Hours',2:'Daily',3:'Weekly',4:'Biweekly'}
sl = Slider(value=0, start=0, end = 4, step =1, show_value = False, title = 'Raw')

#conversion dictionary to pass to callback -> i.e. specific bin sizes
conv_dict = {'12 Hours':pd.to_timedelta('12H').total_seconds()*10**3
             , 'Daily':pd.to_timedelta('1D').total_seconds()*10**3
             ,'Weekly':pd.to_timedelta('7D').total_seconds()*10**3
             ,'Biweekly':pd.to_timedelta('14D').total_seconds()*10**3}

#WHAT are we passing to the callback?
#the datasource driving the renderer, the raw data rearranged for d3.js to do its thing (see TRICK below)
#, the conversion dict (to get bin size)
#, and the slider + the slider_dict (to retrieve the current value of the slider/the bin size selected by user)
#TRICK: convert your df to an array of objects (which d3.js loves to work on) by using the .to_dict('records') method
cb = CustomJS(args = dict(src=src, obj=df.reset_index().to_dict('records')
                          ,conv_dict=conv_dict,sl=sl,slider_dict=slider_dict)
              ,code='''      
              //only transform the raw data if value isn't 0
              if (sl.value>0){
                 //get the bin size from the slider value
                  const binsize = conv_dict[slider_dict[sl.value]]
                  //use d3.extent and d3.range to define your bins (they call them thresholds)
                  const [min, max] = d3.extent(obj, d=>d.time); 
                  const thresholds = d3.range(min, max, binsize); 
                  //create a binning function that will act on the .time property on obj
                  const binner = d3.bin().thresholds(thresholds).value(x=>x.time)
                  //apply that binner to the obj
                  const binned = binner(obj)
                  //do the aggregation using d3.rollup on each bin
                  //the aggregation function will calc the average temperature using d3.mean, and the midpoint of the bin  
                  const ru = binned.map(x=>d3.rollup(x, function(v) {return {'time': (v.x1-v.x0)/2+v.x0,
                                                                             'temperature': d3.mean(v,d=>d.temperature)}}))
                  //this is a cool trick that will convert the array of objects (that d3.js likes) into an object of arrays (that bokeh's CDS likes)
                  var upd_data = d3.rollup(ru, function(v) {return {'time':Array.from(v,d=>d.time)
                                                               ,'temperature':Array.from(v,d=>d.temperature)}})                                                            
                                                        }
              //basically just do that same trick but on the raw data if the slider value is 0                                                                                                                      
              else {var upd_data = d3.rollup(obj, function(v) {return {'time':Array.from(v,d=>d.time)
                                                           ,'temperature':Array.from(v,d=>d.temperature)}})
                    }
              //update the slider title and the src.data
              sl.title = slider_dict[sl.value]
              src.data = upd_data
              src.change.emit()                                  
              
              ''')
              
sl.js_on_change('value',cb)
lo = layout([f,sl])
#call my hacked out "special save" function to gain access to the d3.js resources
save_html_wJSResources(lo,'Test.html',['http://d3js.org/d3.v6.js'],'On the Fly Smoothing')

Check it out:

smoothing

3 Likes