I have Pandas DataFrame that is similar to this:
person activities hours foodeaten
0 1 a 3 12
1 1 b 4 14
2 1 c 6 34
3 2 a 2 45
4 2 b 7 67
5 3 a 5 5
6 3 b 3 -1
7 3 c 2 3
8 3 d 12 5
I would like to plot the hours spent by a person on a activity, per activity. so, plot 1: activity a, with x=persons y=hours for activity a plot 2: activity b, with x=persons y=hours for activity b etc.
I want to link the plots based on persons, not on index. So when I select the circle for person 1 in plot 1, this person's hours in the other plots should highlight, for my purpose as well.
MWE:
from bokeh.io import output_notebook, show, output_file, reset_output
from bokeh.plotting import figure
from bokeh.layouts import gridplot
from bokeh.models import ColumnDataSource, CDSView, BooleanFilter
import pandas as pd
# data
data = {'person':[1,1,1,2,2,3,3,3,3],'activities':['a','b','c','a','b','a','b','c','d'], 'hours':[3,4,6,2,7,5,3,2,12],
'foodeaten':[12,14,34,45,67,5,-1,3,5]}
df = pd.DataFrame(data=data)
source = ColumnDataSource(data=df)
# filtering the data on activity
v1 = df.activities=='a'
v2 = df.activities=='b'
v3 = df.activities=='c'
v4 = df.activities=='d'
# creating a view to filter the source data on activity
view1 = CDSView(source=source, filters=[BooleanFilter(v1.values.tolist())])
view2 = CDSView(source=source, filters=[BooleanFilter(v2.values.tolist())])
view3 = CDSView(source=source, filters=[BooleanFilter(v3.values.tolist())])
view4 = CDSView(source=source, filters=[BooleanFilter(v4.values.tolist())])
# Plot options
tools = 'pan,box_select,lasso_select,help,poly_select,hover,wheel_zoom,reset'
plot_width = 300
plot_height = 300
TOOLTIPS = [("Person", "@person"),
("hours", "@hours"),]
plot_options = dict(plot_width=plot_width, plot_height=plot_height, tools =tools, tooltips= TOOLTIPS)
# plotting
p1 = figure(title ='activity a',**plot_options)
p1.circle('person', y='hours', size=15, view=view1, source=source)
plot_options['x_range']= p1.x_range
p2 = figure(title ='activity b',**plot_options)
p2.circle('person', y='hours', size=15, view=view2, source=source)
p3 = figure(title ='activity c',**plot_options)
p3.circle('person', y='hours', size=15, view=view3, source=source)
p4 = figure(title ='activity d',**plot_options)
p4.circle('person', y='hours', size=15, view=view4, source=source)
p12 = figure(title ='activity a',**plot_options)
p12.circle('person', y='foodeaten', size=15, view=view1, source=source)
plot_options['x_range']= p1.x_range
p22 = figure(title ='activity b',**plot_options)
p22.circle('person', y='foodeaten', size=15, view=view2, source=source)
p32 = figure(title ='activity c',**plot_options)
p32.circle('person', y='foodeaten', size=15, view=view3, source=source)
p42 = figure(title ='activity d',**plot_options)
p42.circle('person', y='foodeaten', size=15, view=view4, source=source)
p = gridplot([[p1,p12],[p2,p22],[p3,p32],[p4,p42]])
output_file('test.html')
show(p)
This example shows the linking behaviour, row wise, but I would like to highlight everything for a person by selecting an attribute of this person in a graph.
Here is a working example for arbitrary number of plots:
from bokeh.plotting import figure, show
from bokeh.layouts import gridplot
from bokeh.models import ColumnDataSource, CDSView, BooleanFilter, CustomJS
import pandas as pd
data = {'person': [1, 1, 1, 2, 2, 3, 3, 3, 3], 'activities':['a', 'b', 'c', 'a', 'b', 'a', 'b', 'c', 'd'], 'hours':[3, 4, 6, 2, 7, 5, 3, 2, 12], 'foodeaten':[12, 14, 34, 45, 67, 5, -1, 3, 5]}
df = pd.DataFrame(data = data)
source = ColumnDataSource(data = df)
views = [(df.activities == l) for l in ['a', 'b', 'c', 'd']]
filtered_views = [CDSView(source = source, filters = [BooleanFilter(view.values.tolist())]) for view in views]
plot_options = dict(plot_width = 250, plot_height = 250, tools = "tap,pan,wheel_zoom,reset,save", tooltips = [("Person", "@person"), ("hours", "@hours")])
plots = [figure(title = 'activity {l}'.format(l = l), **plot_options) for l in ['a', 'b', 'c', 'd', 'a', 'b', 'c', 'd']]
[plot.circle('person', y = name, size = 15, view = view, source = source) for plot, view, name in zip(plots, 2 * filtered_views, 4 * ['hours'] + 4 * ['foodeaten'])]
callback = CustomJS(args = dict(source = source, plots = plots), code = """
const selected_index = source.selected.indices[0]
const person = source.data['person'][selected_index]
var all_selected = [];
for (index in source.data['index']){
if (source.data['person'][index] == person)
all_selected.push(index)
}
source.selected.indices = all_selected; """)
[plot.js_on_event('tap', callback) for plot in plots]
show(gridplot(children = [plot for plot in plots], ncols = 2))
Bokeh does not have anything built in for this kind of automatic linking. However, it is possible to update the selection of one glyph based on the selection of another, using CustomJS
callbacks:
from bokeh.io import show
from bokeh.layouts import row
from bokeh.models import CustomJS
from bokeh.plotting import figure
p1 = figure(plot_width=300, plot_height=300, tools="tap")
r1 = p1.circle(x=[1, 2], y=1, color=["red", "blue"], size=20)
p2 = figure(plot_width=300, plot_height=300, tools="")
r2 = p2.circle(x=[1, 1, 2], y=[1, 2, 1.5], color=["red", "red", "blue"], size=20)
callback = CustomJS(args=dict(s2=r2.data_source), code="""
const s2_inds = []
if (cb_obj.indices.indexOf(0) >= 0) {
s2_inds.push(0)
s2_inds.push(1)
}
if (cb_obj.indices.indexOf(1) >= 0) {
s2_inds.push(2)
}
s2.selected.indices = s2_inds
"""))
show(row(p1, p2))
With this code, selecting the left red circle will select all the right red circles, and the same for the blue:
Note that the code above explicitly and manually hand-codes the relationship between the indices, in order to illustrate the general technique (i.e. it specifies a table by hand). You will probably want to use knowledge and assumptions about your specific data to craft a more general CustomJS
callback that can compute which indices to set automatically.
来源:https://stackoverflow.com/questions/54984453/bokeh-linking-brushing-based-on-column-instead-of-row-indices-index