Rony
Rony

Reputation: 25

JavaScript callback to get selected glyph index in Bokeh

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!

Upvotes: 1

Views: 3935

Answers (1)

bigreddot
bigreddot

Reputation: 34568

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://docs.bokeh.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.

Upvotes: 1

Related Questions