Bokeh has added native support for creating network graph visualizations with configurable interactions between edges and nodes.
The key feature of the GraphRenderer is that it maintains separate sub-GlyphRenderers for the graph nodes and the graph edges. This allows for customizing the nodes by modifying the GraphRenderer’s node_renderer property. It’s possible to replace the default Circle node glyph with any XYGlyph instance, for example a Rect or Ellipse glyph. Similarly, the style properties of the edges can be modified through the edge_renderer property. The edge glyph is currently limited to a MultiLine glyph.
GraphRenderer
node_renderer
edge_renderer
There are a couple of requirements for the data sources belonging to these sub-renderers:
The ColumnDataSource associated with the node sub-renderer must have a column named "index" that contains the unique indices of the nodes.
"index"
The ColumnDataSource associated with the edge sub-renderer has two required columns: "start" and "end". These columns contain the node indices for the start and end of the edges.
"start"
"end"
It’s possible to add extra meta-data to these data sources to in order to add vectorized glyph styling or make data available for callbacks or hover tooltips.
Here’s a code snippet that:
replaces the node glyph with an Ellipse
sets the height and width attributes of the Ellipse as scalar values
height
width
sets the fill_color attribute of the Ellipse as a vectorized field and adds the values to the node data source.
fill_color
import math from bokeh.plotting import figure from bokeh.models import GraphRenderer, Ellipse from bokeh.palettes import Spectral8 N = 8 node_indices = list(range(N)) plot = figure(title="Graph Layout Demonstration", x_range=(-1.1,1.1), y_range=(-1.1,1.1), tools="", toolbar_location=None) graph = GraphRenderer() graph.node_renderer.glyph = Ellipse(height=0.1, width=0.2, fill_color="fill_color") graph.node_renderer.data_source.data = dict( index=node_indices, fill_color=Spectral8) graph.edge_renderer.data_source.data = dict( start=[0]*N, end=node_indices)
No graph will be rendered by running the above code snippet because we haven’t specified how to arrange the graph in 2D space. You can learn how to do that in the following section.
Bokeh uses a separate LayoutProvider model in order to supply the coordinates of a graph in Cartesian space. Currently, the only built-in provider is the StaticLayoutProvider model, which contains a dictionary of (x,y) coordinates for the nodes.
LayoutProvider
StaticLayoutProvider
This example adds a provider to the above code snippet:
import math from bokeh.io import output_file, show from bokeh.models import Ellipse, GraphRenderer, StaticLayoutProvider from bokeh.palettes import Spectral8 from bokeh.plotting import figure N = 8 node_indices = list(range(N)) plot = figure(title='Graph Layout Demonstration', x_range=(-1.1,1.1), y_range=(-1.1,1.1), tools='', toolbar_location=None) graph = GraphRenderer() graph.node_renderer.data_source.add(node_indices, 'index') graph.node_renderer.data_source.add(Spectral8, 'color') graph.node_renderer.glyph = Ellipse(height=0.1, width=0.2, fill_color='color') graph.edge_renderer.data_source.data = dict( start=[0]*N, end=node_indices) ### start of layout code circ = [i*2*math.pi/8 for i in node_indices] x = [math.cos(i) for i in circ] y = [math.sin(i) for i in circ] graph_layout = dict(zip(node_indices, zip(x, y))) graph.layout_provider = StaticLayoutProvider(graph_layout=graph_layout) plot.renderers.append(graph) output_file('graph.html') show(plot)
By default, the StaticLayoutProvider will draw straight-line paths between the supplied node positions. In order to supply explicit edge paths, you may also supply lists of paths to the edge_renderer bokeh.models.sources.ColumnDataSource. The StaticLayoutProvider will look for these paths on the "xs" and "ys" columns of the data source. Note that these paths should be in the same order as the "start" and "end" points. Also note that there is no validation that they match up with the node positions, so be extra careful when setting explicit paths.
bokeh.models.sources.ColumnDataSource
"xs"
"ys"
This example extends the example from above to draw quadratic bezier paths between the nodes:
import math from bokeh.io import output_file, show from bokeh.models import Ellipse, GraphRenderer, StaticLayoutProvider from bokeh.palettes import Spectral8 from bokeh.plotting import figure N = 8 node_indices = list(range(N)) plot = figure(title="Graph Layout Demonstration", x_range=(-1.1,1.1), y_range=(-1.1,1.1), tools="", toolbar_location=None) graph = GraphRenderer() graph.node_renderer.data_source.add(node_indices, 'index') graph.node_renderer.data_source.add(Spectral8, 'color') graph.node_renderer.glyph = Ellipse(height=0.1, width=0.2, fill_color="color") graph.edge_renderer.data_source.data = dict( start=[0]*N, end=node_indices) ### start of layout code circ = [i*2*math.pi/8 for i in node_indices] x = [math.cos(i) for i in circ] y = [math.sin(i) for i in circ] graph_layout = dict(zip(node_indices, zip(x, y))) graph.layout_provider = StaticLayoutProvider(graph_layout=graph_layout) ### Draw quadratic bezier paths def bezier(start, end, control, steps): return [(1-s)**2*start + 2*(1-s)*s*control + s**2*end for s in steps] xs, ys = [], [] sx, sy = graph_layout[0] steps = [i/100. for i in range(100)] for node_index in node_indices: ex, ey = graph_layout[node_index] xs.append(bezier(sx, ex, 0, steps)) ys.append(bezier(sy, ey, 0, steps)) graph.edge_renderer.data_source.data['xs'] = xs graph.edge_renderer.data_source.data['ys'] = ys plot.renderers.append(graph) output_file("graph.html") show(plot)
Bokeh supports quickly plotting a network graph with its networkx integration. The bokeh.plotting.from_networkx convenience method accepts a networkx.Graph object and a networkx layout method in order to return a configured GraphRenderer instance.
bokeh.plotting.from_networkx
networkx.Graph
Here is an example of using the networkx.spring_layout method to layout networkx’s built-in “Zachary’s Karate Club graph” dataset:
networkx.spring_layout
import networkx as nx from bokeh.io import output_file, show from bokeh.plotting import figure, from_networkx G = nx.karate_club_graph() plot = figure(title="Networkx Integration Demonstration", x_range=(-1.1,1.1), y_range=(-1.1,1.1), tools="", toolbar_location=None) graph = from_networkx(G, nx.spring_layout, scale=2, center=(0,0)) plot.renderers.append(graph) output_file("networkx_graph.html") show(plot)
It’s possible to configure the selection or inspection behavior of graphs by setting the GraphRenderer’s selection_policy and inspection_policy attributes. These policy attributes accept a special GraphHitTestPolicy model instance.
selection_policy
inspection_policy
GraphHitTestPolicy
For example, setting selection_policy=NodesAndLinkedEdges() will cause a selected node to also select the associated edges. Similarly, setting inspection_policy=EdgesAndLinkedNodes() will cause the start and end nodes of an edge to also be inspected upon hovering an edge with the HoverTool.
selection_policy=NodesAndLinkedEdges()
inspection_policy=EdgesAndLinkedNodes()
Users may want to customize the selection_glyph, nonselection_glyph, and/or hover_glyph attributes of the edge and node sub-renderers in order to add dynamic visual elements to their graph interactions.
selection_glyph
nonselection_glyph
hover_glyph
Here’s a graph example with added node and edge interactions:
import networkx as nx from bokeh.io import output_file, show from bokeh.models import (BoxSelectTool, Circle, EdgesAndLinkedNodes, HoverTool, MultiLine, NodesAndLinkedEdges, Plot, Range1d, TapTool,) from bokeh.palettes import Spectral4 from bokeh.plotting import from_networkx G=nx.karate_club_graph() plot = Plot(plot_width=400, plot_height=400, x_range=Range1d(-1.1,1.1), y_range=Range1d(-1.1,1.1)) plot.title.text = "Graph Interaction Demonstration" plot.add_tools(HoverTool(tooltips=None), TapTool(), BoxSelectTool()) graph_renderer = from_networkx(G, nx.circular_layout, scale=1, center=(0,0)) graph_renderer.node_renderer.glyph = Circle(size=15, fill_color=Spectral4[0]) graph_renderer.node_renderer.selection_glyph = Circle(size=15, fill_color=Spectral4[2]) graph_renderer.node_renderer.hover_glyph = Circle(size=15, 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 = EdgesAndLinkedNodes() plot.renderers.append(graph_renderer) output_file("interactive_graphs.html") show(plot)
In from_networkx, NetworkX’s node/edge attributes are converted for GraphRenderer’s node_renderer/edge_renderer.
from_networkx
For example, “Zachary’s Karate Club graph” dataset has a node attribute named “club”. It’s possible to hover this information using the node attributes converted in from_networkx. Similarly, node/edge attributes can also be used for color information.
Here’s a graph example that hovers node attributes and changes colors with edge attributes:
import networkx as nx from bokeh.io import output_file, show from bokeh.models import (BoxZoomTool, Circle, HoverTool, MultiLine, Plot, Range1d, ResetTool,) from bokeh.palettes import Spectral4 from bokeh.plotting import from_networkx # Prepare Data G = nx.karate_club_graph() SAME_CLUB_COLOR, DIFFERENT_CLUB_COLOR = "black", "red" edge_attrs = {} for start_node, end_node, _ in G.edges(data=True): edge_color = SAME_CLUB_COLOR if G.nodes[start_node]["club"] == G.nodes[end_node]["club"] else DIFFERENT_CLUB_COLOR edge_attrs[(start_node, end_node)] = edge_color nx.set_edge_attributes(G, edge_attrs, "edge_color") # Show with Bokeh plot = Plot(plot_width=400, plot_height=400, x_range=Range1d(-1.1, 1.1), y_range=Range1d(-1.1, 1.1)) plot.title.text = "Graph Interaction Demonstration" node_hover_tool = HoverTool(tooltips=[("index", "@index"), ("club", "@club")]) plot.add_tools(node_hover_tool, BoxZoomTool(), ResetTool()) graph_renderer = from_networkx(G, nx.spring_layout, scale=1, center=(0, 0)) graph_renderer.node_renderer.glyph = Circle(size=15, fill_color=Spectral4[0]) graph_renderer.edge_renderer.glyph = MultiLine(line_color="edge_color", line_alpha=0.8, line_width=1) plot.renderers.append(graph_renderer) output_file("interactive_graphs.html") show(plot)