Bokeh & CustomJS: integrate HoverTool + CustomJS with TapTool

Question:

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.

Asked By: John Doe

||

Answers:

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.

Answered By: John Doe
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.