2

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"
Community
  • 1
  • 1
Mr K
  • 446
  • 1
  • 4
  • 16

1 Answers1

2

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"
Anthonydouc
  • 3,334
  • 1
  • 16
  • 29
  • Thanks! Your answer works but my problem is a little more complex. Can you please take a look at the update in my question? – Mr K Apr 11 '17 at 14:38
  • Well hold on that did answer your answer your question, so you should accept it. Anyways i think your change still works, it just gets quite laggy due to the size of the data and number of plots. Perhaps there is a better solution to all of this, but it may get more complex. When the plots didnt load, did you wait long enough or did they just freeze – Anthonydouc Apr 11 '17 at 20:56
  • Sorry I forgot to accept the answer. I'll try waiting. – Mr K Apr 11 '17 at 21:06
  • The plots are not updating on waiting. I don't know why. – Mr K Apr 12 '17 at 00:54
  • Ok well, there are two parts to the problem I can see. The method I have used does refresh the entire layout which adds some lag, the second is refreshing all of the plots individually changing which data is displayed. There may be a few other options to streamline it guess its a bit of experimentation. – Anthonydouc Apr 12 '17 at 03:02
  • I am at a loss. If you could update your answer that would be much appreciated. I think accessing the document element corresponding to the plots by id in a customJS and then setting the visibility of children of the plots in a column div would solve the problem better. But I can't get the customJS to work. – Mr K Apr 12 '17 at 03:31
  • Yea I will try a JS solution over the next few days, may take a while to get back to you though. – Anthonydouc Apr 12 '17 at 04:48
  • Thanks a ton! I'll be waiting. – Mr K Apr 12 '17 at 06:16