source.data is updated in bokeh serve but plot does not change - python

I am trying to make a bokeh serve plot with a CheckButtonGroup. I manage to update my source.data but the plot does not get updated. What am I doing wrong?
In reality, I import the dataset from my computer, but for now I will create an example pandas dataframe. I want to select the 'x' column (as x-axis variable) and one or more of the other columns (as y-axis variables).
import pandas as pd
from bokeh.io import curdoc
from bokeh.plotting import figure
from bokeh.layouts import row, widgetbox
from bokeh.models.widgets import CheckboxButtonGroup
from bokeh.models import ColumnDataSource
dataset = pd.DataFrame(columns=['x','y1','y2','y3'])
dataset['x'] = [1, 2, 3, 4]
dataset['y1'] = [10, 20, 30, 40]
dataset['y2'] = [11, 21, 31, 41]
dataset['y3'] = [12, 22, 32, 43]
pos_cols = ['y1', 'y2', 'y3'] # possible column names
col_list = ['y1', 'y2'] # default columns in plotted data
use_data = dataset[col_list]
use_data['x'] = dataset.loc[:, 'x']
source = ColumnDataSource(use_data)
p = figure(
tools="pan,box_zoom,wheel_zoom,reset,save",
x_axis_label='xtitle', y_axis_label='ytitle',
title="Simulations"
)
# make default plot with the two columns
for column in col_list:
p.line('x', column, source=source)
check = CheckboxButtonGroup(labels=["y1", "y2", "y3"], active=[0, 1]) # A check box for every column
def update_lines(new):
col_list = [pos_cols[i] for i in new]
use_data = dataset[col_list]
use_data['x'] = dataset.loc[:, 'x']
source.data = source.from_df(use_data)
print(source.data) # source.data is correctly updated, but does not seem to trigger a new plot
check.on_click(update_lines)
doc = curdoc()
doc.add_root(row(check, p, width=800))
doc.title = "Simulations"
I save the code as try.py and run it from the windows prompt with bokeh serve try.py. The plot is visible at http://localhost:5006

The problem is that you are creating glyphs for columns like 'y3' up front, but not actually sending any column 'y3' to start. Bokeh does not like that (you can see error messages about trying to access non-existent columns in the browser JS console)
A better approach, that also does not unnecessarily re-send all the data, might be to just toggle the .visible attribute of the glyph renderers. Here is a minimal example (that starts with all lines visible, but you could change that):
import pandas as pd
from bokeh.io import curdoc
from bokeh.plotting import figure
from bokeh.layouts import row
from bokeh.models import CheckboxButtonGroup, ColumnDataSource
dataset = pd.DataFrame(columns=['x','y1','y2','y3'])
dataset['x'] = [1, 2, 3, 4]
dataset['y1'] = [10, 20, 30, 40]
dataset['y2'] = [11, 21, 31, 41]
dataset['y3'] = [12, 22, 32, 43]
source = ColumnDataSource(data=dataset)
p = figure( )
lines = []
for column in ['y1', 'y2', 'y3']:
lines.append(p.line('x', column, source=source))
check = CheckboxButtonGroup(labels=["y1", "y2", "y3"], active=[0, 1, 2])
def update_lines(new):
for i in [0, 1, 2]:
if i in new:
lines[i].visible = True
else:
lines[i].visible = False
check.on_click(update_lines)
doc = curdoc()
doc.add_root(row(check, p, width=800))
Alternatively, if you are just looking to be able to hide or mute lines, an much easier way would be to use Bokeh's built in Interactive Legends:
http://docs.bokeh.org/en/latest/docs/user_guide/interaction/legends.html#userguide-interaction-legends

Related

How to define Python Bokeh RangeSlider.on_change callback function to alter IndexFilter for plots?

