Last active
April 16, 2021 18:45
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This is really helpful!