I'm trying to create slider that as you drag the slider, the portion of the graph that is shown is only what is on the slider. For example, if you look at my graph below, if the slider was set to 1990, you would only see the lines from 1990 to 2016. I found a working example with plotly but I wanted to see if it could be done with Bokeh.
This is my code so far:
p = figure(width = 900, height = 450)
p.xaxis.axis_label = 'Year'
p.yaxis.axis_label = 'Aggregated Number of Degrees in Education'
source = ColumnDataSource(df)
fill_source = ColumnDataSource(data=dict(x=[],y=[]))
# Create objects for each line that will be plotted
stem = p.line('year', 'stem', line_color='#8dd3c7', line_width=3, source=source)
stem = p.circle('year', 'stem', line_color='#8dd3c7', line_width=3, source=source)
sped = p.line('year', 'sped', line_color='#fdb462', line_width=3, source=source)
elem = p.line('year', 'elem', line_color='#bebada', line_width=3, source=source)
elem = p.square('year', 'elem', line_color='#bebada', line_width=3, source=source)
other = p.line('year', 'other', line_color='#fb8072', line_width=4, source=source)
aggtotal = p.line('year', 'aggtotal', line_dash=[4,4,], line_color='#80b1d3', line_width=3, source=source)
yaxis = p.select(dict(type=Axis, layout="left"))[0]
yaxis.formatter.use_scientific = False
legend = Legend(items=[("STEM", [stem])
,("SPED" , [sped])
,("Elementary", [elem])
,("Other", [other])
,("Total Education Graduates", [aggtotal])], location=(0, 0))
p.add_tools(HoverTool(tooltips=[("Date", "#year")]))
p.add_layout(legend, 'right')
callback_test = CustomJS(args=dict(source=source,fill_source=fill_source), code="""
var data = source.data;
var fill_data = fill_source.data;
var s_val = cb_obj.value;
fill_data['x']=[];
fill_data['y']=[];
for (i = 0; i < s_val; i++) {
fill_data['y'][i].push(data['y'][i]);
fill_data['x'][i].push(data['x'][i]);
}
fill_source.trigger('change');
""")
sped_slider = Slider(start=1984, end= 2016, value=1, step=1,title="Year",callback=callback_test)
callback_test.args["sped"] = sped_slider
layout = row(p,widgetbox(sped_slider))
This renders a slider but it doesn't do anything and I'm not sure where to go from here.
There are some issues with your callback code. For example:
you loop i from 0 to s_val (which may be 1990) which is not consistent with the length of your arrays.
your glyphs references columns 'stem', etc... but the fill_source has columns 'x' and 'y'
your glyphs reference source as a source but you change and trigger event on fill_source.
All that could probably be fixed but there's a much easier way, adjust the range in the callback. E.g. replace your callback by this:
x_range = p.x_range
callback_test = CustomJS(args=dict(x_range=x_range), code="""
var start = cb_obj.value;
x_range.start = start;
x_range.change.emit();
""")
Note the change to the event trigger. Your version would work but I think it's going to be deprecated.
Also:
this line callback_test.args["sped"] = sped_slider is not necessary
you could add toolbar_location='above' in figure(...) to avoid rendering conflict with the legend
you're still going to have a layout problem between the slider and the legend which can be fixed in different ways (slider under or put the slider and the legend in a column before adding to the right of the plot, etc...)
Related
Need help with below code, my hover over is not showing any data just ???. Im guessing its because I havent defined the source properly or I need to include an argument in the vbar code. Do I need to add more info to the source e.g. column names etc or do I also need to refer to the source and column names in the vbar arguments?
Thanks
def get_width():
mindate = df['local_time'].min()
maxdate = df['local_time'].max()
return 0.8 * (maxdate-mindate).total_seconds()*1000 / len(df['local_time'])
plots = []
sliders = []
for t in df['timeframeID'].unique():
inc = df[df['timeframeID'] == t].close > df[df['timeframeID'] == t].open
dec = df[df['timeframeID'] == t].open > df[df['timeframeID'] == t].close
source = ColumnDataSource(data=df)
TOOLS = "pan,wheel_zoom,box_zoom,crosshair,reset,save"
TOOLTIPS = [('open', '#open'),('high', '#high'),('low', '#low'),('close', '#close')]
name1= figure(plot_width=1600, plot_height = 900, title="Instrument AUDUSD: "+t, tools = TOOLS, tooltips=TOOLTIPS)
name1.xaxis.major_label_overrides = {
i: date.strftime('%b %d') for i, date in enumerate(pd.to_datetime(df["local_time"]))
}
name1.xaxis.bounds = (0, df.index[-1])
name1.segment(df[df['timeframeID'] == t].index[inc], df[df['timeframeID'] == t].high[inc],
df[df['timeframeID'] == t].index[inc],df[df['timeframeID'] == t].low[inc], color="black")
name1.segment(df[df['timeframeID'] == t].index[dec], df[df['timeframeID'] == t].high[dec],
df[df['timeframeID'] == t].index[dec],df[df['timeframeID'] == t].low[dec], color="black")
#name1.y_range.range_padding = 0.05
name1.vbar(df[df['timeframeID']== t].index[inc], 0.5, df[df['timeframeID']== t].open[inc], df[df['timeframeID']== t].close[inc],
fill_color="green", line_color="green")#, width=get_width())
name1.vbar(df[df['timeframeID']== t].index[dec], 0.5, df[df['timeframeID']== t].open[dec], df[df['timeframeID']== t].close[dec],
fill_color="#F2583E", line_color="#F2583E")#, width=get_width())
r = name1.circle(df[df['timeframeID']== t].index, df[df['timeframeID']== t].AV, alpha = 1, radius = .20)
name1.y_range.range_padding = 0.05
callback = CustomJS(args=dict(renderer=r), code="""
renderer.glyph.radius = cb_obj.value;
""")
s = Slider(start=0, end=1.5, value=.20, step=.05, title="Radius - " + t)
s.js_on_change('value', callback)
output_notebook()
output_file("candlestick.html", title="candlestick.py example")
sliders.append(s)
plots.append(name1)
show(column(
row(
*plots),*sliders))
Here is what my dataframe, df, looks like:
Currently you are directly providing data only for the x and y coordinate. Bokeh does not know anything about the other data. To make bokeh aware of all the data you must pass a source via source=source in your vbar method. When you pass the source, bokeh gets all the data so that it can look at different columns to show when hovering.
When you pass a source, you cannot pass the x, top and bottom coordinates directly, because otherwise bokeh would not know how to associate these values to the source you passed¹. So when you pass a source, you want to pass the names of the x, top and bottom coordinate columns, instead of the data directly. So you want to write something like:
name1.vbar("index", "open", "close", source=source, fill_color="green", line_color="green")
To do this you need to construct a source/DataFrame which already has the data you want, instead of doing the filtering you are doing in the vbar call. Without seeing your data I cannot tell you how you would construct such a Dataframe though.
1: Actually bokeh associates directly passed data via the index, so the first value is associated with the first line in the source.
I try to visualize my data in a hex map. For this I use python bokeh and the corresponding hex_tile function in the figure class. My data belongs to one of 8 different classes, each having a different color. The image below shows the current visualization:
I would like to add the possibility to change the color of the element (and ideally all its class members) when the mouse hovers over it.
I know, that it is somewhat possible, as bokeh themselves provide the following example:
https://docs.bokeh.org/en/latest/docs/gallery/hexbin.html
However, I do not know how to implement this myself (as this seems to be a feature for the hexbin function and not the simple hex_tile function)
Currently I provide my data in a ColumnDataSource:
source = ColumnDataSource(data=dict(
r=x_row,
q=y_col,
color=colors_array,
ipc_class=ipc_array
))
where "ipc_class" describes one of the 8 classes the element belongs to.
For the mouse hover tooltip I used the following code:
TOOLTIPS = [
("index", "$index"),
("(r,q)", "(#r, #q)"),
("ipc_class", "#ipc_class")
]
and then I visualized everything with:
p = figure(plot_width=1600, plot_height=1000, title="Ipc to Hexes with colors", match_aspect=True,
tools="wheel_zoom,reset,pan", background_fill_color='#440154', tooltips=TOOLTIPS)
p.grid.visible = False
p.hex_tile('q', 'r', source=source, fill_color='color')
I would like the visualization to add a function, where hovering over one element will result in one of the following:
1. Highlight the current element by changing its color
2. Highlight multiple elements of the same class when one is hovered over by changing its color
3. Change the color of the outer line of the hex_tile element (or complete class) when the element is hovered over
Which of these features is possible with bokeh and how would I go about it?
EDIT:
After trying to reimplement the suggestion by Tony, all elements will turn pink as soon as my mouse hits the graph and the color won´t turn back. My code looks like this:
source = ColumnDataSource(data=dict(
x=x_row,
y=y_col,
color=colors_array,
ipc_class=ipc_array
))
p = figure(plot_width=800, plot_height=800, title="Ipc to Square with colors", match_aspect=True,
tools="wheel_zoom,reset,pan", background_fill_color='#440154')
p.grid.visible = False
p.hex_tile('x', 'y', source=source, fill_color='color')
###################################
code = '''
for (i in cb_data.renderer.data_source.data['color'])
cb_data.renderer.data_source.data['color'][i] = colors[i];
if (cb_data.index.indices != null) {
hovered_index = cb_data.index.indices[0];
hovered_color = cb_data.renderer.data_source.data['color'][hovered_index];
for (i = 0; i < cb_data.renderer.data_source.data['color'].length; i++) {
if (cb_data.renderer.data_source.data['color'][i] == hovered_color)
cb_data.renderer.data_source.data['color'][i] = 'pink';
}
}
cb_data.renderer.data_source.change.emit();
'''
TOOLTIPS = [
("index", "$index"),
("(x,y)", "(#x, #y)"),
("ipc_class", "#ipc_class")
]
callback = CustomJS(args=dict(colors=colors), code=code)
hover = HoverTool(tooltips=TOOLTIPS, callback=callback)
p.add_tools(hover)
########################################
output_file("hexbin.html")
show(p)
basically, I removed the tooltips from the figure function and put them down to the hover tool. As I already have red in my graph, I replaced the hover color to "pink". As I am not quite sure what each line in the "code" variable is supposed to do, I am quite helpless with this. I think one mistake may be, that my ColumnDataSource looks somewhat different from Tony's and I do not know what was done to "classifiy" the first and third element, as well as the second and fourth element together. For me, it would be perfect, if the classification would be done by the "ipc_class" variable.
Following the discussion from previous post here comes the solution targeted for the OP code (Bokeh v1.1.0). What I did is:
1) Added a HoverTool
2) Added a JS callback to the HoverTool which:
Resets the hex colors to the original ones (colors_array passed in the callback)
Inspects the index of currently hovered hex (hovered_index)
Gets the ip_class of currently hovered hex (hovered_ip_class)
Walks through the data_source.data['ip_class'] and finds all hexagons with the same ip_class as the hovered one and sets a new color for it (pink)
Send source.change.emit() signal to the BokehJS to update the model
The code:
from bokeh.plotting import figure, show, output_file
from bokeh.models import ColumnDataSource, CustomJS, HoverTool
colors_array = ["green", "green", "blue", "blue"]
x_row = [0, 1, 2, 3]
y_col = [1, 1, 1, 1]
ipc_array = ['A', 'B', 'A', 'B']
source = ColumnDataSource(data = dict(
x = x_row,
y = y_col,
color = colors_array,
ipc_class = ipc_array
))
p = figure(plot_width = 800, plot_height = 800, title = "Ipc to Square with colors", match_aspect = True,
tools = "wheel_zoom,reset,pan", background_fill_color = '#440154')
p.grid.visible = False
p.hex_tile('x', 'y', source = source, fill_color = 'color')
###################################
code = '''
for (let i in cb_data.renderer.data_source.data['color'])
cb_data.renderer.data_source.data['color'][i] = colors[i];
if (cb_data.index.indices != null) {
const hovered_index = cb_data.index.indices[0];
const hovered_ipc_class = cb_data.renderer.data_source.data['ipc_class'][hovered_index];
for (let i = 0; i < cb_data.renderer.data_source.data['ipc_class'].length; i++) {
if (cb_data.renderer.data_source.data['ipc_class'][i] == hovered_ipc_class)
cb_data.renderer.data_source.data['color'][i] = 'pink';
}
}
cb_data.renderer.data_source.change.emit();
'''
TOOLTIPS = [
("index", "$index"),
("(x,y)", "(#x, #y)"),
("ipc_class", "#ipc_class")
]
callback = CustomJS(args = dict(ipc_array = ipc_array, colors = colors_array), code = code)
hover = HoverTool(tooltips = TOOLTIPS, callback = callback)
p.add_tools(hover)
########################################
output_file("hexbin.html")
show(p)
Result:
Maybe something like this to start with (Bokeh v1.1.0):
from bokeh.plotting import figure, show
from bokeh.models import ColumnDataSource, CustomJS, HoverTool
colors = ["green", "blue", "green", "blue"]
source = ColumnDataSource(dict(r = [0, 1, 2, 3], q = [1, 1, 1, 1], color = colors))
plot = figure(plot_width = 300, plot_height = 300, match_aspect = True)
plot.hex_tile('r', 'q', fill_color = 'color', source = source)
code = '''
for (i in cb_data.renderer.data_source.data['color'])
cb_data.renderer.data_source.data['color'][i] = colors[i];
if (cb_data.index.indices != null) {
hovered_index = cb_data.index.indices[0];
hovered_color = cb_data.renderer.data_source.data['color'][hovered_index];
for (i = 0; i < cb_data.renderer.data_source.data['color'].length; i++) {
if (cb_data.renderer.data_source.data['color'][i] == hovered_color)
cb_data.renderer.data_source.data['color'][i] = 'red';
}
}
cb_data.renderer.data_source.change.emit();
'''
callback = CustomJS(args = dict(colors = colors), code = code)
hover = HoverTool(tooltips = [('R', '#r')], callback = callback)
plot.add_tools(hover)
show(plot)
Result:
Another approach is to update cb_data.index.indices to include all those indices that have ipc_class in common, and add hover_color="pink" to hex_tile. So in the CustomJS code one would loop the ipc_class column and get the indices that match the ipc_class of the currently hovered item.
In this setup there is not need to update the color column in the data source.
Code below tested used Bokeh version 3.0.2.
from bokeh.plotting import figure, show, output_file
from bokeh.models import ColumnDataSource, CustomJS, HoverTool
colors_array = ["green", "green", "blue", "blue"]
x_row = [0, 1, 2, 3]
y_col = [1, 1, 1, 1]
ipc_array = ['A', 'B', 'A', 'B']
source = ColumnDataSource(data = dict(
x = x_row,
y = y_col,
color = colors_array,
ipc_class = ipc_array
))
plot = figure(
width = 800,
height = 800,
title = "Ipc to Square with colors",
match_aspect = True,
tools = "wheel_zoom,reset,pan",
background_fill_color = '#440154'
)
plot.grid.visible = False
plot.hex_tile(
'x', 'y',
source = source,
fill_color = 'color',
hover_color = 'pink' # Added!
)
code = '''
const hovered_index = cb_data.index.indices;
const src_data = cb_data.renderer.data_source.data;
if (hovered_index.length > 0) {
const hovered_ipc_class = src_data['ipc_class'][hovered_index];
var idx_common_ipc_class = hovered_index;
for (let i = 0; i < src_data['ipc_class'].length; i++) {
if (i === hovered_index[0]) {
continue;
}
if (src_data['ipc_class'][i] === hovered_ipc_class) {
idx_common_ipc_class.push(i);
}
}
cb_data.index.indices = idx_common_ipc_class;
cb_data.renderer.data_source.change.emit();
}
'''
TOOLTIPS = [
("index", "$index"),
("(x,y)", "(#x, #y)"),
("ipc_class", "#ipc_class")
]
callback = CustomJS(code = code)
hover = HoverTool(
tooltips = TOOLTIPS,
callback = callback
)
plot.add_tools(hover)
output_file("hexbin.html")
show(p)
I am having a Pandas DataFrame with a TimeSeries Index. The idea is to use the bokeh server in order to have a DateSlider. This means the values that I pass over the function are dates. However, I think my problem is the data is not updated when I am running the bokeh server. The Slider appears but the data is not updating while moving the cursor from the Slider. I would like to have your view. This is my code:
#The file was already imported and cleaned as DF: trt_file
day_one = str(min(trt_file.index)) #Checking point file
day_last = str(max(trt_file.index)) #Checking point file
# Creating the data source which will be selected from the whole file
source = ColumnDataSource(data={
"x" : trt_file.loc[day_one]["Depth [m]"],
"y" : trt_file.loc[day_one]["Temperature"],
"Temperature" : trt_file.loc[day_one]["Temperature"],
"Depth" : trt_file.loc[day_one]["Depth [m]"]
})
#Minimum values for axis generation
xmin, xmax = min(trt_file["Depth [m]"]), max(trt_file["Depth [m]"])
ymin, ymax = min(trt_file["Temperature"]), max(trt_file["Temperature"])
#Set the figure fopr the initial draw
p = figure(title="2018-07-09", x_axis_label="Depth [m]", y_axis_label="Temperature", plot_height=300, plot_width=1500, x_range=(xmin, xmax), y_range=(ymin, ymax),tools=[HoverTool(tooltips=[("Temperature", "#Temperature"), ("Depth [m]", "#Depth")])] )
#Draw the values from the source we defined first
p.circle(x="x", y="y", fill_alpha=0.5, source=source)
#Set the axis names
p.xaxis.axis_label = "Kable lenght [m]"
p.yaxis.axis_label = "Temperature [C]"
#Creating the DateTime for the Slider
day_one_dt = (min(trt_file.index)) #Checking point file
day_last_dt = (max(trt_file.index))
#Slider
slider = DateSlider(start=day_one, end=day_last, value=day_one, step=1, title="TRT Day")
#Creating the bokeh interactive figure
def update_plot(attr, old, new): #attribute to change, old_val, new_val
#set day for the slider value
day_start = (str(slider.value)) #Value from the slider
new_data = {
"x" : trt_file.iloc[day_start]["Depth [m]"],
"y" : trt_file.iloc[day_start]["Temperature"],
"Temperature": trt_file.iloc[day_start]["Temperature"],
"Depth" : trt_file.iloc[day_start]["Depth [m]"]
}
source.trt_file = new_data
p.title.text = "Day TRT"
slider.on_change("value", update_plot)
layout = row(widgetbox(slider), p)
curdoc().title = "Temp"
curdoc().add_root(layout)
Bokeh knows nothing at all about a "trt file" so setting source.trt_file is not something Bokeh will notice or do anything with. The property Bokeh knows about and responds to is data:
source.data = new_data
I am using Bokeh 1.0.1. I am unable to update the data source in the Update method i.e src.data.update(new_src.data) doesn't seem to work. Below is the full code.
def modify_doc(doc):
def create_dataset(df, resample='D'):
# Resample the data
src = df.resample(resample).mean()
# Reset index for hovering
src.reset_index(inplace=True)
return ColumnDataSource(src)
def create_plot(src):
# Blank plot with correct labels
p = figure(plot_width=700, plot_height=300, x_axis_type="datetime",
title = 'Variation of Pollution',
x_axis_label = 'Time', y_axis_label = 'Pollution (µg/m³)')
p.line(source=src, x='Date & Time', y='pm2.5', line_width=2,
color='firebrick', line_alpha=0.5, legend='Actual')
hover = HoverTool(tooltips=[('Pollution', '#{pm2.5} µg/m³'),
('Air Temp', '#{Air Temp.} °C'),
('Temp', '(#{Min. Temp.}{0.2f}, #{Max. Temp.}{0.2f}) °C'),
('Dew Pt.', '#{Dew Pt.} °C'),
('Rainfall', '#Rainfall mm'),
('Wind Dir.', '#{Wind Dir.} °'),
('Wind Speed', '#{Wind Speed} km/hr'),
('Relative humidity', '(#{Min. RH}{0.2f}, #{Max. RH}{0.2f}) %')],
mode='vline')
p.add_tools(hover)
p.legend.click_policy = 'hide'
return p
# Update function takes three default parameters
def update(attr, old, new):
# Resampling list
re_list = ['D', 'W', 'M', 'A']
# Make a new dataset based on the selected carriers and the
# make_dataset function defined earlier
new_src = create_dataset(df,
resample = re_list[resample_button_group.active])
# Update the source used the quad glpyhs
src.data.update(new_src.data)
resample_button_group = RadioButtonGroup(labels=["Day", "Week", "Month", "Year"], active=1)
resample_button_group.on_change('active', update)
controls = WidgetBox(resample_button_group)
# Initial Plot
src = create_dataset(df)
p = create_plot(src.data)
layout = row(controls, p)
doc.add_root(layout)
# Set up an application
handler = FunctionHandler(modify_doc)
app = Application(handler)
You should be able to update the line glyph directly.
First, modify your plotting code to assign a name to the line glyph:
pm_line = p.line(
source=src,
x='Date & Time',
y='pm2.5',
line_width=2,
color='firebrick',
line_alpha=0.5,
legend='Actual',
name='pm_line' # Add this!
)
Then in your update function, replace your existing update line with the following:
pm_line = p.select_one({'name':'pm_line'})
pm_line.data_source.data = new_src.data
I have the following bokeh code how do you add a horizontal line or glyph to the chart dynamically do I need a callback?
In the documentation http://docs.bokeh.org/en/latest/docs/user_guide/interaction/callbacks.html it says "Custom callbacks like these can be set using a CustomJS object and passing it as the callback argument to a Widget object."
So do I set the call back before or after the update...
source = ColumnDataSource(dict(
time=[], average=[], low=[], high=[], open=[], close=[],
ma=[], macd=[], macd9=[], macdh=[], color=[]
))
#main chart
p = figure(plot_height=500, tools="xpan,xwheel_zoom,xbox_zoom,reset,crosshair,hover", x_axis_type=None, y_axis_location="right")
p.x_range.follow = "end"
p.x_range.follow_interval = 100
p.x_range.range_padding = 0
p.axis.minor_tick_in = -2
p.axis.minor_tick_out = 5
p.segment(x0='time', y0='low', x1='time', y1='high', line_width=2, color='black', source=source)
p.segment(x0='time', y0='open', x1='time', y1='close', line_width=8, color='color', source=source)
...
#count()
def update(t):
open, high, low, close, average = _get_prices(t)
color = "green" if open < close else "red"
new_data = dict(
time=[t],
open=[open],
high=[high],
low=[low],
close=[close],
color=[color],
)
print(open)
source.stream(new_data, 300)
output_notebook()
curdoc().add_periodic_callback(update, 50)
curdoc().title = "OHLC"