I'm trying to implement a python callback function for a RangeSlider. The Slider Value should tell which Index a IndexFilter should get for display.
For example: If rangeslider.value is (3, 25) my plots should only contain/view data with the Index from 3 to 25.
from bokeh.io import output_file, show
from bokeh.models import ColumnDataSource, GMapOptions, CustomJS, CDSView, IndexFilter
from bokeh.plotting import gmap, ColumnDataSource, figure
from bokeh.layouts import column, row
from bokeh.models.widgets import RangeSlider
import numpy as np
def slider_callback(attr, old, new):
p.view = CDSView(source=source, filters=[IndexFilter(np.arange(new.value[0], new.value[1]))])
v.view = CDSView(source=source, filters=[IndexFilter(np.arange(new.value[0], new.value[1]))])
# data set
lon = [[48.7886, 48.7887, 48.7888, 48.7889, 48.789],
[48.7876, 48.7877, 48.78878, 48.7879, 48.787],
[48.7866, 48.7867, 48.7868, 48.7869, 48.786],
[48.7856, 48.7857, 48.7858, 48.7859, 48.785],
[48.7846, 48.7847, 48.7848, 48.7849, 48.784]]
lat = [[8.92, 8.921, 8.922, 8.923, 8.924],
[8.91, 8.911, 8.912, 8.913, 8.914],
[8.90, 8.901, 8.902, 8.903, 8.904],
[8.89, 8.891, 8.892, 8.893, 8.894],
[8.88, 8.881, 8.882, 8.883, 8.884]]
time = [0, 1, 2, 3, 4, 5]
velocity = [23, 24, 25, 24, 20]
lenght_dataset = len(lon)
# define source and map
source = ColumnDataSource(data = {'x': lon, 'y': lat, 't': time, 'v': velocity})
view = CDSView(source=source, filters=[IndexFilter(np.arange(0, lenght_dataset))])
map_options = GMapOptions(lat=48.7886, lng=8.92, map_type="satellite", zoom=13)
p = gmap("MY_API_KEY", map_options, title="Trajectory Map")
v = figure(plot_width=400, plot_height=400, title="Velocity")
# plot lines on map
p.multi_line('y', 'x', view=view, source=source, line_width=1)
v.line('t', 'v', view=view, source=source, line_width=3)
# slider to limit plotted data
range_slider = RangeSlider(title="Data Range Slider: ", start=0, end=lenght_dataset, value=(0, lenght_dataset), step=1)
range_slider.on_change('value', slider_callback)
# Layout to plot and output
layout = row(column(p, range_slider),
column(v)
)
output_file("diag_plot_bike_data.html")
show(layout)
Some notes:
time is longer than the rest of the columns - you will receive a warning about it. In my code below, I just removed its last element
view with filters in general should not be used for continuous glyphs like lines (v.line in particular - multi_line is fine). You will receive a warning about it. But if the indices in IndexFilter are always continuous, then you should be fine. Either way, you can use the segment glyph to avoid the warning
In your callback, you're trying to set view on the figures - views only exist on glyph renderers
In general, you don't want to recreate views, you want to recreate as few Bokeh models as possible. Ideally, you would have to just change the indices field of the filter. But there's some missing wiring in Bokeh, so you will have to set the filters field of the view, as below
new argument of Python callbacks receives the new value for the attribute passed as the first parameter to the corresponding on_change call. In this case, it will be a tuple, so instead of new.value[0] you should use new[0]
Since you've decided to use Python callbacks, you can no longer use show and have a static HTML file - you will have to use curdoc().add_root and bokeh serve. The UI needs that Python code to run somewhere in runtime
When changing the slider values, you will notice that the separate segments of multi_line will be joined together - it's a bug and I just created https://github.com/bokeh/bokeh/issues/10589 for it
Here's a working example:
from bokeh.io import curdoc
from bokeh.layouts import column, row
from bokeh.models import GMapOptions, CDSView, IndexFilter
from bokeh.models.widgets import RangeSlider
from bokeh.plotting import gmap, ColumnDataSource, figure
lon = [[48.7886, 48.7887, 48.7888, 48.7889, 48.789],
[48.7876, 48.7877, 48.78878, 48.7879, 48.787],
[48.7866, 48.7867, 48.7868, 48.7869, 48.786],
[48.7856, 48.7857, 48.7858, 48.7859, 48.785],
[48.7846, 48.7847, 48.7848, 48.7849, 48.784]]
lat = [[8.92, 8.921, 8.922, 8.923, 8.924],
[8.91, 8.911, 8.912, 8.913, 8.914],
[8.90, 8.901, 8.902, 8.903, 8.904],
[8.89, 8.891, 8.892, 8.893, 8.894],
[8.88, 8.881, 8.882, 8.883, 8.884]]
time = [0, 1, 2, 3, 4]
velocity = [23, 24, 25, 24, 20]
lenght_dataset = len(lon)
# define source and map
source = ColumnDataSource(data={'x': lon, 'y': lat, 't': time, 'v': velocity})
view = CDSView(source=source, filters=[IndexFilter(list(range(lenght_dataset)))])
map_options = GMapOptions(lat=48.7886, lng=8.92, map_type="satellite", zoom=13)
p = gmap("API_KEY", map_options, title="Trajectory Map")
v = figure(plot_width=400, plot_height=400, title="Velocity")
p.multi_line('y', 'x', view=view, source=source, line_width=1)
v.line('t', 'v', view=view, source=source, line_width=3)
range_slider = RangeSlider(title="Data Range Slider: ", start=0, end=lenght_dataset, value=(0, lenght_dataset), step=1)
def slider_callback(attr, old, new):
view.filters = [IndexFilter(list(range(*new)))]
range_slider.on_change('value', slider_callback)
layout = row(column(p, range_slider), column(v))
curdoc().add_root(layout)

