问题
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