Network graphs#

Bokeh lets you create network graph visualizations and configure interactions between edges and nodes.

Edge and node renderers#

The GraphRenderer model maintains separate sub-GlyphRenderers for graph nodes and edges. This lets you customize nodes by modifying the node_renderer property of the GraphRenderer. You can replace the default Circle node glyph with any instance of the XYGlyph such as Rect or Ellipse glyph. You can similarly modify the style properties of edges through the edge_renderer property. To work with edge glyphs, use the multi_line glyph method.

Observe the following requirements for the data sources belonging to these sub-renderers:

  • The ColumnDataSource of the node sub-renderer must have an "index" column with the unique indices of the nodes.

  • The ColumnDataSource of the edge sub-renderer must have a "start" and "end" column. These columns contain the node indices for the start and end of the edges.

You can add extra meta-data to these sources to enable vectorized glyph styling or make data available for callbacks or hover tooltips.

The following code snippet

  • replaces a node glyph with an Ellipse,

  • assigns scalar values to the height and width attributes of the Ellipse,

  • assigns a palette to the fill_color attribute of the Ellipse,

  • and adds the assigned values to the node data source.

import math
from bokeh.plotting import figure, show
from bokeh.models import GraphRenderer, Ellipse, StaticLayoutProvider
from bokeh.palettes import Spectral8

# list the nodes and initialize a plot
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()

# replace the node glyph with an ellipse
# set its height, width, and fill_color
graph.node_renderer.glyph = Ellipse(height=0.1, width=0.2,
                                    fill_color="fill_color")

# assign a palette to ``fill_color`` and add it to the data source
graph.node_renderer.data_source.data = dict(
    index=node_indices,
    fill_color=Spectral8)

# add the rest of the assigned values to the data source
graph.edge_renderer.data_source.data = dict(
    start=[0]*N,
    end=node_indices)

Bokeh comes with a built-in LayoutProvider model that includes a dictionary of (x,y) coordinates for nodes. This lets you arrange plot elements in Cartesian space.

The following codes snippet uses this provider model to produce a plot based on the setup above.

# generate ellipses based on the ``node_indices`` list
circ = [i*2*math.pi/8 for i in node_indices]

# create lists of x- and y-coordinates
x = [math.cos(i) for i in circ]
y = [math.sin(i) for i in circ]

# convert the ``x`` and ``y`` lists into a dictionary of 2D-coordinates
# and assign each entry to a node on the ``node_indices`` list
graph_layout = dict(zip(node_indices, zip(x, y)))

# use the provider model to supply coourdinates to the graph
graph.layout_provider = StaticLayoutProvider(graph_layout=graph_layout)

# render the graph
plot.renderers.append(graph)

# display the plot
show(plot)

Put together, the above code snippets produce the following result:

Explicit paths#

By default, the StaticLayoutProvider model draws straight-line paths between the supplied node positions. To set explicit edge paths, supply lists of paths to the bokeh.models.sources.ColumnDataSource data source of the edge_renderer. The StaticLayoutProvider model looks for these paths in the "xs" and "ys" columns of the data source. The paths should be in the same order as the "start" and "end" points. Be extra careful when setting explicit paths because there is no validation to check if they match with node positions.

The following extends the example above and draws quadratic bezier curves between the nodes:

import math

from bokeh.models import Ellipse, GraphRenderer, StaticLayoutProvider
from bokeh.palettes import Spectral8
from bokeh.plotting import figure, show

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)

# create a static layout
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)

show(plot)

NetworkX integration#

Bokeh integrates the NetworkX package so you can quickly plot network graphs. The bokeh.plotting.from_networkx convenience method accepts a networkx.Graph object and a NetworkX layout method and returns a configured instance of the GraphRenderer model.

Here is how the networkx.spring_layout method lays out the “Zachary’s karate club graph” data set built into NetworkX:

import networkx as nx

from bokeh.palettes import Category20_20
from bokeh.plotting import figure, from_networkx, show

G = nx.desargues_graph() # always 20 nodes

p = figure(x_range=(-2, 2), y_range=(-2, 2),
           x_axis_location=None, y_axis_location=None,
           tools="hover", tooltips="index: @index")
