How to combine CustomJS for HoverTool with CustomJS for TapTool

Howdy. This topic is a continuation of the discussion originating in this topic:

I have a graph with 5300 nodes in 1300 groups. The group with the most members has 1300 nodes in it.

#1: Right now, I’ve successfully repurposed the code in this example to make it so that when you hover over a node, a line segment appears between that node and every other node that’s in the same group (see below); you can use the dataset from my github, or you can copy the truncated data from my earlier question:

# 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)

#2: I have successfully modified the code from this StackOverflow question to make it so that, in addition to the Tooltop displaying when hovering over a point, if you click a point, the Tooltip remains until you click somewhere else:

from bokeh.models import ColumnDataSource, CustomJS, Div, HoverTool, Row,TapTool
from bokeh.plotting import figure, output_file, show


output_file("toolbar.html")

source5 = ColumnDataSource(data = dict(
xo = [1, 2, 3, 3, 4],
yo = [2, 5, 8, 3, 6],
desc = ['A', 'B', 'C', 'D', 'E'],
imgs = ['https://docs.bokeh.org/static/snake.jpg',
        'https://docs.bokeh.org/static/snake2.png',
        'https://dods.bokeh.org/static/snake3D.png',
        'https://docs.bokeh.org/static/snake4_TheRevenge.png',
        'https://docs.bokeh.org/static/snakebite.jpg'],
fonts = ['<i>italics</i>',
         '<pre>pre</pre>',
         '<b>bold</b>',
         '<small>small</small>',
         '<del>del</del>' ]))

TOOLTIPS2 = """
<div>
    <div>
        <img
            src="@imgs" height="84" alt="@imgs" width="84"
            style="float: left; margin: 0px 15px 15px 0px;"
            border="2"/>
    </div>
    <div width=60px>
        <span style="font-size: 17px; font-weight: bold;">@desc</span>
        <span style="font-size: 15px; color: #966;">[$index]</span>
    </div>
    <div>
        <span>@fonts{safe}</span>
    </div>
    <div>
        <span style="font-size: 15px;">Location</span>
        <span style="font-size: 10px; color: #696;">($x, $y)</span>
    </div>
</div> """

p2 = figure(plot_width = 400, plot_height = 400, x_range = (0, 6), y_range = (1, 9),
      title = "Mouse over the dots", tools = 'pan,wheel_zoom,save,reset,tap')
circles = p2.circle('xo', 'yo', size = 20, source = source5)
div = Div(text = '<div id="tooltip" style="position: absolute; display: none"></div>', name = 'tooltip')

code2 = '''  if (cb_data.source.selected.indices.length > 0){
                var selected_index = cb_data.source.selected.indices[0];
                var tooltip = document.getElementById("tooltip");
                tooltip.style.display = 'block';
                tooltip.style.left = Number(cb_data.geometries.sx) + Number(20) + 'px';
                tooltip.style.top = Number(cb_data.geometries.sy) - Number(20) + 'px';
                tp = tp.replace('@imgs', cb_data.source.data.imgs[selected_index]);
                tp = tp.replace('@desc', cb_data.source.data.desc[selected_index]);
                tp = tp.replace('@fonts{safe}', cb_data.source.data.fonts[selected_index]);
                tp = tp.replace('$index', selected_index);
                tp = tp.replace('$x', Math.round(cb_data.geometries.x));
                tp = tp.replace('$y', Math.round(cb_data.geometries.y));
                tooltip.innerHTML = tp;
          } '''
p2.select(TapTool).callback = CustomJS(args = {'circle': circles, 'plot': p2, 'tp': TOOLTIPS2}, code = code2)
#p2.select(TapTool).callback = CustomJS(args = {'circles': circles, 'plot': p2}, code = code2)
p2.add_tools(HoverTool(tooltips=TOOLTIPS2))

source5.selected.js_on_change('indices', CustomJS(code = 'if (cb_obj.indices.length == 0) document.getElementById("tooltip").style.display = \"none\"'))
layout = Row(p2, div)
show(layout)

Now my question is this: how do I combine them? I want the lines to appear when I hover over a point, but when I click that point, I want the lines to remain until I click somewhere else. I’ve tried just copying the CustomJS code for the HoverTool callback in the first example into the TapTool CustomJS callback, but that doesn’t seem to do anything, and I don’t know anything about JavaScript, so I don’t know how to go about debugging what’s wrong.

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.