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