Visualizing Network Graphs

Bokeh has added native support for creating network graph visualizations with configurable interactions between edges and nodes.

Edge and Node Renderers

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 Oval glyph. Similarly, the style properties of the edges can modified through the edge_renderer property. The edge glyph is currently limited to a MultiLine glyph.

There are a couple 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.

  • The ColumnDataSource associated with the edge sub-renderer has two required columns: "start" and "end". These columns contain the node indices of for the start and end of the edges.

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 Oval

  • sets the height and width attributes of the Oval as scalar values

  • sets the fill_color attribute of the Oval as a vectorized field and adds the values to the node data source.

import math
from bokeh.plotting import figure
from bokeh.models import GraphRenderer, Oval
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 = Oval(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.

Layout Providers

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.

This example adds a provider to the above code snippet:

import math

from bokeh.io import show, output_file
from bokeh.plotting import figure
from bokeh.models import GraphRenderer, StaticLayoutProvider, Oval
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.data_source.add(node_indices, 'index')
graph.node_renderer.data_source.add(Spectral8, 'color')
graph.node_renderer.glyph = Oval(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)

Explicit Paths

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.

This example extends the example from above to draw quadratic bezier paths between the nodes:

import math

from bokeh.io import show, output_file
from bokeh.plotting import figure
from bokeh.models import GraphRenderer, StaticLayoutProvider, Oval
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.data_source.add(node_indices, 'index')
graph.node_renderer.data_source.add(Spectral8, 'color')
graph.node_renderer.glyph = Oval(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)

Networkx Integration

Bokeh supports quickly plotting a network graph with its networkx integration. The bokeh.models.graphs.from_networkx convenience method accepts a networkx.Graph object and a networkx layout method in order to return a configured GraphRenderer instance.

Here is an example of using the networkx.spring_layout method to layout networkx’s built-in “Zachary’s Karate Club graph” dataset:

import networkx as nx

from bokeh.io import show, output_file
from bokeh.plotting import figure
from bokeh.models.graphs import 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)

Interaction Policies

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.

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.

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.

Here’s a graph example with added node and edge interactions:

import networkx as nx

from bokeh.io import show, output_file
from bokeh.models import Plot, Range1d, MultiLine, Circle, HoverTool, TapTool, BoxSelectTool
from bokeh.models.graphs import from_networkx, NodesAndLinkedEdges, EdgesAndLinkedNodes
from bokeh.palettes import Spectral4

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)

Node and Edge Attributes

In from_networkx, NetworkX’s node/edge attributes are converted for GraphRenderer’s node_renderer/edge_renderer.

For example, “Zachary’s Karate Club graph” dataset has a node attribute named “club”. It’s possible to hover these 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 show, output_file
from bokeh.models import Plot, Range1d, MultiLine, Circle, HoverTool, BoxZoomTool, ResetTool
from bokeh.models.graphs import from_networkx
from bokeh.palettes import Spectral4

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