This is part of a series of questions related to this project: I have a dataset with 5000+ (x,y) coordinates divvied up into 1300+ groups. Using bokeh and its HoverTool function, I can plot the points and make it such that, when I hover over a given point, line segments appear between that point and every other point in that group. However, while I'm passable in python, I know precisely zero JavaScript.
TapTool Interaction with CustomJS
In addition to appearing when I hover over point, if I click on a point, I want those line segments to remain until I click somewhere else.
I know that bokeh has the TapTool, and I'm reasonably certain that, natively, the HoverTool and the TapTool can coexist quite happily. However, since the HoverTool functionality is implemented in JavaScript, I think that the TapTool functionality needs to be similarly constructed (though I'm happy to be proven wrong).
Here's what I've got so far:
# import packages
import pandas as pd
from bokeh.models import ColumnDataSource, CustomJS, HoverTool, TapTool
from bokeh.plotting import figure, output_file, show
# Read in the DataFrame
data = pd.read_csv("sanitized_data.csv")
# Generate (x, y) data from DataFrame
x = list(data["X"])
y = list(data["Y"])
# Generate dictionary of links; for each Col1, I want
# there to be a link between each Col3 and every other Col3 in the same Col1
links = {data["Col2"].index[index]:list(data[(data["Col1"]==data["Col1"][index]) & (data["Col2"]!=data["Col2"][index])].index) for index in data["Col2"].index}
# bokeh ColumnDataSource1 = (x, y) placeholder coordinates for start and end points of each link
source1 = ColumnDataSource({'x0': [], 'y0': [], 'x1': [], 'y1': []})
# bokeh ColumnDataSource2 = DataFrame, itself
source2 = ColumnDataSource(data)
# bokeh ColumnDataSource3 = associating Col2, Col1, and (x, y) coordinates
source3 = ColumnDataSource({"x":x,"y":y,'Col2':list(data['Col2']),'Col1':list(data['Col1'])})
# Set up Graph and Output file
output_file("sanitized_example.html")
p = figure(width=1900, height=700)
# Set up line segment and circle frameworks
sr = p.segment(x0='x0', y0='y0', x1='x1', y1='y1', color='red', alpha=0.4, line_width=.5, source=source1, )
cr = p.circle(x='x', y='y', color='blue', size=5, alpha=1, hover_color='orange', hover_alpha=1.0, source=source3)
# Add JS string for HoverTool Callback
code = """
const links = %s
const data = {'x0': [], 'y0': [], 'x1': [], 'y1': []}
const indices = cb_data.index.indices
for (let i = 0; i < indices.length; i++) {
const start = indices[i]
for (let j = 0; j < links[start].length; j++) {
const end = links[start][j]
data['x0'].push(circle.data.x[start])
data['y0'].push(circle.data.y[start])
data['x1'].push(circle.data.x[end])
data['y1'].push(circle.data.y[end])
}
}
segment.data = data
""" % links
# Add JS string for DataTable populating
# Establish Tooltips
TOOLTIPS = [("Col2","#Col2"), ("Col1","#Col1")]
# Create JavaScript Callbacks
callback = CustomJS(args={'circle': cr.data_source, 'segment': sr.data_source}, code=code)
# Add tools to the Graph
p.add_tools(HoverTool(tooltips=TOOLTIPS, callback=callback, renderers=[cr]))
p.add_tools(TapTool())
# Show the graph
show(p)
A dataset is available at the linked github page. Unfortunately, I don't even know where to start from here.
Edit: I found something! This SO post may have the answer I'm looking for.
Okay, a guy at work helped me out immensely: according to the documentation, when you use the HoverTool(callback) parameter, cb_data contains an index field. However, when you use the TapTool(callback) parameter, cb_data doesn't contain that field. Instead, the indices are accessed through the cb_obj object:
# import packages
import pandas as pd
from bokeh.models import ColumnDataSource, CustomJS, HoverTool, TapTool
from bokeh.plotting import figure, output_file, show
# Read in the DataFrame
data = pd.read_csv("sanitized_data.csv")
# Generate (x, y) data from DataFrame
x = list(data["X"])
y = list(data["Y"])
# Generate dictionary of links; for each Col1, I want
# there to be a link between each Col3 and every other Col3 in the same Col1
links = {data["Col2"].index[index]:list(data[(data["Col1"]==data["Col1"][index]) & (data["Col2"]!=data["Col2"][index])].index) for index in data["Col2"].index}
links_list = []
for key in links.keys():
links_list.append(links[key])
# bokeh ColumnDataSource1 = (x, y) placeholder coordinates for start and end points of each link
source1 = ColumnDataSource({'x0': [], 'y0': [], 'x1': [], 'y1': []})
# bokeh ColumnDataSource2 = DataFrame, itself
source2 = ColumnDataSource({'x0': [], 'y0': [], 'x1': [], 'y1': []})
# bokeh ColumnDataSource3 = associating Col2, Col1, and (x, y) coordinates
source3 = ColumnDataSource({"x":x,"y":y,'Col2':list(data['Col2']),'Col1':list(data['Col1']), 'Links':links_list})
# Set up Graph and Output file
output_file("sanitized_example.html")
p = figure(width=1900, height=700)
# Set up line segment and circle frameworks
sr1 = p.segment(x0='x0', y0='y0', x1='x1', y1='y1', color='red', alpha=0.4, line_width=.5, source=source1)
sr2 = p.segment(x0='x0', y0='y0', x1='x1', y1='y1', color='red', alpha=0.4, line_width=.5, source=source2)
cr = p.circle(x='x', y='y', color='blue', size=5, alpha=1, hover_color='orange', hover_alpha=1.0, source=source3)
# Add JS string for HoverTool Callback
code_for_hover = """
const links = %s
const data = {'x0': [], 'y0': [], 'x1': [], 'y1': []}
const indices = cb_data.index.indices
for (let i = 0; i < indices.length; i++) {
const start = indices[i]
for (let j = 0; j < links[start].length; j++) {
const end = links[start][j]
data['x0'].push(circle.data.x[start])
data['y0'].push(circle.data.y[start])
data['x1'].push(circle.data.x[end])
data['y1'].push(circle.data.y[end])
}
}
segment.data = data
""" % links
# Add JS string for Tap segments
code_for_tap = """
const links = %s;
const data = {'x0': [], 'y0': [], 'x1': [], 'y1': []};
const indices = cb_obj.indices;
for (let i = 0; i < indices.length; i++) {
const start = indices[i];
for (let j = 0; j < links[start].length; j++) {
const end = links[start][j];
data['x0'].push(circle.data.x[start]);
data['y0'].push(circle.data.y[start]);
data['x1'].push(circle.data.x[end]);
data['y1'].push(circle.data.y[end]);
}
}
segment.data = data;
""" % links
# Establish Tooltips
TOOLTIPS = [("Col2","#Col2"), ("Col1","#Col1")]
# Create JavaScript Callbacks
callback_on_hover = CustomJS(args={'circle': cr.data_source, 'segment': sr1.data_source}, code=code_for_hover)
callback_on_tap = CustomJS(args={'circle': cr.data_source, 'segment': sr2.data_source}, code=code_for_tap)
# Add tools to the Graph
p.add_tools(HoverTool(tooltips=TOOLTIPS, callback=callback_on_hover, renderers=[cr]))
p.add_tools(TapTool())
source3.selected.js_on_change('indices', callback_on_tap)
# Show the graph
show(p)
The only caveat is that it seems like I needed to have a duplicate ColumnDataSource so that the HoverTool and the TapTool can have unfettered access to the data without the other tool interfering.
Related
I have a very long mathematical formula expression (sympy_expr, obtained from Sympy) with a few free parameters. I want to make an interactive Bokeh widget with a few sliders for tuning of the free parameters, similarly to the example code below but with my long mathematical formula from Sympy instead of A*Math.sin(x[i]). I know how to convert the Sympy expression to a string in JS-format with jscode(sympy_expr). I do not know how to include the expression in the CustomJS callback function automatically (without copy-pasting, which would be horrible for readability). I am working with jupyter-notebook and would also be open to a jupyter-notebook solution. However, I'd prefer a html solution.
import numpy as np
from bokeh.layouts import column, row
from bokeh.models import CustomJS, Slider
from bokeh.plotting import ColumnDataSource, figure, show
x = np.linspace(0, 10, 500)
y = np.sin(x)
source = ColumnDataSource(data=dict(x=x, y=y))
plot = figure()
plot.line('x', 'y', source=source)
amp_slider = Slider(start=0.1, end=10, value=1, step=.1)
callback = CustomJS(args=dict(source=source, amp=amp_slider),
code="""
const data = source.data;
const A = amp.value;
const x = data['x']
const y = data['y']
for (let i = 0; i < x.length; i++) {
y[i] = A*Math.sin(x[i]);
}
source.change.emit();
""")
amp_slider.js_on_change('value', callback)
layout = row(
plot,
column(amp_slider),
)
show(layout)
Of course, I could make a python string code_str of the JS-code
code_str = 'const data = source.data; const A = amp.value; const x = data[\"x\"]; const y = data[\"y\"] for (let i = 0; i < x.length; i++) {y[i] = A*Math.sin(x[i]);}source.change.emit();'
and substitute the A*Math.sin(x[i]) segment with the math formula string jscode(sympy_expr) from Sympy, but that solution seems icky.
I'm relatively new to Python and Stackoverflow so apologies if this question has already been asked but I've searched for quite a while now and can't really find a solution to what I'm trying to do.
Problem:
I've been trying to create a very basic model of the COVID-19 epidemic to familiarise myself with Python. My intention is to produce a basic SIR model that calculates susceptible, infected and removed individuals in a population. So far so good.
My problem is that I would like the plot to have an interactive slider that alters one of the constants in the differential equation.
I am using Bokeh and am trying to use Javascript Callbacks for this however I am having difficulty with the Javascript. All examples I have seen so far use line equations where y is a function of x, and which are relatively straightforward to code. In my case, since its a system of differential equations I'm not sure how I should be going about this.
I've also tried using scipy but I still encounter the same problem.
Code below. Any help/feedback/suggestions would be greatly appreciated.
Thanks!
from bokeh.layouts import column, row
from bokeh.models import CustomJS, Slider
from bokeh.plotting import ColumnDataSource, figure, output_file, show
t = []
for i in range(200):
t.append(i)
slst = []
ilst = []
rlst = []
s = 489599/489609
i = 10/489609
r = 0/489609
bet = 0.28
gam = 0.189
for f in range(200):
ds = (-bet * (s * i))
di = ((bet * (s * i)) - (gam * i))
dr = gam * i
s = s + (ds)
slst.append(s)
i = i + (di)
ilst.append(i)
r = r + (dr)
rlst.append(r)
source = ColumnDataSource(data=dict(x=t, y=[], s=slst, i=ilst))
plot = figure(plot_width=400, plot_height=400)
plot.line('x', 'y', source=source, line_width=3, line_alpha=0.6)
callback = CustomJS(args=dict(source=source), code="""
????????????
""")
slider = Slider(start=0.1, end=4, value=1, step=.1, title="Beta ")
slider.js_on_change('value', callback)
layout = column(slider, plot)
show(layout)
Here's what I've come up with. It's interesting to see that with high values of beta, the susceptible line goes below 0. Maybe I've made a mistake while porting your code to JavaScript - please correct me if so.
from bokeh.core.property.instance import Instance
from bokeh.io import save
from bokeh.layouts import column
from bokeh.model import Model
from bokeh.models import CustomJS, Slider, Callback
from bokeh.plotting import ColumnDataSource, figure
source = ColumnDataSource(data=dict(t=[], s=[], i=[], r=[]))
plot = figure(plot_width=400, plot_height=400)
plot.line('t', 's', source=source, line_width=3, line_alpha=0.6)
plot.line('t', 'i', source=source, line_width=3, line_alpha=0.6, color='orange')
plot.line('t', 'r', source=source, line_width=3, line_alpha=0.6, color='green')
callback = CustomJS(args=dict(source=source), code="""\
const N = 200;
let s = 489599 / 489609;
let i = 10 / 489609;
let r = 0 / 489609;
const bet = cb_obj.value;
const gam = 0.189;
const tlst = source.data.t = [];
const slst = source.data.s = [];
const ilst = source.data.i = [];
const rlst = source.data.r = [];
for (let t = 0; t < N; ++t) {
s -= bet * s * i;
i += bet * s * i - gam * i;
r += gam * i;
tlst.push(t);
slst.push(s);
ilst.push(i);
rlst.push(r);
}
source.change.emit();
""")
slider = Slider(start=0.1, end=4, value=1, step=.1, title="Beta ")
slider.js_on_change('value', callback)
class IdleDocObserver(Model):
"""Work around https://github.com/bokeh/bokeh/issues/4272."""
on_idle = Instance(Callback)
# language=TypeScript
__implementation__ = """\
import {View} from "core/view"
import {Model} from "model"
import * as p from "core/properties"
export class IdleDocObserverView extends View {}
export namespace IdleDocObserver {
export type Attrs = p.AttrsOf<Props>
export type Props = Model.Props & {on_idle: p.Property<any>}
}
export interface IdleDocObserver extends IdleDocObserver.Attrs {}
export class IdleDocObserver extends Model {
static init_IdleDocObserver(): void {
this.prototype.default_view = IdleDocObserverView
this.define<IdleDocObserver.Props>({on_idle: [p.Any]})
}
_doc_attached(): void {
super._doc_attached()
const execute = () => this.on_idle!.execute(this)
if (this.document!.is_idle)
execute();
else
this.document!.idle.connect(execute);
}
}
"""
idle_doc_observer = IdleDocObserver(on_idle=CustomJS(args=dict(callback=callback, slider=slider),
code="callback.execute(slider);"))
layout = column(slider, plot)
save([idle_doc_observer, layout])
As per my comments, one can use RK4 in a javascript implementation to integrate the ODE in the html document. There appears no way in bokeh to implement javascript functions outside of callbacks, utility functions and common computations. So to avoid code duplication one has to make the one callback universal enough that it can serve for all slider change events. (Alternatively, one could implement a "recompute" button.)
To look more professional, make 2 plots, one for all components, and one for I alone.
# Set up the plots and their data source
source = ColumnDataSource(data=dict(T=[], S=[], I=[], R=[]))
SIR_plot = figure(plot_width=400, plot_height=400)
SIR_plot.line('T', 'S', source=source, legend_label="S", line_width=3, line_alpha=0.6, color='blue')
SIR_plot.line('T', 'I', source=source, legend_label="I", line_width=3, line_alpha=0.6, color='orange')
SIR_plot.line('T', 'R', source=source, legend_label="R", line_width=3, line_alpha=0.6, color='green')
I_plot = figure(plot_width=400, plot_height=400)
I_plot.line('T', 'I', source=source, line_width=3, line_alpha=0.6, color='orange')
Next set up 4 sliders for parameters one is likely to want to influence
# declare the interactive interface elements
trans_rate = Slider(start=0.01, end=0.4, value=0.3, step=.01, title="transmission rate ")
recov_rate = Slider(start=0.01, end=0.4, value=0.1, step=.01, title="recovery rate")
I_init = Slider(start=0.01, end=0.1, value=0.05, step=.002, title="initial infected [proportion] ")
max_time = Slider(start=10, end=200, value=50, step=1, title="time range [days] ")
Now as the main change to the answer of Eugene Pakhomov, make one call-back for all sliders (see bokeh gallery slider demo) and use a vectorized RK4 method to do the ODE integration
callback = CustomJS(args=dict(source=source, I_init=I_init, max_time=max_time,
trans_rate=trans_rate, recov_rate=recov_rate),
code="""\
let i = I_init.value;
let s = 1-i;
let r = 0;
const bet = trans_rate.value;
const gam = recov_rate.value;
let tf = max_time.value;
const dt = 0.1;
const tlst = source.data.T = [0];
const slst = source.data.S = [s];
const ilst = source.data.I = [i];
const rlst = source.data.R = [r];
function odefunc(t,sir) {
let tr = bet*sir[0]*sir[1];
let rc = gam*sir[1];
return [-tr, tr-rc, rc];
}
let sir = [s,i,r];
for (let t = 0; t < tf; t+=dt) {
sir = RK4Step(t,sir,dt);
tlst.push(t+dt);
slst.push(sir[0]);
ilst.push(sir[1]);
rlst.push(sir[2]);
}
source.change.emit();
function axpy(a,x,y) {
// returns a*x+y for arrays x,y of the same length
var k = y.length >>> 0;
var res = new Array(k);
while(k-->0) { res[k] = y[k] + a*x[k]; }
return res;
}
function RK4Step(t,y,h) {
var k0 = odefunc(t , y );
var k1 = odefunc(t+0.5*h, axpy(0.5*h,k0,y));
var k2 = odefunc(t+0.5*h, axpy(0.5*h,k1,y));
var k3 = odefunc(t+ h, axpy( h,k2,y));
// ynext = y+h/6*(k0+2*k1+2*k2+k3);
return axpy(h/6,axpy(1,k0,axpy(2,k1,axpy(2,k2,k3))),y);
}
""")
trans_rate.js_on_change('value', callback)
recov_rate.js_on_change('value', callback)
I_init.js_on_change('value', callback)
max_time.js_on_change('value', callback)
Finally, string all together in some layout
# generate the layout
parameters_panel = column(trans_rate, recov_rate)
initials_panel = column(I_init,max_time)
plots = row(SIR_plot, I_plot)
inputs = row(parameters_panel, initials_panel)
simulation = column(plots, inputs)
show(simulation)
To avoid the initial empty plots I refer to the answer of Eugene Pakhomov, as it is the plots appear after the first slider is moved.
I am making standalone HTML objects, and I would like to know how to use more commands than the ones like Math.sin(), etc, when using CustomJS. Ideally I would like to use flexx to write my callback in python language, and pass an array created with numpy to the callback. My immediate concern is the inclusion of complex numbers, and my more general direction would involve fft code.
In a jupyter notebook, using Python 3, I can run the following:
#created by Kyle Johnston
import numpy as np
from ipywidgets import interact
from bokeh.models.widgets import Panel, Tabs
from bokeh.plotting import figure, output_file, show, save
from bokeh.models.layouts import Row, Column, WidgetBox
from bokeh.io import output_notebook, show, push_notebook, save, file_html, output_file
from bokeh.layouts import gridplot,layout
from bokeh.models import ColumnDataSource, HoverTool, CustomJS, Range1d, Slider
from bokeh.palettes import Spectral4
################## Defintions #########################################
c= 3e8
IR = 2000e-9
VIS = 2000e-9
t = np.linspace(-200.*1e-15,200.*1e-15,2000)
E_IR=1; w_IR= c*2*np.pi/IR; tau_IR= 70.*1e-15
E_VIS=1; w_VIS= c*2*np.pi/VIS; tau_VIS= 6*1e-15
E_field_IR = E_IR*np.exp(1j*t*w_IR)*np.exp(-2*np.log(2)*(t/tau_IR)**2)
E_field_VIS = E_VIS*np.exp(1j*t*w_VIS)*np.exp(-2*np.log(2)*(t/tau_VIS)**2)
E_f_IR_Real = np.real(E_field_IR)
E_f_VIS_Real = np.real(E_field_VIS)
tau_1= tau_IR; w_1=w_IR;
tau_2= tau_VIS; w_2=w_VIS;
x1 = t
x2 = t
y1 = np.sin(2*np.pi*c*x1/IR)*np.exp(-(x1/tau_1)**2)
y2 = np.sin(2*np.pi*c*x2/IR)*np.exp(-(x1/tau_2)**2)
y3 = y1+y2
# y1 = np.sin(w_1*x1)
# y2 = np.sin(w_2*x2)
source1 = ColumnDataSource(data=dict(x1=x1, y1=y1))
source2 = ColumnDataSource(data=dict(x2=x2, y2=y2))
source3 = ColumnDataSource(data=dict(x2=x2, y3=y3))
################ Figures ##############################################
p = figure(y_range=(-5, 5), plot_width=700, plot_height=600)
p.line('x1', 'y1', source=source1, line_width=3, line_alpha=1,\
color='green',legend='wave1')
p.line('x2', 'y2', source=source2, line_width=3, line_alpha=1,\
color='blue',legend='wave2')
p.line('x2', 'y3', source=source3, line_width=2, line_alpha=0.5,\
color='purple',legend='sum')
################# Callback to update figures ##########################################
callback = CustomJS(args=dict(source1=source1,source2=source2,source3=source3), code="""
var data1 = source1.data;
var data2 = source2.data;
var data3 = source3.data;
var delay = 1e-15*delay.value;
var A1 = amp1.value;
var k1 = wave1.value;
var phi1 = phase1.value;
var B1 = offset1.value;
var tau1 = 1e-15*tau1.value;
var A2 = amp2.value;
var k2 = wave2.value;
var phi2 = phase2.value;
var B2 = offset2.value;
var tau2 = 1e-15*tau2.value;
x1 = data1['x1']
y1 = data1['y1']
x2 = data2['x2']
y2 = data2['y2']
y3 = data3['y3']
for (i = 0; i < x1.length; i++) {
y1[i] = B1+A1*Math.sin(phi1+3e8*2*Math.PI*(delay+x1[i])/(k1*1e-9))*Math.exp(-Math.pow((delay+x1[i])/tau1,2));
y2[i] = B2+A2*Math.sin(phi2+3e8*2*Math.PI*x2[i]/(k2*1e-9))*Math.exp(-Math.pow(x2[i]/tau2,2));
y3[i] = y1[i]+y2[i];
}
source1.change.emit();
""")
delay_slider_1 = Slider(start=-400, end=400, value=0, step=1,
title="Delay (fs)", callback=callback)
callback.args["delay"] = delay_slider_1
amp_slider_1 = Slider(start=0.1, end=1, value=1, step=.1,
title="Amplitude 1", callback=callback)callback.args["amp1"] = amp_slider_1
wave_slider_1 = Slider(start=700, end=3000, value=2000, step=10,
title="Wavelength 1 (nm)", callback=callback)
callback.args["wave1"] = wave_slider_1
phase_slider_1 = Slider(start=-10*np.pi, end=10*np.pi, value=0,
step=np.pi/10, title="Phase 1", callback=callback)
callback.args["phase1"] = phase_slider_1
offset_slider_1= Slider(start=-5, end=5, value=0, step=.1,
title="Offset 1", callback=callback)
callback.args["offset1"] = offset_slider_1
tau_slider_1 = Slider(start=1, end=100, value=70, step=1,
title="tau 1 (fs)", callback=callback)
callback.args["tau1"] = tau_slider_1
######################### Wave 2 ###################################
amp_slider_2 = Slider(start=0.1, end=1, value=1, step=.1,
title="Amplitude 2", callback=callback)
callback.args["amp2"] = amp_slider_2
wave_slider_2 = Slider(start=700, end=3000, value=2000, step=10,
title="Wavelength 2 (nm)", callback=callback)
callback.args["wave2"] = wave_slider_2
phase_slider_2 = Slider(start=-10*np.pi, end=10*np.pi, value=0,
step=np.pi/10,title="Phase 2", callback=callback)
callback.args["phase2"] = phase_slider_2
offset_slider_2 = Slider(start=-5, end=5, value=0, step=.1,
title="Offset 2", callback=callback)
callback.args["offset2"] = offset_slider_2
tau_slider_2 = Slider(start=1, end=100, value=5, step=1,
title="tau 2 (fs)", callback=callback)
callback.args["tau2"] = tau_slider_2
###################### Make Layout and activate clickable plot #######################################
p.legend.location = "top_left"
p.legend.click_policy="hide"
p.y_range=Range1d(-2.1,2.1)
p.x_range
p.xaxis.axis_label = 'time(s)'
p.yaxis.axis_label = 'Amplitude (arb)'
l = layout(Column(
p,WidgetBox(delay_slider_1),
Row(Column(WidgetBox(amp_slider_1, wave_slider_1, phase_slider_1, \
offset_slider_1, tau_slider_1)),
Column(WidgetBox(amp_slider_2, wave_slider_2, phase_slider_2,\
offset_slider_2, tau_slider_2)))
))
show(l)
and this produces a wonderful interactive plot.
You can see that I rewrote the math in the callback because I do not know how to include complex numbers in the callback. I would like to know how to link the apache commons math package into my notebook, and/or how to create this using numpy inside the callback. As far as I can see, flexx enables python scripting inside the callback for everything EXCEPT the actual math portions.
Is something like jpype the thing I need to use?
I hope the example above helps someone, and that someone can help me.
P.S. Also, as this is my first ever question, I hope the working example is complete. Do I really have to go line by line and add 4 spaces, or is there a quicker way?
I have a lasso and a hover tool in bokeh, each with a similar callback to interact with a secondary plot (the hover will display meta data associated with a single data point, while the lasso will display the same meta data averaged across points).
The callbacks work individually for hover and for lasso, but when both are active, the hover dominates. I'd like to make is so that the user can choose either lasso or hover but not both.
Is there a way to trigger a callback when the active tool is changed, preferably in CustomJS so I don't have to run bokeh server?
Below is some sample code and here is a link to the output.
from bokeh.io import output_file
from bokeh.layouts import row
from bokeh.plotting import figure, show
from bokeh.models import (
ColumnDataSource, CustomJS,
HoverTool, LassoSelectTool)
output_file('toggle_lasso_hover.html')
s1 = ColumnDataSource({
'x': [0, 1, 2],
'y': [1, 1, 1]
})
s2 = ColumnDataSource({
'x': [],
'y': []
})
js_args = {'s1': s1, 's2': s2}
js_code = """
var d1 = s1.data;
var d2 = {'x': [], 'y': []};
for (i=0; i < inds.length; i++) {
j = inds[i];
d2['x'].push(d1['x'][j]);
d2['y'].push(d1['y'][j]);
}
s2.data = d2;
s2.change.emit();
"""
on_hover = CustomJS(args=js_args, code="""
var inds = cb_data.index['1d'].indices;
%s
""" % js_code)
on_lasso = CustomJS(args=js_args, code="""
var inds = cb_obj.selected['1d'].indices;
%s
""" % js_code)
p1 = figure(
width=300, height=300, tools="reset", active_drag=None)
p1.circle('x', 'y', source=s1, size=20, color='blue')
# define tools
hover = HoverTool(tooltips=None, callback=on_hover)
lasso = LassoSelectTool()
p1.add_tools(hover)
p1.add_tools(lasso)
s1.callback = on_lasso
p2 = figure(
width=300, height=300, x_range=p1.x_range, y_range=p1.y_range,
toolbar_location=None)
p2.circle('x', 'y', source=s2, size=20, color='green')
lout = row(p1, p2)
show(lout)
I posted a similar question earlier (Retrieving R object attributes in JavaScript). In that earlier post, I oversimplified my MWE, and so the answer I rewarded unfortunately does not really apply to my real problem. Here, I am showing why I may need to retrieve R object attributes in JavaScript (unless there is another option that I am not aware of).
I have a 5-variable dataset with 100 observations. I used hexagon binning and created a scatterplot matrix. Each of the 10 scatterplots has somewhere between 12-18 hexagons. In order to save the rows of the 100 observations that are in each of the hexagon bins for all 10 scatterplots, I used the base::attr function in R. In the code below, this is done at:
attr(hexdf, "cID") <- h#cID
I am trying to create an interactive R Plotly object of the hexagon binning so that if a user were to click on a given hexagon bin (regardless of which scatterplot), they would obtain the rows of the 100 observations that were grouped into that bin. I have part of this goal completed. My MWE is below:
library(plotly)
library(data.table)
library(GGally)
library(hexbin)
library(htmlwidgets)
set.seed(1)
bindata <- data.frame(ID = paste0("ID",1:100), A=rnorm(100), B=rnorm(100), C=rnorm(100), D=rnorm(100), E=rnorm(100))
bindata$ID <- as.character(bindata$ID)
maxVal = max(abs(bindata[,2:6]))
maxRange = c(-1*maxVal, maxVal)
my_fn <- function(data, mapping, ...){
x = data[,c(as.character(mapping$x))]
y = data[,c(as.character(mapping$y))]
h <- hexbin(x=x, y=y, xbins=5, shape=1, IDs=TRUE, xbnds=maxRange, ybnds=maxRange)
hexdf <- data.frame (hcell2xy (h), hexID = h#cell, counts = h#count)
attr(hexdf, "cID") <- h#cID
p <- ggplot(hexdf, aes(x=x, y=y, fill = counts, hexID=hexID)) + geom_hex(stat="identity")
p
}
p <- ggpairs(bindata[,2:6], lower = list(continuous = my_fn))
pS <- p
for(i in 2:p$nrow) {
for(j in 1:(i-1)) {
pS[i,j] <- p[i,j] +
coord_cartesian(xlim = c(maxRange[1], maxRange[2]), ylim = c(maxRange[1], maxRange[2]))
}
}
ggPS <- ggplotly(pS)
myLength <- length(ggPS[["x"]][["data"]])
for (i in 1:myLength){
item =ggPS[["x"]][["data"]][[i]]$text[1]
if (!is.null(item))
if (!startsWith(item, "co")){
ggPS[["x"]][["data"]][[i]]$hoverinfo <- "none"
}
}
ggPS %>% onRender("
function(el, x, data) {
el = el;
x=x;
var data = data[0];
console.log(el)
console.log(x)
console.log(data)
myLength = Math.sqrt(document.getElementsByClassName('cartesianlayer')[0].childNodes.length);
console.log(myLength)
el.on('plotly_click', function(e) {
console.log(e.points[0])
xVar = (e.points[0].xaxis._id).replace(/[^0-9]/g,'')
if (xVar.length == 0) xVar = 1
yVar = (e.points[0].yaxis._id).replace(/[^0-9]/g,'')
if (yVar.length == 0) yVar = 1
myX = myLength + 1 - (yVar - myLength * (xVar - 1))
myY = xVar
cN = e.points[0].curveNumber
split1 = (x.data[cN].text).split(' ')
hexID = (x.data[cN].text).split(' ')[2]
counts = split1[1].split('<')[0]
console.log(myX)
console.log(myY)
console.log(hexID)
console.log(counts)
})}
", data = pS[5,2]$data)
This creates an image as shown below:
As an example, if I click on the hexagon highlighted in the green box, I can determine which subplot it occurred in ("myX" and "myY"), the ID of the hexagon clicked ("hexID"), and the number of observation points that were binned into that hexagon ("counts"). For this particular hexagon, myX=5, myY=2, hexID=39, and counts=1. So, the user just clicked on hexagon with ID39 in the scatterplot on the fifth row and second column and there should be 1 data point that it binned.
If I leave the onRender() function, and simply type into R the following code:
myX <- 5
myY <- 2
hexID <- 39
obsns <- which(attr(pS[myX,myY]$data, "cID")==hexID)
dat <- bindata[obsns,]
Then, I can obtain the row of the data frame that contains the one observation that was binned into that clicked hexagon:
> dat
ID A B C D E
95 ID95 1.586833 -1.208083 1.778429 -0.1101588 3.810277
My problem is simply in this last step. I am unable to figure out how to use the base::attr() function from within the onRender() function in order to obtain the "obsns" object. Is there any workaround for this issue, or another possible approach I should consider taking? Thank you for any ideas/advice!
I'm not sure you can access the hex IDs from plotly or whether it keeps this data somewhere so one option is to pass all the data needed for your purpose to the onRender function.
First you could add to your bindata dataframe a column per hexplot, called mX-mY (where you replace mX and mY by their value for each column), that would hold for each observation the hexbin it belongs to for that plot:
for(i in 2:5) {
for(j in 1:4) {
bindata[[paste(i,j,sep="-")]] <- attr(pS[i,j]$data, "cID")
}
}
You can then pass bindata to the onRender function and whever you click on a hexagon in one of the plot, check in the corresponding column in bindata which observations belong to that hexbin:
ggPS %>% onRender("
function(el, x, data) {
myLength = Math.sqrt(document.getElementsByClassName('cartesianlayer')[0].childNodes.length);
el.on('plotly_click', function(e) {
xVar = (e.points[0].xaxis._id).replace(/[^0-9]/g,'')
if (xVar.length == 0) xVar = 1
yVar = (e.points[0].yaxis._id).replace(/[^0-9]/g,'')
if (yVar.length == 0) yVar = 1
myX = myLength + 1 - (yVar - myLength * (xVar - 1))
myY = xVar
cN = e.points[0].curveNumber
split1 = (x.data[cN].text).split(' ')
hexID = (x.data[cN].text).split(' ')[2]
counts = split1[1].split('<')[0]
var selected_rows = [];
data.forEach(function(row){
if(row[myX+'-'+myY]==hexID) selected_rows.push(row);
});
console.log(selected_rows);
})}
", data = bindata)