Visualisation of network graphs¶
Bokeh natively supports the creation of network graphs with configurable interactions between edges and nodes.
Edge and node renderers¶
The main feature of GraphRenderer
is that there are separate GlyphRenderers for diagram nodes and diagram edges. This makes it possible to customise the nodes by changing a property of the node_renderer
. It is possible to replace the usual circle node glyph with any XYGlyph instance, for example a rectangular or elliptical glyph. The style properties of the edges can also be changed via the edge_renderer
property. However, edges can currently only be rendered as multi-line
characters.
Some requirements apply to the data sources that belong to these two sub-renderers:
The
ColumnDataSource
associated with thenode_renderer
must have a column named"index"
that contains the unique indices of the nodes.The
ColumnDataSource
assigned to theedge_renderer
contains two required columns:"start"
and"end"
. These columns contain the node indices for the start and end of the edges.
It is possible to add additional metadata to these data sources to add vectorised glyph styling or make data available for callbacks or tooltips.
Below I show a code snippet that:
displays a node elliptically
sets the
height
andwidth
attributes of the ellipsesets the
fill_color
attribute of the ellipse
[1]:
import math
from bokeh.io import output_notebook, show
from bokeh.plotting import figure
output_notebook()
[2]:
import math
from bokeh.models import Ellipse, GraphRenderer
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.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)
Executing the code snippet above will not render a diagram as we have not specified how the diagram should be arranged in 2D space. You can find out how to do this in the following section.
Layout Providers¶
Bokeh uses a separate LayoutProvider
model to specify the coordinates of a graph in Cartesian space. Currently, the StaticLayoutProvider
model is the only built-in provider that contains a dictionary of (x,y) coordinates for the nodes.
In our example, a provider is added to the code snippet above with:
[3]:
from bokeh.models import Ellipse, GraphRenderer, StaticLayoutProvider
from bokeh.palettes import Spectral8
N = 8
node_indices = list(range(N))
p = 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)
p.renderers.append(graph)
show(p)
Explicit paths¶
By default, the StaticLayout
provider draws straight paths between the specified nodes. However, to provide explicit edge paths, lists with paths for the edge_renderer
can also be specified with "xs"
and "ys"
columns of the ColumnDataSource
. Note, however, that on the one hand these paths should be in the same order as the "start"
and "end"
points and on the other hand that there is no validation that matches the node positions. You should therefore be very careful
when defining explicit paths.
In the following example, we extend the above example to include quadratic Bezier paths between the nodes:
[4]:
import math
from bokeh.io import 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))
p = 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.0 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
p.renderers.append(graph)
show(p)
NetworkX integration¶
Bokeh supports quickly drawing a network graph with its NetworkX integration. bokeh.models.graphs.from_networkx
accepts a networkx.Graph
object and a networkx-Layout
method to return a configured GraphRenderer
instance.
Here is an example of using the networkx.spring_layout
method to layout the NetworkX integrated dataset karate_club_graph
:
[5]:
import networkx as nx
from bokeh.io import show
from bokeh.plotting import figure, from_networkx
G = nx.karate_club_graph()
p = 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))
p.renderers.append(graph)
show(p)
Interactions¶
You can configure the selection or inspection behaviour of graphs by setting the selection_policy
and inspection_policy
attributes of the GraphRenderer
. These attributes accept a special GraphHitTestPolicy
instance.
For example, selection_policy=NodesAndLinkedEdges()
can cause the associated edges to be selected with a selected node. Similarly, inspection_policy=EdgesAndLinkedNodes()
also checks the start and end nodes of an edge when the mouse is moved over an edge with the HoverTool.
With the selection_glyph
, nonselection_glyph
and hover_glyph
attributes of the edge and node renderers, the diagram interactions can also add dynamic visual elements.
Here is an example with such node and edge interactions:
[6]:
import networkx as nx
from bokeh.io import show
from bokeh.models import (
BoxSelectTool,
Circle,
HoverTool,
MultiLine,
Plot,
Range1d,
TapTool,
)
from bokeh.models.graphs import EdgesAndLinkedNodes, NodesAndLinkedEdges
from bokeh.palettes import Spectral4
from bokeh.plotting import from_networkx
G = nx.karate_club_graph()
p = Plot(
width=400,
height=400,
x_range=Range1d(-1.1, 1.1),
y_range=Range1d(-1.1, 1.1),
)
p.title.text = "Graph Interaction Demonstration"
p.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()
p.renderers.append(graph_renderer)
show(p)
Node and edge attributes¶
In from_networkx
, the node and edge attributes of NetworkX are converted for the node_renderer
or edge_renderer
.
For example, the dataset karate_club_graph
has a node attribute with the name club
. With from_networkx
it is possible to move this information. Other node and edge attributes can also be used for colour information in a similar way:
[7]:
import networkx as nx
from bokeh.io import 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
p = Plot(
width=400,
height=400,
x_range=Range1d(-1.1, 1.1),
y_range=Range1d(-1.1, 1.1),
)
p.title.text = "Graph Interaction Demonstration"
node_hover_tool = HoverTool(tooltips=[("index", "@index"), ("club", "@club")])
p.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
)
p.renderers.append(graph_renderer)
show(p)