Skip to content

Instantly share code, notes, and snippets.

@luerhard
Last active April 16, 2021 18:45
Show Gist options
  • Save luerhard/daa537362eef802f8d808782fc962bf2 to your computer and use it in GitHub Desktop.
Save luerhard/daa537362eef802f8d808782fc962bf2 to your computer and use it in GitHub Desktop.
The BokehGraph class creates super easy to use interactive plots for one-mode networkx graphs. Hover over nodes to see their attributes and color nodes by communities as shown in the docstring.
import networkx as nx
from collections import namedtuple
from math import sqrt
import bokeh
from bokeh import models, plotting, io
from bokeh.colors import RGB
import random
#corresponding package on pypi is confusingly called python-louvain
import community
class BokehGraph(object):
"""
This is instanciated with a (one-mode) networkx graph object with BokehGraph(nx.Graph())
working example:
graph = nx.barbell_graph(5,6)
plot = BokehGraph(graph, width=800, height=600)
plot.communities = True
plot.betweenness_centrality = True
plot.layout(shrink_factor = 0.6)
plot.draw(inline=True)
The instance can be configured to show communities made by with the Louvain-Algorithm
as node colors with BokehGraph().communities = True
- the communities can be fetched as dict by print(BokehGraph().communities) or assigning it to a variable
The instance can be configured to show betweenness centralities for all node in the HoverTool
by setting BokehGraph().betweenness_centrality = True (or to a edge_attrbute that works as weight)
- the betweenness_centrality can be fetched as dict by print(BokehGraph().communities) or assigning it to a variable
The plot is drawn by BokehGraph.draw(node_color="firebrick", community_colors = None, inline=False, save_to_path=False)
- node_color, line_color can be set to every value that bokeh recognizes, including a bokeh.colors.RGB instance.
serveral other parameters can be found in the .draw method.
- community_colors can be set to a dict that has the community numbers as keys and bokeh-valid colors as values
if None: colors will be chosen randomly.
- inline can be set to True to draw plot inside a JupyterNotebook. Otherwise a separate browser-window will open.
- save_to_path can be set to a valid path to save the plot as .html-file
"""
def __init__(self, graph, width=800, height=600):
self.graph = graph
self._directed = nx.is_directed(graph)
if not self._directed:
self._connected = nx.is_connected(graph)
else:
self._connected = True
self.width = width
self.height = height
self._layout = None
self._nodes = None
self._edges = None
self._betweenness = False
self._communities = False
self.degrees = nx.degree(self.graph)
self._hovertool = None
self.fig = None
self._tooltips = [('name', '@name'), ('node_id', '$index'), ('degree', '@degree')]
self._button_options = ["single_color"]
@property
def betweenness_centrality(self):
if self._betweenness:
return self._betweenness
else:
return False
@betweenness_centrality.setter
def betweenness_centrality(self, value):
tip = ("betweenness", "@betweenness")
if not self._connected:
print("Betweenness for unconnected networks is highly dubios ! Try using the largest component instead.")
return False
if not isinstance(value, bool):
if nx.get_edge_attributes(self.graph, value):
self._betweenness = nx.betweenness_centrality(self.graph, weight = value)
if tip not in self._tooltips:
self._tooltips.append(tip)
else:
print("Set valid edge attribute as weight or boolean")
elif value == True:
self._betweenness = nx.betweenness_centrality(self.graph)
if tip not in self._tooltips:
self._tooltips.append(tip)
return True
elif value == False:
self._betweenness = False
if tip in self._tooltips:
self._tooltips.remove(("betweenness", "@betweenness"))
else:
print("Set boolean or edge attribute as weight !")
@property
def communities(self):
if self._communities:
return self._communities
else:
False
@communities.setter
def communities(self, value):
tip = ("community", "@community")
button = "color_by_community"
if self._directed:
print("Communities not eligible for directed networks !")
return False
if value == True:
self._communities = community.best_partition(self.graph)
if tip not in self._tooltips:
self._tooltips.append(tip)
if button not in self._button_options:
self._button_options.append("color_by_community")
return True
elif value == False:
self._communities = False
if tip in self._tooltips:
self._tooltips.remove(("community", "@community"))
if button in self._button_options:
self._button_options.remove("color_by_community")
else:
print("Set boolean !")
def gen_edge_coordinates(self):
if not self._layout:
self.layout()
xs = []
ys = []
val = namedtuple("edges", "xs ys")
for edge in self.graph.edges():
from_node = self._layout[edge[0]]
to_node = self._layout[edge[1]]
xs.append([from_node[0],to_node[0]])
ys.append([from_node[1], to_node[1]])
return val(xs=xs, ys=ys)
def gen_node_coordinates(self):
if not self._layout:
self.layout()
names, coords = zip(*self._layout.items())
xs, ys = zip(*coords)
val = namedtuple("nodes", "names xs ys")
return val(names=names, xs=xs, ys=ys)
def layout(self, shrink_factor=0.8, iterations=50, scale=1):
self._nodes = None
self._edges = None
self._layout = nx.spring_layout(self.graph,
k=1/(sqrt(self.graph.number_of_nodes() * shrink_factor)),
iterations=iterations,
scale = scale)
return
def gen_hovertool(self):
self._hovertool = models.HoverTool(tooltips=self._tooltips, names=["show_hover"])
return
def gen_fig(self, logo=None, axis_visible=False, x_grid_line_color=None, y_grid_line_color=None):
if not self._hovertool:
self.gen_hovertool()
self.fig = bokeh.plotting.figure(width=self.width, height=self.height,
tools=[self._hovertool, "box_zoom", "reset", "wheel_zoom", "pan", "lasso_select"])
self.fig.toolbar.logo = logo
self.fig.axis.visible = axis_visible
self.fig.xgrid.grid_line_color = x_grid_line_color
self.fig.ygrid.grid_line_color = y_grid_line_color
return
def draw(self, node_color="firebrick", community_colors = None, inline=False, save_to_path=False,
line_color='navy', edge_alpha=0.17, node_alpha=0.7, node_size=9):
if not self._nodes:
self._nodes = self.gen_node_coordinates()
if not self._edges:
self._edges = self.gen_edge_coordinates()
self.gen_fig()
# Draw Edges
source_edges = bokeh.models.ColumnDataSource(dict(xs=self._edges.xs, ys=self._edges.ys))
self.fig.multi_line('xs', 'ys', line_color=line_color, source=source_edges, alpha=edge_alpha)
#Draw circles
n_color = [node_color for _ in range(nx.number_of_nodes(self.graph))]
node_properties = dict(xs = self._nodes.xs,
ys = self._nodes.ys,
name = self._nodes.names,
single_color = n_color,
degree = [self.degrees[node] for node in self._nodes.names])
if self.betweenness_centrality:
node_properties["betweenness"] = [self.betweenness_centrality[node] for node in self._nodes.names]
if self.communities:
if not community_colors:
colormap_communities = {x: RGB(random.randrange(0,256),random.randrange(0,256),random.randrange(0,256))
for x in set(self.communities.values())}
else:
statement = """
community_colors has to be a dict with one key for each community.
Get communities by setting BokehGraph.communities to True and the print out BokehGraph.communities.
If set to None, random colors will be generated.
"""
assert isinstance(community_colors, dict), statement
assert set(community_colors.keys()) == set(self.communities.values()), statement
colormap_communities = community_colors
node_properties["community"] = [self.communities[node] for node in self._nodes.names]
node_properties["color_by_community"] = [colormap_communities[self.communities[node]] for node in self._nodes.names]
source_nodes = bokeh.models.ColumnDataSource(node_properties)
r_circles = self.fig.circle('xs', 'ys', fill_color='color_by_community', line_color='single_color',
source = source_nodes, alpha=node_alpha, size=node_size, name="show_hover")
#Create Color-Selector
colorcallback = bokeh.models.callbacks.CustomJS(args=dict(source_nodes=source_nodes, r_circles=r_circles), code="""
var color_select = cb_obj.value;
r_circles.glyph.line_color.field = color_select;
r_circles.glyph.fill_color.field = color_select;
source_nodes.change.emit();
""")
if len(self._button_options) > 1:
button = bokeh.models.widgets.Select(title="Color", value="color_by_community",
options=self._button_options,
callback=colorcallback)
else:
button = None
# set grid layout
if button and not inline:
layout_plot = bokeh.layouts.gridplot([[self.fig, button]])
elif button and inline:
layout_plot = bokeh.layouts.gridplot([[button], [self.fig]])
else:
layout_plot = bokeh.layouts.gridplot([[self.fig]])
#save_to_path
if save_to_path:
if isinstance(save_to_path, str):
save_to_path = save_to_path.rstrip(".html")
bokeh.io.output_file(f"{save_to_path}.html")
bokeh.io.save(layout_plot)
else:
print("Set path as str or False for save_to_path !")
#inline for jupyter notebooks
if inline:
bokeh.io.output_notebook()
bokeh.plotting.show(layout_plot, notebook_handle=True)
else:
bokeh.plotting.show(layout_plot)
@sidharthbolar
Copy link

This is really helpful!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment