4

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.

4 Answers 4

3

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

12
  • Thank you very much for your answer! I have to work this through, but it looks exactly what it should do. At the moment I have difficulties to "print" variables from inside the callback function. Do you have any hints how this works? When i open the browsers console nothing is printed there.
    – thommy bee
    Commented Nov 4, 2020 at 15:53
  • Glad it helped! You can only print Javascript variables from inside the callback script - like I added in the example code: console.log(cb_obj.value); or console.log(data);
    – gherka
    Commented Nov 4, 2020 at 16:14
  • it looked like what i wanted to have at first, but my next step would have been a linear trend line of visible data. setting the non-visible range to -100 is probably not the right solution then. I'll play with it, maybe I can get where I want to be.
    – thommy bee
    Commented Nov 4, 2020 at 16:23
  • I ran the code but I can't find the output, must be me ¯\ (ツ) /¯
    – thommy bee
    Commented Nov 4, 2020 at 16:25
  • 1
    It would be in the Console section of your browser's dev tools - Ctrl + Shift + K in Firefox or F12 in Chrome
    – gherka
    Commented Nov 4, 2020 at 16:52
2

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

1
  • Thank you very much, finally that's the right solution. I don't know how @gherka made his gif, because only your code is doing the right thing.
    – thommy bee
    Commented Dec 15, 2020 at 14:36
1

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)
1
  • God bless you. This should be the default answer for recent versions of bokeh. No custom JS code. Clean and neat!
    – ibarrond
    Commented Oct 13, 2022 at 10:52
0

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)

Not the answer you're looking for? Browse other questions tagged or ask your own question.