bokeh update vbar data source - python

I'm building an histogram plot with running a bokeh server to dynamically change data for an histogram plot. Datasource should change by clicking on button - but it's not working as expected.
from bokeh.plotting import figure
from bokeh.layouts import layout, widgetbox
from bokeh.io import curdoc
from bokeh.transform import factor_cmap
from bokeh.palettes import Spectral6
from bokeh.models import FactorRange, ColumnDataSource
from bokeh.models.widgets import Button
button = Button(label="ChangeValue", button_type="success")
fruits = ['Apples', 'Pears', 'Nectarines', 'Plums', 'Grapes', 'Strawberries']
val = [5, 3, 4, 2, 4, 6]
data_dict = {'x':fruits,'y':val}
source_table_hist = ColumnDataSource(data=data_dict)
h = figure(x_range=data_dict['x'],plot_height=350, title="Histogram")
h.vbar(x='x', top='y', width=0.2, source=source_table_hist, legend="x", line_color='black',
fill_color=factor_cmap('x', palette=Spectral6, factors=data_dict['x']))
h.xgrid.grid_line_color = None
h.y_range.start = 0
inputs = widgetbox(button)
def update():
fruits = ['Banana', 'Orange']
val = [15, 23]
data_dict = {'x':fruits,'y':val}
h.x_range=FactorRange(factors=data_dict['x'])
source_table_hist.data = data_dict
button.on_click(update)
l = layout([inputs,h])
curdoc().add_root(l)
curdoc().title = "Test"

The use-case that Bokeh creators had in mind, and the use case that is therefore currently best-supported, is to set up an app, then update the data and properties in place (i.e. as opposed to replacing whole objects like ranges). Your code works for me if this line
h.x_range=FactorRange(factors=data_dict['x']) # replace entire range (bad)
is changed to this instead:
h.x_range.factors=data_dict['x'] # update existing range (good)
The new bars are rendered as grey, since the color mapper you configure does not know anything about the new factors you changed to. Some options:
configure the color mapper up front for all possible factors that will ever be set
update the properties of the color mapper to adjust for the new factors
do a color mapping in Python (and put a "color" column in the CDS) instead of using LinearColormapper

Related

Can bokeh dynamically update the number of columns in a gridplot?

I have 2 bokeh rows. The top row contains a DataTable and a TextInput, both of which are able to stretch_width in order to fit the width of the browser. The bottom row contains a gridplot, which is able to stretch_width, but only does so by distorting the scale of the image. Ideally, I would like the gridplot to update the amount of columns displayed based on the size of the browser.
Consider the following example:
import pandas as pd
from bokeh.models.widgets import DataTable, TableColumn
from bokeh.models import ColumnDataSource, TextInput
from bokeh.plotting import figure, output_file, save
from bokeh.layouts import row, column, gridplot
def get_datatable():
"""this can stretch width without issue"""
df = pd.DataFrame({'a': [0, 1, 2], 'b': [2, 3, 4]})
source = ColumnDataSource(df)
Columns = [TableColumn(field=i, title=i) for i in df.columns]
data_table = DataTable(columns=Columns, source=source, sizing_mode='stretch_width', max_width=9999)
return data_table
def get_text_input():
"""this can stretch width without issue"""
return TextInput(value='Example', title='Title', sizing_mode="stretch_width", max_width=9999)
def get_gridplot():
"""
this requires columns to be hard-coded
stretch_width is an option, but only distorts the images if enabled
"""
figs = []
for _ in range(30):
fig = figure(x_range=(0,10), y_range=(0,10))
_ = fig.image_rgba(image=[], x=0, y=0)
figs.append(fig)
return gridplot(children=figs, ncols=2)
top_row = row([get_datatable(), get_text_input()], max_width=9999, sizing_mode='stretch_width')
bottom_row = row(get_gridplot())
col = column(top_row, bottom_row, sizing_mode="stretch_width")
output_file("example.html")
save(col)
My end goal is to have the gridplot automatically update the amount of columns based on the width of the browser. Is there a way to do this natively in bokeh? If not, is it possible to do this via a CustomJs javascript callback?
Solution
Consider using sizing_mode=“scale_width” when calling figure.
fig = figure(x_range=(0,10), y_range=(0,10), sizing_mode=“scale_width”)
Note
It may be preferable to use scale_width instead of stretch_width more generally.
Bokeh Doc Example: https://docs.bokeh.org/en/latest/docs/user_guide/layout.html#multiple-objects

