Related
I am trying to show two plots vertically. I get an error message at show(column(west_fig, east_fig))
Could you please help me understand the error message?
Version: BokehJS 1.4.0 successfully loaded.
Note: I am running this code in Jupyter notebook
## Detailed error message:
--------------------------------------------------------------------------- RuntimeError Traceback (most recent call
last) in
8
9 # Plot the two visualizations in a vertical configuration
---> 10 show(column(west_fig, east_fig))
RuntimeError: Models must be owned by only a single document,
DaysTicker(id='2330', ...) is already in a doc
# Bokeh libraries
from bokeh.io import output_file, output_notebook
from bokeh.plotting import figure, show
from bokeh.layouts import column
# output to notebook
output_notebook()
# Plot the two visualizations in a vertical configuration
show(column(west_fig, east_fig))
####### west_fig #######
# Bokeh libraries
from bokeh.plotting import figure, show
from bokeh.io import output_file, output_notebook
from bokeh.models import ColumnDataSource, CDSView, GroupFilter
# Output to notebook
output_notebook()
# Convert `stDate` column to datetime column
west_top_2['stDate'] = pd.to_datetime(west_top_2['stDate'], format = '%Y-%m-%d')
# Create a ColumnDataSource
west_cds = ColumnDataSource(west_top_2)
# Create views for each team
rockets_view = CDSView(source = west_cds,
filters = [GroupFilter(column_name = 'teamAbbr', group = 'HOU')]
)
warriors_view = CDSView(source = west_cds,
filters = [GroupFilter(column_name = 'teamAbbr', group = 'GS')]
)
# Create and configure the figure
west_fig = figure(x_axis_type = 'datetime',
plot_height = 500,
plot_width = 600,
title = 'Western Conference Top 2 Teams Wins Race, 2017-18',
x_axis_label = 'Date',
y_axis_label = 'Wins',
toolbar_location = None
)
# Render the race as step lines
west_fig.step('stDate', 'gameWon', source = west_cds, view = rockets_view, color = '#CE1141', legend = 'Rockets')
west_fig.step('stDate', 'gameWon', source = west_cds, view = warriors_view, color = '#006BB6', legend = 'Warriors')
# Move the legend to the upper top-left corner
west_fig.legend.location = "top_left"
# Show the plot
show(west_fig)
####### east_fig #########
import pandas as pd
# Bokeh libraries
from bokeh.plotting import figure, show
from bokeh.io import output_file, output_notebook
from bokeh.models import ColumnDataSource, CDSView, GroupFilter
# Output to notebook
output_notebook()
# Convert stDate to datetime column
standings['stDate'] = pd.to_datetime(standings['stDate'], format = '%Y-%m-%d')
# Create a ColumnDataSource
standings_cds = ColumnDataSource(standings)
# Create views for each team
celtics_view = CDSView(source=standings_cds,
filters = [GroupFilter(column_name = 'teamAbbr', group = 'BOS')]
)
raptors_view = CDSView(source=standings_cds,
filters = [GroupFilter(column_name = 'teamAbbr', group = 'TOR')]
)
# Configure Figure object
east_fig = figure(x_axis_type = 'datetime',
plot_height = 500,
plot_width = 600,
title = 'Eastern Conference Top 2 Teams Wins Race, 2017-18',
x_axis_label = 'Date',
y_axis_label = 'Wins',
toolbar_location = None
)
# Render the race as step lines
east_fig.step('stDate', 'gameWon', color = '#007A33', legend = 'Celtics',
source = standings_cds, view = celtics_view)
east_fig.step('stDate', 'gameWon', color = '#CE1141', legend = 'Raptors',
source = standings_cds, view = raptors_view)
# Move the legend to the upper left hand corner
east_fig.legend.location = "top_left"
# Show the plot
show(east_fig)
Make only one show statement at the end of your code. This implies deleting show(column(west_fig, east_fig)), show(west_fig), show(east_fig) lines and adding at the end of the code show(column(west_fig, east_fig))
I am plotting a complex interactive donut chart in bokeh. The code below is a simplification of a component of this chart.
I have a function which compiles a dataframe of data for the donut, and then converts it to a CDS. This data is then plotted as annular wedges.
A radiobutton group should trigger a switch to a different dataframe (as CDS), and replot the annular wedge glyphs.
The example provide (for Jupyter Lab) works in one direction. When initially plotted the button.active == 0 (outer_1). When outer_2 is selected, the chart correctly changes to plot the second dataframe (cds).
But when the outer_1 button is pressed, the glyphs do not change back. The callback is triggered - as the title changes. But the glyphs do not change.
Why are the glyphs not changing in subsequent button presses / callbacks?
I've read a few similar SO posts, and also reviewed a number of bokeh examples (the weather example here is similar)
import pandas as pd
from math import pi
from bokeh.plotting import figure
from bokeh.layouts import column, row
from bokeh.models.widgets import RadioButtonGroup
from bokeh.io import curdoc
from bokeh.models.sources import ColumnDataSource
from bokeh.layouts import column, row
from bokeh.plotting import show, output_notebook
output_notebook()
df = pd.DataFrame({'start':[pi/2, pi, 3*pi/2],
'end' :[pi/2+1.5, pi+1.5, (3*pi/2)+1.5],
'inner': [100,100,100],
'outer': [200,200,200],
'color':['red','green','blue']})
df_2 = pd.DataFrame({'start':[pi/2, pi, 3*pi/2],
'end' :[pi/2+1, pi+1, (3*pi/2)+1],
'inner': [100,100,100],
'outer': [250,300,350],
'color':['orange','gray','purple']})
data_1 = ColumnDataSource(data=df)
data_2 = ColumnDataSource(data=df_2)
def create_doc(doc):
button = RadioButtonGroup(labels=["outer_1", "outer_2"], active=0)
inputs = column(button)
p = figure(plot_width=600, plot_height=600, title="data_1",
x_axis_type=None, y_axis_type=None,
x_range=(-300, 300), y_range=(-300, 300),
min_border=0, outline_line_color=None,
background_fill_color='white', toolbar_location="above")
circle = p.circle(0,0, radius=100, fill_alpha=0, line_color='grey', line_alpha=0.4)
source = [data_1, data_2][button.active]
segments = p.annular_wedge(0,0,'inner', 'outer', 'start', 'end', color='color', alpha=0.6, source=source, name='segments')
r = row (inputs,p)
def callback(attr, old, new):
if button.active == 1:
p.title.text = 'data_2 {}'.format(button.active)
source.data.update(data_2.data)
elif button.active == 0:
p.title.text = 'data_1 {}'.format(button.active)
source.data.update(data_1.data)
button.on_change('active', callback)
doc.add_root(r)
show(create_doc)
The glpyhs change successfully once, but not again, although the changing title text (on button presses) indicate that the buttons & callback continue to work partially.
Why converting to DataFrame knowing that internally the data of ColumnDataSource is a dictionary? The following code works fine for Bokeh v1.1.0
from math import pi
from bokeh.plotting import figure, show, curdoc, Row, Column, output_notebook
from bokeh.models import RadioButtonGroup
output_notebook()
data1 = {'start':[pi/2, pi, 3*pi/2],
'end' :[pi/2+1.5, pi+1.5, (3*pi/2)+1.5],
'inner': [100,100,100],
'outer': [200,200,200],
'color':['red','green','blue']}
data2 = {'start':[pi/2, pi, 3*pi/2],
'end' :[pi/2+1, pi+1, (3*pi/2)+1],
'inner': [100,100,100],
'outer': [250,300,350],
'color':['orange','gray','purple']}
def create_doc(doc):
button = RadioButtonGroup(labels=["outer_1", "outer_2"], active=0)
p = figure(plot_width=600, plot_height=600, title="data_1",
x_axis_type=None, y_axis_type=None,
x_range=(-300, 300), y_range=(-300, 300),
min_border=0, outline_line_color=None,
background_fill_color='white', toolbar_location="above")
circle = p.circle(0,0, radius=100, fill_alpha=0, line_color='grey', line_alpha=0.4)
segments = p.annular_wedge(0,0,'inner', 'outer', 'start', 'end', color='color', alpha=0.6, source = data1, name='segments')
def callback(attr, old, new):
if button.active == 0:
print '000'
p.title.text = 'data_1 {}'.format(button.active)
segments.data_source.data = data1
elif button.active == 1:
print '111'
p.title.text = 'data_2 {}'.format(button.active)
segments.data_source.data = data2
button.on_change('active', callback)
inputs = Column(button)
r = Row(inputs,p)
doc.add_root(r)
show(create_doc)
I would love some help, I'm going in circles here. I know I'm doing something stupid but my plot isn't updating. I can't debug to see if my filter function isn't working or there's a problem that my inputs for the plot aren't linked the dynamic source input. Since even the starting plot doesn't take the initialized parameters I think it's something there. PS- any advice on having a select all, including all in the categorical choices for the select boxes would be amazing too.
Cheers,
Tom
import pandas as pd
import numpy as np
from bokeh.io import show, output_notebook, push_notebook, curdoc
from bokeh.plotting import figure
from bokeh.models import CategoricalColorMapper, HoverTool, ColumnDataSource, Panel, Div
from bokeh.models.widgets import (CheckboxGroup, Slider, Select, TextInput, RangeSlider, Tabs, CheckboxButtonGroup, TableColumn, DataTable, Select)
from bokeh.layouts import layout, column, row, Widgetbox
from bokeh.layouts import widgetbox, row, column
from bokeh.palettes import Category20_16
from bokeh.application.handlers import FunctionHandler
from bokeh.application import Application
weather = pd.read_csv('YYYYYY.csv', dayfirst=True, parse_dates=True, index_col=[1], encoding = "ISO-8859-1")
def style(p):
# Title
p.title.align = 'center'
p.title.text_font_size = '20pt'
p.title.text_font = 'serif'
# Axis titles
p.xaxis.axis_label_text_font_size = '14pt'
p.xaxis.axis_label_text_font_style = 'bold'
p.yaxis.axis_label_text_font_size = '14pt'
p.yaxis.axis_label_text_font_style = 'bold'
# Tick labels
p.xaxis.major_label_text_font_size = '12pt'
p.yaxis.major_label_text_font_size = '12pt'
return p
def make_plot(src):
p = figure(plot_height=600, plot_width=700, title="'2018'", toolbar_location="below", tooltips=TOOLTIPS)
p.circle(x="Deal_Number", y="USD_Base", source=src, size=7, line_color=None)
p = style(p)
return p
TOOLTIPS=[
("Name", "#Target"),
("$", "#Round"),
("Country", "#CC")
]
def get_dataset(deal_num, ccstring, descstring, vertstring):
df_filter = weather[weather['USD_Base'] >=(deal_num) & weather['CC'].str.contains(ccstring) & weather['Description'].str.contains(descstring) & weather['Vertical Market'].str.contains(vertstring)]
return ColumnDataSource(df_filter)
def update_plot(attr, old, new):
deal_num = int(deal_select.value)
ccstring = str(cc_select.value)
descstring = str(description_select.value)
vertstring = str(vert_select.value)
new_src = get_dataset(deal_num, ccstring, descstring, vertstring)
src.data.update(new_src.data)
# Create Input controls
deal_select = Slider(title="$ Invested", value=0, start=0, end=200, step=2)
cclist = weather["CC"].unique().tolist()
cc_select = Select(title="Country Name:", options= cclist, value='GB')
description_select = TextInput(title="Company description contains")
vertlist = weather["Vertical Market"].unique().tolist()
vert_select = Select(title="Vertical:", options= ['All'] + vertlist, value='None')
controls = widgetbox(deal_select, cc_select, description_select, vert_select)
deal_select.on_change('value', update_plot)
cc_select.on_change('value',update_plot)
description_select.on_change('value',update_plot)
vert_select.on_change('value',update_plot)
# Make the deal data source
src = get_dataset(deal_num = deal_select.value,
ccstring = cc_select.value,
descstring = description_select.value,
vertstring = vert_select.value)
# Make the deal plot
p = make_plot(src)
layout = row(controls, p)
# Make a tab with the layout
tab = Panel(child=layout, title = '2018')
# Put all the tabs into one application
tabs = Tabs(tabs = [tab])
# Put the tabs in the current document for display
curdoc().add_root(tabs)
If you are updating a glyph, you need to change the datasource for that glyph directly. In your case, you should assign the circle glyph to a variable, such as:
circle = p.circle(x="Deal_Number", y="USD_Base", source=src, size=7, line_color=None)
Then in your update_plot(attr, old, new) function try this:
circle = p.select_one({'name':'circle'})
circle.data_source.data = new_src.data
For selecting all, possibly the MultiSelect Widget would work?
I'm trying to create a bokeh plot of the US States, and color each of the state according to some data. Now using this tutorial I managed to create this, but I also want to enhance it, and add a slider to it, to change the values displayed. For example like displaying separate years.
With the help of this tutorial, I managed to add the slider, and the underlying data does change, according to the hover text, but the colors aren't recalculated, and so the visual representation does not match the values.
This is the code I've used, from a Jupyter notebook, so anybody who wants to try can reproduce
from bokeh.io import show, output_notebook
from bokeh.models import (
ColumnDataSource,
HoverTool,
LogColorMapper,
Range1d, CustomJS, Slider
)
from bokeh.palettes import Inferno256 as palette
from bokeh.plotting import figure
from bokeh.layouts import row, widgetbox
from bokeh.sampledata.us_counties import data as counties
from bokeh.sampledata.us_states import data as states
from bokeh.sampledata.unemployment import data as unemployment
import pandas as pd
import random
output_notebook()
palette.reverse()
states_accumulated ={}
available_state_codes = states.keys()
for key, value in counties.items():
state_name = value["state"].upper()
if state_name in states.keys() and "number" not in states[state_name]:
states[state_name]["number"] = key[0]
for key,state in states.items():
state["code"] = key
state_list = []
for key,state in states.items():
state_list.append(state)
unemployment_transf = []
for key,value in unemployment.items():
unemployment_transf.append({
"State":key[0],
"County":key[1],
"Value":value
})
unemp_df = pd.DataFrame(unemployment_transf)
unemp_sum = unemp_df.groupby("State").mean()["Value"]
unemp_sum = unemp_sum.sort_index()
unemp_sum_flat = {key:value for key, value in unemp_sum.items()}
for state in state_list:
state["value"] = unemp_sum_flat[state["number"]]
state_df = pd.DataFrame(state_list)
color_mapper = LogColorMapper(palette=palette)
state_xy = (list(state_df["lons"].values),list(state_df["lats"].values))
max_x = max([max(l) for l in state_xy[0]])
max_y = max([max(l) for l in state_xy[1]])
min_x = min([min(l) for l in state_xy[0]])
min_y = min([min(l) for l in state_xy[1]])
data=dict(
x=state_xy[0],
y=state_xy[1],
name=list(state_df["name"].values),
used = list(state_df["value"].values)
)
data['1999'] = list(state_df["value"].values)
data['2000'] = [random.randrange(0,10) for i in range(len(state_xy[0]))]
source = ColumnDataSource(data)
TOOLS = "pan,wheel_zoom,reset,hover,save"
p = figure(
title="States", tools=TOOLS,
x_axis_location=None, y_axis_location=None
)
p.width=450
p.height = 450
p.x_range= Range1d(-170,-60)
p.y_range = Range1d(min_y-10,max_y+10)
p.grid.grid_line_color = None
renderer = p.patches('x', 'y', source=source,
fill_color={'field': 'used', 'transform': color_mapper},
fill_alpha=0.7, line_color="white", line_width=0.5)
hover = p.select_one(HoverTool)
hover.point_policy = "follow_mouse"
hover.tooltips = [
("Name", "#name"),
("Unemployment rate)", "#used%"),
("(Long, Lat)", "($x, $y)"),
]
callback = CustomJS(args=dict(source=source,plot=p,color_mapper = color_mapper,renderer = renderer), code="""
var data = source.data;
var year = year.value;
used = data['used']
should_be = data[String(year)]
for (i = 0; i < should_be.length; i++) {
used[i] = should_be[i];
}
""")
year_slider = Slider(start=1999, end=2000, value=1999, step=1,
title="year", callback=callback)
callback.args["year"] = year_slider
layout = row(
p,
widgetbox(year_slider),
)
show(layout)
Sample images of the plot:
What I would like to accomplish, is that when I change the slider, the colors on the plot should change. Now I think the JS callback should call some kind of redraw or recalculate, but I haven't found any documentation about it. Is there a way to do this?
append source.change.emit() to the Javascipt code to trigger the change event.
Appending source.trigger("change"); to the CustomJS seems to solve the problem, now as the slider changes, the colors change.
If I have a scatter plot in bokeh and I've enabled the Box Select Tool, suppose I select a few points with the Box Select Tool. How can I access the (x,y) position location information of the points that I've selected?
%matplotlib inline
import numpy as np
from random import choice
from string import ascii_lowercase
from bokeh.models.tools import *
from bokeh.plotting import *
output_notebook()
TOOLS="pan,wheel_zoom,reset,hover,poly_select,box_select"
p = figure(title = "My chart", tools=TOOLS)
p.xaxis.axis_label = 'X'
p.yaxis.axis_label = 'Y'
source = ColumnDataSource(
data=dict(
xvals=list(range(0, 10)),
yvals=list(np.random.normal(0, 1, 10)),
letters = [choice(ascii_lowercase) for _ in range(10)]
)
)
p.scatter("xvals", "yvals",source=source,fill_alpha=0.2, size=5)
select_tool = p.select(dict(type=BoxSelectTool))[0]
show(p)
# How can I know which points are contained in the Box Select Tool?
I can't call the "callback" attribute and the "dimensions" attribute just returns a list ["width", "height"]. If I can just get the dimensions and the location of the Selected Box, I can figure out which points are in my dataset from there.
You can use a callback on the ColumnDataSource that updates a Python variable with the indices of the selected data:
%matplotlib inline
import numpy as np
from random import choice
from string import ascii_lowercase
from bokeh.models.tools import *
from bokeh.plotting import *
from bokeh.models import CustomJS
output_notebook()
TOOLS="pan,wheel_zoom,reset,hover,poly_select,box_select"
p = figure(title = "My chart", tools=TOOLS)
p.xaxis.axis_label = 'X'
p.yaxis.axis_label = 'Y'
source = ColumnDataSource(
data=dict(
xvals=list(range(0, 10)),
yvals=list(np.random.normal(0, 1, 10)),
letters = [choice(ascii_lowercase) for _ in range(10)]
)
)
p.scatter("xvals", "yvals",source=source,fill_alpha=0.2, size=5)
select_tool = p.select(dict(type=BoxSelectTool))[0]
source.callback = CustomJS(args=dict(p=p), code="""
var inds = cb_obj.get('selected')['1d'].indices;
var d1 = cb_obj.get('data');
console.log(d1)
var kernel = IPython.notebook.kernel;
IPython.notebook.kernel.execute("inds = " + inds);
"""
)
show(p)
Then you can access the desired data attributes using something like
zip([source.data['xvals'][i] for i in inds],
[source.data['yvals'][i] for i in inds])
Here is a working example with Python 3.7.5 and Bokeh 1.4.0
public github link to this jupyter notebook:
https://github.com/surfaceowl-ai/python_visualizations/blob/master/notebooks/bokeh_save_linked_plot_data.ipynb
environment report:
virtual env python version: Python 3.7.5
virtual env ipython version: 7.9.0
watermark package reports:
bokeh 1.4.0
jupyter 1.0.0
numpy 1.17.4
pandas 0.25.3
rise 5.6.0
watermark 2.0.2
# Generate linked plots + TABLE displaying data + save button to export cvs of selected data
from random import random
from bokeh.io import output_notebook # prevent opening separate tab with graph
from bokeh.io import show
from bokeh.layouts import row
from bokeh.layouts import grid
from bokeh.models import CustomJS, ColumnDataSource
from bokeh.models import Button # for saving data
from bokeh.models.widgets import DataTable, DateFormatter, TableColumn
from bokeh.models import HoverTool
from bokeh.plotting import figure
# create data
x = [random() for x in range(500)]
y = [random() for y in range(500)]
# create first subplot
plot_width = 400
plot_height = 400
s1 = ColumnDataSource(data=dict(x=x, y=y))
fig01 = figure(
plot_width=plot_width,
plot_height=plot_height,
tools=["lasso_select", "reset", "save"],
title="Select Here",
)
fig01.circle("x", "y", source=s1, alpha=0.6)
# create second subplot
s2 = ColumnDataSource(data=dict(x=[], y=[]))
# demo smart error msg: `box_zoom`, vs `BoxZoomTool`
fig02 = figure(
plot_width=400,
plot_height=400,
x_range=(0, 1),
y_range=(0, 1),
tools=["box_zoom", "wheel_zoom", "reset", "save"],
title="Watch Here",
)
fig02.circle("x", "y", source=s2, alpha=0.6, color="firebrick")
# create dynamic table of selected points
columns = [
TableColumn(field="x", title="X axis"),
TableColumn(field="y", title="Y axis"),
]
table = DataTable(
source=s2,
columns=columns,
width=400,
height=600,
sortable=True,
selectable=True,
editable=True,
)
# fancy javascript to link subplots
# js pushes selected points into ColumnDataSource of 2nd plot
# inspiration for this from a few sources:
# credit: https://stackoverflow.com/users/1097752/iolsmit via: https://stackoverflow.com/questions/48982260/bokeh-lasso-select-to-table-update
# credit: https://stackoverflow.com/users/8412027/joris via: https://stackoverflow.com/questions/34164587/get-selected-data-contained-within-box-select-tool-in-bokeh
s1.selected.js_on_change(
"indices",
CustomJS(
args=dict(s1=s1, s2=s2, table=table),
code="""
var inds = cb_obj.indices;
var d1 = s1.data;
var d2 = s2.data;
d2['x'] = []
d2['y'] = []
for (var i = 0; i < inds.length; i++) {
d2['x'].push(d1['x'][inds[i]])
d2['y'].push(d1['y'][inds[i]])
}
s2.change.emit();
table.change.emit();
var inds = source_data.selected.indices;
var data = source_data.data;
var out = "x, y\\n";
for (i = 0; i < inds.length; i++) {
out += data['x'][inds[i]] + "," + data['y'][inds[i]] + "\\n";
}
var file = new Blob([out], {type: 'text/plain'});
""",
),
)
# create save button - saves selected datapoints to text file onbutton
# inspriation for this code:
# credit: https://stackoverflow.com/questions/31824124/is-there-a-way-to-save-bokeh-data-table-content
# note: savebutton line `var out = "x, y\\n";` defines the header of the exported file, helpful to have a header for downstream processing
savebutton = Button(label="Save", button_type="success")
savebutton.callback = CustomJS(
args=dict(source_data=s1),
code="""
var inds = source_data.selected.indices;
var data = source_data.data;
var out = "x, y\\n";
for (i = 0; i < inds.length; i++) {
out += data['x'][inds[i]] + "," + data['y'][inds[i]] + "\\n";
}
var file = new Blob([out], {type: 'text/plain'});
var elem = window.document.createElement('a');
elem.href = window.URL.createObjectURL(file);
elem.download = 'selected-data.txt';
document.body.appendChild(elem);
elem.click();
document.body.removeChild(elem);
""",
)
# add Hover tool
# define what is displayed in the tooltip
tooltips = [
("X:", "#x"),
("Y:", "#y"),
("static text", "static text"),
]
fig02.add_tools(HoverTool(tooltips=tooltips))
# display results
# demo linked plots
# demo zooms and reset
# demo hover tool
# demo table
# demo save selected results to file
layout = grid([fig01, fig02, table, savebutton], ncols=3)
output_notebook()
show(layout)
# things to try:
# select random shape of blue dots with lasso tool in 'Select Here' graph
# only selected points appear as red dots in 'Watch Here' graph -- try zooming, saving that graph separately
# selected points also appear in the table, which is sortable
# click the 'Save' button to export a csv
# TODO: export from Bokeh to pandas dataframe