With Bokeh, how do I get a handle to the Renderer (or GlyphRenderer) for an Annotation? Is this possible?
I would like to be able to toggle a Band (which is an Annotation) on and off with an interactive legend, so I need to be able to pass a list of Renderers to the LegendItem constructor.
This code:
maxline = fig.line(x='Date', y=stn_max, line_width=0.5, legend=stn_max, name="{}_line".format(stn_max), color=stn_color, alpha=0.75, source=source)
minline = fig.line(x='Date', y=stn_min, line_width=0.5, legend=stn_min, name="{}_line".format(stn_min), color=stn_color, alpha=0.75, source=source)
band = bkm.Band(base='Date', lower=stn_min, upper=stn_max, fill_alpha=0.50, line_width=0.5, fill_color=stn_color, source=source)
bkm.LegendItem(label=stn, renderers=[maxline, minline, band])
Produces this error
...
ValueError: expected an element of List(Instance(GlyphRenderer)), got seq with invalid items [Band(id='1091', ...)]
For LegendItem only instances of GlyphRenderer can be passed to its renderers attribute and Band is not based on GlyphRenderer so it gives error. In the code below the Band visibility is being toggled by means of a callback:
from bokeh.plotting import figure, show
from bokeh.models import Band, ColumnDataSource, Legend, LegendItem, CustomJS
import pandas as pd
import numpy as np
x = np.random.random(2500) * 140 - 20
y = np.random.normal(size = 2500) * 2 + 5
df = pd.DataFrame(data = dict(x = x, y = y)).sort_values(by = "x")
sem = lambda x: x.std() / np.sqrt(x.size)
df2 = df.y.rolling(window = 100).agg({"y_mean": np.mean, "y_std": np.std, "y_sem": sem})
df2 = df2.fillna(method = 'bfill')
df = pd.concat([df, df2], axis = 1)
df['lower'] = df.y_mean - df.y_std
df['upper'] = df.y_mean + df.y_std
source = ColumnDataSource(df.reset_index())
p = figure(tools = "pan,wheel_zoom,box_zoom,reset,save")
scatter = p.scatter(x = 'x', y = 'y', line_color = None, fill_alpha = 0.3, size = 5, source = source)
band = Band(base = 'x', lower = 'lower', upper = 'upper', source = source)
p.add_layout(band)
p.title.text = "Rolling Standard Deviation"
p.xaxis.axis_label = 'X'
p.yaxis.axis_label = 'Y'
callback = CustomJS(args = dict(band = band), code = """
if (band.visible == false)
band.visible = true;
else
band.visible = false; """)
legend = Legend(items = [ LegendItem(label = "x", renderers = [scatter, band.source.selection_policy]) ])
legend.click_policy = 'hide'
scatter.js_on_change('visible', callback)
p.add_layout(legend)
show(p)
Result:
Related
I want to create a graph where I want to add range to the secondary y-axis. The graph which I have right now is like this.
The code for this graph is like this:
import plotly.offline as pyo
import plotly.graph_objs as go
from plotly import tools
from plotly.subplots import make_subplots
import pandas as pd
import xlwings as xw
import logging
fileName = 'solar data.xlsx'
app = xw.App(visible=False)
try:
wb = app.books.open(fileName)
sheet = wb.sheets[4]
lastCell = sheet.range('A1').end('down').last_cell.row
solarOne = sheet.range('B2:B'+ str(lastCell)).value
bessOne = sheet.range('D2:D'+ str(lastCell)).value
socOne = sheet.range('E2:E'+ str(lastCell)).value
solarOne_Plus_bessOne = [i+j for i,j in zip(solarOne,bessOne)]
# solarTwo = sheet.range('F2:F' + str(lastCell)).value
# bessTwo = sheet.range('H2:H' + str(lastCell)).value
# socTwo = sheet.range('I2:I' + str(lastCell)).value
# solarTwo_Plus_bessTwo = [i + j for i, j in zip(solarTwo, bessTwo)]
except Exception as e:
logging.exception("Something awful happened!")
print(e)
finally:
app.quit()
app.kill()
fig = go.Figure()
projectTime = pd.date_range("2020-10-01 00:00:00", "2020-10-01 23:59:00", freq="1T")
fig = make_subplots(specs=[[{"secondary_y": True}]])
fig.add_trace(go.Scatter(x = projectTime, y = solarOne, name = 'Solar', fill='tozeroy', line=dict(width=0.5, color='rgb(255,167,0)')), secondary_y=False)
fig.add_trace(go.Scatter(x = projectTime, y = bessOne, name = 'Storage', fill='tozeroy', line=dict(width=0.5, color='rgb(43, 201, 34)')), secondary_y=False)
fig.add_trace(go.Scatter(x = projectTime, y = socOne, name = 'SoC', fill='tozeroy', line=dict(width=0.5, color='rgb(250, 67, 67)')), secondary_y=True)
fig.add_trace(go.Scatter(x = projectTime, y = solarOne_Plus_bessOne, name = 'Solar + BESS', fill='tozeroy',), secondary_y=False)
# Add figure title
fig.update_layout(
title_text="Solar with 0.5MW 0.5Hour storage"
)
# Set x-axis title
fig.update_xaxes(title_text="Time")
# Set y-axes titles
fig.update_yaxes(title_text="Dispatch(MW) and SoC(MWh)")
pyo.plot(fig, filename='Solar with 0.5MW 0.5Hour storage example 1.html')
I have tried to add layout_yaxis_range=[-0.6, 0.7] in secondary y-axis but it throws an error
TypeError: add_trace() got an unexpected keyword argument 'layout_yaxis_range'
I want this secondary y-axis with the same x-axis. Can anyone please help?
This works for me in similar cases:
fig.update_yaxes(range=[-0.6, 0.7], secondary_y=True)
When you hover over the legend, all lines except the corresponding line will be hidden. How to achieve this via bokeh, python or javascript. I have no idea what to do to achieve this function. It would be great if we could provide a simple example with three lines.Thanks for your help.My code example is as follows:
import bokeh.palettes as bp
from bokeh.plotting import figure, output_file, show, ColumnDataSource
from bokeh.models import LinearAxis, Range1d, NumeralTickFormatter, Legend
from bokeh.layouts import column
import numpy as np
if __name__ == '__main__':
num = 5
color_list2 = bp.magma(num)
color_list1 = bp.viridis(num)
plotTools = 'box_zoom, wheel_zoom, pan, tap, crosshair, hover, reset, save'
p = figure(plot_width=800, plot_height=400, x_range=(0, 1000), y_range=(-2.5, -1.5),
tools=plotTools, toolbar_location='right', active_scroll='wheel_zoom', )
p.title.text = 'Hover and Hide'
items_c1 = []
i = 0
pictures = []
labels = ['a', 'b', 'c', 'd', 'e']
for label in labels:
n = np.random.randint(low=3, high=6)
xs = np.random.random(n) * 1000
y1s = np.random.random(n) - 2.5
temp_line = p.line(xs, y1s, line_width=2, color=color_list1[i % num],
alpha=0.3, hover_color='red', hover_alpha=0.9) # , legend_label=label
items_c1.append((label + '_BER', [temp_line]))
i = i + 1
if i % num == 0:
legend_1 = Legend(items=items_c1)
p.add_layout(legend_1, 'left')
p.xaxis.axis_label = 'run_time'
p.yaxis[0].axis_label = 'BER'
p.legend[0].orientation = 'vertical'
p.legend[0].location = 'bottom_center'
p.legend[0].click_policy = 'hide'
pictures.append(p)
p = figure(plot_width=800, plot_height=400, x_range=(0, 1000), y_range=(-2.5, -1.5),
tools=plotTools, toolbar_location='right', active_scroll='wheel_zoom', )
items_c1 = []
file = "test_ask_5"
file_path = file + '.html'
output_file(file_path)
show(column(pictures))
The solution below hides all lines but the one that is being clicked (not hovered). This code works for Bokeh v1.3.4
import numpy as np
from bokeh.plotting import figure, show
from bokeh.models import CustomJS
colors = ['orange', 'cyan', 'lightgreen']
p = figure()
lines = [p.line(np.arange(10), np.random.random(10), line_color = colors[i], line_width = 3, legend=colors[i], name=colors[i]) for i in range(3)]
code = '''if(Bokeh != 'undefined' && (Bokeh.active_line === 'undefined' || Bokeh.active_line == null))
{
Bokeh.active_line = cb_obj.name;
}'''
[line.js_on_change('visible', CustomJS(code = code)) for line in lines]
code = ''' for(i = 0; i < lines.length; i++) {
if (lines[i].name == Bokeh.active_line) {
lines[i].visible = true
}
else {
lines[i].visible = false
}
}
Bokeh.active_line = null'''
p.js_on_event('tap', CustomJS(args = {'lines' : lines}, code = code))
code = ''' for(i = 0; i < lines.length; i++) {
lines[i].visible = true
}
Bokeh.active_line = null'''
p.js_on_event('reset', CustomJS(args = dict(lines = lines), code = code))
p.legend.click_policy = 'hide'
show(p)
The first callback is applied to all glyph renderers (lines) and is being triggered when the line must be hidden, that is when a user clicks a legend item. The callback just sets the global variable Bokeh.active_line which remembers the renderer (line) name, e.g. "orange" or "cyan"
The second callback is attached to the plot canvas and is triggered every time the user clicks anywhere on the plot. What is basically does is inverting the glyphs (lines) visibility. It only shows the line specified by
Bokeh.active_line
The third callback is attached to the plot and is triggered when user clicks on the reset tool in the toolbar. It restores visibility of all lines.
I want to create a multiline Bokeh plot with datetime axis and a hover tool that shows the datetime of the data point. This should be supported and I have tried to obtain the intended behaviour in two ways:
Use hover.formatters to format the x-value. This has no effect on the plot.
Add a description variable with the correctly formatted date/time values. This results in a hover tool where all date/time values are displayed in a list for each point.
I have included a smaller example of my code that illustrates my approach and the result. It is used in conjunction with a checkboxgroup that updates the data. This is why a new ColumnDataSource is made from the dataframe.
import pandas as pd
import numpy as np
from bokeh.io import output_file, show
from bokeh.plotting import figure
from bokeh.models import HoverTool, ColumnDataSource
from bokeh.palettes import Spectral4
from bokeh.layouts import column
#output_file("demo.html")
available_quant = ["LACTIC_ACID", "GLUCOSE", "XYLOSE", "FORMIC_ACID"]
quant_legend = ["Lactic acid", "Glucose", "Xylose", "Formic acid"]
Create a dataframe with 4 quantities and the time
datelist = pd.date_range(end = pd.datetime.today(), periods=100).tolist()
desc = datelist
for i, date in enumerate(datelist):
desc[i] = str(date)
RT_x = np.linspace(-5, 5, num=100)
lactic = RT_x**2
data = {'time': datelist, 'desc': desc, 'LACTIC_ACID': RT_x**2 + 2, 'GLUCOSE': RT_x**2, 'XYLOSE': RT_x**2 - 2, 'FORMIC_ACID': RT_x**2 - 4}
df = pd.DataFrame.from_dict(data)
df['time'] = pd.to_datetime(df['time'], format = "%Y-%m-%d %H:%M:%S")
Copy the relevant data to a columndatasource
substance_colors = Spectral4
quant_to_plot = available_quant
xs = []
ys = []
xsprint = []
colors = []
labels = []
for i, substance in enumerate(quant_to_plot):
xs.append(list(df['time']))
ys.append(list(df[substance]))
xsprint.append(list(df['desc']))
index = available_quant.index(substance)
colors.append(substance_colors[index])
labels.append(quant_legend[index])
new_src = ColumnDataSource(data={'x': xs, 'y': ys, 'desc': xsprint, 'color': colors, 'label': labels})
Make the first plot using hover.formatters
p = figure(plot_width=800, plot_height=400, x_axis_type="datetime", title = 'Demo', x_axis_label = 'Time', y_axis_label = 'c [g/mL]')
p.multi_line('x','y', color = 'color', legend = 'label', source = new_src)
hover = HoverTool(tooltips=[('Type','#label'),
('Time','$x'),
('Conc','$y')],
formatters={'Time': 'datetime'},
mode = 'mouse',
line_policy='next')
p.add_tools(hover)
p.legend.location = "top_left"
p.legend.orientation = "horizontal"
Make second plot using description variable
p2 = figure(plot_width=800, plot_height=400, x_axis_type="datetime", title = 'Demo', x_axis_label = 'Time', y_axis_label = 'c [g/mL]')
p2.multi_line('x','y', color = 'color', legend = 'label', source = new_src)
hover = HoverTool(tooltips=[('Type','#label'),
('Time','#desc'),
('Conc','$y')],
mode = 'mouse',
line_policy='nearest')
p2.add_tools(hover)
mylayout = column(p, p2)
show(mylayout)
Am I missing something trivial? I am running Bokeh 0.13.0 and python 3.6.4.
The first approach works with the following modification of the hovertool:
hover = HoverTool(tooltips=[('Type','#label'),
('Time','$x{%F}'),
('Conc','$y')],
formatters={'$x': 'datetime'},
mode = 'mouse',
line_policy='nearest')
I am trying to produce a dashboard like interactions for my bar chart using callback function without using bokeh serve functionality. Ultimately, I would like to be able to change the plot if any of the two drop-down menus is changed. So far this only works when threshold value is hard-coded. I only know how to extract cb_obj value but not from dropdown that is not actually called. I have looked at this and this answer to formulate first attempt.
Here is my code:
from bokeh.io import show, output_notebook, output_file
from bokeh.models import ColumnDataSource, Whisker
from bokeh.plotting import figure
from bokeh.transform import factor_cmap
from bokeh.models import CustomJS, ColumnDataSource, Slider, Select
from bokeh.layouts import column
import numpy as np
import pandas as pd
def generate_data(factor=10):
rawdata = pd.DataFrame(np.random.rand(10,4)*factor, columns = ["A","B","C","D"])
idx = pd.MultiIndex.from_product([["Exp "+str(i) for i in range(5)],
[20,999]],names=["Experiment","Threshold"])
rawdata.index = idx
return rawdata.reset_index()
# Generate data
output_notebook()
count_data = generate_data()
error_data = generate_data(factor=2)
groups = ["A","B","C","D"]
initial_counts = count_data[(count_data.Experiment == "Exp 0")
& (count_data.Threshold == 20)][["A","B","C","D"]].values[0]
initial_errors = error_data[(error_data.Experiment == "Exp 0")
& (error_data.Threshold == 20)][["A","B","C","D"]].values[0]
# Create primary sources of data
count_source = ColumnDataSource(data=count_data)
error_source = ColumnDataSource(data=error_data)
# Create plotting source of data
source = ColumnDataSource(data=dict(groups=groups, counts=initial_counts,
upper=initial_counts+initial_errors,
lower=initial_counts-initial_errors))
# Bar chart and figure
p = figure(x_range=groups, plot_height=350, toolbar_location=None, title="Values", y_range=(0,20))
p.vbar(x='groups', top='counts', width=0.9, source=source, legend="groups",
line_color='white', fill_color=factor_cmap('groups', palette=["#962980","#295f96","#29966c","#968529"],
factors=groups))
# Error bars
p.add_layout(
Whisker(source=source, base="groups", upper="upper", lower="lower", level="overlay")
)
def callback(source=source, count_source = count_source, error_source=error_source, window=None):
def slicer(data_source, experiment, threshold, dummy_col, columns):
""" Helper function to enable lookup of data."""
count = 0
for row in data_source[dummy_col]:
if (data_source["Experiment"][count] == experiment) & (data_source["Threshold"][count] == threshold):
result = [data_source[col][count] for col in columns]
count+=1
return result
# Initialise data sources
data = source.data
count_data = count_source.data
error_data = error_source.data
# Initialise values
experiment = cb_obj.value
threshold = 20
counts, upper, lower = data["counts"], data["upper"], data["lower"]
tempdata = slicer(count_data, experiment, threshold,"Experiment", ["A","B","C","D"])
temperror = slicer(error_data, experiment, threshold,"Experiment", ["A","B","C","D"])
# Select values and emit changes
for i in range(len(counts)):
counts[i] = tempdata[i]
for i in range(len(counts)):
upper[i] = counts[i]+temperror[i]
lower[i] = counts[i]-temperror[i]
source.change.emit()
exp_dropdown = Select(title="Select:", value="Exp 0", options=list(count_data.Experiment.unique()))
thr_dropdown = Select(title="Select:", value="12", options=list(count_data.Threshold.astype(str).unique()))
exp_dropdown.callback = CustomJS.from_py_func(callback)
p.xgrid.grid_line_color = None
p.legend.orientation = "horizontal"
p.legend.location = "top_center"
layout = column(exp_dropdown,thr_dropdown, p)
show(layout)
The solution to the question is that Select menu needs to be defined before callback function. This code works:
from bokeh.io import show, output_notebook, output_file
from bokeh.models import ColumnDataSource, Whisker
from bokeh.plotting import figure
from bokeh.transform import factor_cmap
from bokeh.models import CustomJS, ColumnDataSource, Slider, Select
from bokeh.layouts import column
import numpy as np
import pandas as pd
def generate_data(factor=10):
rawdata = pd.DataFrame(np.random.rand(10,4)*factor, columns = ["A","B","C","D"])
idx = pd.MultiIndex.from_product([["Exp "+str(i) for i in range(5)],
[20,999]],names=["Experiment","Threshold"])
rawdata.index = idx
return rawdata.reset_index()
# Generate data
output_notebook()
count_data = generate_data()
error_data = generate_data(factor=2)
groups = ["A","B","C","D"]
initial_counts = count_data[(count_data.Experiment == "Exp 0")
& (count_data.Threshold == 20)][["A","B","C","D"]].values[0]
initial_errors = error_data[(error_data.Experiment == "Exp 0")
& (error_data.Threshold == 20)][["A","B","C","D"]].values[0]
# Create primary sources of data
count_source = ColumnDataSource(data=count_data)
error_source = ColumnDataSource(data=error_data)
# Create plotting source of data
source = ColumnDataSource(data=dict(groups=groups, counts=initial_counts,
upper=initial_counts+initial_errors,
lower=initial_counts-initial_errors))
# Bar chart and figure
p = figure(x_range=groups, plot_height=350, toolbar_location=None, title="Values", y_range=(0,20))
p.vbar(x='groups', top='counts', width=0.9, source=source, legend="groups",
line_color='white', fill_color=factor_cmap('groups', palette=["#962980","#295f96","#29966c","#968529"],
factors=groups))
# Error bars
p.add_layout(
Whisker(source=source, base="groups", upper="upper", lower="lower", level="overlay")
)
exp_dropdown = Select(title="Select:", value="Exp 0", options=list(count_data.Experiment.unique()))
thr_dropdown = Select(title="Select:", value="20", options=list(count_data.Threshold.astype(str).unique()))
def callback(source=source, count_source = count_source, error_source=error_source, exp_dropdown = exp_dropdown,
thr_dropdown=thr_dropdown,window=None):
def slicer(data_source, experiment, threshold, dummy_col, columns):
""" Helper function to enable lookup of data."""
count = 0
for row in data_source[dummy_col]:
if (data_source["Experiment"][count] == experiment) & (data_source["Threshold"][count] == threshold):
result = [data_source[col][count] for col in columns]
count+=1
return result
# Initialise data sources
data = source.data
count_data = count_source.data
error_data = error_source.data
# Initialise values
experiment = exp_dropdown.value
threshold = thr_dropdown.value
counts, upper, lower = data["counts"], data["upper"], data["lower"]
tempdata = slicer(count_data, experiment, threshold,"Experiment", ["A","B","C","D"])
temperror = slicer(error_data, experiment, threshold,"Experiment", ["A","B","C","D"])
# Select values and emit changes
for i in range(len(counts)):
counts[i] = tempdata[i]
for i in range(len(counts)):
upper[i] = counts[i]+temperror[i]
lower[i] = counts[i]-temperror[i]
source.change.emit()
exp_dropdown.callback = CustomJS.from_py_func(callback)
thr_dropdown.callback = CustomJS.from_py_func(callback)
p.xgrid.grid_line_color = None
p.legend.orientation = "horizontal"
p.legend.location = "top_center"
layout = column(exp_dropdown,thr_dropdown, p)
show(layout)
I'm trying to create a plotly graph with a Scatter and Graph elements. It all goes nicely, but one issue - the two Y axis don't align around 0.
I have tried playing with different attributes, such as 'mirror' and tick0, I also tried following the examples on plotly's site, but it's mostly multiple y-axis with the same graph type.
What can I do to fix this?
import utils
import pandas as pd
import plotly.plotly as py
import plotly.graph_objs as go
import plotly
pd_data ['dt'] = ... dates
pd_data['price'] = ... prices
pd_data['car'] = ... cars
price = go.Scatter(
x = pd_data['dt'],
y = pd_data['price'],
mode = 'lines',
name = 'Price',
xaxis = 'x',
yaxis='y1',
marker = dict(
color = utils.prep_color_string('orange'),
),
line = dict(
width = utils.line_width,
),
)
car = go.Bar(
x = pd_data['dt'],
y = pd_data['car'],
#mode = 'lines',
name = 'Cars',
xaxis = 'x',
yaxis='y2',
marker = dict(
color = utils.prep_color_string('light_green'),
),
#line = dict(
# width = utils.line_width,
#),
)
data = [price, car]
layout = dict(
title = 'Price/Car',
geo = dict(
showframe = True,
showcoastlines = True,
projection = dict(
type = 'Mercator'
)
),
yaxis=dict(
title = 'Price',
tickprefix = "$",
overlaying='y2',
anchor = 'x'
),
yaxis2=dict(
title = 'Car',
dtick = 1,
#tickprefix = "",
side = 'right',
anchor = 'x',
),
)
fig = dict( data=data, layout=layout)
div = plotly.offline.plot( fig, validate=False, output_type = 'file',filename='graph.html' ,auto_open = False)
I have been struggling with this as well. Exact same problem, but I am using R. The way I figured around it was to use the rangemode="tozero" for both the yaxis and yaxis2 layouts.
I think in your case, it would look like this:
layout = dict(
title = 'Price/Car',
geo = dict(
showframe = True,
showcoastlines = True,
projection = dict(
type = 'Mercator'
)
),
yaxis=dict(
title = 'Price',
tickprefix = "$",
overlaying='y2',
anchor = 'x',
rangemode='tozero'
),
yaxis2=dict(
title = 'Car',
dtick = 1,
#tickprefix = "",
side = 'right',
anchor = 'x',
rangemode = 'tozero'
),
)
Let me know if that works for you.