Reputation: 83
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
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
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
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
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)
Upvotes: 3