What's the command to "reset" a bokeh plot? - python

I have a bokeh figure that has a reset button in the toolbar. Basically, I want to "reset" the figure when I update the data that I'm plotting in the figure. How can I do that?

UPDATE: A PR has been submitted for this feature. After Bokeh 0.12.16 is released, the following will work:
from bokeh.io import show
from bokeh.layouts import column
from bokeh.models import Button, CustomJS
from bokeh.plotting import figure
p = figure(tools="reset,pan,wheel_zoom,lasso_select")
p.circle(list(range(10)), list(range(10)))
b = Button()
b.js_on_click(CustomJS(args=dict(p=p), code="""
p.reset.emit()
"""))
show(column(p, b))
As of Bokeh 0.12.1 there is no built in function to do this. It would possible to make a custom extension that does this. However, that would take a little work and experimentation and dialogue. If you'd like to pursue that option, I'd encourage you to come to the public mailing list which is better suited to iterative collaboration and discussion than SO. Alternatively, please feel free to open a feature request on the project issue tracker

Example with a radiogroup callback, that's the best way I found to reset while changing plots, just get the range of the data and set it to the range:
from bokeh.plotting import Figure
from bokeh.models import ColumnDataSource, CustomJS, RadioGroup
from bokeh.layouts import gridplot
from bokeh.resources import CDN
from bokeh.embed import file_html
x0 = range(10)
x1 = range(100)
y0 = [i for i in x0]
y1 = [i*2 for i in x1][::-1]
fig=Figure()
source1=ColumnDataSource(data={"x":[],"y":[]})
source2=ColumnDataSource(data={"x0":x0,"x1":x1,"y0":y0,"y1":y1})
p = fig.line(x='x',y='y',source=source1)
callback=CustomJS(args=dict(s1=source1,s2=source2,px=fig.x_range,py=fig.y_range), code="""
var d1 = s1.get("data");
var d2 = s2.get("data");
var val = cb_obj.active;
d1["y"] = [];
var y = d2["y"+val];
var x = d2["x"+val];
var min = Math.min( ...y );
var max = Math.max( ...y );
py.set("start",min);
py.set("end",max);
var min = Math.min( ...x );
var max = Math.max( ...x );
px.set("start",min);
px.set("end",max);
for(i=0;i<=y.length;i++){
d1["y"].push(d2["y"+val][i]);
d1["x"].push(d2["x"+val][i]);
}
s1.trigger("change");
""")
radiogroup=RadioGroup(labels=['First plot','Second plot'],active=0,callback=callback)
grid = gridplot([[fig,radiogroup]])
outfile=open('TEST.html','w')
outfile.write(file_html(grid,CDN,'Reset'))
outfile.close()
The Bokeh website is seriously lacking in examples for different ways to set callbacks for the different widgets.

I was struggling to make it work with Bokeh 2.2.1, but this JS p.reset.emit() does not seem to work.
What worked for me was to manually set the Figure renderers attribute to an empty list inside a callback function, called via on_click(). This only works with a Bokeh server running, though:
$ bokeh serve --show example.py
example.py:
from bokeh.layouts import column
from bokeh.models import Button
from bokeh.plotting import curdoc, figure
p = figure(tools="reset,pan,wheel_zoom,lasso_select")
p.circle(list(range(10)), list(range(10)))
def clear_plot(attr):
p.renderers = []
b = Button(label="Clear plot")
b.on_click(clear_plot)
curdoc().add_root(column(p, b))

Related

Bokeh Slider Not Updating in JupyterLab

