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-GlyphRenderersfor 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:

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

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 GraphRenderermodel.

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