Enable lines in Bokeh using Multiselect

独自空忆成欢 提交于 2019-12-30 14:45:10

问题


I read out loggers with time and humidity data on a few locations. To explore the data and distribute it I use Python (via jupyter notebook) and Bokeh.
To simplify the data exploration I want to be able to enable and disable the I read out loggers with time and humidity data on a few locations. To explore the data and distribute it I use Python (via jupyter notebook) and Bokeh.
To simplify the data exploration I want to be able to enable and disable the graphs of locations (and in the future also humidity and/or temperature). To do this I want to use multiselect.

I got so far as to select multiple lines, based on this post, but when I try it, it picks the first n locations, not the ones I selected.

Imports

import numpy as np
import itertools
from collections import OrderedDict

from bokeh.io import push_notebook, show, output_notebook, output_file
from bokeh.layouts import row
from bokeh.palettes import Set1_6
from bokeh.plotting import figure as bf
from bokeh.models import MultiSelect, CustomJS, Range1d, LinearAxis, ColumnDataSource
from bokeh.resources import CDN
output_notebook()

Helper functions

One function generates example data

def generate_example_data(x, param=1):   
    t = 20 + param + np.sin(x * (1 + param))
    rh = 50 + param + 10 * np.tan(x * (1 + param))
    return {"x": x.copy(), "t": t, "rh": rh}

Another function generates the javascript code to make a line visible or invisible. I tried a few ways, but none give correct results. I also added logging to be able to see what happens and which if-else clauses get triggered.

def generate_selector_code(locations):
    for index, location in enumerate(locations):
        res_str = """    if (%(index)i in multiselect.attributes.value) {
        %(loc)s_t.visible = true;
        %(loc)s_rh.visible = true;
        console.log('enabling0 %(loc)s' );
    } else {
        %(loc)s_t.visible = false;
        %(loc)s_rh.visible = false;
        console.log('disabling0 %(loc)s' );
    }
    if ('%(index)i' in multiselect.attributes.value) {
        %(loc)s_t.visible = true;
        %(loc)s_rh.visible = true;
        console.log('enabling1 %(loc)s' );
    } else {
        %(loc)s_t.visible = false;
        %(loc)s_rh.visible = false;
        console.log('disabling1 %(loc)s' );
    }
    if ('%(loc)s' in multiselect.attributes.value) {
        %(loc)s_t.visible = true;
        %(loc)s_rh.visible = true;
        console.log('enabling2 %(loc)s' );
    } else {
        %(loc)s_t.visible = false;
        %(loc)s_rh.visible = false;
        console.log('disabling2 %(loc)s' );
    }
    """%({"index": index, "loc": location})
# other method's I've tested but which result into an error which states that Object does not have an attribute includes
#     if (multiselect.attributes.value.includes('%(index)i')) {
#         %(loc)s_t.visible = true;
#         %(loc)s_rh.visible = true;
#         console.log('enabling3 %(loc)s' );
#     } else {
#         %(loc)s_t.visible = false;
#         %(loc)s_rh.visible = false;
#         console.log('disabling3 %(loc)s' );
#     }
#     if (multiselect.attributes.value.includes('%(loc)s')) {
#         %(loc)s_t.visible = true;
#         %(loc)s_rh.visible = true;
#         console.log('enabling4 %(loc)s' );
#     } else {
#         %(loc)s_t.visible = false;
#         %(loc)s_rh.visible = false;
#         console.log('disabling4 %(loc)s' );
#     }

        yield res_str

Setting up the plot

Generates the example data and selects which tools to use

locations = ["loc_one", "loc_two", "loc_three"]
x = np.linspace(0, 4 * np.pi, 20)
data_per_loc = OrderedDict()
for i, loc in enumerate(locations):
    data_per_loc[loc] = generate_example_data(x, i)

tools="pan,box_zoom,reset,resize,save,crosshair,hover,xbox_zoom, wheel_zoom"