I am running an adapted version of https://github.com/bokeh/bokeh/issues/9431
Bokeh 2.2.3
Python 3.8.6
JupyterLab 2.2.9
In JupyterLab, it displays just fine, but it is not updating the color bar and image when the Range Slider is adjusted. Does anyone know how to fix this?
import numpy as np
from bokeh.plotting import figure, show
from bokeh.models import Row
from bokeh.models import LinearColorMapper, BasicTicker, ColorBar, CustomJS
from bokeh.models.widgets import RangeSlider
import bokeh.io
from bokeh.resources import INLINE
bokeh.io.output_notebook(INLINE)
data = np.random.rand(10,10)
color_mapper = LinearColorMapper(palette="Viridis256", low=0, high=1)
figure = figure(x_range=(0,1), y_range=(0,1))
img = figure.image(image=[data], color_mapper=color_mapper,
dh=[1.0], dw=[1.0], x=[0], y=[0])
color_bar = ColorBar(color_mapper=color_mapper, ticker= BasicTicker(),
location=(0,0))
figure.add_layout(color_bar, 'right')
range_slider = RangeSlider(start=data.min(), end=data.max(), value=(data.min(), data.max()), step=.1, title="Stuff", width=400)
range_slider.js_on_change("value", CustomJS(code="""
range = cb_obj.range
img.glyph.color_mapper.low = cb_obj.value[0];
img.glyph.color_mapper.high = cb_obj.value[1];
"""))
show(Row(figure, range_slider))
If you look in the browser JS console you can see the relevant error messages, e.g. "range is not defined". You need to pass any objects you want to refer to in the JS code in the args paramter of CustomJS. This is described in the docs. Here is a working version (tested w/ Bokeh 2.3, Jupyterlab >=3):
cb = CustomJS(args=dict(slider=range_slider, glyph=img.glyph), code="""
glyph.color_mapper.low = slider.value[0];
glyph.color_mapper.high = slider.value[1];
""")
range_slider.js_on_change("value", cb)
Also, FYI it's generally a bad idea to shadow existing function names:
figure = figure(...) # BAD, now you can never call figure again

Use new values from bokeh widgets for interactive python plotting

I need to use the values we receive from Bokeh widgets to run some code and plot newly generated data. I cannot modify the data inside javascript that goes into CustomJS as there not available the libraries that are available in python. I understand that I can use bokeh server and run python callbacks, but this has to happen in a Databricks notebook. Is this possible?
As I cannot expose the code that I am working on, I prepared a sample code that hopefully conveys what I am trying to do.
import numpy as np
from bokeh.io import curdoc, show
from bokeh.models import ColumnDataSource, Grid, LinearAxis, Patches, Plot, Rect
from bokeh.embed import file_html
from bokeh.plotting import figure, output_file, show
from bokeh.resources import CDN
x = [x*0.005 for x in range(0, 200)]
y = x
source = ColumnDataSource(data=dict(x=x, y=y))
button_cds = ColumnDataSource(data=dict(button_val=[1]))
plot = figure(plot_width=400, plot_height=400)
plot.line('x', 'y', source=source, line_width=3, line_alpha=0.6)
callback = CustomJS(args=dict(button_cds = button_cds), code="""
var data = button_cds.data;
var f = cb_obj.value;
data['button_val'] = f;
button_cds.change.emit();
""")
slider = Slider(start=0.1, end=4, value=1, step=.1, title="power")
slider.js_on_change('value', callback)
for i in range(len(source.data['x'])):
source.data['y'][i] = x[i]**button_cds.data['button_val'][0]
layout = column(slider, plot)
displayHTML(file_html(layout, CDN))
Please note that I can do modifying the source.data in CustomJS, but I have to use python code.

How to retrieve coordinates of PointDrawTool in Bokeh?