Bokeh get selected glyphs/annotations from TapTool

For my project I need to add and remove glpyhs and annotations in bokeh (line, multiline and arrows). I want to make it as interactive as possible. So in order to remove a glyph/annotation in want to select it with a mouse click and then e.g. delete it with a button. The minimal example would look like that:
import numpy as np
import random
from bokeh.plotting import figure, ColumnDataSource
from bokeh.models import Button, TapTool,Arrow,NormalHead
from bokeh.layouts import layout
from bokeh.application import Application
from bokeh.server.server import Server
from bokeh.application.handlers.function import FunctionHandler
plot = figure(plot_height=300, plot_width=600, x_range=(0, 8), y_range=(0, 11),
title="Testplot", tools='save, reset, tap')
Lay = layout(children=[])
#adds the glyphs/annotaions to figure
def Click_action():
x = np.array((random.randrange(1,10),random.randrange(1,10)))
y = np.array((random.randrange(1,10),random.randrange(1,10)))
source = ColumnDataSource(data=dict(x = x,
y = y))
arro = Arrow(end=NormalHead(size=5, fill_color="#C0392B"),
x_start=random.randrange(0,10),
y_start=random.randrange(0,10),
x_end=random.randrange(0,10),
y_end=random.randrange(0,10),
line_width=3,
line_color="#C0392B")
plot.multi_line(xs=[[1,5],[1,1],[3,3],[5,5]],ys=[[5,5],[5,1],[5,1],[5,1]], color='blue', selection_color='red' )
plot.add_layout(arro)
plot.line(x='x',y='y', source = source,selection_color='red')
def Click_delet():
""" Delete the selected Glyphs/Annotations"""
def make_document(doc):
btn1 = Button(label="Click", button_type="success")
btn2 = Button(label="Click_delet", button_type="success")
btn1.on_click(Click_action)
btn2.on_click(Click_delet)
Lay.children.append(plot)
Lay.children.append(btn1)
Lay.children.append(btn2)
doc.add_root(Lay)
if __name__ == '__main__':
bkapp = {'/': Application(FunctionHandler(make_document))}
server = Server(bkapp, port=5004)
server.start()
server.io_loop.add_callback(server.show, "/")
server.io_loop.start()
The problems I currently have are:
How can I select the arrow ?
How do I get all selected glyphs and annotations? (If possible without a CoustomJS callback since I do not know java that well)
Is it possible to select the multiline as one glyph?
I have already solved the issue how to delete lines and arrows from a plot. But I would need the value stored in plot.renders and plot.center in order to delete them and link them to different classes in my project.
Annotations are not interactive in Bokeh
See a minimal example below
No
But I would need the value stored in plot.renders and plot.center in order to delete them and link them to different classes in my project.
Ideally, your workflow should abstain from dynamically creating and removing Bokeh models, especially low-levels ones such as glyphs. If you need to remove a glyph and add a new one with new properties, consider just changing properties of the old glyph. Or maybe just clear the data of the old glyph to hide it.
from bokeh.io import curdoc
from bokeh.layouts import column
from bokeh.models import Button
from bokeh.plotting import figure, ColumnDataSource
line_ds = ColumnDataSource(dict(x=[0, 3, 7],
y=[1, 8, 2]))
multi_line_ds = ColumnDataSource(dict(xs=[[1, 5], [1, 1], [3, 3], [5, 5]],
ys=[[5, 5], [5, 1], [5, 1], [5, 1]]))
p = figure(x_range=(0, 8), y_range=(0, 11), tools='save, reset, tap')
p.line('x', 'y', source=line_ds, selection_color='red')
p.multi_line('xs', 'ys', source=multi_line_ds, color='blue', selection_color='red')
b = Button(label="Delete selected", button_type="success")
def delete_rows(ds, indices):
print(indices)
if indices:
print(ds.data)
ds.data = {k: [v for i, v in enumerate(vs) if i not in set(indices)]
for k, vs in ds.data.items()}
print(ds.data)
def delete_selected():
delete_rows(line_ds, line_ds.selected.line_indices)
delete_rows(multi_line_ds, multi_line_ds.selected.indices)
b.on_click(delete_selected)
curdoc().add_root(column(p, b))

