I am trying to plot several scatter glyphs to one figure, using a loop. The goal is to use an own glyph for each brand and to update the values if another value is chosen by the Select widgets.
But the figure shows an empty graph. I assume that the problem is the ColumnDataSource and the update in the function "update()".
This is an executable example. Do you have any idea?
from bokeh.layouts import column, row
from bokeh.models import ColumnDataSource, Select, HoverTool
from bokeh.plotting import figure, show
import pandas as pd
brands = ['a', 'b', 'c', 'a', 'b', 'b', 'c']
product = ['v1', 'v2', 'v3', 'v4', 'v5', 'v6', 'v7']
price = [2, 3, 54, 48, 9, 2, 4]
size = [10, 11, 12, 13, 14, 15, 16]
value = [5, 4, 3, 8, 1, 0, 1]
id = [1, 2, 3, 4, 5, 6, 7]
col = ['ID', 'brand', 'product', 'price', 'size', 'value']
label = ['price', 'size', 'value']
df = pd.DataFrame(zip(brands, product, price, size, value, id), columns=col)
# Widgets:
select_x_axis = Select(title="x-Axis:", value=label[0], options=label)
select_y_axis = Select(title="y-Axis:", value=label[1], options=label)
# Set up figure
hover = HoverTool(tooltips=[
("index", "#id"),
('Brand', '#brand'),
('Product', '#product'),
(select_x_axis.value, '#x'),
(select_y_axis.value, '#y')
])
# Set up plots:
fig = figure(plot_height=400, plot_width=800, title="xyz",
# tooltips=TOOLTIPS,
tools=[hover, 'reset'],
x_axis_label=select_x_axis.value,
y_axis_label=select_y_axis.value)
source = {}
plots = {}
for brand in brands:
# Create Column Data Source that will be used by the plot
source[brand] = ColumnDataSource(data=dict(x=[], y=[], id=[], product=[], brand=[]))
plots[brand] = fig.scatter(x='x', y='y', size=5, source=source[brand])
def update():
x_name = select_x_axis.value
y_name = select_y_axis.value
fig.xaxis.axis_label = x_name
fig.yaxis.axis_label = y_name
for brand in brands:
df1 = df.loc[df['brand'] == brand]
source[brand].data = dict(
x=df1[x_name],
y=df1[y_name],
id=df1['ID'],
product=df1['product'],
brand=df1['brand']
)
# Set up layouts and add to document
controls = [select_x_axis, select_y_axis]
for control in controls:
control.on_change('value', lambda attr, old, new: update())
inputs = column(select_x_axis, select_y_axis)
update() # initial load of the data
show(row(inputs, fig, width=1000))
#curdoc().add_root(row(inputs, fig, width=1000))
#curdoc().title = "xyz"
resultig plot
Something like that ?
from bokeh.layouts import column, row
from bokeh.models import ColumnDataSource, Select, HoverTool, CustomJS
from bokeh.plotting import figure, show
import pandas as pd
def make_data() :
brands = ['a', 'b', 'c', 'a', 'b', 'b', 'c']
product = ['v1', 'v2', 'v3', 'v4', 'v5', 'v6', 'v7']
price = [2, 3, 54, 48, 9, 2, 4]
size = [10, 11, 12, 13, 14, 15, 16]
value = [5, 4, 3, 8, 1, 0, 1]
ident = [1, 2, 3, 4, 5, 6, 7]
col = ['ID', 'brand', 'product', 'price', 'size', 'value']
label = ['price', 'size', 'value']
return pd.DataFrame(zip(brands, product, price, size, value, ident), columns=col)
if __name__ == "__main__" :
df = make_data()
default_xcol = "price"
default_ycol = "size"
df["xvalues"] = df[default_xcol]
df["yvalues"] = df[default_ycol]
# Scatter plot
hover = HoverTool(tooltips=[(name, "#" + name) for name in df.columns])
figure = figure(tools=[hover, 'reset'])
# Source
source = ColumnDataSource(df)
figure.scatter("xvalues", "yvalues", source=source)
# Selects
options = ["product", "price", "size", "value"]
select_x_axis = Select(title="x-Axis:", value=default_xcol, options=options)
select_y_axis = Select(title="y-Axis:", value=default_ycol, options=options)
# callback
callback = CustomJS(args={"source":source, "axis":figure.xaxis[0]}, code="""
source.data['xvalues'] = source.data[cb_obj.value];
source.change.emit();
axis.axis_label = cb_obj.value;
""")
select_x_axis.js_on_change("value", callback)
callback = CustomJS(args={"source":source, "axis":figure.yaxis[0]}, code="""
source.data['yvalues'] = source.data[cb_obj.value];
source.change.emit();
axis.axis_label = cb_obj.value;
""")
select_y_axis.js_on_change("value", callback)
show(row(
column(select_x_axis, select_y_axis),
figure, width=1000))
#jsgounot Thanks for your help. I figured something out that works quiet well at the moment:
from bokeh.layouts import column, row
from bokeh.models import ColumnDataSource, Select, HoverTool, CustomJS
from bokeh.plotting import figure, show
from bokeh.palettes import d3
import pandas as pd
brands = ['a', 'b', 'c', 'a', 'b', 'b', 'c']
product = ['v1', 'v2', 'v3', 'v4', 'v5', 'v6', 'v7']
price = [2, 3, 54, 48, 9, 2, 4]
size = [10, 11, 12, 13, 14, 15, 16]
value = [5, 4, 3, 8, 1, 0, 1]
id = [1, 2, 3, 4, 5, 6, 7]
col = ['ID', 'brand', 'product', 'price', 'size', 'value']
label = ['price', 'size', 'value']
colors = d3["Category20c"][len(brands)]
markers = ['circle', 'square', 'triangle', 'asterisk', 'circle_x', 'square_x', 'inverted_triangle', 'x', 'circle_cross', 'square_cross', 'diamond', 'cross']
df = pd.DataFrame(zip(id, brands, product, price, size, value), columns=col)
default_xcol = "price"
default_ycol = "size"
df["xvalues"] = df[default_xcol]
df["yvalues"] = df[default_ycol]
# Widgets:
select_x_axis = Select(title="x-Axis:", value=label[0], options=label)
select_y_axis = Select(title="y-Axis:", value=label[1], options=label)
# Set up figure
hover = HoverTool(tooltips=[
("index", "#ID"),
('Brand', '#brand'),
('Product', '#product'),
(select_x_axis.value, '#xvalues'),
(select_y_axis.value, '#yvalues')
])
# Set up plots:
fig = figure(plot_height=400, plot_width=800, title="xyz",
# tooltips=TOOLTIPS,
tools=[hover, 'reset'],
x_axis_label=select_x_axis.value,
y_axis_label=select_y_axis.value)
df_brand = []
sources = []
plots = {}
for index, brand in enumerate(df.brand.unique()):
df_brand.append(df.loc[df['brand'] == brand])
sources.append(ColumnDataSource(df_brand[index]))
plots[brand] = fig.scatter(x='xvalues', y='yvalues', source=sources[index],
legend_label=brand,
marker=markers[index],
color=colors[index])
fig.legend.click_policy="hide"
callback_x = CustomJS(args={'sources': sources, 'axis': fig.xaxis[0], 'brands': df.brand.unique()}, code="""
for (var i = 0; i <= 2; i++){
var source = sources[i]
source.data['xvalues'] = source.data[cb_obj.value];
source.change.emit();
}
axis.axis_label = cb_obj.value;
""")
callback_y = CustomJS(args={'sources': sources, 'axis': fig.yaxis[0], 'hov': fig.hover, 'brands': df.brand.unique()}, code="""
for (var i = 0; i <= 2; i++){
var source = sources[i]
source.data['yvalues'] = source.data[cb_obj.value];
source.change.emit();
}
axis.axis_label = cb_obj.value;
""")
select_x_axis.js_on_change('value', callback_x)
select_y_axis.js_on_change('value', callback_y)
show(row(
column(select_x_axis, select_y_axis),
fig, width=1000))
Related
import hvplot.pandas
from bokeh.sampledata.autompg import autompg_clean
autompg_clean['origin']=autompg_clean.origin.map({'North America': 'North America '*5,
'Asia': 'Asia '*5,
'Europe': 'Europe '*5,
})
Here is the corresponding annotated output. I have tried using p=hv.render() to get the Bokeh figure object back, but doing something like p.yaxis.major_label_text_align = 'left' does not seem to do anything even if I inject newline \n characters into the long string label.
Multiline labels are available with the newline charactert \n for categorical factors.
I was not able to reproduce your example, but I think the solution is to set you y-axis to a FactorRange and set the factors with a list of your wanted strings, which can include \n.
See the example below, which is adapted from here.
from bokeh.io import output_file, show
from bokeh.models import ColumnDataSource
from bokeh.palettes import GnBu3, OrRd3
from bokeh.plotting import figure
output_file("stacked_split.html")
fruits = [f'{item}\n{item}' for item in ['Apples', 'Pears', 'Nectarines', 'Plums', 'Grapes', 'Strawberries']]
years = ["2015", "2016", "2017"]
exports = {'fruits' : fruits,
'2015' : [2, 1, 4, 3, 2, 4],
'2016' : [5, 3, 4, 2, 4, 6],
'2017' : [3, 2, 4, 4, 5, 3]}
imports = {'fruits' : fruits,
'2015' : [-1, 0, -1, -3, -2, -1],
'2016' : [-2, -1, -3, -1, -2, -2],
'2017' : [-1, -2, -1, 0, -2, -2]}
p = figure(y_range=fruits, height=250, x_range=(-16, 16), title="Fruit import/export, by year",
toolbar_location=None)
p.hbar_stack(years, y='fruits', height=0.9, color=GnBu3, source=ColumnDataSource(exports),
legend_label=["%s exports" % x for x in years])
p.hbar_stack(years, y='fruits', height=0.9, color=OrRd3, source=ColumnDataSource(imports),
legend_label=["%s imports" % x for x in years])
p.y_range.range_padding = 0.1
p.ygrid.grid_line_color = None
p.legend.location = "top_left"
p.axis.minor_tick_line_color = None
p.outline_line_color = None
show(p)
Output
I'm trying to write a code which adds hovertool performing customer js-code over figure.line object. In order to do it I used userguide code as example (https://docs.bokeh.org/en/latest/docs/user_guide/interaction/callbacks.html#customjs-for-hover) but simplified and modified (see below). I've found that it works over figure.segment however doesn't work over figure.line.
Here is complete example:
from bokeh.models import CustomJS, HoverTool
from bokeh.plotting import figure, output_file, show
output_file("hover_callback.html")
# define some points and a little graph between them
x = [2, 3, 5, 6, 8, 7]
y = [6, 4, 3, 8, 7, 5]
p = figure(plot_width=400, plot_height=400, tools="", toolbar_location=None)
lines = p.line(x, y, line_color='blue', line_width=5)
seg = p.segment(x[0:3], y[0:3], x1=x[3:], y1=y[3:], color='olive', alpha=0.6, line_width=3)
code = """
const indices = cb_data.index.indices
if (indices.length > 0) {
console.log('Hello!')
}
"""
callback = CustomJS(code=code)
p.add_tools(HoverTool(tooltips=None, callback=callback, renderers=[lines, seg]))
show(p)
cb_data.index.indices is not empty when I put pointer over segment (olive-color) but not over line (blue).
Is it sort of expected behaviour? If so I would appreciate reference to some doc where it's explained. THanks!
You can activate the HoverTool for lines, too. But you have to adapt your JavaScript quite a bit. Lines return the information of the index a bit different, you have to ask for const line_indices = cb_data.index.line_indices.
Here is your complete example:
from bokeh.models import CustomJS, HoverTool
from bokeh.plotting import figure, output_file, show
output_file("hover_callback.html")
# define some points and a little graph between them
x = [2, 3, 5, 6, 8, 7]
y = [6, 4, 3, 8, 7, 5]
p = figure(plot_width=400, plot_height=400, tools="", toolbar_location=None)
lines = p.line(x, y, line_color='blue', line_width=5)
seg = p.segment(x[0:3], y[0:3], x1=x[3:], y1=y[3:], color='olive', alpha=0.6, line_width=3)
code = """
const indices = cb_data.index.indices
const line_indices = cb_data.index.line_indices
if (indices.length + line_indices.length > 0) {
console.log('Hello!')
}
"""
callback = CustomJS(code=code)
p.add_tools(HoverTool(tooltips=None, callback=callback, renderers=[lines, seg]))
show(p)
Any ideas what's supposed to go where the triple '?'s are?
import pandas as pd
from bokeh.layouts import column
from bokeh.models import CustomJS, ColumnDataSource, Slider, Select
import bokeh.plotting as bp
from bokeh.plotting import Figure, output_file, show
from bokeh.models import HoverTool, DatetimeTickFormatter
# Create an output file
bp.output_file('columnDataSource.html')
# Create your plot as a bokeh.figure object
myPlot = bp.figure(height = 600,
width = 800,
y_range=(0,3))
x_values = [1, 2, 3, 4, 5]
y_values = [1, 2, 3, 4, 5]
myPlot.line(x = x_values, y= y_values, line_width=2)
callback = CustomJS(args={
'source1': {'x': [1,2,3,4], 'y':[1,1,1,1]},
'source2': {'x': [0,0,0,0], 'y':[2,2,2,2]},
'source3': {'x': [1,2,3,4], 'y':[1,1,1,1]}},
code="""
var data1 = source1;
var data2 = source2;
var data3 = source3;
var f = cb_obj.value;
if(f == 'A'){
console.log("A selected from dropdown.");
data1.x = data1.x;
data1.y = data1.y;
}
else if(f == 'B'){
// Substitute all old data1 values in with data2 values
console.log("B selected from dropdown.");
data1.x = data2.x;
data1.y = data2.y;
}
else{
console.log("C selected.");
// Substitute all old data1 values in with data3 values
data1.x = data3.x;
data1.y = data3.y;
}
// Problematic line!
???.change.emit();
""")
select = Select(title='Choose', value='A', options=['A','B','C'])
select.js_on_change('value', callback)
layout = column(select, myPlot)
show(layout) # et voilĂ .
I expect my x and y values to change and plot accordingly to my Bokeh graph.
Nothing is changing at the moment as I don't know what object's "trigger" function I'm supposed to be calling. Please help, I'm new to Bokeh.
You do ColumnDataSource.change.emit() if you updated the data source fields by reference e.g. when you update only x or only y:
ColumnDataSource.data['x'] = [4, 3, 2, 1]
ColumnDataSource.change.emit()
When you update them both you do:
ColumnDataSource.data = new_data
Where new_data is a new json object like {'x': [1], 'y':[2]}.
The reason for this is that JS can automatically detect a change when existing object is replaced with a new one but it cannot detect changes by reference so in those cases you need explicitly to call: ColumnDataSource.change.emit() to update the BokehJS model.
Here is your modified code:
from bokeh.models import CustomJS, ColumnDataSource, Select, Column
from bokeh.plotting import figure, show
myPlot = figure(y_range = (0, 4))
data = {'A': {'x': [1, 2, 3, 4], 'y':[1, 1, 1, 1]},
'B': {'x': [1, 2, 3, 4], 'y':[2, 2, 2, 2]},
'C': {'x': [1, 2, 3, 4], 'y':[3, 3, 3, 3]} }
source = ColumnDataSource(data['A'])
myPlot.line('x', 'y', line_width = 2, source = source)
callback = CustomJS(args = {'source': source, 'data': data},
code = """source.data = data[cb_obj.value]; """)
select = Select(title = 'Choose', value = 'A', options = ['A', 'B', 'C'])
select.js_on_change('value', callback)
layout = Column(select, myPlot)
show(layout)
I plot data in a bar chart. The data is grouped in two levels where one level is year. I add a range slider in order to alter the x_range of the plot with respect to which years to show. This I have tried to implement through a CustomJS callback (first time I try CustomJS).
Using the slider the factors on the x-axis gets updated as expected. However if I then use the zoom tool and afterwards use the reset tool I get an error message in the web console:
Error: duplicate factor or subfactor: 2016
Not sure what I'm doing wrong with the setup of the data. Is the update of the factor range wrong?
I'm using version 1.1.0 of Bokeh on MacOS. Same error observed in Safari and Firefox.
The code below will reproduce the error.
from bokeh.models import ColumnDataSource, FactorRange, RangeSlider, CustomJS
from bokeh.plotting import figure
from bokeh.layouts import column
import pandas as pd
output_file("grouped_customJS.html")
fruits = ['Apples', 'Pears', 'Nectarines', 'Plums', 'Grapes', 'Strawberries']
data = {'fruits' : fruits,
'2015' : [2, 1, 4, 3, 2, 4],
'2016' : [5, 3, 3, 2, 4, 6],
'2017' : [3, 2, 4, 4, 5, 3]}
df = pd.DataFrame.from_dict(data)
df=df.set_index('fruits').stack().reset_index()
df=df.rename(columns={'level_1':'year', 0:'value'})
# add year as int column for slider
df['year_int'] = df['year'].astype(int)
df=df.set_index(['fruits','year'])
cats = df.index.values
source = ColumnDataSource(
data = {
'categories': cats,
'values': df['value'],
'year': df['year_int']
}
)
p = figure(
x_range=FactorRange(*cats),
plot_height=250,
title="Fruit Counts by Year",
)
p.vbar(x='categories', top='values', width=0.9, source=source)
p.y_range.start = 0
p.x_range.range_padding = 0.1
p.xaxis.major_label_orientation = 1
p.xgrid.grid_line_color = None
slider = RangeSlider(
start=df['year_int'].min(),
end=df['year_int'].max(),
step = 1,
value = (df['year_int'].min(), df['year_int'].max()),
)
callback = CustomJS(args=dict(slider=slider, source=source, plt = p), code="""
plt.x_range.factors = [];
for (var i = 0; i < source.get_length(); i++){
if (source.data['year'][i] >= slider.value[0] && source.data['year'][i] <= slider.value[1]){
plt.x_range.factors.push(source.data['categories'][i]);
}
}
""")
slider.js_on_change('value', callback)
p.x_range.js_on_change('factors', callback)
show(column(p, slider))
Try this (works fine with Bokeh v1.1.0):
callback = CustomJS(args = dict(slider = slider, source = source, plt = p), code = """
var factors = []
for (var i = 0; i < source.get_length(); i++){
if (source.data['year'][i] >= slider.value[0] && source.data['year'][i] <= slider.value[1]){
factors.push(source.data['categories'][i]);
}
}
plt.x_range.factors = factors; """)
I would like to create a standalone document, similar to the server app example 'Crossfilter': to select different columns for coloring the circles and to update the colorbar as well.
I define a CustomJS with the code below, where I create a new LinearColorMapper with the calculated low and high values. (For debugging purposes, I intentionally add different palette than the one set via the Python code).
var low = Math.min.apply(Math,source.data[cb_obj.value]);
var high = Math.max.apply(Math,source.data[cb_obj.value]);
var color_mapper = new Bokeh.LinearColorMapper({palette:'Viridis5', low:low, high:high});
cir.glyph.fill_color = {field: cb_obj.value, transform: color_mapper};
cir.glyph.line_color = {field: cb_obj.value, transform: color_mapper};
color_bar.color_mapper = color_mapper;
source.change.emit();
As a result, when selecting the column, the circles become white, the line black, the ticks of the color bar change correctly, but the palette does not change.
Could you help me setting the proper attributes in the callback? Thank you in advance.
Original state, color mapper set from Python code
After selecting the column 'd'
I created a "minimal working example" to show how far I got. The full project with the template it can be found here: https://github.com/pintergreg/bokehjscolormapperexample
It looks like you cannot reference a colour pallet like this in BokehJS. Just pass Viridis5 variable to JS callback and it works (tested on Bokeh v1.0.4):
import pandas as pd
from bokeh.models import ColumnDataSource, ColorBar, Select, CustomJS
from bokeh.plotting import figure, show
from bokeh.layouts import gridplot
from bokeh.palettes import Spectral5, Viridis5
from bokeh.transform import linear_cmap
from bokeh.embed import components
from jinja2 import Environment, FileSystemLoader
df = pd.DataFrame({"a": [2, 6, 5, 3, 7, 8, 1, 9, 2, 4],
"b": [3, 5, 7, 1, 0, 6, 5, 4, 2, 9],
"c": [11, 12, 13, 14, 11, 13, 15, 14, 15, 12],
"d": [21, 23, 24, 25, 21, 22, 23, 24, 25, 22]})
source = ColumnDataSource(df)
mapper = linear_cmap(field_name = "c", palette = Spectral5,
low = min(df["c"]), high = max(df["c"]))
fig = figure(plot_width = 400, plot_height = 400)
cir = fig.circle(x = "a", y = "b", size = 12,
source = source, line_color = mapper, color = mapper)
color_bar = ColorBar(color_mapper = mapper["transform"], width = 8,
location = (0, 0))
fig.add_layout(color_bar, "right")
codec = """
var low = Math.min.apply(Math,source.data[cb_obj.value]);
var high = Math.max.apply(Math,source.data[cb_obj.value]);
var color_mapper = new Bokeh.LinearColorMapper({palette:viridis5, low:low, high:high});
cir.glyph.fill_color = {field: cb_obj.value, transform: color_mapper};
cir.glyph.line_color = {field: cb_obj.value, transform: color_mapper};
color_bar.color_mapper.low = low;
color_bar.color_mapper.high = high;
color_bar.color_mapper.palette = viridis5;
source.change.emit();
"""
cb_cselect_c = CustomJS(args = dict(cir = cir, source = source, color_bar = color_bar, viridis5 = Viridis5),
code = codec)
c_select = Select(title = "Select variable for color: ", value = "None",
options = ["c", "d"], callback = cb_cselect_c)
layout = gridplot([[fig], [c_select]])
show(layout)
# env = Environment(loader=FileSystemLoader("."))
# template = env.get_template("template.html")
#
# script, div = components(layout)
#
# with open("output.html", "w") as f:
# print(template.render(script=script, div=div), file=f)
You also need to manually add this line to header section of the generated HTML file:
<script type="text/javascript" src="http://cdn.bokeh.org/bokeh/release/bokeh-api-1.0.4.min.js"></script>