Plotly: How to add dropdown menu for every subplot? - button

I need to create two subplots with a dropdown menu and title for each graph. (side-by-side comparison). In addition, I 'd like to have a shared y-axis.
As for now, I have only one dropdown menu that change both graphs.
The code is following: (note that a df consists of 2 columns and datetimeindex).
import plotly.offline as py
import plotly.graph_objs as go
from plotly.offline import init_notebook_mode, iplot, plot
from plotly import tools
labels = ["Vol", "R"]
fig = tools.make_subplots(rows=1, cols=2)
trace1 = go.Scatter(x=df.index,
y=df['Stock1'].rolling(window=12).std(),
mode='lines'
)
trace2 = go.Scatter(x=df.index,
y=df['Stock1'],
mode='lines'
)
fig.append_trace(trace1, 1, 1)
fig.append_trace(trace2, 1, 1)
trace1 = go.Scatter(x=df.index,
y=df['Stock2'].rolling(window=12).std(),
mode='lines'
)
trace2 = go.Scatter(x=df.index,
y=df['Stock2'],
mode='lines'
)
fig.append_trace(trace1, 1, 2)
fig.append_trace(trace2, 1, 2)
# Create buttons for drop down menu
buttons = []
for i, label in enumerate(labels):
visibility = [i==j for j in range(len(labels))]
button = dict(
label = label,
method = 'update',
args = [{'visible': visibility},
{'title': label}])
buttons.append(button)
updatemenus = list([
dict(active=-1,
x=-0.15,
buttons=buttons
)
])
fig['layout']['title'] = 'Title'
fig['layout']['showlegend'] = False
fig['layout']['updatemenus'] = updatemenus
iplot(fig, filename='dropdown')

According to empet's helpful answer in the Plotly forum, the important thing to know is that the visible key has length equal to the total number of traces in fig.data.
In your case, you have four traces that correspond to Stock 1 (Vol), Stock 1 (R), Stock 2 (Vol) and Stock 2 (R) which is the order in which you added these traces. So we can create 4 buttons to toggle the visibility feature of each trace and pass them as a list to the updatemenus dictionary.
import numpy as np
import pandas as pd
import plotly.offline as py
import plotly.graph_objs as go
from plotly.offline import init_notebook_mode, iplot, plot
from plotly import tools
## recreate some random data
np.random.seed(42)
df = pd.DataFrame(data=np.random.randint(0,100,(365,2)), columns=['Stock1','Stock2'], index=pd.date_range(start='1/1/2019', end='12/31/2019'))
labels = ["Vol", "R"]
fig = tools.make_subplots(rows=1, cols=2)
trace1 = go.Scatter(x=df.index,
y=df['Stock1'].rolling(window=12).std(),
mode='lines'
)
trace2 = go.Scatter(x=df.index,
y=df['Stock1'],
mode='lines'
)
fig.append_trace(trace1, 1, 1)
fig.append_trace(trace2, 1, 1)
trace1 = go.Scatter(x=df.index,
y=df['Stock2'].rolling(window=12).std(),
mode='lines'
)
trace2 = go.Scatter(x=df.index,
y=df['Stock2'],
mode='lines'
)
fig.append_trace(trace1, 1, 2)
fig.append_trace(trace2, 1, 2)
## visible key for traces are in the order you append them
button1 = dict(method='update',
args=[{"visible": [True, False, False, False] }],
label="Stock 1, Vol" )
button2 = dict(method='update',
args=[{"visible": [False, True, False, False] }],
label="Stock 1, R" )
button3 = dict(method='update',
args=[{"visible": [False, False, True, False] }],
label="Stock 2, Vol" )
button4 = dict(method='update',
args=[{"visible": [False, False, False, True] }],
label="Stock 2, R" )
updatemenus = list([
dict(active=-1,
x=-0.15,
buttons=[button1, button2, button3, button4]
)
])
fig['layout']['title'] = 'Title'
fig['layout']['showlegend'] = False
fig['layout']['updatemenus'] = updatemenus
iplot(fig, filename='dropdown')

Related

Once a dropdown option is selected, how do I "change.emit" or "trigger change" on the plot?

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)