Using Hovertool in Python with Bokeh

I am a new python learner and am trying to make a plot with bokeh. I want to use the hover tool and it is working when I scroll over the dots. However, the X and Y values are showing ??? instead of the actual values. I'm not quite sure what I'm doing incorrectly because the hovertool itself is working, but the values are not displaying.
from bokeh.plotting import figure`
from bokeh.io import show, output_notebook
get_provider(Vendors.CARTODBPOSITRON)
from bokeh.models import ColumnDataSource, HoverTool
# Create a blank figure with labels
p = figure(plot_width = 600, plot_height = 600,
title = 'Example Glyphs',
x_axis_label = 'X', y_axis_label = 'Y')
hover = HoverTool(tooltips=[
("X", "#X "),
("Y","#Y")])
p = figure(x_axis_type="mercator",
y_axis_type="mercator",
tools=[hover, 'wheel_zoom','save'])
p.add_tile(CARTODBPOSITRON)
# Example data
circles_x = [1, 3, 4, 5, 8]
circles_y = [8, 7, 3, 1, 10]
circles_x = [9, 12, 4, 3, 15]
circles_y = [8, 4, 11, 6, 10]
# Add squares glyph
p.circle(squares_x, squares_y, size = 12, color = 'navy', alpha = 0.6)
# Add circle glyph
p.circle(circles_x, circles_y, size = 12, color = 'red')
# Set to output the plot in the notebook
output_notebook()
# Show the plot
show(p)
If you aren't using an explicit ColumnDataSource (which would allow you to use and refer to whatever column names you want), then you must refer to the default column names Bokeh uses. In this case, for circle, the default column names are "x" and "y" (lower case, not upper case as you have above). So:
hover = HoverTool(tooltips=[
("X", "#x"),
("Y", "#y"),
])

Bar chart showing count of each category month-wise using Bokeh

I have data as shown below:
So, from this, I need to display the count in each category year_month_id wise. Since I have 12 months there will be 12 sub-divisions and under each count of
ID within each class.
Something like the image below is what I am looking for.
Now the examples in Bokeh use ColumnDataSource and dictionary mapping, but how do I do this for my dataset.
Can someone please help me with this?
Below is the expected output in tabular and chart format.
I believe the pandas Python package would come in handy for preparing your data for plotting. It's useful for manipulating table-like data structures.
Here is how I went about your problem:
from pandas import DataFrame
from bokeh.io import show
from bokeh.plotting import figure
from bokeh.models import ColumnDataSource
from bokeh.palettes import Viridis5
# Your sample data
df = DataFrame({'id': [1, 2, 3, 4, 5, 6, 7, 8, 9, 1],
'year_month_id': [201612, 201612, 201612, 201612, 201612, 201612, 201612, 201612, 201612, 201701],
'class': ['A', 'D', 'B', 'other', 'other', 'other', 'A', 'other', 'A', 'B']
})
# Get counts of groups of 'class' and fill in 'year_month_id' column
df2 = DataFrame({'count': df.groupby(["year_month_id", "class"]).size()}).reset_index()
df2 now looks like this:
# Create new column to make plotting easier
df2['class-date'] = df2['class'] + "-" + df2['year_month_id'].map(str)
# x and y axes
class_date = df2['class-date'].tolist()
count = df2['count'].tolist()
# Bokeh's mapping of column names and data lists
source = ColumnDataSource(data=dict(class_date=class_date, count=count, color=Viridis5))
# Bokeh's convenience function for creating a Figure object
p = figure(x_range=class_date, y_range=(0, 5), plot_height=350, title="Counts",
toolbar_location=None, tools="")
# Render and show the vbar plot
p.vbar(x='class_date', top='count', width=0.9, color='color', source=source)
show(p)
So the Bokeh plot looks like this:
Of course you can alter it to suit your needs. The first thing I thought of was making the top of the y_range variable so it could accommodate data better, though I have not tried it myself.

How can create Python iplot graph, colors changes with value?

Here you are part of my data.
I count my data
count_interests = interests.count()
then made a graph
count_interests.iplot(kind = 'bar', xTitle='Interests', yTitle='Number of Person', colors='Red')
I tried many times to find a function change columns color with values so bigger and smaller columns looks different colors.
I know there is colorscale and color functions and I tried many times I couldn't find. Does anyone know any function?
You could define a function which returns a color for each value and then pass the colors for each bar in a list.
import pandas as pd
import plotly
def color(val, median, std):
if val > median + std:
return 'darkgreen'
if val < median - std:
return 'darkred'
return 'orange'
df = pd.DataFrame({'cinema': [1, 2, 5, 3, 3, None],
'theatre': [3, 0, 8, 4, 0, 4],
'wine': [3, 2, 5, None, 1, None],
'beer': [4, 8, 2, None, None, None]})
med = df.count().median()
std = df.count().std()
colors = [color(i, med, std) for i in df.count()]
fig = plotly.graph_objs.Bar(x=df.columns,
y=df.count(),
marker=dict(color=colors))
plotly.offline.plot([fig])
The bars could be also colored either by pd.pivot_table() the rows to columns or by creating a separate list of traces for bars. Here, each column was aggregated by taking a sum() as an example. Code below:
# Import libraries
import datetime
from datetime import date
import pandas as pd
import numpy as np
from plotly import __version__
%matplotlib inline
import cufflinks as cf
from plotly.offline import download_plotlyjs, init_notebook_mode, plot, iplot
init_notebook_mode(connected=True)
init_notebook_mode(connected=True)
cf.go_offline()
import plotly.graph_objs as go
import plotly.offline as pyo
# Create dataframe
INT_M_PUB = [0,0,0,0,0,1,0,0,0,0]
INT_M_CINEMA = [1,1,1,0,0,0,0,0,0,1]
INT_M_THEATRE = [1,0,1,0,0,1,0,1,0,1]
INT_M_GYM = [0,0,0,0,0,1,0,0,0,1]
INT_M_ENTERTAIN = [0,0,1,1,0,1,0,1,0,1]
INT_M_EATOUT = [0,1,1,0,0,1,0,0,1,1]
INT_M_WINE = [0,0,0,0,0,1,0,0,0,1]
interests = pd.DataFrame({'INT_M_PUB':INT_M_PUB, 'INT_M_CINEMA':INT_M_CINEMA, 'INT_M_THEATRE':INT_M_THEATRE,
'INT_M_GYM':INT_M_GYM, 'INT_M_ENTERTAIN':INT_M_ENTERTAIN, 'INT_M_EATOUT':INT_M_EATOUT,
'INT_M_WINE':INT_M_WINE
})
interests.head(2)
dfm = interests.sum().reset_index().rename(columns={'index':'interests', 0:'value'})
dfm
# Re-creating the plot similar to that in question (note: y-axis scales are different)
df = dfm.copy()
col_list = df.columns
df.iplot(kind = 'bar', x='interests', y='value', xTitle='Interests', yTitle='Number of Person', title='These bars need to be colored', color='red')
# Color plots by creating traces
# Initialize empty list named data to collect traces for each bar
data = []
for col_name in col_list:
trace = go.Bar(
x=[col_name],
y=df[col_name],
name=col_name
)
data.append(trace)
data = data
layout = go.Layout(
barmode='group',
title='Interests',
xaxis=dict(title='Interests'),
yaxis=dict(title='Number of Person')
)
fig = go.Figure(data=data, layout=layout)
pyo.iplot(fig, filename='grouped-bar')
# Creating plot by pivoting the table
df = pd.pivot_table(dfm, values='value', columns='interests')
df.iplot(kind = 'bar',xTitle='Interests', yTitle='Number of Person')

Categories