Network graphs (original) (raw)
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, 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 = [i2math.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 thebokeh.models.sources.ColumnDataSource data source of theedge_renderer
. The StaticLayoutProvidermodel 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 = [i2math.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)2start + 2(1-s)scontrol + s2*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 specialGraphHitTestPolicy
model instance.
For example, setting selection_policy
to NodesAndLinkedEdges()
lets you select a node and all associated edges. Similarly, settinginspection_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:
NodesAndLinkedEdges
import networkx as nx
from bokeh.models import (BoxSelectTool, HoverTool, MultiLine, NodesAndLinkedEdges, Plot, Range1d, Scatter, 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))
scatter_glyph = Scatter(size=15, fill_color=Spectral4[0]) graph_renderer.node_renderer.glyph = scatter_glyph graph_renderer.node_renderer.selection_glyph = scatter_glyph.clone(fill_color=Spectral4[2]) graph_renderer.node_renderer.hover_glyph = scatter_glyph.clone(fill_color=Spectral4[1])
ml_glyph = MultiLine(line_color="#CCCCCC", line_alpha=0.8, line_width=5) graph_renderer.edge_renderer.glyph = ml_glyph graph_renderer.edge_renderer.selection_glyph = ml_glyph.clone(line_color=Spectral4[2], line_alpha=1) graph_renderer.edge_renderer.hover_glyph = ml_glyph.clone(line_color=Spectral4[1], line_width=1)
graph_renderer.selection_policy = NodesAndLinkedEdges() graph_renderer.inspection_policy = NodesAndLinkedEdges()
plot.renderers.append(graph_renderer)
show(plot)
EdgesAndLinkedNodes
import networkx as nx
from bokeh.models import (BoxSelectTool, EdgesAndLinkedNodes, HoverTool, MultiLine, Plot, Range1d, Scatter, 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))
scatter_glyph = Scatter(size=15, fill_color=Spectral4[0]) graph_renderer.node_renderer.glyph = scatter_glyph graph_renderer.node_renderer.selection_glyph = scatter_glyph.clone(fill_color=Spectral4[2]) graph_renderer.node_renderer.hover_glyph = scatter_glyph.clone(fill_color=Spectral4[1])
ml_glyph = MultiLine(line_color="#CCCCCC", line_alpha=0.8, line_width=5) graph_renderer.edge_renderer.glyph = ml_glyph graph_renderer.edge_renderer.selection_glyph = ml_glyph.clone(line_color=Spectral4[2], line_alpha=1) graph_renderer.edge_renderer.hover_glyph = ml_glyph.clone(line_color=Spectral4[1], line_width=1)
graph_renderer.selection_policy = EdgesAndLinkedNodes() graph_renderer.inspection_policy = EdgesAndLinkedNodes()
plot.renderers.append(graph_renderer)
show(plot)
NodesAndAdjacentNodes
import networkx as nx
from bokeh.models import (BoxSelectTool, HoverTool, MultiLine, NodesAndAdjacentNodes, Plot, Range1d, Scatter, 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))
scatter_glyph = Scatter(size=15, fill_color=Spectral4[0]) graph_renderer.node_renderer.glyph = scatter_glyph graph_renderer.node_renderer.selection_glyph = scatter_glyph.clone(fill_color=Spectral4[2]) graph_renderer.node_renderer.hover_glyph = scatter_glyph.clone(fill_color=Spectral4[1])
ml_glyph = MultiLine(line_color="#CCCCCC", line_alpha=0.8, line_width=5) graph_renderer.edge_renderer.glyph = ml_glyph graph_renderer.edge_renderer.selection_glyph = ml_glyph.clone(line_color=Spectral4[2], line_alpha=1) graph_renderer.edge_renderer.hover_glyph = ml_glyph.clone(line_color=Spectral4[1], line_width=1)
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 MultiLine, Scatter 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 = Scatter(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)