#!/usr/bin/env python
# coding: utf-8
# # NetworkX Visualization Powered by Bokeh
#
#
# ### EuroPython 2016 (2016-07-22)
#
# ###### Björn Meier
# ### Original Objectives:
# * exploring networks for patterns based on properties like node centrality or clustering
# * visualization in a browser
#
#
# ### Original Solution:
# * generate networks and properties
# * RESTful Flask app to provide data
# * D3.js app for browser visualization
# ## Can we solve this easier?
# ### NetworkX + Bokeh
#
# * define the visualization in pure python
# * interact with your network
# * change views or change showed properties
# * manipulate the network and get an updated plot
# * possible embedding in a web app
# #### Now, I will show you how it is done ...
# ## Step 0: Example Data
#
#
# * tweets during Europython 2016 @europython
# * author will be linked with users mentioned in a tweet
# ## Step 1: Create a Network
# In[1]:
import networkx
from math import sqrt
network = networkx.read_gml('ep2016.gml')
layout = networkx.spring_layout(network,
k=1.1/sqrt(network.number_of_nodes()),
iterations=100)
# https://en.wikipedia.org/wiki/Force-directed_graph_drawing
# ![elastic network](images/elastic_network.png)
#
# Image: https://en.wikipedia.org/wiki/Spring_system
# * Only nice took look at
# * Flattened into 2D
# * Not accurate
# ## The Cornerstone is the ColumnDataSource
#
# | (id) | x | y | node_name | ... |
# |:-----:|:---:|:---:|:---------:|:---:|
# | 0 | 21 | 3 | da_bjoerni| ... |
# | 1 | 14 | 7 | user47 | ... |
# | ... | ... | ... | ... | ... |
#
# * column based data class
# * columns must be sequences of data e.g. lists, pandas.DataFrame, ...
# * changes will effect the plot during a rerendering
# * selection information inside a plot
# In[2]:
from bokeh.models import ColumnDataSource
nodes, nodes_coordinates = zip(*sorted(layout.items()))
nodes_xs, nodes_ys = list(zip(*nodes_coordinates))
nodes_source = ColumnDataSource(dict(x=nodes_xs, y=nodes_ys,
name=nodes))
# ## Step 2: Plot a Network
# In[3]:
from bokeh.plotting import show, figure
from bokeh.io import output_notebook
from bokeh.models import HoverTool
hover = HoverTool(tooltips=[('name', '@name'), ('id', '$index')])
plot = figure(plot_width=800, plot_height=400,
tools=['tap', hover, 'box_zoom', 'reset'])
r_circles = plot.circle('x', 'y', source=nodes_source, size=10,
color='blue', level = 'overlay')
# In[4]:
output_notebook(); show(plot)
# # Step 3: Add the edges
#
# ### Prepare the networkX edges
# In[5]:
def get_edges_specs(_network, _layout):
d = dict(xs=[], ys=[], alphas=[])
weights = [d['weight'] for u, v, d in _network.edges(data=True)]
max_weight = max(weights)
calc_alpha = lambda h: 0.1 + 0.6 * (h / max_weight)
# example: { ..., ('user47', 'da_bjoerni', {'weight': 3}), ... }
for u, v, data in _network.edges(data=True):
d['xs'].append([_layout[u][0], _layout[v][0]])
d['ys'].append([_layout[u][1], _layout[v][1]])
d['alphas'].append(calc_alpha(data['weight']))
return d
# ### Create a ColumnDataSource for the edges
# In[6]:
lines_source = ColumnDataSource(get_edges_specs(network, layout))
# In[7]:
r_lines = plot.multi_line('xs', 'ys', line_width=1.5,
alpha='alphas', color='navy',
source=lines_source)
show(plot)
# # Step 4: Visualize properties
#
# ### Add Clustering and Centrality to ColumnDataSource
# In[13]:
centrality =\
networkx.algorithms.centrality.betweenness_centrality(network)
# first element are nodes again
_, nodes_centrality = zip(*sorted(centrality.items()))
max_centraliy = max(nodes_centrality)
nodes_source.add([7 + 10 * t / max_centraliy
for t in nodes_centrality],
'centrality')
# In[9]:
import community # python-louvain
partition = community.best_partition(network)
p_, nodes_community = zip(*sorted(partition.items()))
nodes_source.add(nodes_community, 'community')
community_colors = ['#e41a1c','#377eb8','#4daf4a','#984ea3','#ff7f00','#ffff33','#a65628', '#b3cde3','#ccebc5','#decbe4','#fed9a6','#ffffcc','#e5d8bd','#fddaec','#1b9e77','#d95f02','#7570b3','#e7298a','#66a61e','#e6ab02','#a6761d','#666666']
nodes_source.add([community_colors[t % len(community_colors)]
for t in nodes_community],
'community_color')
# ### Update the Renderer
# In[10]:
r_circles.glyph.size = 'centrality'
r_circles.glyph.fill_color = 'community_color'
# In[11]:
show(plot)
# # Step 5: Interactions
# ### Get Selected Nodes
#
# `ColumnDataSource.selected`
# ```python
# {
# # line or patch glyph selections
# '0d': {'get_view': {}, 'glyph': None,
# 'indices': []},
# # all other glyph selections
# '1d': {'indices': [73]},
# # [multi]line or patch selections
# '2d': {'indices': []}
# }
# ```
# ### Remove Node
# In[12]:
def remove_node():
idx = nodes_source.selected['1d']['indices'][0]
# update networkX network
node = nodes_source.data['name'][idx]
network.remove_node(node)
# update layout
layout.pop(node)
# update nodes ColumnDataSource
new_source_data = dict()
for column in nodes_source.column_names:
new_source_data[column] =\
[e for i, e in enumerate(nodes_source.data[column])
if i != idx]
nodes_source.data = new_source_data
# update lines ColumnDataSource
lines_source.data = get_edges_specs(network, layout)
# ### Notebook Limitations
# * no direct redraw after changing the ColumnDataSource
# * getting selected nodes is not working
# * can not run arbitrary python code started by widges, e.g. buttons
#
#
#
# ### Bokeh Server
# ```
# bokeh serve your_app.py
# ```
#
# [http://localhost:5006/bokehx_app](http://localhost:5006/bokehx_app)
# # Thank You
#
# The slides and code can be found at:
#
# [https://github.com/blue-yonder/documents](https://github.com/blue-yonder/documents)
#
#
# ## NetworkX
# [https://networkx.github.io](https://networkx.github.io)
#
#
# ## Bokeh
# [http://bokeh.pydata.org](http://bokeh.pydata.org)