p.grid.grid_line_color = None

graph = from_networkx(G, nx.spring_layout, scale=1.8, center=(0,0))
p.renderers.append(graph)

# Add some new columns to the node renderer data source
graph.node_renderer.data_source.data['index'] = list(range(len(G)))
graph.node_renderer.data_source.data['colors'] = Category20_20

graph.node_renderer.glyph.update(size=20, fill_color="colors")

show(p)

Interaction policies#

You can configure the selection or inspection behavior of graphs by setting the selection_policy and inspection_policy attributes of the GraphRenderer. These policy attributes accept a special GraphHitTestPolicy model instance.

For example, setting selection_policy to NodesAndLinkedEdges() lets you select a node and all associated edges. Similarly, setting inspection_policy to EdgesAndLinkedNodes() lets you inspect the "start" and "end" nodes of an edge by hovering over it with the HoverTool. NodesAndAdjacentNodes() lets you inspect a node and all other nodes connected to it by a graph edge.

You can customize the selection_glyph, nonselection_glyph, and/or hover_glyph attributes of the edge and node sub-renderers to add dynamic visual elements to your graph interactions.

Below are examples of graphs with added node and edge interactions:

import networkx as nx

from bokeh.models import (BoxSelectTool, Circle, HoverTool, MultiLine,
                          NodesAndLinkedEdges, Plot, Range1d, TapTool)
from bokeh.palettes import Spectral4
from bokeh.plotting import from_networkx, show

G = nx.karate_club_graph()

plot = Plot(width=400, 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 = NodesAndLinkedEdges()

plot.renderers.append(graph_renderer)

show(plot)
import networkx as nx

from bokeh.models import (BoxSelectTool, Circle, EdgesAndLinkedNodes,
                          HoverTool, MultiLine, Plot, Range1d, TapTool)
from bokeh.palettes import Spectral4
from bokeh.plotting import from_networkx, show

G = nx.karate_club_graph()

plot = Plot(width=400, 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 = EdgesAndLinkedNodes()
graph_renderer.inspection_policy = EdgesAndLinkedNodes()

plot.renderers.append(graph_renderer)

show(plot)
import networkx as nx

from bokeh.models import (BoxSelectTool, Circle, HoverTool, MultiLine,
                          NodesAndAdjacentNodes, Plot, Range1d, TapTool)
from bokeh.palettes import Spectral4
from bokeh.plotting import from_networkx, show

G = nx.karate_club_graph()

plot = Plot(width=400, 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 = NodesAndAdjacentNodes()
graph_renderer.inspection_policy = NodesAndAdjacentNodes()

plot.renderers.append(graph_renderer)

show(plot)

Node and edge attributes#

The from_networkx method converts node and edge attributes of the NetworkX package for use with node_renderer and edge_renderer of the GraphRenderer model.

For example, “Zachary’s karate club graph” data set has a node attribute named “club”. You can hover this information with node attributes converted with the from_networkx method. You can also use node and edge attributes for color information.

Here is an example of a graph that hovers node attributes and changes colors with edge attributes:

import networkx as nx

from bokeh.models import Circle, MultiLine
from bokeh.plotting import figure, from_networkx, show

G = nx.karate_club_graph()

SAME_CLUB_COLOR, DIFFERENT_CLUB_COLOR = "darkgrey", "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")

plot = figure(width=400, height=400, x_range=(-1.2, 1.2), y_range=(-1.2, 1.2),
              x_axis_location=None, y_axis_location=None, toolbar_location=None,
              title="Graph Interaction Demo", background_fill_color="#efefef",
              tooltips="index: @index, club: @club")
plot.grid.grid_line_color = None

graph_renderer = from_networkx(G, nx.spring_layout, scale=1, center=(0, 0))
graph_renderer.node_renderer.glyph = Circle(size=15, fill_color="lightblue")
graph_renderer.edge_renderer.glyph = MultiLine(line_color="edge_color",
                                               line_alpha=1, line_width=2)
plot.renderers.append(graph_renderer)

show(plot)