Generating the plot

The function which does the actual plot generation. It creates the actual Bokeh figure and concatenates the javascript code to enable or disable the different lines

def generate_plot(data_per_loc):
    palet = itertools.cycle(Set1_6)
    p = bf(title="test", plot_height=500, plot_width=1000, tools=tools, y_range=(17, 27), 
       toolbar_location="above")
    p.xaxis.axis_label = "x"

    p.yaxis.axis_label = "Temperature [°C]"
    p.extra_y_ranges = {"humidity": Range1d(start=30, end=80)}
    p.add_layout(LinearAxis(y_range_name="humidity", axis_label="Relative Humidity [%Rh]"), 'right')

    plot_locations = OrderedDict()
    for location, datadict in data_per_loc.items():
        colour = next(palet)
        source = ColumnDataSource(datadict)
        t = p.line(x='x', y='t', color=colour, source=source, legend=location)
        rh = p.line(x='x', y='rh', source=source, color=colour,
               legend=location, y_range_name='humidity',
               line_dash="dashed", )
        plot_locations.update({location+"_t": t, location+"_rh": rh})

    code = "console.log('value: ' + multiselect.attributes.value);\n " + "console.log('value_type: ' + Object.prototype.toString.call(multiselect.attributes.value).slice(8, -1));\n " +             "console.log('options: ' + multiselect.attributes.options);\n " + "".join(generate_selector_code(data_per_loc.keys()))
    return p, code, plot_locations

The resulting code looks like this:

"console.log('value: ' + multiselect.attributes.value);
 console.log('value_type: ' + Object.prototype.toString.call(multiselect.attributes.value).slice(8, -1));
 console.log('options: ' + multiselect.attributes.options);
 if (0 in multiselect.attributes.value) {
    loc_one_t.visible = true;
    loc_one_rh.visible = true;
    console.log('enabling0 loc_one' );
} else {
    loc_one_t.visible = false;
    loc_one_rh.visible = false;
    console.log('disabling0 loc_one' );
}
if ('0' in multiselect.attributes.value) {
    loc_one_t.visible = true;
    loc_one_rh.visible = true;
    console.log('enabling1 loc_one' );
} else {
    loc_one_t.visible = false;
    loc_one_rh.visible = false;
    console.log('disabling1 loc_one' );
}
if ('loc_one' in multiselect.attributes.value) {
    loc_one_t.visible = true;
    loc_one_rh.visible = true;
    console.log('enabling2 loc_one' );
} else {
    loc_one_t.visible = false;
    loc_one_rh.visible = false;
    console.log('disabling2 loc_one' );
}
    if (1 in multiselect.attributes.value) {
    loc_two_t.visible = true;
    loc_two_rh.visible = true;
    console.log('enabling0 loc_two' );
} else {
    loc_two_t.visible = false;
    loc_two_rh.visible = false;
    console.log('disabling0 loc_two' );
}
if ('1' in multiselect.attributes.value) {
    loc_two_t.visible = true;
    loc_two_rh.visible = true;
    console.log('enabling1 loc_two' );
} else {
    loc_two_t.visible = false;
    loc_two_rh.visible = false;
    console.log('disabling1 loc_two' );
}
if ('loc_two' in multiselect.attributes.value) {
    loc_two_t.visible = true;
    loc_two_rh.visible = true;
    console.log('enabling2 loc_two' );
} else {
    loc_two_t.visible = false;
    loc_two_rh.visible = false;
    console.log('disabling2 loc_two' );
}
    if (2 in multiselect.attributes.value) {
    loc_three_t.visible = true;
    loc_three_rh.visible = true;
    console.log('enabling0 loc_three' );
} else {
    loc_three_t.visible = false;
    loc_three_rh.visible = false;
    console.log('disabling0 loc_three' );
}
if ('2' in multiselect.attributes.value) {
    loc_three_t.visible = true;
    loc_three_rh.visible = true;
    console.log('enabling1 loc_three' );
} else {
    loc_three_t.visible = false;
    loc_three_rh.visible = false;
    console.log('disabling1 loc_three' );
}
if ('loc_three' in multiselect.attributes.value) {
    loc_three_t.visible = true;
    loc_three_rh.visible = true;
    console.log('enabling2 loc_three' );
} else {
    loc_three_t.visible = false;
    loc_three_rh.visible = false;
    console.log('disabling2 loc_three' );
}
"

