#!/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)