Cannot get a Bokeh checkbox group to update graph

I am trying to create a simple interactive graph with a checkbox group. I want the checkboxes to result in showing the appropriate line on the graph when ticked. I am doing this within Jupyter Notebook.
I've managed to get it embedded in Jupyter, and I wrote a callback function that does execute code. I am able to create a new ColumnDataSource from the checkbox selection. However, the graph is just not updating.
I've gone through every post on here I could, and look at every tutorial I could find. Most of them simply have an update callback which creates the new source, then sets the graph's source to the new one, which I believe is supposed to update the graph. I have also seen variations where people assign it as oldsource.data = newsource.data. This doesn't work for me either.
I am wondering whether there is any inherent limitations in embedding to Jupyter Notebook that I need Javascript for, or limitations to how sources can be updated. Or maybe I am just missing something very obvious? Code below:
import os
import pandas as pd
import numpy as np
import bokeh.plotting as bk
import bokeh.layouts as ly
import bokeh.models as md
import bokeh.colors as cl
import bokeh.palettes as plet
from bokeh.io import curdoc
from bokeh.io import show as io_show
from bokeh.models.widgets import CheckboxGroup, Select, Button
from bokeh.plotting import output_file, show, figure, output_notebook, reset_output, curdoc
data_list = ["one", "two", "three", "four", "five", "six"]
data_list2 = ["one", "two"]
data_fac = [1, 2, 3, 4, 5, 6]
data_fac_dict = dict(zip(data_list,data_fac))
data_x = np.linspace(0,100)
df = pd.DataFrame(columns = data_list)
def modify_doc(doc):
def make_data(data_list):
#Make new source with appropriate datasets
df = pd.DataFrame(columns = data_list)
for case in data_list:
df[case] = data_x * data_fac_dict[case]
result = md.ColumnDataSource(df)
return result
#Make colors
list_colors = plet.Dark2[len(data_list)]
dict_colors = dict(zip(data_list,list_colors))
#Default source with one datapoint
src = make_data(["one"])
print(src.data.keys())
#Plot graphs
p = bk.figure()
for case in src.data.keys():
if case != "index":
p.line(source = src, x = 'index', y = case, color = dict_colors[case])
print("plotting loop")
def update(attr,old,new):
#Callback
print("update triggered")
selection = list()
for i in wg_chk.active:
selection.append(data_list[i])
src = make_data(selection)
print(selection)
wg_chk = CheckboxGroup(labels = data_list, active = [0]*len(data_list))
wg_chk.on_change('active', update)
layout = ly.row(wg_chk,p)
doc.add_root(layout)
bk.show(modify_doc, notebook_url='localhost:8888')
UPDATE #1
I changed the code in the callback to make the appropriate dataframe, then create a dict using ColumnDataSource.from_df, then set src.data equal to it as below. Still doesn't seem to work. I used a print to make sure data_new has correct keys.
df_new = make_df(selection)
data_new = md.ColumnDataSource.from_df(df_new)
src.data = data_new
For clarity, I am using the newest version of Bokeh and Python as of today (Bokeh 1.0.2, Python 3.7.1)
UPDATE #2
As per the comments, I pre-generated all the required glyphs ahead of time, so they are, in essence, "slots for data" instead of being generated on demand for any amount of datasets. As they are now persistent, this allows me to toggle them on/off with the .visible property easily. I now have six "slots" for data to be plotted with corresponding glyphs, and I added a function within the callback to update their respective data sources (in this case, changing a linear to a quadratic curve). I also updated Bokeh to the newest version (1.3.4). Note that this is specifically embeddable in a Jupyter Notebook.
Here is the code for reference:
import os
import pandas as pd
import numpy as np
import bokeh.plotting as bk
import bokeh.layouts as ly
import bokeh.models as md
import bokeh.colors as cl
import bokeh.palettes as plet
from bokeh.io import curdoc
from bokeh.io import show as io_show
from bokeh.models.widgets import CheckboxGroup, Select, Button, RadioGroup
from bokeh.plotting import output_file, show, figure, output_notebook, reset_output, curdoc
data_list = ["one", "two", "three", "four", "five", "six"]
data_list2 = ["one", "two"]
data_fac = [1, 2, 3, 4, 5, 6]
data_fac_dict = dict(zip(data_list,data_fac))
data_x = np.linspace(0,100)
df = pd.DataFrame(columns = data_list)
for case in data_list:
df[case] = data_x * data_fac_dict[case] + np.power(data_x, 3) * data_fac_dict[case]
def modify_doc(doc):
#Make colors
list_colors = plet.Dark2[len(data_list)]
dict_colors = dict(zip(data_list,list_colors))
p = bk.figure()
def make_line(case):
line = p.line(x = 'index', y = case, source = src_store[case], color = dict_colors[case])
return line
#Make six sources, make one line per source, and set them to invisible
src_store = dict()
list_lines = dict()
for case in data_list:
src_store[case] = md.ColumnDataSource(df[[case]])
list_lines[case] = make_line(case)
list_lines[case].visible = False
#First checkbox defaults to ticked, so let's show it by default.
list_lines["one"].visible = True
def modify_data(order):
#Modify the data and update the six sources' data with it
df = pd.DataFrame(columns = data_list)
src_store_new = dict()
data_new = dict()
for case in data_list:
df[case] = data_x * data_fac_dict[case] + np.power(data_x,order) * data_fac_dict[case]
data_new[case] = md.ColumnDataSource.from_df(df[[case]])
src_store[case].data = data_new[case]
def update(attr,old,new):
#Callback
print("update triggered")
#Get selection of lines to display
selection = list()
for i in wg_chk.active:
selection.append(data_list[i])
#Set visibility according to selection
for case in data_list:
list_lines[case].visible = case in selection
#Get line multiplier from radio buttons and update sources
order = wg_rad.active + 1
modify_data(order)
print(selection)
wg_rad = RadioGroup(labels=["x*0", "x*1"], active = 0)
wg_chk = CheckboxGroup(labels = data_list, active = [0]*len(data_list))
wg_chk.on_change('active', update)
wg_rad.on_change('active', update)
layout = ly.row(ly.column(wg_chk,wg_rad),p)
doc.add_root(layout)
bk.show(modify_doc, notebook_url='localhost:8888')
When you plot a Bokeh glyph, that glyph object has an associated data source. If you want the glyph to update, you need to update that existing datasource, i.e. modify it by setting it's .data property. The code above does not do that. It creates a new data source, that is not attached to or configured on anything, and then immediate throws it away (it's a local variable in a function, since nothing keeps a reference to it, it disappears when the function finishes).
You need to update whatever existing data source that you used initially:
source.data = new_data # plain python dict
And, at least as of Bokeh 1.3.4 new_data must be a plain Python dictionary. It is not supported to "migrate" a .data value from one CDS to another:
source1.data = source2.data # BAD! WILL NOT WORK
Attempting to do so will probably raise an explicit error in the near future. There is a from_df static method on ColumnDataSource you can use to convert DataFrames to the right kind of dict.

