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
andwidth
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
from bokeh.models import GraphRenderer, Ellipse
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)
# specify the name of the output file
output_file('graph.html')
# 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=0.8, line_width=1.5)
plot.renderers.append(graph_renderer)
show(plot)