I'm trying to get xy coordinates of points drawn by the user. I want to have them as a dictionary, a list or a pandas DataFrame.
I'm using Bokeh 2.0.2 in Jupyter. There'll be a background image (which is not the focus of this post) and on top, the user will create points that I could use further.
Below is where I've managed to get to (with some dummy data). And I've commented some lines which I believe are the direction in which I'd have to go. But I don't seem to get the grasp of it.
from bokeh.plotting import figure, show, Column, output_notebook
from bokeh.models import PointDrawTool, ColumnDataSource, TableColumn, DataTable
output_notebook()
my_tools = ["pan, wheel_zoom, box_zoom, reset"]
#create the figure object
p = figure(title= "my_title", match_aspect=True,
toolbar_location = 'above', tools = my_tools)
seeds = ColumnDataSource({'x': [2,14,8], 'y': [-1,5,7]}) #dummy data
renderer = p.scatter(x='x', y='y', source = seeds, color='red', size=10)
columns = [TableColumn(field="x", title="x"),
TableColumn(field="y", title="y")]
table = DataTable(source=seeds, columns=columns, editable=True, height=100)
#callback = CustomJS(args=dict(source=seeds), code="""
# var data = source.data;
# var x = data['x']
# var y = data['y']
# source.change.emit();
#""")
#
#seeds.x.js_on_change('change:x', callback)
draw_tool = PointDrawTool(renderers=[renderer])
p.add_tools(draw_tool)
p.toolbar.active_tap = draw_tool
show(Column(p, table))
From the documentation at https://docs.bokeh.org/en/latest/docs/user_guide/tools.html#pointdrawtool:
The tool will automatically modify the columns on the data source corresponding to the x and y values of the glyph. Any additional columns in the data source will be padded with the declared empty_value, when adding a new point. Any newly added points will be inserted on the ColumnDataSource of the first supplied renderer.
So, just check the corresponding data source, seeds in your case.
The only issue here is if you want to know exactly what point has been changed or added. In this case, the simplest solution would be to create a custom subclass of PointDrawTool that does just that. Alternatively, you can create an additional "original" data source and compare seeds to it each time it's updated.
The problem is that the execute it in Python. But show create a static version. Here is a simple example that fix it! I removed the table and such to make it a bit cleaner, but it will also work with it:
from bokeh.plotting import figure, show, output_notebook
from bokeh.models import PointDrawTool
output_notebook()
#create the figure object
p = figure(width=400,height=400)
renderer = p.scatter(x=[0,1], y=[1,2],color='red', size=10)
draw_tool = PointDrawTool(renderers=[renderer])
p.add_tools(draw_tool)
p.toolbar.active_tap = draw_tool
# This part is imporant
def app(doc):
global p
doc.add_root(p)
show(app) #<-- show app and not p!

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.

Python / Bokeh - FuncTickFormatter

EDIT: thanks to #tmwilson26 I was able to fix it using javascript code(see comments below). However, I would still be interested to know if there is a solution using from_py_func.
I am using Bokeh and struggling to format my axis using FuncTickFormatter.
Specifically I am using the FuncTickFormatter.from_py_func function.
My below code example doesn't produce any result (but also no error message).
from bokeh.models import ColumnDataSource,Label, FuncTickFormatter,DatetimeTickFormatter,NumeralTickFormatter, Select, FixedTicker, Slider,TableColumn,DatePicker, DataTable, TextInput, HoverTool,Range1d,BoxZoomTool, ResetTool
from bokeh.plotting import figure, output_file, show, curdoc
from bokeh.layouts import row, column, widgetbox, layout
from bokeh.io import output_notebook, push_notebook, show
output_notebook()
x = np.arange(10)
y = [random.uniform(0,5000) for el in x]
xfactors = list("abcdefghi")
yrange = Range1d(0,5000)
p = figure(x_range = xfactors, y_range = yrange,y_minor_ticks = 10)
p.circle(x,y, size = 14, line_color = "grey" , fill_color = "lightblue", fill_alpha = 0.2)
def ticker():
a = '{:0,.0f}'.format(tick).replace(",", "X").replace(".", ",").replace("X", ".")
return a
# If I comment below line out, code is running just fine
p.yaxis.formatter = FuncTickFormatter.from_py_func(ticker)
show(p)
If I comment the FuncTickFormatter line out the code is just running fine. Also the defined function ticker works if I use it outside this code.
Any advice on what I am doing wrong would be very helpful.
Thanks!
If from_py_func is giving you trouble, try using straight Javascript. Here is an example below:
p.yaxis.formatter = FuncTickFormatter(code="""
function(tick){
function markCommas(x) {
return x.toFixed(1).replace(/\B(?=(\d{3})+(?!\d))/g, "X");
}
return markCommas(tick).replace('.',',').replace("X",'.')
}
""")
In some of the documentation, it might not need you to define a function with tick as an input argument, so you may need to remove that outer function, but on my version 0.12.2, this works to produce numbers like you asked for, e.g. 5.000,0
In the newer version, it might look something like this:
p.yaxis.formatter = FuncTickFormatter(code="""
function markCommas(x) {
return x.toFixed(1).replace(/\B(?=(\d{3})+(?!\d))/g, "X");
}
return markCommas(tick).replace('.',',').replace("X",'.')
""")
If the sub-function doesn't work, here is a one-line return statement:
p.yaxis.formatter = FuncTickFormatter(code="""
return tick.toFixed(1).replace(/\B(?=(\d{3})+(?!\d))/g, "X").replace('.',',').replace("X",'.');
""")

Categories

Resources