I've got a program that displays stock market info for a google. I can adjust it with a DateRangeSlider to increase the dates that we're looking at. I also have a hovertool set on the chart to display the various aspects of the data for each respective day.
The issue I'm having is on callback. Instead of the hover data being refreshed with each call, it retains memory of previous calls and just stacks the previous hover info on top of the new.
Here's my code for running in Jupyter:
from pandas_datareader import data
from pandas import Timedelta
from datetime import datetime, date
from bokeh.plotting import figure, show
from bokeh.layouts import column, row
from bokeh.models import ColumnDataSource, HoverTool, NumeralTickFormatter, DatetimeTickFormatter, TextInput, DateRangeSlider
from bokeh.io import output_notebook, curdoc
output_notebook()
def inc_dec(c, o):
"""Determines if the financial day with and Increase or Decrease"""
if c > o:
return "Increase"
elif c == 0:
return "Equal"
else:
return "Decrease"
def get_sources(start = date(2016, 3, 1), end = date(2016, 3, 10), company = "GOOG"):
"""Gets all my dataframes for each glyph below"""
#Gather data for df's
df = data.DataReader(company, data_source="yahoo", start=start, end=end)
df["Status"] = [inc_dec(c, o) for c, o in zip(df.Close, df.Open)]
df["Mid"] = (df.Open + df.Close) / 2
df["Height"] = abs(df.Open - df.Close)
df["Width"] = 12*60*60*1000
df["Segment, Left"] = df.index - Timedelta(hours=2)
df["Segment, Right"] = df.index + Timedelta(hours=2)
#my df is complete. Split them
inc_df = df[df.Status == "Increase"]
dec_df = df[df.Status == "Decrease"]
#inc_s = ColumnDataSource(inc_df)
#dec_s = ColumnDataSource(dec_df)
#tot_s = ColumnDataSource(df)
#Dictionary of dataframes, Increasing, Decreasing, and Total
dfs = {"I": inc_df, "D": dec_df, "T": df}
return dfs
def fin(doc):
"""Initialize the graphs and make an application"""
#Get df's and convert to CDS
dfs = get_sources()
inc_s = ColumnDataSource(dfs["I"])
dec_s = ColumnDataSource(dfs["D"])
tot_s = ColumnDataSource(dfs["T"])
hover = HoverTool(names=["inc","dec"],
tooltips = [("Date", "#Date{%m/%d/%Y}"),
("High", "#High{$0.00}"),
("Low","#Low{$0.00}"),
("Open","#Open{$0.00}"),
("Close","#Close{$0.00}"),
("Volume", "#Volume{0,0}")],
formatters = {"#Date" : "datetime"})
#Formatting the plot, p
p = figure(x_axis_type="datetime", width=1000, height=300, sizing_mode="scale_width", tools="crosshair,pan,wheel_zoom,box_zoom,reset")
#p.title.text = "{}: Finacial Data".format(company) #Change this with company later
p.title.text = "GOOG: Financial Data"
p.title.align = "center"
p.xaxis.axis_label="Date"
p.yaxis.axis_label="Price"
p.yaxis.formatter = NumeralTickFormatter(format="$0,0.00")
p.grid.grid_line_alpha = 0.3
#Add glyphs
p.segment(x0='Date', y0='High', x1='Date', y1='Low', source=tot_s) #vertical
p.segment(x0='Segment, Left', y0='High', x1='Segment, Right', y1='High', source=tot_s) #high
p.segment(x0='Segment, Left', y0='Low', x1='Segment, Right', y1='Low', source=tot_s) #low
p.rect(x="Date", y="Mid", width="Width", height="Height", name="inc",
fill_color="#00FFFF", line_color="black", source=inc_s)
p.rect(x="Date", y="Mid", width="Width", height="Height", name="dec",
fill_color="#B22222", line_color="black", source=dec_s)
p.add_tools(hover)
#the callbacks
def cb_date(attr, new, old):
"""Callback for the DateRangeSlider"""
if not isinstance(new[0], int):
return None
else:
#Get new dates, pass them to get_sources to get df's, add new CDS's
start = date.fromtimestamp(new[0]/1000)
end = date.fromtimestamp(new[1]/1000)
dfs = get_sources(start=start, end=end)
inc_s.stream(dfs["I"])
dec_s.stream(dfs["D"])
tot_s.stream(dfs["T"])
#def cb_com(attr, new, old):
#Changes the company name, do later
#add widgets
slider = DateRangeSlider(start=date(2016, 1, 1),
end=date(2016, 5, 1),
value=(date(2016, 3, 1), date(2016, 3, 10)),
step=1,
title="Change Dates:")
slider.on_change('value', cb_date)
doc.add_root(column(slider,p))
show(fin)
Let me know what how I can go forward with this. I'm pretty lost.
I cannot run your code without some test data, but it seems to me that the issue is with the calls to the stream function. Streaming data does not remove the old data, even if the X coordinate is the same (data sources don't know and don't care about coordinates). If you want to replace the data for some particular date, you should use patch. If you want to replace the whole data, you should just assign the new value to the data attribute of a data source.
Related
I am currently trying to write a program that will switch between two sets of data when different options are chosen from the select widget. I am trying to make this program as autonomous as possible so in the future when people update the data they don't have to modify the code at all and the updates will happen automatically.
Currently, my issue is that when I select 'White' I want the plot to update but nothing is happening.
The two data sets are currently a dict of lists, one labeled 'White_dict' and the other labeled 'black_dict' solely to represent the color of the material for the data (I know its kinda ironic).
from bokeh.plotting import figure, curdoc
from bokeh.models import ColumnDataSource, Legend
from bokeh.models import Select
from bokeh.layouts import column
import pandas as pd
from plot_tools import add_hover
import itertools
from collections import defaultdict
bokeh_doc = curdoc()
material_types = pd.read_csv('data/material-information.csv')
df = pd.read_csv('data/Black_Materials_total_reflecatance.csv')
black_df = pd.read_csv('data/Black_Materials_total_reflecatance.csv')
white_df = pd.read_csv('data/SPIE18_white_all.csv')
names = []
w_names = []
black_dict = defaultdict(list)
white_dict = defaultdict(list)
for name, w_name in zip(df, white_df):
names.append(name)
w_names.append(w_name)
data = pd.read_csv('data/Black_Materials_total_reflecatance.csv', usecols = names)
w_data = pd.read_csv('data/SPIE18_white_all.csv', usecols = w_names)
for name, w_name in zip(names, w_names):
for i in range(0, 2250):
black_dict[name].append(data[name][i])
white_dict[w_name].append(w_data[w_name][i])
mySource = ColumnDataSource(data = black_dict)
#create total reflectance figure
total_fig = figure(plot_width = 650, plot_height = 350,
title = 'Total Reflectance',
x_axis_label = 'Wavelength(nm)', y_axis_label = 'Total Reflectance',
x_range = (250, 2500), y_range = (0,10),
title_location = 'above', sizing_mode = "scale_both",
toolbar_location = "below",
tools = "box_zoom, pan, wheel_zoom, save")
select = Select(title="Material Type", options=['Black', 'White'])
def update_plot(attr, old, new):
if new == 'White':
mySource.data = white_dict
else:
mySource.data = black_dict
for name, color in zip(mySource.data, Turbo256):
if name != 'nm':
total_fig.line('nm', name, line_width = .7, source = mySource, color = color)
select.on_change('value', update_plot)
bokeh_doc.add_root(total_fig)
bokeh_doc.add_root(select)
I'm currently using bokeh serve bokehWork.py to launch the server. If anyone has any idea on what I should fix it would be much appreciated! Thanks!
EDIT:
Adding data for Black_materials_total_reflectance.csv
Black Reflectance Data sample
Adding data for White_all.csv
White Reflectance Data sample
There are two main issues with your code:
You read the same files multiple times and you do a lot of work that Pandas and Bokeh can already do for you
(the main one) You do not take into account the fact that different CSV files have different column names
Here's a fixed version. Notice also the usage of the palette. With just Turbo256 you were getting almost the same color for all lines.
import pandas as pd
from bokeh.models import ColumnDataSource, Select
from bokeh.palettes import turbo
from bokeh.plotting import figure, curdoc
black_ds = ColumnDataSource(pd.read_csv('/home/p-himik/Downloads/Black_material_data - Sheet1.csv').set_index('nm'))
white_ds = ColumnDataSource(pd.read_csv('/home/p-himik/Downloads/White Materials Sample - Sheet1.csv').set_index('nm'))
total_fig = figure(plot_width=650, plot_height=350,
title='Total Reflectance',
x_axis_label='Wavelength(nm)', y_axis_label='Total Reflectance',
title_location='above', sizing_mode="scale_both",
toolbar_location="below",
tools="box_zoom, pan, wheel_zoom, save")
total_fig.x_range.range_padding = 0
total_fig.x_range.only_visible = True
total_fig.y_range.only_visible = True
palette = turbo(len(black_ds.data) + len(white_ds.data))
def plot_lines(ds, color_offset, visible):
renderers = []
for name, color in zip(ds.data, palette[color_offset:]):
if name != 'nm':
r = total_fig.line('nm', name, line_width=.7, color=color,
source=ds, visible=visible)
renderers.append(r)
return renderers
black_renderers = plot_lines(black_ds, 0, True)
white_renderers = plot_lines(white_ds, len(black_ds.data), False)
select = Select(title="Material Type", options=['Black', 'White'], value='Black')
def update_plot(attr, old, new):
wv = new == 'White'
for r in white_renderers:
r.visible = wv
for r in black_renderers:
r.visible = not wv
select.on_change('value', update_plot)
bokeh_doc = curdoc()
bokeh_doc.add_root(total_fig)
bokeh_doc.add_root(select)
I've taken the below code from another source - it is not my own code.
The code allows you to select a cell in the data table, and the 'downloads' data for that cell will chart based on the row of the cell selected.
How do I expand this code such that if I have multiple variables (eg. 'downloads' and 'uploads') and so more columns in the data table, I can chart data based on that cell (so where row AND column are important)? Alternatively, how can I define as a variable the column number of a selected cell (in the same way selected_row below can be used to define the row number)?
from datetime import date
from random import randint
from bokeh.models import ColumnDataSource, Column
from bokeh.plotting import figure, curdoc
from bokeh.models.widgets import DataTable, DateFormatter, TableColumn, Div
import numpy as np
data = dict(dates = [date(2014, 3, i + 1) for i in range(10)],
downloads = [randint(0, 100) for i in range(10)])
d_source = ColumnDataSource(data)
columns = [TableColumn(field = "dates", title = "Date", formatter = DateFormatter()),
TableColumn(field = "downloads", title = "Downloads")]
data_table = DataTable(source = d_source, columns = columns, width = 400, height = 280)
def table_select_callback(attr, old, new):
selected_row = new[0]
download_count = data['downloads'][selected_row]
chart_data = np.random.uniform(0, 100, size = download_count)
p = figure(title = 'bla')
r = p.line(x = range(len(chart_data)), y = chart_data)
root_layout.children[1] = p
d_source.selected.on_change('indices', table_select_callback)
root_layout = Column(data_table, Div(text = 'Select Date'))
curdoc().add_root(root_layout)
This is the final working code (run from command line with bokeh serve --show app.py):
from datetime import date
from random import randint
from bokeh.models import ColumnDataSource, Column, TableColumn, DateFormatter, DataTable, CustomJS
from bokeh.plotting import figure, curdoc
source = ColumnDataSource(dict(dates = [date(2014, 3, i + 1) for i in range(10)], downloads = [randint(0, 100) for i in range(10)]))
columns = [TableColumn(field = "dates", title = "Date", formatter = DateFormatter()), TableColumn(field = "downloads", title = "Downloads")]
data_table = DataTable(source = source, columns = columns, width = 400, height = 280, editable = True, reorderable = False)
info_source = ColumnDataSource(dict(row = [], column = []))
source_code = """
var grid = document.getElementsByClassName('grid-canvas')[0].children;
var row, column = '';
for (var i = 0,max = grid.length; i < max; i++){
if (grid[i].outerHTML.includes('active')){
row = i;
for (var j = 0, jmax = grid[i].children.length; j < jmax; j++)
if(grid[i].children[j].outerHTML.includes('active')) {
column = j;
source2.data = {row: [row], column: [column]};
}
}
}"""
callback = CustomJS(args = dict(source = source, source2 = info_source), code = source_code)
source.selected.js_on_change('indices', callback)
def py_callback(attr, old, new):
source.selected.update(indices = [])
print(info_source.data)
source.selected.on_change('indices', py_callback)
curdoc().add_root(Column(data_table))
My suggestion is to use my another post that uses a JS callback to access the row and column of the selected cell. This must be a JS callback as it uses HTML elements to walk through. So the steps are as follows:
Define a new ColumnDataSource that will contain the row and column number of a clicked cell
info_source = ColumnDataSource(dict(row = [], column = []))
Use the JS callback from this post to fill in the row and column values in this new table_info_source like this:
callback=CustomJS(args=dict(source=d_source,source2=info_source),code=source_code)
source.selected.js_on_change('indices', callback)
Inside the JS callback store the row and column index like this:
source2.data = {row:[row],column:[column]};
Access the info_source.data information in your Python callback to draw a plot
print (info_source.data)
Both callback are attached to the same source but usually JS callback is executed first so the data should be available in the Python callback on time. Having the index of row and column of the clicked cell you should be able to retrieve the required data and draw your chart.
import pandas as pd
import numpy as np
from bokeh.io import show, output_notebook, push_notebook
from bokeh.plotting import figure
from bokeh.models import CategoricalColorMapper, HoverTool, ColumnDataSource, Panel
from bokeh.models.widgets import CheckboxGroup, Slider, RangeSlider, Tabs
from bokeh.layouts import column, row, WidgetBox
from bokeh.palettes import Category20_16
from bokeh.application.handlers import FunctionHandler
from bokeh.application import Application
output_notebook()
def histogram_tab(webs):
def make_dataset(params_list, range_start = 0.0, range_end = 1, bin_width = 0.005):
#check to make sure the start is less than the end
assert range_start < range_end, "Start must be less than end!"
#by_params = pd.DataFrame(columns=[ ,'Max', 'Avarage', 'Min','color'])
by_params = pd.DataFrame(columns=[ 'left','right', 'proportion', 'p_proportion','p_interval', 'name', 'w_name','color'])
#
range_extent = range_end - range_start
values = ['Min', "Avarage", 'Max']
# Iterate through all the parameters
for i, para_name in enumerate(params_list):
#print para_name
# Subset to the parameter
subset = webs[para_name]
# note: subset have to be a list of values
# [webs.columns[i%6]]
# Create a histogram with specified bins and range
arr_hist, edges = np.histogram(subset,
bins = int(range_extent / bin_width),
range = [range_start, range_end])
# Divide the counts by the total to get a proportion and create df
arr_df= pd.DataFrame({'proportion': arr_hist ,
'left': edges[:-1], 'right': edges[1:]}) #/ np.sum(arr_hist)
# Format the proportion
arr_df['p_proportion'] = ['%0.00005f' % proportion for proportion in arr_df['proportion']]
# Format the interval
arr_df['p_interval'] = ['%d to %d scale' % (left, right) for left,
right in zip(arr_df['left'], arr_df['right'])]
# Assign the parameter for labels
arr_df['name'] = para_name
arr_df['w_name'] = webs['Site name']
# Color each parametr differently
arr_df['color'] = Category20_16[i]
# Add to the overall dataframe
by_params = by_params.append(arr_df)
# Overall dataframe
by_params = by_params.sort_values(['name','left'])
return ColumnDataSource(by_params)
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):
# Blank plot with correct labels
p = figure(plot_width = 700, plot_height = 700,
title = "Histogram of Parametes for the websites",
x_axis_label = 'parameters', y_axis_label = "values")
# Quad glyphs to create a histogram
p.quad(source=src, bottom =0,left = 'left', right = 'right', color ='color', top= 'proportion',fill_alpha = 0.7, hover_fill_color = 'color', legend = 'name',
hover_fill_alpha = 1.0, line_color = 'white') #top='proportion',
# Hover tool with vline mode
hover = HoverTool(tooltips=[('Parameter','#name'),
('Website','#w_name'),
('Proportion','p_proportion')
],
mode='vline')
p.add_tools(hover)
# Stypling
p = style(p)
return p
# Update function takes three default parameters
def update(attr, old, new):
# Get the list of parameter for the graph
parameter_to_plot = [para_selection.labels[i] for i in para_selection.active]
# Make a new dataset based on the selected parameter and the
# make_dataset function defined earlier
new_src = make_dataset(parameter_to_plot, range_start = 0, range_end = 1, bin_width = 0.005) # note range are not specified
# Convert dataframe to column data source
new_src = ColumnDataSource(new_src)
# Update the source used the quad glpyhs
src.data.update(new_src.data)
list_of_params = list(webs.columns[1:].unique())
list_of_params.sort()
para_selection = CheckboxGroup(labels=list_of_params, active = [0,1])
para_selection.on_change('active',update)
binwidth_select = Slider(start =0, end = 1,
step = 0.00025, value = 0.0005,
title = 'Change in parameter')
binwidth_select.on_change('value', update)
range_select = RangeSlider(start=0, end=1, value =(0,1),
step=0.00025, title = 'Change in range')
range_select.on_change('value', update)
initial_params = [para_selection.labels[i] for i in para_selection.active]
src = make_dataset(initial_params,
range_start = range_select.value[0],
range_end = range_select.value[1],
bin_width = binwidth_select.value)
p = make_plot(src)
#show(p)
# Put controls in a single element
controls = WidgetBox(para_selection, binwidth_select, range_select)
# Create a row layout
layout = row(controls, p)
# Make a tab with the layout
tab = Panel(child = layout, title = 'Histogram')
#return tab
tabs = Tabs(tabs=[tab])
webs.add_root(tabs)
# Set up an application
handler = FunctionHandler(histogram_tab(webs))
app = Application(handler)
add_root is a method on Document, you are trying to call it on a DataFrame called webs, apparently, which is why you get that message. The structure of a Bokeh app in a notebook should look like this:
# create a function to define the app, must accept "doc" as the parameter
def myfunc(doc):
# make Bokeh objects
# add stuff to doc
doc.add_root(stuff)
# pass the function, but *don't* execute it
handler = FunctionHandler(myfunc)
app = Application(handler)
Note that the last two lines are not necessary in recent version of Bokeh, you can just call:
show(myfunc)
directly. There is a full example in the repo:
https://github.com/bokeh/bokeh/blob/master/examples/howto/server_embed/notebook_embed.ipynb
Your code should be structured very similarly to that.
I am trying to implement the DateRangeSlider Widget into my bokeh project. I am using the widget to adjust the amount of stock date present over a range that I have accumulated in the csv file. Whenever I move the slider widget
it gives me an error.
I am assuming that the datatypes used are incorrect. The issue lies in my update function.
How can I resolve this ?
from math import pi
import pandas as pd
import numpy as np
import datetime
import time
from datetime import date
from bokeh.layouts import row, widgetbox, column
from bokeh.models import ColumnDataSource, PrintfTickFormatter, CDSView,BooleanFilter
from bokeh.models.widgets import PreText, Select, DateRangeSlider, Button, DataTable, TableColumn, NumberFormatter
from bokeh.io import curdoc, show, reset_output
from bokeh.plotting import figure, output_file
DEFAULT_TICKERS = ['AAPL','GOOG','NFLX', 'TSLA']
ticker1 = Select(value='AAPL', options = DEFAULT_TICKERS)
range_slider1 = DateRangeSlider(start=date(2014,1,1) , end=date(2017,1,1), value=(date(2014,2,1),date(2016,3,1)), step=1)
def load_ticker(ticker):
fname = ( '%s.csv' % ticker.lower())
data = pd.read_csv( fname, header = None, parse_dates = ['Date'],
names =['Date','Open','High','Low','Close','AdjClose','Volume'])
return data
def get_data(t1):
data = load_ticker(t1)
return data
def ticker1_change(attrname, old, new):
update()
def range_slider_change(attrname, old, new):
update()
def update(selected=None):
t1 = ticker1.value
sd = str(range_slider1.value[0])
ed = str(range_slider1.value[1])
start_date = sd
end_date = ed
datarange = get_data(t1)
datarange['Date'] = pd.to_datetime(datarange['Date'])
mask = (datarange['Date'] > start_date) & (datarange['Date'] <= end_date)
data = datarange.loc[mask]
source.data = source.from_df(data)
p.title.text = t1
data = get_data(ticker1.value)
source = ColumnDataSource(data)
p = figure(plot_width=900, plot_height=400, x_axis_type='datetime')
p.grid.grid_line_alpha = 0.3
p.line('Date', 'Close', source=source)
ticker1.on_change('value', ticker1_change)
range_slider1.on_change('value', range_slider_change)
update()
layout = column(ticker1,range_slider1, p)
curdoc().add_root(layout)
curdoc().title = "Stock"
You are converting start_date and end_date to strings:
sd = str(range_slider1.value[0])
ed = str(range_slider1.value[1])
start_date = sd
end_date = ed
And then using them where a TimeStamp is needed:
mask = (datarange['Date'] > start_date) & (datarange['Date'] <= end_date)
Which is exactly what the message "could not convert string to Timestamp" is telling you.
You need to convert the slider values to real TimeStamp objects. There is a slight wrinkle in that the slider value can be a date or and int (You might set it initially as a date, but at least as of Bokeh 0.13 it always comes back as a numeric timestamp, specifically, the number of millisecond since epoch. You have to handle both cases, here is one way:
if isinstance(range_slider1.value[0], (int, float)):
# pandas expects nanoseconds since epoch
start_date = pd.Timestamp(float(range_slider1.value[0])*1e6)
end_date = pd.Timestamp(float(range_slider1.value[1])*1e6)
else:
start_date = pd.Timestamp(range_slider1.value[0])
end_date = pd.Timestamp(range_slider1.value[1])
This inconsistent behavior is considered a bug to be fixed in future releases. You can follow the issue here for more details: https://github.com/bokeh/bokeh/issues/8015
Hi I'm fairly new to Python, Plotly and Jupyter Notebook. I would like to use a slider to select the number of days as the range in a query to which a graph is created from. My only issue is that I want the graph to automatically update on interaction with the slider, without having to re-run the query and graph creation. My code is below:
slider = widgets.IntSlider()
display(slider)
sliderVal = slider.value
df = pd.read_sql(f"""
SELECT CASE WHEN SiteID LIKE 3 THEN 'BLAH'
WHEN SiteID LIKE 4 THEN 'BLAHBLAH'
END AS Website,
COUNT(1) AS Count
FROM viewName
WHERE (TimeStamp > DATEADD(DAY, -{sliderVal}, GETDATE()))
GROUP BY SiteId
ORDER BY Count DESC
""", conn)
data = [go.Bar(x=df.Website, y=df.Count)]
layout = go.Layout(
xaxis=dict(
title='Website'),
yaxis=dict(
title='Exception count'),
title=f'Number of exceptions per user in the last {sliderVal} days')
chart = go.Figure(data=data, layout=layout, )
py.iplot(chart, filename='WebExceptions')
Thanks in advance!
If you do not want to rerun the query, then your data frame df must contain the results for all the values that you want the intslider widget to take, the function linked to the widget will then simply filter the data and redraw the graph with the new filtered data.
Here's an example with some dummy data:
import ipywidgets as widgets
import plotly.offline as py
import plotly.graph_objs as go
import pandas as pd
py.init_notebook_mode(connected = True)
# Dummy data, to be replaced with your query result for the range of sliderVal
df = pd.DataFrame({'Days': [1] * 3 + [2] * 4 + [3] * 5,
'Website': [1,2,3, 4,5,6,7, 8,9,10,11,12],
'Count': [10,5,30, 15,20,25,12, 18,17,30,23,27]})
def update_plot(sliderVal):
filtered_df = df.query('Days== ' + str(sliderVal))
data = [go.Bar(x = filtered_df.Website,
y = filtered_df.Count)]
layout = go.Layout(
xaxis = dict(title = 'Website'),
yaxis = dict(title = 'Exception count'),
title = f'Number of exceptions per user in the last {sliderVal} days')
chart = go.Figure(data = data, layout = layout, )
py.iplot(chart, filename = 'WebExceptions')
# links an IntSlider taking values between 1 and 3 to the update_plot function
widgets.interact(update_plot, sliderVal = (1, 3))
and here is the result with sliderVal = 2: