Hide plot in bokeh on user input - custom JS? - javascript

I have a python/bokeh app in which I am displaying time series data for a number of individuals, separate plot for each individual. The plots are laid out in a column.
Let us say that I have for every individual, information about their body temperature at the resolution of every hour for 1 year (just a fictitious example). I plot the temperature on y-axis for each of them vs time on x-axis. I store all these plots in a dictionary. So I have a dictionary ind such that the keys in ind are individual names and the corresponding values are bokeh.plotting figure objects. Then, I use column() to render figures.
I have a number of individuals that I am looking at, say 26. So when I launch the app, I have a column of 26 plots which is a lot to compare at once. (Please understand that this is a fictitious example just for communication. My project requires that I don't overlay data for all individuals in the same figure. I know bokeh allows us to 'mute' and 'hide' layers in a plot by legend if I were to plot them in same figure but I can't do that.)
Hence, I provide a CheckboxGroup with all individual names such that the user can select which individuals they want to see right now. My users may want to examine any number of individuals at the same time. It could be 3 or 4 or 10. So, I can't fix the number of plots that I would arrange in a column.
On user's selection in the CheckboxGroup widget, I need to interactively hide or show plots. I think there should be some CustomJS way of doing it but I can't figure out how. I know we can change visibility of objects using Javascript as described here: Show/hide 'div' using JavaScript
Any help will be appreciated. Additionally, if you can show how to reorder the plots based on user input (which could be as a TextInput as in example below) that will be very helpful.
Here is a minimum working example:
import string
import random
from bokeh.plotting import figure
from bokeh.layouts import layout, widgetbox, column
from bokeh.models.widgets import CheckboxGroup, TextInput
from bokeh.models import Range1d
from bokeh.io import curdoc, show
data= dict(zip(list(string.ascii_lowercase), [{"x": [i for i in range(8760)], "y": [random.uniform(35, 40) for x in range(8760)]} for i in range(26)]))
checkbox = CheckboxGroup(labels=list(string.ascii_lowercase), active=[0, 1, 4, 7, 10])
order = TextInput(title="Arrange plots in order as in this string")
ind = {}
for f in list(string.ascii_lowercase):
ind[f] = figure(plot_width= 800, plot_height= 100, tools ='save, reset, resize')
ind[f].vbar(x= "x", source= data[f], width= 0.5, bottom= 0, top= "y")
ind[f].y_range= Range1d(start= 32, end= 43)
ind[f].title.text = f
p = column(*ind.values())
inputs = widgetbox(*[order, checkbox], sizing_mode='fixed')
l = layout([
[inputs, p],
], sizing_mode='fixed')
show(p)
curdoc().add_root(l)
curdoc().title = "test"
NB: Python 3 and latest version of bokeh.
UPDATE: Okonomiyaki's answer below does the job as described above but in a slightly more complicated situation, it is inadequate. I think some addition to Okonomiyaki's answer will do it. Basically, I have for each individual, two different observations, another one being, let us say, weight. The users can select the observation they want to study from a drop down menu. The plotted data is reactively bound to a Select widget. On playing with the checkboxes for a while after default load, if I change the selection to weight, the x-axis for some of the individuals does not update. Here is an updated minimum working example (including Okonomiyaki's answer):
import string
import random
from bokeh.plotting import figure
from bokeh.layouts import layout, widgetbox, column
from bokeh.models.widgets import CheckboxGroup, TextInput, Select
from bokeh.models import Range1d, ColumnDataSource
from bokeh.io import curdoc, show
data1= dict(zip(list(string.ascii_lowercase), [{"x": [i for i in range(8760)], "y": [random.uniform(35, 40) for x in range(8760)]} for i in range(26)]))
data2= dict(zip(list(string.ascii_lowercase), [{"x": [i for i in range(870)], "y": [random.uniform(140, 200) for x in range(870)]} for i in range(26)]))
select_data = Select(title= "Select dataset", value= "data1", options= ["data1", "data2"])
def data_update(attr, new, old):
for f in list(string.ascii_lowercase):
print(select_data.value + '\n\n')
data[f].data= dict(x= globals()[select_data.value][f]["x"], y= globals()[select_data.value][f]["y"])
data= {f: ColumnDataSource(data= dict(x= data1[f]["x"], y= data1[f]["y"])) for f in list(string.ascii_lowercase)}
checkbox = CheckboxGroup(labels=list(string.ascii_lowercase), active=[0, 1, 4, 7, 10])
order = TextInput(title="Arrange plots in order as in this string")
ind = {}
for f in list(string.ascii_lowercase):
ind[f] = figure(plot_width= 800, plot_height= 100, tools ='save, reset, resize')
ind[f].vbar(x= "x", source= data[f], width= 0.5, bottom= 0, top= "y")
ind[f].y_range= Range1d(start= 32, end= 43)
ind[f].title.text = f
p = column(*ind.values())
def checkboxchange(attr,new,old):
plots = []
for aind in checkbox.active:
plots.append(ind[checkbox.labels[aind]])
l.children[0].children[1].children = plots
def orderchange(attr,new,old):
# check the checkbox
chval = []
for aind in checkbox.active:
chval.append(checkbox.labels[aind])
# change the order if all the values in the string are also plotted currently
plots=[]
orderlist = [order.value[i] for i in range(len(order.value))]
if(len(set(orderlist+chval)) == len(chval)):
for aind in orderlist:
plots.append(ind[aind])
l.children[0].children[1].children = plots
order.on_change('value', orderchange)
checkbox.on_change('active', checkboxchange)
select_data.on_change('value', data_update)
inputs = widgetbox(*[select_data, order, checkbox], sizing_mode='fixed')
l = layout([
[inputs, p],
], sizing_mode='fixed')
#show(p)
plots = []
for aind in checkbox.active:
plots.append(ind[checkbox.labels[aind]])
l.children[0].children[1].children = plots
curdoc().add_root(l)
curdoc().title = "test"

Implemented an example for both the string ordering (only if the user inputs a string with values currently checked)
import string
import random
from bokeh.plotting import figure
from bokeh.layouts import layout, widgetbox, column
from bokeh.models.widgets import CheckboxGroup, TextInput
from bokeh.models import Range1d
from bokeh.io import curdoc, show
data= dict(zip(list(string.ascii_lowercase), [{"x": [i for i in range(8760)], "y": [random.uniform(35, 40) for x in range(8760)]} for i in range(26)]))
checkbox = CheckboxGroup(labels=list(string.ascii_lowercase), active=[0, 1, 4, 7, 10])
order = TextInput(title="Arrange plots in order as in this string")
ind = {}
for f in list(string.ascii_lowercase):
ind[f] = figure(plot_width= 800, plot_height= 100, tools ='save, reset, resize')
ind[f].vbar(x= "x", source= data[f], width= 0.5, bottom= 0, top= "y")
ind[f].y_range= Range1d(start= 32, end= 43)
ind[f].title.text = f
p = column(*ind.values())
def checkboxchange(attr,new,old):
plots = []
for aind in checkbox.active:
plots.append(ind[checkbox.labels[aind]])
l.children[0].children[1].children = plots
def orderchange(attr,new,old):
# check the checkbox
chval = []
for aind in checkbox.active:
chval.append(checkbox.labels[aind])
# change the order if all the values in the string are also plotted currently
plots=[]
orderlist = [order.value[i] for i in range(len(order.value))]
if(len(set(orderlist+chval)) == len(chval)):
for aind in orderlist:
plots.append(ind[aind])
l.children[0].children[1].children = plots
order.on_change('value', orderchange)
checkbox.on_change('active', checkboxchange)
inputs = widgetbox(*[order, checkbox], sizing_mode='fixed')
l = layout([
[inputs, p],
], sizing_mode='fixed')
#show(p)
plots = []
for aind in checkbox.active:
plots.append(ind[checkbox.labels[aind]])
l.children[0].children[1].children = plots
curdoc().add_root(l)
curdoc().title = "test"

Related

js_on_change not working for network graph on Bokeh

Following this example, I am trying to build a network graph using Bokeh where I can use a select widget to filter my data. My data will look something like this:
source target var1 var2
a c 1.0 0.0
b g 2.0 0.0
c e 3.0 0.0
e a 0.0 1.0
f h 0.0 2.0
And can be recreated using this code:
d = {'weight': [1, 2,3,1,2], 'var': ["var1","var1","var1","var2", "var2"], 'source': ["a", "b","c","e","f"], 'target': ["c","g","e","a","h"]}
df1 = pd.DataFrame(data=d)
df2 = df1.pivot( index= ["source","target"], values = "weight", columns = "var").reset_index()
df2 = df2.fillna(0)
Basically, I want to create the network graph where I can filter the columns (var1, var2) and they will become the weight attribute in my graph. (Filtering for when this weight value is greater than 0.)
To accomplish this I tried the following. But even though the graph renders, when I change the selected value nothing happens. I don't see any errors in the console either. I am not sure what I am doing wrong, probably something in the JS call because I am new to this, but I am trying to reproduce the example as closely as possible and still not sure where im going wrong. Please help!
from bokeh.plotting import from_networkx
from bokeh.plotting import figure, output_file, show
from bokeh.models import Plot, Range1d, MultiLine, Circle, HoverTool,NodesAndLinkedEdges,EdgesAndLinkedNodes, TapTool, BoxSelectTool,ColumnDataSource
from bokeh.models import CustomJS, ColumnDataSource, Select, Column
HOVER_TOOLTIPS = [("Search Term", "#index")]
title = "my title"
plot = figure(tooltips = HOVER_TOOLTIPS,
tools="pan,wheel_zoom",
active_scroll='wheel_zoom',
x_range=Range1d(-10.1, 10.1),
y_range=Range1d(-10.1, 10.1),
title=title, plot_width=1000
)
category_default = "var1"
unique_categories = ["var1","var2"]
subset = df2
subset_data = subset[["var1","var2"]].to_dict("list")
source = ColumnDataSource({
"weight": subset[category_default],
"source": subset.source,
"target": subset.target
})
a = pd.DataFrame(source.data)
G = nx.from_pandas_edgelist(a[a["weight"] >0], edge_attr = "weight")
network_graph = from_networkx(G, networkx.spring_layout, scale=10, center=(0, 0), )
network_graph.edge_renderer.glyph = MultiLine(line_alpha=0.5, line_width="weight" )
select = Select(title='Category Selection', value=category_default, options=unique_categories)
callback = CustomJS(
args={"subset_data": subset_data, "source": source},
code="""
source.data['weight'] = subset_data[cb_obj.value];
source.change.emit();
""")
plot.renderers.append(network_graph)
select.js_on_change("value", callback)
show(Column(plot, select))

How to use Javascript in Bokeh map?

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))

Update selection indices of bokeh circle with TapTool

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

JavaScript callback to get selected glyph index in Bokeh

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.

Clickable dots on Bokeh Plots

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.

Categories