thommy bee
thommy bee

Reputation: 83

Bokeh custom JS callback date range slider

I try to create an interactive diagram which plots values Y over dates X. So far so good. Now I want to adjust the limits xmin and xmax of my x-axis via a DateRangeSlider but I don't understand the js callback function (I want to have a standalone html file at the end) and since I don't even know how to print values from inside the function and without any errors produced, I have no idea what to do now.

here is a running example of code:

import numpy as np
import pandas as pd
from datetime import datetime
from bokeh.models import ColumnDataSource, DatetimeTickFormatter, HoverTool
from bokeh.models.widgets import DateRangeSlider
from bokeh.layouts import layout, column
from bokeh.models.callbacks import CustomJS
from bokeh.plotting import figure, output_file, show, save

datesX = pd.date_range(start='1/1/2018', periods=100)
valuesY = pd.DataFrame(np.random.randint(0,25,size=(100, 1)), columns=list('A'))

source = ColumnDataSource(data={'x': datesX, 'y': valuesY['A']}) 

# output to static HTML file
output_file('file.html')

hover = HoverTool(tooltips=[('Timestamp', '@x{%Y-%m-%d %H:%M:%S}'), ('Value', '@y')],
                           formatters={'x': 'datetime'},)
    
date_range_slider = DateRangeSlider(title="Zeitrahmen", start=datesX[0], end=datesX[99], \
                                        value=(datesX[0], datesX[99]), step=1, width=300)

# create a new plot with a title and axis labels
p = figure(title='file1', x_axis_label='Date', y_axis_label='yValue',  x_axis_type='datetime', 
               tools="pan, wheel_zoom, box_zoom, reset", plot_width=300, plot_height=200)

# add a line renderer with legend and line thickness
    
p.line(x='x', y='y', source=source, line_width=2)
p.add_tools(hover)
       
callback = CustomJS(args=dict(source=source), code="""

    ##### what to do???

    source.change.emit();
    """)
    
date_range_slider.js_on_change('value', callback)
layout = column(p, date_range_slider)

# show the results
show(layout)

I tried to adjust and adapt similar examples of people on stackoverflow and from the bokeh demos, but i didn't manage to produce running code. Hope everything is clear and You can help.

Upvotes: 4

Views: 3408

Answers (4)

Paul Whitney
Paul Whitney

Reputation: 11

Interesting problem and discussion. Adding the following two lines (one of which was lifted directly from the documentation) allowed the slider to work without using the CustomJS and js_on_change function - using the js_link function instead:

date_range_slider.js_link('value', p.x_range, 'start', attr_selector=0)
date_range_slider.js_link('value', p.x_range, 'end', attr_selector=1)

Upvotes: 1

Statmike
Statmike

Reputation: 1

I had similar success to @Jan Burger but using the CustonJS to directly change the plots x_range rather than filtering the datasource.

callback = CustomJS(args=dict(p=p), code="""
    p.x_range.start = cb_obj.value[0]
    p.x_range.end = cb_obj.value[1]
    p.x_range.change.emit()
    """)

date_range_slider.js_on_change('value_throttled', callback)

Upvotes: 0

Jan Burger
Jan Burger

Reputation: 21

I found out that the answer above does not work since the timestamps of the ref_source data are different than the parsed timestamps which come from the bokeh Slider Object (cb_obj).

So for example the timestamps from the ref_source data create the following output when being parsed with new Date(source.data.["x"]);:

01/01/2020 02:00:00

The timestamps coming from the bokeh Slider Object cb_obj always have a time of 00:00:00. Therefore the timestamps cant be found when using const from_pos = ref["date"].indexOf(date_from);.

To parse the dates from the ref_source correctly I created a new array new_ref and added the correctly parsed dates to this array. However, I have to emphasize here that I am not a JavaScript expert and I am pretty sure that the code can be written more efficiently here.

This is my working example:

// print out array of date from, date to
console.log(cb_obj.value); 

// dates returned from slider are not at round intervals and include time;
const date_from = Date.parse(new Date(cb_obj.value[0]).toDateString());
const date_to = Date.parse(new Date(cb_obj.value[1]).toDateString());
console.log(date_from, date_to)

// Creating the Data Sources
const data = source.data;
const ref = ref_source.data;

// Creating new Array and appending correctly parsed dates
let new_ref = []
ref["x"].forEach(elem => {
    elem = Date.parse(new Date(elem).toDateString());
    new_ref.push(elem);
    console.log(elem);
})

// Creating Indices with new Array
const from_pos = new_ref.indexOf(date_from);
const to_pos = new_ref.indexOf(date_to) + 1;


// re-create the source data from "reference"
data["y"] = ref["y"].slice(from_pos, to_pos);
data["x"] = ref["x"].slice(from_pos, to_pos);

source.change.emit();

I hope it helped you a bit :)

Upvotes: 2

gherka
gherka

Reputation: 1446

You need to create a new source.data when changing the sliders. To do that, you also need a "back-up" source that you don't change and which serves as a reference for what data to include. Passing both as arguments to the callback function makes them available to the Javascript code.

datesX = pd.date_range(start='1/1/2018', periods=100)
valuesY = pd.DataFrame(np.random.randint(0,25,size=(100, 1)), columns=list('A'))

# keep track of the unchanged, y-axis values
source = ColumnDataSource(data={'x': datesX, 'y': valuesY['A']}) 
source2 = ColumnDataSource(data={'x': datesX, 'y': valuesY['A']})

# output to static HTML file
output_file('file.html')

hover = HoverTool(
    tooltips=[('Timestamp', '@x{%Y-%m-%d %H:%M:%S}'), ('Value', '@y')],
    formatters={'x': 'datetime'},)
    
date_range_slider = DateRangeSlider(
    title="Zeitrahmen", start=datesX[0], end=datesX[99],
    value=(datesX[0], datesX[99]), step=1, width=300)

# create a new plot with a title and axis labels
p = figure(
    title='file1', x_axis_label='Date', y_axis_label='yValue',
    y_range=(0, 30), x_axis_type='datetime',
    tools="pan, wheel_zoom, box_zoom, reset",
    plot_width=600, plot_height=200)

# add a line renderer with legend and line thickness
    
p.line(x='x', y='y', source=source, line_width=2)
p.add_tools(hover)

callback = CustomJS(args=dict(source=source, ref_source=source2), code="""
    
    // print out array of date from, date to
    console.log(cb_obj.value); 
    
    // dates returned from slider are not at round intervals and include time;
    const date_from = Date.parse(new Date(cb_obj.value[0]).toDateString());
    const date_to = Date.parse(new Date(cb_obj.value[1]).toDateString());
    
    const data = source.data;
    const ref = ref_source.data;
    
    const from_pos = ref["x"].indexOf(date_from);
    // add + 1 if you want inclusive end date
    const to_pos = ref["x"].indexOf(date_to);
        
    // re-create the source data from "reference"
    data["y"] = ref["y"].slice(from_pos, to_pos);
    data["x"] = ref["x"].slice(from_pos, to_pos);
    
    source.change.emit();
    """)
    
date_range_slider.js_on_change('value', callback)
layout = column(p, date_range_slider)

# show the results
show(layout)

enter image description here

Upvotes: 3

Related Questions