Spaces:
Running
Running
File size: 10,206 Bytes
b9a0f21 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 |
```python
import numpy as np
import pandas as pd
import holoviews as hv
import networkx as nx
from holoviews import opts
hv.extension('bokeh')
defaults = dict(width=400, height=400)
hv.opts.defaults(
opts.EdgePaths(**defaults), opts.Graph(**defaults), opts.Nodes(**defaults))
```
Visualizing and working with network graphs is a common problem in many different disciplines. HoloViews provides the ability to represent and visualize graphs very simply and easily with facilities for interactively exploring the nodes and edges of the graph, especially using the bokeh plotting interface.
The ``Graph`` ``Element`` differs from other elements in HoloViews in that it consists of multiple sub-elements. The data of the ``Graph`` element itself are the abstract edges between the nodes. By default the element will automatically compute concrete ``x`` and ``y`` positions for the nodes and represent them using a ``Nodes`` element, which is stored on the Graph. The abstract edges and concrete node positions are sufficient to render the ``Graph`` by drawing straight-line edges between the nodes. In order to supply explicit edge paths we can also declare ``EdgePaths``, providing explicit coordinates for each edge to follow.
To summarize a ``Graph`` consists of three different components:
* The ``Graph`` itself holds the abstract edges stored as a table of node indices.
* The ``Nodes`` hold the concrete ``x`` and ``y`` positions of each node along with a node ``index``. The ``Nodes`` may also define any number of value dimensions, which can be revealed when hovering over the nodes or to color the nodes by.
* The ``EdgePaths`` can optionally be supplied to declare explicit node paths.
#### A simple Graph
Let's start by declaring a very simple graph connecting one node to all others. If we simply supply the abstract connectivity of the ``Graph``, it will automatically compute a layout for the nodes using the ``layout_nodes`` operation, which defaults to a circular layout:
```python
# Declare abstract edges
N = 8
node_indices = np.arange(N, dtype=np.int32)
source = np.zeros(N, dtype=np.int32)
target = node_indices
simple_graph = hv.Graph(((source, target),))
simple_graph
```
#### Accessing the nodes and edges
We can easily access the ``Nodes`` and ``EdgePaths`` on the ``Graph`` element using the corresponding properties:
```python
simple_graph.nodes + simple_graph.edgepaths
```
#### Displaying directed graphs
When specifying the graph edges the source and target node are listed in order, if the graph is actually a directed graph this may used to indicate the directionality of the graph. By setting ``directed=True`` as a plot option it is possible to indicate the directionality of each edge using an arrow:
```python
simple_graph.relabel('Directed Graph').opts(directed=True, node_size=5, arrowhead_length=0.05)
```
The length of the arrows can be set as an fraction of the overall graph extent using the ``arrowhead_length`` option.
#### Supplying explicit paths
Next we will extend this example by supplying explicit edges:
```python
def bezier(start, end, control, steps=np.linspace(0, 1, 100)):
return (1-steps)**2*start + 2*(1-steps)*steps*control+steps**2*end
x, y = simple_graph.nodes.array([0, 1]).T
paths = []
for node_index in node_indices:
ex, ey = x[node_index], y[node_index]
paths.append(np.column_stack([bezier(x[0], ex, 0), bezier(y[0], ey, 0)]))
bezier_graph = hv.Graph(((source, target), (x, y, node_indices), paths))
bezier_graph
```
## Interactive features
#### Hover and selection policies
Thanks to Bokeh we can reveal more about the graph by hovering over the nodes and edges. The ``Graph`` element provides an ``inspection_policy`` and a ``selection_policy``, which define whether hovering and selection highlight edges associated with the selected node or nodes associated with the selected edge, these policies can be toggled by setting the policy to ``'nodes'`` (the default) and ``'edges'``.
```python
bezier_graph.relabel('Edge Inspection').opts(inspection_policy='edges')
```
In addition to changing the policy we can also change the colors used when hovering and selecting nodes:
```python
bezier_graph.opts(
opts.Graph(inspection_policy='nodes', tools=['hover', 'box_select'],
edge_hover_line_color='green', node_hover_fill_color='red'))
```
#### Additional information
We can also associate additional information with the nodes and edges of a graph. By constructing the ``Nodes`` explicitly we can declare additional value dimensions, which are revealed when hovering and/or can be mapped to the color by setting the ``color`` to the dimension name ('Weight'). We can also associate additional information with each edge by supplying a value dimension to the ``Graph`` itself, which we can map to various style options, e.g. by setting the ``edge_color`` and ``edge_line_width``.
```python
node_labels = ['Output']+['Input']*(N-1)
np.random.seed(7)
edge_labels = np.random.rand(8)
nodes = hv.Nodes((x, y, node_indices, node_labels), vdims='Type')
graph = hv.Graph(((source, target, edge_labels), nodes, paths), vdims='Weight')
(graph + graph.opts(inspection_policy='edges', clone=True)).opts(
opts.Graph(node_color='Type', edge_color='Weight', cmap='Set1',
edge_cmap='viridis', edge_line_width=hv.dim('Weight')*10))
```
If you want to supply additional node information without specifying explicit node positions you may pass in a ``Dataset`` object consisting of various value dimensions.
```python
node_info = hv.Dataset(node_labels, vdims='Label')
hv.Graph(((source, target), node_info)).opts(node_color='Label', cmap='Set1')
```
## Working with NetworkX
NetworkX is a very useful library when working with network graphs and the Graph Element provides ways of importing a NetworkX Graph directly. Here we will load the Karate Club graph and use the ``circular_layout`` function provided by NetworkX to lay it out:
```python
G = nx.karate_club_graph()
hv.Graph.from_networkx(G, nx.layout.circular_layout).opts(tools=['hover'])
```
It is also possible to pass arguments to the NetworkX layout function as keywords to ``hv.Graph.from_networkx``, e.g. we can override the k-value of the Fruchteran Reingold layout
```python
hv.Graph.from_networkx(G, nx.layout.fruchterman_reingold_layout, k=1)
```
Finally if we want to layout a Graph after it has already been constructed, the ``layout_nodes`` operation may be used, which also allows applying the ``weight`` argument to graphs which have not been constructed with networkx:
```python
from holoviews.element.graphs import layout_nodes
graph = hv.Graph([
('a', 'b', 3),
('a', 'c', 0.2),
('c', 'd', 0.1),
('c', 'e', 0.7),
('c', 'f', 5),
('a', 'd', 0.3)
], vdims='weight')
layout_nodes(graph, layout=nx.layout.fruchterman_reingold_layout, kwargs={'weight': 'weight'})
```
## Adding labels
If the ``Graph`` we have constructed has additional metadata we can easily use those as labels, we simply get a handle on the nodes, cast them to hv.Labels and then overlay them:
```python
graph = hv.Graph.from_networkx(G, nx.layout.fruchterman_reingold_layout)
labels = hv.Labels(graph.nodes, ['x', 'y'], 'club')
(graph * labels.opts(text_font_size='8pt', text_color='white', bgcolor='gray'))
```
## Animating graphs
Like all other elements ``Graph`` can be updated in a ``HoloMap`` or ``DynamicMap``. Here we animate how the Fruchterman-Reingold force-directed algorithm lays out the nodes in real time.
```python
hv.HoloMap({i: hv.Graph.from_networkx(G, nx.spring_layout, iterations=i, seed=10) for i in range(5, 30, 5)},
kdims='Iterations')
```
## Real world graphs
As a final example let's look at a slightly larger graph. We will load a dataset of a Facebook network consisting a number of friendship groups identified by their ``'circle'``. We will load the edge and node data using pandas and then color each node by their friendship group using many of the things we learned above.
```python
kwargs = dict(width=800, height=800, xaxis=None, yaxis=None)
opts.defaults(opts.Nodes(**kwargs), opts.Graph(**kwargs))
colors = ['#000000']+hv.Cycle('Category20').values
edges_df = pd.read_csv('../assets/fb_edges.csv')
fb_nodes = hv.Nodes(pd.read_csv('../assets/fb_nodes.csv')).sort()
fb_graph = hv.Graph((edges_df, fb_nodes), label='Facebook Circles')
fb_graph.opts(cmap=colors, node_size=10, edge_line_width=1,
node_line_color='gray', node_color='circle')
```
## Bundling graphs
The datashader library provides algorithms for bundling the edges of a graph and HoloViews provides convenient wrappers around the libraries. Note that these operations need ``scikit-image`` which you can install using:
```
conda install scikit-image
```
or
```
pip install scikit-image
```
```python
from holoviews.operation.datashader import datashade, bundle_graph
bundled = bundle_graph(fb_graph)
bundled
```
## Datashading graphs
For graphs with a large number of edges we can datashade the paths and display the nodes separately. This loses some of the interactive features but will let you visualize quite large graphs:
```python
(datashade(bundled, normalization='linear', width=800, height=800) * bundled.nodes).opts(
opts.Nodes(color='circle', size=10, width=1000, cmap=colors, legend_position='right'))
```
### Applying selections
Alternatively we can select the nodes and edges by an attribute that resides on either. In this case we will select the nodes and edges for a particular circle and then overlay just the selected part of the graph on the datashaded plot. Note that selections on the ``Graph`` itself will select all nodes that connect to one of the selected nodes. In this way a smaller subgraph can be highlighted and the larger graph can be datashaded.
```python
datashade(bundle_graph(fb_graph), normalization='linear', width=800, height=800) *\
bundled.select(circle='circle15').opts(node_fill_color='white')
```
To select just nodes that are in 'circle15' set the ``selection_mode='nodes'`` overriding the default of 'edges':
```python
bundled.select(circle='circle15', selection_mode='nodes')
```
|