I've created a visual graph using Bokeh that shows a network I created using Networkx. I now want to use TapTool to show information pertinent to any node on the graph that I click on. The graph is just nodes and edges. I know I should be able to use var inds = cb_obj.selected['1d'].indices;
in the JavaScript callback function to get the indices of the nodes (glyphs) that were clicked on, but that's not working somehow and I get the error message, Uncaught TypeError: Cannot read property '1d' of undefined
. A nudge in the right direction would be greatly appreciated.
Below is my code. Please note that I've defined my plot as a Plot() and not as a figure(). I don't think that's the reason for the issue, but just wanted to mention it. Also, I'm using window.alert(inds);
just to see what values I get. That's not my ultimate purpose, but I expect that bit to work anyway.
def draw_graph_____(self, my_network):
self.graph_height, self.graph_width, self.graph_nodes, self.graph_edges, self.node_coords, self.node_levels = self.compute_graph_layout(my_network)
graph = nx.DiGraph()
graph.add_nodes_from(self.graph_nodes)
graph.add_edges_from(self.graph_edges)
plot = Plot(plot_width = self.graph_width, plot_height = self.graph_height, x_range = Range1d(0.0, 1.0), y_range = Range1d(0.0, 1.0))
plot.title.text = "Graph Demonstration"
graph_renderer = from_networkx(graph, self.graph_layout, scale = 1, center = (-100, 100))
graph_renderer.node_renderer.data_source.data["node_names"] = self.graph_nodes
graph_renderer.node_renderer.data_source.data["index"] = self.graph_nodes
graph_renderer.node_renderer.glyph = Circle(size = 40, fill_color = Spectral4[0])
graph_renderer.node_renderer.selection_glyph = Circle(size = 40, fill_color = Spectral4[2])
graph_renderer.node_renderer.hover_glyph = Circle(size = 40, fill_color = Spectral4[1])
graph_renderer.edge_renderer.glyph = MultiLine(line_color = "#CCCCCC", line_alpha = 0.8, line_width = 5)
graph_renderer.edge_renderer.selection_glyph = MultiLine(line_color = Spectral4[2], line_width = 5)
graph_renderer.edge_renderer.hover_glyph = MultiLine(line_color = Spectral4[1], line_width = 5)
graph_renderer.selection_policy = NodesAndLinkedEdges()
graph_renderer.inspection_policy = NodesAndLinkedEdges()
x_coord = [coord[0] for coord in self.node_coords]
y_coord = [coord[1] for coord in self.node_coords]
y_offset = []
for level in self.node_levels:
for item in self.node_levels[level]:
if self.node_levels[level].index(item) % 2 == 0:
y_offset.append(20)
else:
y_offset.append(-40)
graph_renderer.node_renderer.data_source.data["x_coord"] = x_coord
graph_renderer.node_renderer.data_source.data["y_coord"] = y_coord
graph_renderer.node_renderer.data_source.data["y_offset"] = y_offset
labels_source = graph_renderer.node_renderer.data_source
labels = LabelSet(x = "x_coord", y = "y_coord", text = 'node_names', text_font_size = "12pt", level = 'glyph',
x_offset = -50, y_offset = "y_offset", source = labels_source, render_mode = 'canvas')
plot.add_layout(labels)
callback = CustomJS(args = dict(source = graph_renderer.node_renderer.data_source), code =
"""
console.log(cb_obj)
var inds = cb_obj.selected['1d'].indices;
window.alert(inds);
""")
plot.add_tools(HoverTool(tooltips = [("Node", "@node_names"), ("Recomm", "Will put a sample recommendation message here later")]))
plot.add_tools(TapTool(callback = callback))
plot.renderers.append(graph_renderer)
output_file("interactive_graphs.html")
show(plot)
My imports are as follows, by the way:
import collections
import networkx as nx
import numpy as np
from bokeh.io import output_file, show
from bokeh.models import Circle, ColumnDataSource, CustomJS, Div, HoverTool, LabelSet, MultiLine, OpenURL, Plot, Range1d, TapTool
from bokeh.models.graphs import from_networkx, NodesAndLinkedEdges
from bokeh.palettes import Spectral4
I'm sorry for not posting entire code, but that would require quite a few changes to make dummy data and show other files and functions (which I should have), but I thought just this one function may suffice for the identification of the issue. If not, I'm happy to share more code. Thanks!
The problem is that the callback is not attached to a data source. The value of cb_obj
is whatever object triggers the callback. But only ColumnDataSource
objects have a selected
property, so only callbacks on data sources will have cb_obj.selected
. If you are wanting to have a callback fire whenever a selection changes, i.e. whenever a node is clicked on, then you'd want to have the callback on the data source. [1]
However, if you want to have a callback when a node is merely hovered over (but not clicked on) that is an inspection, not a selection. You will want to follow this example:
https://bokeh.pydata.org/en/latest/docs/user_guide/interaction/callbacks.html#customjs-for-hover
Although it is not often used (and thus not documented terribly well) the callback for hover tools gets passed additional information in a cb_data
parameter. This cb_data
parameter is used as a catch-all mechanism for tools to be able to pass extra things, specific to the tool, on to the callback. In the case of hover tools, cb_data
is an object that has .index
and .geometry
attributes. So cb_data.index['1d'].indices
has the indices of the points that are currently hovered over. The .geometry
attribute as information about the kind of hit test that was performed (i.e. was a single point? or a vertical or horizontal span? And what was the location of the point or span?)
[1] Alternatively, tap tools also pass a specialized cb_data
as described above. It is an object with a .source
property that the the data source that made a selection. So cb_data.source.selected
should work. In practice I never use this though, since a callback on the data source works equally well.
来源:https://stackoverflow.com/questions/47640995/javascript-callback-to-get-selected-glyph-index-in-bokeh