Bokeh: Duplicate factor or sub factor error with CustomJS change of x_range

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; """)

Slider based on Networkx node attribute value with Bokeh

I am attempting to develop a slider which will limit the number of nodes visible in a network graph based on the value of a node's attribute. The below Pandas DataFrame (df) represents the nodes, and the node's associated attributes (count information).
source target source_count target_count
A C 15 10
A D 15 20
A E 15 30
B F 25 10
B G 25 20
B H 25 30
I have used the following code to generate a network graph for the nodes and their associated attributes.
import pandas as pd
from bokeh.layouts import column, widgetbox,layout,
from bokeh.plotting import figure, show, output_file,
from bokeh.models import HoverTool, value,PanTool, LabelSet, Legend, ColumnDataSource,Circle,Plot, Range1d, MultiLineBoxSelectTool,ResetTool,LassoSelectTool,Slider
from bokeh.models.callbacks import CustomJS
from bokeh.models.graphs import from_networkx, NodesAndLinkedEdges, EdgesAndLinkedNodes
df = pd.DataFrame({
"source":["A", "A", "A", "B", "B","B"],
"target":["C", "D", "E", "F", "G","H"],
"source_count":["15", "15", "15", "25","25","25"]
"target_count":["10", "20", "30", "10","20","30"]
})
net_graph = from_pandas_edgelist(df, 'source', 'target')
#assign attributes
for index, row in df.iterrows():
net_graph.nodes[row['source']]['yearly_count'] = row['source_count']
net_graph.nodes[row['target']]['yearly_count'] = row['target_count']
graph_plot= Plot(plot_width=800, plot_height=600,
x_range=Range1d(-1.1, 1.1), y_range=Range1d(-1.1, 1.1))
node_hover_tool = HoverTool(tooltips=[("Name", "#index"),("Yearly Count", "#yearly_count")])
graph_plot.add_tools(node_hover_tool)
graph_setup = from_networkx(net_graph, nx.spring_layout, scale=1, center=(0, 0))
graph_setup.node_renderer.glyph = Circle(size=20,fill_color = 'blue')
graph_setup.edge_renderer.glyph = MultiLine(line_color="red", line_alpha=0.8, line_width=1)
graph_plot.renderers.append(graph_setup)
output_file("test_1.html")
show(graph_plot)
The slider I am trying to would use the yearly_count attribute to limit the number of nodes on display. I know that Bokeh allows the embedding of a JavaScript Callback, however, I have not seen a use-case integrated with NetworkX.
Any assistance that anyone could provide would be greatly appreciated.
If you can run your app with bokeh serve then I would try:
from bokeh.models import Slider
graph_plot= Plot()
graph_setup.node_renderer.glyph = Circle()
graph_setup.edge_renderer.glyph = MultiLine()
def callback(attr, old, new):
//filter your data here to show less nodes and edges based
graph_setup.node_renderer.data_source.data = data
graph_setup.edge_renderer.data_source.data = data
slider = Slider()
slider.on_change('value', callback)
If you want to run a Bokeh standalone app then replace slider callback with:
code = """
//filter your data here to show less nodes and edges
graph_setup.node_renderer.data_source.data = data;
graph_setup.edge_renderer.data_source.data = data; """
callback = CustomJS(args = dict(graph_setup = graph_setup, data = data), code = code)
slider = Slider()
slider.js_on_change('value', callback)
See complete JS callback example below:
import networkx as nx
from bokeh.io import show, output_file
from bokeh.models import Plot, Range1d, MultiLine, Circle, TapTool, OpenURL, HoverTool, CustomJS, Slider, Column
from bokeh.models.graphs import from_networkx, EdgesAndLinkedNodes
from bokeh.palettes import Spectral4
from dask.dataframe.core import DataFrame
import pandas as pd
data = {'source': ['A', 'A', 'A', 'A', 'A', 'A'], 'target': ['C', 'D', 'E', 'F', 'G', 'H'], 'source_count': [15, 15, 15, 25, 25, 25], 'target_count': [10, 20, 30, 10, 20, 30]}
df = pd.DataFrame(data)
net_graph = nx.from_pandas_edgelist(df, 'source', 'target')
for index, row in df.iterrows():
net_graph.nodes[row['source']]['yearly_count'] = row['source_count']
net_graph.nodes[row['target']]['yearly_count'] = row['target_count']
graph_plot = Plot(plot_width = 800, plot_height = 600, x_range = Range1d(-1.1, 1.1), y_range = Range1d(-1.1, 1.1))
node_hover_tool = HoverTool(tooltips = [("Name", "#index"), ("Yearly Count", "#yearly_count")])
graph_plot.add_tools(node_hover_tool)
graph_setup = from_networkx(net_graph, nx.spring_layout, scale = 1, center = (0, 0))
graph_setup.node_renderer.glyph = Circle(size = 20, fill_color = 'blue')
graph_setup.edge_renderer.glyph = MultiLine(line_color = "red", line_alpha = 0.8, line_width = 1)
graph_plot.renderers.append(graph_setup)
code = """
var new_start = start.slice();
var new_end = end.slice();
new_index = end.slice();
new_start = new_start.splice(0, cb_obj.value)
new_end = new_end.splice(0, cb_obj.value)
new_index = ['A'].concat(new_end)
new_data_edge = {'start': new_start, 'end': new_end};
new_data_nodes = {'index': new_index};
graph_setup.edge_renderer.data_source.data = new_data_edge;
graph_setup.node_renderer.data_source.data = new_data_nodes;
"""
callback = CustomJS(args = dict(graph_setup = graph_setup,
start = df['source'].values,
end = df['target'].values), code = code)
slider = Slider(title = 'Slider', start = 1, end = 6, value = 6)
slider.js_on_change('value', callback)
layout = Column(graph_plot, slider)
show(layout)
Result:
Newer versions of Bokeh uses strict mode for JavaScript (see release log), which implies that code from Tony's accepted answer does not work for Bokeh version 2.0.0 and upwards. Only a few small explicit declarations of variables are needed for the code to work for newer Bokeh versions:
code = '''
var new_start = start.slice();
var new_end = end.slice();
var new_index = end.slice();
new_start = new_start.splice(0, cb_obj.value)
new_end = new_end.splice(0, cb_obj.value)
new_index = ['A'].concat(new_end)
var new_data_edge = {'start': new_start, 'end': new_end};
var new_data_nodes = {'index': new_index};
graph_setup.edge_renderer.data_source.data = new_data_edge;
graph_setup.node_renderer.data_source.data = new_data_nodes;
'''

"ValueError: year is out of range" when attempting to use matplotlib pyplot

I am attempting to get a matplotlib plotting function to be able to produce a graph with the x-axis set as a time axis. However, when I attempt to plot some values against UNIX times, I encounter the error ValueError: year is out of range. What is going wrong and how can it be addressed?
import os
import time
import matplotlib.dates
import matplotlib.pyplot
import shijian
def main():
data = [
[1484611200.0, 844.4333],
[1484524800.0, 783.3373],
[1484438400.0, 774.194 ],
[1484352000.0, 769.2299]
]
save_graph_matplotlib(
values = data,
line = True,
line_width = 0.5,
title_axis_x = "time",
title_axis_y = "value",
#time_axis_x = True
)
def save_graph_matplotlib(
values = None,
title = None,
title_axis_x = None,
title_axis_y = None,
filename = None,
directory = ".",
overwrite = True,
color = "black",
LaTeX = False,
markers = True,
marker_size = 1,
aspect = None,
line = False,
line_style = "-",
line_width = 0.2,
font_size = 20,
scientific_notation = False,
time_axis_x = False
):
# 1D or 2D data
if isinstance(values[0], list):
x = [element[0] for element in values]
y = [element[1] for element in values]
else:
x = range(0, len(values))
y = values
matplotlib.pyplot.ioff()
if LaTeX is True:
matplotlib.pyplot.rc("text", usetex = True)
matplotlib.pyplot.rc("font", family = "serif")
if filename is None:
if title is None:
filename = "graph.png"
else:
filename = shijian.propose_filename(
filename = title + ".png",
overwrite = overwrite
)
else:
filename = shijian.propose_filename(
filename = filename,
overwrite = overwrite
)
figure = matplotlib.pyplot.figure()
if title is not None:
figure.suptitle(
title,
fontsize = font_size
)
if markers is True:
matplotlib.pyplot.scatter(
x,
y,
s = marker_size,
c = color,
edgecolors = "none",
)
if line is True:
matplotlib.pyplot.plot(
x,
y,
line_style,
c = color,
linewidth = line_width
)
# Turn on or off axes scientific notation.
if scientific_notation is False:
matplotlib.pyplot.gca().get_xaxis().\
get_major_formatter().set_scientific(False)
matplotlib.pyplot.gca().get_yaxis().\
get_major_formatter().set_scientific(False)
# Set axes titles.
if title_axis_x is not None:
matplotlib.pyplot.xlabel(title_axis_x, fontsize = font_size)
if title_axis_y is not None:
matplotlib.pyplot.ylabel(title_axis_y, fontsize = font_size)
# Set axes font size.
matplotlib.pyplot.xticks(fontsize = font_size)
matplotlib.pyplot.yticks(fontsize = font_size)
# Set or do not set axis x as time.
if time_axis_x:
time_formatter = matplotlib.dates.DateFormatter("%Y-%m-%d")
matplotlib.pyplot.axes().xaxis_date()
matplotlib.pyplot.axes().xaxis.set_major_formatter(time_formatter)
matplotlib.pyplot.xticks(rotation = -90)
# Set the aspect ratio.
if aspect is None:
matplotlib.pyplot.axes().set_aspect(
1 / matplotlib.pyplot.axes().get_data_ratio()
)
else:
matplotlib.pyplot.axes().set_aspect(aspect)
if not os.path.exists(directory):
os.makedirs(directory)
matplotlib.pyplot.savefig(
directory + "/" + filename,
dpi = 700
)
matplotlib.pyplot.close()
if __name__ == "__main__":
main()
You need to convert your timestamp-like x data to a python datetime object, which can then be used in matplotlib and be understood by the matplotlib.dates.DateFormatter.
This can be done using the datetime.datetime.fromtimestamp() method.
import datetime
import matplotlib.dates
import matplotlib.pyplot as plt
data = [
[1484611200.0, 844.4333],
[1484524800.0, 783.3373],
[1484438400.0, 774.194 ],
[1484352000.0, 769.2299]
]
x = [datetime.datetime.fromtimestamp(element[0]) for element in data]
y = [element[1] for element in data]
plt.plot( x, y, ls="-", c= "b", linewidth = 2 )
plt.xlabel("Dates")
time_formatter = matplotlib.dates.DateFormatter("%Y-%m-%d")
plt.axes().xaxis.set_major_formatter(time_formatter)
plt.axes().xaxis_date() # this is not actually necessary
plt.show()
Whilst not directly addressing the text of the question, the error mentioned in the title can also occur when one attempts to plot data on an existing axis whose timeline units don't match those of the plot data (e.g. seconds vs datetime).

How to switch active device with gWidgets tabbed notebook?

I have created a two-tab GUI with gWidgets. A graph is embedded in each of the two tabs. The problem is that both graphs are sent to tab 2. How can I switch or choose the active device? The vignette of the gwidgets package suggests either addHandlerChanged or ggraphicsnotebook. In using ggraphicsnotebook, some unwanted buttons are generated. So I am wondering how to do it with addHandlerChanged or other methods. A small example is attached. Thanks.
# library
library(gWidgets); library(cairoDevice)
options(guiToolkit = "RGtk2")
# first tab -----------------------------------------------------------
r.main <- gwindow(title = "Correlation", visible = TRUE)
r.nb <- gnotebook(container = r.main)
rSta <- ggroup(container = r.nb, horizontal = TRUE, label = "Static")
rDyn <- ggroup(container = r.nb, horizontal = FALSE, label = "Dynamic")
ggraphics(container = rSta)
obj.ptNum <- gradio(items = c("100", "1,000", "5,000"),
selected = 2, horizontal = FALSE, container = rSta,
handler = function(h, ...) {plot(1:10, main = "Static graph")})
# second tab -----------------------------------------------
obj.plotNum <- gradio(items = c("10", "50", "300", "400"),
selected = 2, horizontal = TRUE, container = rDyn,
handler = function(h, ...) {
plot(30:35, col = 'red', main = "Dynamic graph")})
ggraphics(container = rDyn)
Edit: I found one solution. Note the problem is not about selecting tab, but about selecting the current active embedded device. For future reference, my code is copied below. One small problem that I cannot figure out: how to show the graph when the application is launched without clicking? Thanks all.
library(gWidgets); library(cairoDevice); library(gWidgetsRGtk2)
library(RGtk2)
options(guiToolkit = "RGtk2")
# first tab -----------------------------------------------------------
r.main <- gwindow(title = "Correlation", visible = TRUE)
r.nb <- gnotebook(container = r.main)
rSta <- ggroup(container = r.nb, horizontal = TRUE, label = "Static")
ggraphics(container = rSta)
staPlot <- function(h, ...) {
sel <- ifelse(test = length(dev.list()) >= 2, yes = 2, no = 1)
dev.set(which = sel)
plot(1:as.numeric(svalue(obj.ptNum)), main = "Static graph")}
obj.ptNum <- gradio(items = c("100", "150", "200"),
selected = 2, horizontal = FALSE, container = rSta,
handler = staPlot)
# second tab -----------------------------------------------
rDyn <- ggroup(container = r.nb, horizontal = FALSE, label = "Dynamic")
dynPlot <- function(h, ...) {
sel <- ifelse(test = length(dev.list()) >= 2, yes = 3, no = 1)
dev.set(which = sel)
plot(1:as.numeric(svalue(obj.plotNum)), col = 'red',
main = "Dynamic graph")}
obj.plotNum <- gradio(items = c("10", "50", "300", "400"),
selected = 2, horizontal = TRUE, container = rDyn,
handler = dynPlot)
ggraphics(container = rDyn)
svalue(r.nb) <- 1
It's bad to use dev.set. If you to create a plot in a graphics device, you can use the following code.
graph <- ggraphics()
visible(graph) <- TRUE
boxplot(...)

Resources