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"