I am using a radio button with Bokeh. I want to be able to show a loading indicator when a radio button is clicked and then show that it has finished after the python callback has completed. How do you do this with Bokeh?
I've tried using combinations of js_onchange with onchange for the radio buttons. I just can't come up with a way to notify the JS side of things when the Python callback is completed.
callback = CustomJS(args={}, code="""
document.getElementById('message_display').innerHTML = 'loading...';
""")
radio_button.js_on_change('active', callback)
radio_button.on_change('active', some_other_callback)
When I run it the innerHTML gets set to loading and the on_change Python callback runs and the graph updates but I have no way to trigger a change on the JS side of things change the innerHTML to say done.
Assuming the user already have a plot in view one option would be to set a callback on the start attribute of the plot's range so it will be triggered when the plot gets updated.
from bokeh.models import CustomJS
p = figure()
def python_callback()
p.y_range = Range1d(None, None)
# get your data here and update the plot
code = "document.getElementById('message_display').innerHTML = 'loading finished';"
callback = CustomJS(args = dict(), code = code)
p.y_range.js_on_change('start', callback)
See working example below:
import numpy as np
from bokeh.plotting import figure, show
from bokeh.models import CustomJS, ColumnDataSource
points = np.random.rand(50, 2)
cds = ColumnDataSource(data = dict(x = points[:, 0], y = points[:, 1]))
p = figure(x_range = (0, 1), y_range = (0, 1))
p.scatter(x = 'x', y = 'y', source = cds)
cb_to_make_selection = CustomJS(args = {'cds': cds}, code = """
function getRandomInt(max){return Math.floor(Math.random() * Math.floor(max));}
cds.selected.indices = [getRandomInt(cds.get_length()-1)]
""")
p.x_range.js_on_change('start', cb_to_make_selection)
show(p)
Related
I want to change the name of a variable as I will use it later to specify exactly what I need to plot. However, Javascript is giving me more trouble than I thought. I am using a dropdown menu and as I select a value , the variable should also change, but that is not happening. Any suggestions? I am still very new to Javascript, so any advice would be appreciated
column="justastring"
selecthandler = CustomJS(args=dict(column=column), code="""
var col=column.value;
if (cb_obj.value=="Ozone"){
col='OZONE';
}
if (cb_obj.value=="O2"){
col='O_2';
}
if(cb_obj.value=="DO2"){
col='DO2';
}
column.change.emit();
""")
select.js_on_change('value',selecthandler)
You didn't provide a runnable code so I've created a minimal example from scratch:
from bokeh.layouts import column
from bokeh.models import ColumnDataSource, Select, CustomJS
from bokeh.plotting import figure, show
from bokeh.transform import linear_cmap
from bokeh.palettes import Viridis3
ds = ColumnDataSource(dict(x=[0, 1, 2],
a=[0, 0, 1],
b=[3, 2, 1]))
p = figure()
renderer = p.rect(x='x', y=0, width=1, height=1,
fill_color=linear_cmap('a', Viridis3, 0, 1), source=ds)
s = Select(value='a', options=['a', 'b'])
s.js_on_change('value',
CustomJS(args=dict(r=renderer, ds=ds),
code="""
const c = cb_obj.value;
const fc = r.glyph.fill_color;
fc.field = c;
fc.transform.low = Math.min(...ds.data[c]);
fc.transform.high = Math.max(...ds.data[c]);
// If you don't change `low` and `high` fields above, you will
// have to uncomment this line to trigger value change signals
// so that the glyphs are re-rendered.
//r.glyph.fill_color = fc;
"""))
show(column(s, p))
Is there a way in Bokeh to trigger a JavaScript callback when a ColumnDataSource is updated with new data?
I'm looking for something similar to this...
cds1 = ColumnDataSource(some_pandas_dataframe)
cds1.js_onchange('', callback) # attach some kind of callback here to do something when cds1.data changes.
cds2 = ColumnDataSource(some_pandas_dataframe)
cds1.data.update(cds2.data)
One way to do it in Bokeh v1.1.0 is to attach a callback to the data attribute of ColumnDataSource:
import numpy as np
from bokeh.plotting import Figure, show, curdoc
from bokeh.models import CustomJS, ColumnDataSource, Button, Column
from bokeh.events import Tap
cds = ColumnDataSource(data = dict(x = [0.1, 0.9], y = [0.1, 0.9]))
p = Figure(x_range = (0, 1), y_range = (0, 1), tools = "")
p.scatter(x = 'x', y = 'y', source = cds)
data = dict(x = [0.5], y = [0.5])
callback = CustomJS(args = {'cds': cds, 'data': data}, code = "cds.data = data")
button = Button(label = 'click me')
button.js_on_event('button_click', callback)
cds.js_on_change('data', CustomJS(code = "alert('data_change_detected');"))
show(Column(p, button))
I'm having trouble with a CustomJS callback on the TapTool. I would like to force the selection of the 50 points after the one clicked. Therefore I have made a javascript callback that modify the list of indices selected in the datasource and should update the plot. I can see, with the console, that the datasource is updated but the plot is not.
I have made a test version from the documentation example
https://docs.bokeh.org/en/latest/docs/user_guide/interaction/callbacks.html
but it doesn't work neither. Is it because there is a different way to update the plot when the selection is changed ?
Here is the test version I have made :
from bokeh.layouts import column
from bokeh.models import CustomJS, ColumnDataSource, Slider
from bokeh.plotting import Figure, output_file, show
output_notebook()
x = [x*0.005 for x in range(0, 200)]
y = x
source = ColumnDataSource(data=dict(x=x, y=y))
plot = Figure(plot_width=400, plot_height=400)
plot.circle('x', 'y', source=source, line_width=3, line_alpha=0.6)
callback = CustomJS(args=dict(source=source), code="""
var l_selected=source.selected;
var indices = l_selected['1d'].indices;
if(indices.length <= 1) {
var new_indices = Array.from(new Array(50), (x,i) => i + indices[0]);
l_selected['1d'].indices=new_indices;
}
source.selected=l_selected;
console.log(source.selected)
source.change.emit();
""")
slider = Slider(start=0.1, end=4, value=1, step=.1, title="power")
slider.js_on_change('value', callback)
plot.add_tools(TapTool(callback=callback))
layout = column(slider, plot)
show(layout)
I do not know if it can be useful, but I am using the 0.12.16 version of Bokeh and I am trying to make it work in a Jupyter notebook
Seb gave the answer in comments. Since bokeh 0.12.15 source.selected['1d'] became source.selected.indices
I've created a visual graph using Bokeh that shows a network I created using Networkx. I now want to use TapTool to show information pertinent to any node on the graph that I click on. The graph is just nodes and edges. I know I should be able to use var inds = cb_obj.selected['1d'].indices; in the JavaScript callback function to get the indices of the nodes (glyphs) that were clicked on, but that's not working somehow and I get the error message, Uncaught TypeError: Cannot read property '1d' of undefined. A nudge in the right direction would be greatly appreciated.
Below is my code. Please note that I've defined my plot as a Plot() and not as a figure(). I don't think that's the reason for the issue, but just wanted to mention it. Also, I'm using window.alert(inds); just to see what values I get. That's not my ultimate purpose, but I expect that bit to work anyway.
def draw_graph_____(self, my_network):
self.graph_height, self.graph_width, self.graph_nodes, self.graph_edges, self.node_coords, self.node_levels = self.compute_graph_layout(my_network)
graph = nx.DiGraph()
graph.add_nodes_from(self.graph_nodes)
graph.add_edges_from(self.graph_edges)
plot = Plot(plot_width = self.graph_width, plot_height = self.graph_height, x_range = Range1d(0.0, 1.0), y_range = Range1d(0.0, 1.0))
plot.title.text = "Graph Demonstration"
graph_renderer = from_networkx(graph, self.graph_layout, scale = 1, center = (-100, 100))
graph_renderer.node_renderer.data_source.data["node_names"] = self.graph_nodes
graph_renderer.node_renderer.data_source.data["index"] = self.graph_nodes
graph_renderer.node_renderer.glyph = Circle(size = 40, fill_color = Spectral4[0])
graph_renderer.node_renderer.selection_glyph = Circle(size = 40, fill_color = Spectral4[2])
graph_renderer.node_renderer.hover_glyph = Circle(size = 40, fill_color = Spectral4[1])
graph_renderer.edge_renderer.glyph = MultiLine(line_color = "#CCCCCC", line_alpha = 0.8, line_width = 5)
graph_renderer.edge_renderer.selection_glyph = MultiLine(line_color = Spectral4[2], line_width = 5)
graph_renderer.edge_renderer.hover_glyph = MultiLine(line_color = Spectral4[1], line_width = 5)
graph_renderer.selection_policy = NodesAndLinkedEdges()
graph_renderer.inspection_policy = NodesAndLinkedEdges()
x_coord = [coord[0] for coord in self.node_coords]
y_coord = [coord[1] for coord in self.node_coords]
y_offset = []
for level in self.node_levels:
for item in self.node_levels[level]:
if self.node_levels[level].index(item) % 2 == 0:
y_offset.append(20)
else:
y_offset.append(-40)
graph_renderer.node_renderer.data_source.data["x_coord"] = x_coord
graph_renderer.node_renderer.data_source.data["y_coord"] = y_coord
graph_renderer.node_renderer.data_source.data["y_offset"] = y_offset
labels_source = graph_renderer.node_renderer.data_source
labels = LabelSet(x = "x_coord", y = "y_coord", text = 'node_names', text_font_size = "12pt", level = 'glyph',
x_offset = -50, y_offset = "y_offset", source = labels_source, render_mode = 'canvas')
plot.add_layout(labels)
callback = CustomJS(args = dict(source = graph_renderer.node_renderer.data_source), code =
"""
console.log(cb_obj)
var inds = cb_obj.selected['1d'].indices;
window.alert(inds);
""")
plot.add_tools(HoverTool(tooltips = [("Node", "#node_names"), ("Recomm", "Will put a sample recommendation message here later")]))
plot.add_tools(TapTool(callback = callback))
plot.renderers.append(graph_renderer)
output_file("interactive_graphs.html")
show(plot)
My imports are as follows, by the way:
import collections
import networkx as nx
import numpy as np
from bokeh.io import output_file, show
from bokeh.models import Circle, ColumnDataSource, CustomJS, Div, HoverTool, LabelSet, MultiLine, OpenURL, Plot, Range1d, TapTool
from bokeh.models.graphs import from_networkx, NodesAndLinkedEdges
from bokeh.palettes import Spectral4
I'm sorry for not posting entire code, but that would require quite a few changes to make dummy data and show other files and functions (which I should have), but I thought just this one function may suffice for the identification of the issue. If not, I'm happy to share more code. Thanks!
The problem is that the callback is not attached to a data source. The value of cb_obj is whatever object triggers the callback. But only ColumnDataSource objects have a selected property, so only callbacks on data sources will have cb_obj.selected. If you are wanting to have a callback fire whenever a selection changes, i.e. whenever a node is clicked on, then you'd want to have the callback on the data source. [1]
However, if you want to have a callback when a node is merely hovered over (but not clicked on) that is an inspection, not a selection. You will want to follow this example:
https://docs.bokeh.org/en/latest/docs/user_guide/interaction/callbacks.html#customjs-for-hover
Although it is not often used (and thus not documented terribly well) the callback for hover tools gets passed additional information in a cb_data parameter. This cb_data parameter is used as a catch-all mechanism for tools to be able to pass extra things, specific to the tool, on to the callback. In the case of hover tools, cb_data is an object that has .index and .geometry attributes. So cb_data.index['1d'].indices has the indices of the points that are currently hovered over. The .geometry attribute as information about the kind of hit test that was performed (i.e. was a single point? or a vertical or horizontal span? And what was the location of the point or span?)
[1] Alternatively, tap tools also pass a specialized cb_data as described above. It is an object with a .source property that the the data source that made a selection. So cb_data.source.selected should work. In practice I never use this though, since a callback on the data source works equally well.
I'm working on a dashboard where the user clicks on one of many dots on a regular scatter plot to get more information about that dot. Each dot represents a group of data, and when clicked on, the user should be able to see a table in which the related group of data is listed.
The table will be listed right next to the plot, and the rows will change every time a new dot (or multiple dots) is selected.
I'll then need to add filters to this table, so it needs to be interactive too. The plot does not change during filtering, only the related data in the table will.
I've seen the following example, which achieves the exact opposite that I want to achieve:
from bokeh.plotting import Figure, output_file, show
from bokeh.models import CustomJS
from bokeh.models.sources import ColumnDataSource
from bokeh.layouts import column, row
from bokeh.models.widgets import DataTable, TableColumn, Toggle
from random import randint
import pandas as pd
output_file("data_table_subset_example.html")
data = dict(
x=[randint(0, 100) for i in range(10)],
y=[randint(0, 100) for i in range(10)],
z=['some other data'] * 10
)
df = pd.DataFrame(data)
#filtering dataframes with pandas keeps the index numbers consistent
filtered_df = df[df.x < 80]
#Creating CDSs from these dataframes gives you a column with indexes
source1 = ColumnDataSource(df) # FIGURE
source2 = ColumnDataSource(filtered_df) # TABLE - FILTERED
fig1 = Figure(plot_width=200, plot_height=200)
fig1.circle(x='x', y='y', source=source1)
columns = [
TableColumn(field="x", title="X"),
TableColumn(field="z", title="Text"),
]
data_table = DataTable(source=source2, columns=columns, width=400, height=280)
button = Toggle(label="Select")
button.callback = CustomJS(args=dict(source1=source1, source2=source2), code="""
var inds_in_source2 = source2.get('selected')['1d'].indices;
var d = source2.get('data');
var inds = []
if (inds_in_source2.length == 0) { return; }
for (i = 0; i < inds_in_source2.length; i++) {
inds.push(d['index'][i])
}
source1.get('selected')['1d'].indices = inds
source1.trigger('change');
""")
show(column(fig1, data_table, button))
I tried replacing source1 and source2 inside the button callback in an attempt to reverse the filtering (i.e. choose a point on the figure and filter the data table). But the data table is not filtered at all, instead the row that corresponds to the data point is simply selected. Any idea how to filter out the rest of the rows that are not selected on the plot?
I found the answer in another question: Bokeh DataTable won't update after trigger('change') without clicking on header
Apparently the data table change needs to be triggered too.