First attempt at bringing it together

output_file("c:\html\multiselect_loc.html")
p, code, plot_locations = generate_plot(data_per_loc)

ms_options = locations
ms_value = locations

callback = CustomJS(code=code, args={})
multiselect = MultiSelect(title="Location:", options=ms_options, value=ms_value, callback=callback)
callback.args = dict(**plot_locations, multiselect=multiselect)


layout = row(p, multiselect)
show(layout)

Second attempt at bringing it together

I thought perhaps the javascript had problems with the strings as values, so I tried using ints as values for the multiselect

output_file("c:\html\multiselect_val.html")
p, code, plot_locations = generate_plot(data_per_loc) 

ms_options = [(str(i), v) for i , v in enumerate(locations)]
ms_value = [str(i) for i in range(len(locations))]

callback = CustomJS(code=code, args={})
multiselect = MultiSelect(title="Location:", options=ms_options value=ms_value, callback=callback)
callback.args = dict(**plot_locations, multiselect=multiselect)


layout = row(p, multiselect)
show(layout)

The results

Bokeh plotted the lines without problems, but the selection acted weird. Instead of showing the locations I wanted, it gave the first n lines with first 2 selection methods, and nothing with the 3rd.

The javascript console returns something like:

value: loc_two
value_type: Array
options: loc_one,loc_two,loc_three
enabling0 loc_one
enabling1 loc_one
disabling2 loc_one
disabling0 loc_two
disabling1 loc_two
disabling2 loc_two
disabling0 loc_three
disabling1 loc_three
disabling2 loc_three
value: loc_two,loc_three
value_type: Array
options: loc_one,loc_two,loc_three
enabling0 loc_one
enabling1 loc_one
disabling2 loc_one
enabling0 loc_two
enabling1 loc_two
disabling2 loc_two
disabling0 loc_three
disabling1 loc_three
disabling2 loc_three

I've added all example code on my github repo my github repo, as well as the javascript console outputs. All this was tested with IE11 (company restrictions).


回答1:


I've found the culprit, and it is indeed the fact in checks in the keys of the array (0 to len-1) instead of the values

    res_str = """\
if (multiselect.attributes.value.indexOf('%(loc)s')>-1) {
    %(loc)s_t.visible = true;
    %(loc)s_rh.visible = true;
    console.log('enabling5 %(loc)s' );
} else {
    %(loc)s_t.visible = false;
    %(loc)s_rh.visible = false;
    console.log('disabling5 %(loc)s' );
}
"""%({"index": index, "loc": location})

is the correct test in def generate_selector_code(locations):

I noticed this when reviewing the code in the bokeh example which uses CustomJS.from_coffeescript() and browsing throught the CoffeeScript documentation

of  => in 
in  => no JS equivalent 



回答2:


another method would be to use CustomJS.from_coffeescript() and change the code from JS to CoffeeScript

res_str = """\
if '%(loc)s' in multiselect.attributes.value
    %(loc)s_t.visible = true
    %(loc)s_rh.visible = true
    console.log 'enabling %(loc)s'
} else {
    %(loc)s_t.visible = false
    %(loc)s_rh.visible = false
    console.log 'disabling %(loc)s'
}
"""%({"index": index, "loc": location})


来源:https://stackoverflow.com/questions/40592165/enable-lines-in-bokeh-using-multiselect

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!