Can't update CDSView in a bokeh plot

I know that many topics on this subject has already been discussed but i can't really see what's wrong in my code
I have a very big amount of data to plot (lines), and i want to highlight some of them (by circle) according to user choice via a select button)
I have tried to make my code as the most simple as possible to reflect my problem.
The line is plotted, the circle based on the "by defaut" choice of the select button are plotted, but nothing is updated when selecting another "stupid_label" in my select widget
import pandas as pd
import numpy as np
from bokeh.io import curdoc
from bokeh.plotting import figure
from bokeh.models import ColumnDataSource, CDSView, GroupFilter
from bokeh.models.widgets import Select
from bokeh.layouts import row, column
def update_plot(attr, old, new):
view.filters[0] = GroupFilter(column_name='stupid_label', group=stupidlabel.value)
def make_plot(fim):
TOOLS = "save,pan,box_zoom,reset,wheel_zoom"
p = figure(title="the plot that makes me mad !",plot_width=800, plot_height=400,tools=TOOLS)
p.line(fim.mydates,fim.myvalues,color='blue')
return p
def main():
f = {'mydates': [19123, 19124, 19125, 19126,19127,19128,19129,19129,19130], 'myvalues': [34, 41, 12, 7, 27, 40, 32, 11, 1], 'stupid_label': ['POUET', 'POUET','BANZAI','BANZAI','BANZAI', 'YOUPI','YOUPI','POUET','POUET']}
fim = pd.DataFrame(data=f)
p = make_plot(fim)
u_stupid_label=np.unique(fim.stupid_label)
stupidlabel = Select(value=u_stupid_label[0],options=list(u_stupid_label))
src= ColumnDataSource(fim)
view = CDSView(source=src,filters=[GroupFilter(column_name='stupid_label', group=stupidlabel.value)])
p.circle('mydates','myvalues',source=src,view=view,color='black')
stupidlabel.on_change('value', update_plot)
layout = row(p, stupidlabel)
curdoc().add_root(layout)
curdoc().title = "please.. works !!!"
main()
All,
I removed my def main(), and main() at the end, and it works.

Setting absolute screen position of Bokeh Charts in Web App

I am trying to set the absolute position of a Bokeh Chart inside a Layout so that one of the plots is shown on top of another plot. Right now when I am plotting something like this:
from bokeh.io import curdoc
from bokeh.plotting import figure
from bokeh.layouts import layout
import numpy as np
x = np.arange(1,10.1,0.1)
y = [i**2 for i in x]
categories = ['A', 'B']
values = [1000, 1500]
fig1 = figure(width=600,plot_height=600, title="First Plot")
fig1.line(x=x, y=y)
fig2 = figure(width=200,plot_height=250,x_range=categories,
title="Second Plot") fig2.vbar(x=categories, top=values, width=0.2)
l = layout([[fig1,fig2]])
curdoc().add_root(l)
The result will be this:
What I am searching for is some way to make it look like that:
How can this result be achieved?
Thank you!
This is what I came up with (works for Bokeh v1.0.4). You need to move your mouse over the plot to get the other one jump inside but you could also copy the JS code from the callback and manually add it to the HTML generated by Bokeh so you achieve the same result.
from bokeh.plotting import figure, show
from bokeh.layouts import Row
from bokeh.models import ColumnDataSource, CDSView, BooleanFilter, CustomJS, BoxSelectTool, HoverTool
import pandas as pd
plot = figure(tools = 'hover', tooltips = [("x", "#x"), ("y", "#y")])
circles = plot.circle('x', 'y', size = 20, source = ColumnDataSource({'x': [1, 2, 3], 'y':[1, 2, 3]}))
inner_plot = figure(name = 'inner_plot', plot_width = 200, plot_height = 200)
lines = inner_plot.line('x', 'y', source = ColumnDataSource({'x': [8, 9, 10], 'y':[8, 6, 8]}))
code = """ div = document.getElementsByClassName('bk-root')[0];
tooltip_plot = div.children[0].children[1]
tooltip_plot.style = "position:absolute; left: 340px; top: 350px;"; """
callback = CustomJS(code = code)
plot.js_on_event('mousemove', callback)
show(Row(plot, inner_plot))
Result:

Add hover to Bokeh without losing rest of tools

So with Bokeh I can do something like this to create a hover option:
From bokeh.models import HoverTool #add hover functionality
Hover = HoverTool(tooltips=[(name1:#column1), (name2:#columns2)])
Plot = figure(tools=[hover])
Plot.circle(x,y,hover_color=’red’)
However, by doing so, I lose the standard tools you get when you call figure() like pan, box_zoom, wheel_zoom, etc. I know I can add them back 1 by 1 inside the figure(tools=[]), but is there a way to only add hover to the rest of the default tools of figure(), after it is defined??
Thanks!
Use the add_tools() method, as outlined in the docs:
https://docs.bokeh.org/en/latest/docs/user_guide/tools.html#specifying-tools
Slightly modified example from the docs:
from bokeh.plotting import figure, output_file, show
from bokeh.models import HoverTool
output_file("toolbar.html")
# create a new plot with the toolbar below
p = figure(plot_width=400, plot_height=400,
title=None, toolbar_location="below")
p.circle([1, 2, 3, 4, 5], [2, 5, 8, 2, 7], size=10)
p.add_tools(HoverTool())
show(p)

Categories

Resources