【Python】Dash Cytoscapeで有向グラフを描く

## 動かせる有向グラフを描きたい

RのvisNetworkのように、ブラウザ上で動かせる有向グラフをPythonでも描きたいと思いました.

- [visNetwork, an R package for interactive network visualization](https://datastorm-open.github.io/visNetwork/)

Pythonのgraph libraryとしてはNetworkXが有名なようですが、今回RのvisNetworkに近いイメージで使えるのは[Dash Cytoscape](https://dash.plotly.com/cytoscape)かなと思ったので、サンプルを書き残してみます.

ローカルでサーバーを立ち上げて、ブラウザからグラフを確認します.


## 目標

- from, toの2列のデータフレームから、有向グラフを可視化します.
- input イメージ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143058/7498c034-ec22-e2da-ef2b-521ca02c6a88.png)


- 成果物イメージ
![forqiita2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143058/70c6fcbe-110b-a6dd-49c1-b7bb86caae35.gif)


## 環境

- OS : Windows10
- Python : Python 3.8.3
- dash == 1.4.1
- dash-core-components == 1.3.1
- dash-html-components == 1.0.1
- **dash-cytoscape == 0.2.0**

## 準備

##### from・toを持つedgeのデータフレームと、nodesを用意します

```python:app.py
import pandas as pd

edges = pd.DataFrame.from_dict({'from':['TABLE_B', 'TABLE_C', 'TABLE_D', 'TABLE_A', 'TABLE_X', 'TABLE_X'],
                               'to': ['TABLE_A', 'TABLE_A', 'TABLE_A','TABLE_X', 'TABLE_K', 'TABLE_Z']})
nodes = set()
```

##### edgeとnodeの内容をリストに格納します

```python:app.py
cy_edges = []
cy_nodes = []

for index, row in edges.iterrows():
    source, target = row['from'], row['to']

    if source not in nodes:
        nodes.add(source)
        cy_nodes.append({"data": {"id": source, "label": source}})
    if target not in nodes:
        nodes.add(target)
        cy_nodes.append({"data": {"id": target, "label": target}})

    cy_edges.append({
        'data': {
            'source': source,
            'target': target
        }
    })
```
<img width=""30%" alt="img" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143058/7b672b05-fa5b-8f75-f2df-97f48e0ddfbd.png">

##### node・edgeのstyleを定義する

```python:app.py
stylesheet = [
    {
        "selector": 'node', # すべてのnodeに対して
        'style': {
            "opacity": 0.9,
            "label": "data(label)", # 表示させるnodeのラベル
            "background-color": "#07ABA0", # nodeの色
            "color": "#008B80" # nodeのラベルの色
        }
    },
    {
        "selector": 'edge', # すべてのedgeに対して
        "style": {
            "target-arrow-color": "#C5D3E2", # 矢印の色
            "target-arrow-shape": "triangle", # 矢印の形
            "line-color": "#C5D3E2", # edgeのcolor
            'arrow-scale': 2, # 矢印のサイズ
            'curve-style': 'bezier' # デフォルトのcurve-styleだと矢印が表示されないため指定する
    }
}]
```

- 矢印のスタイルなど、以下を参考にカスタマイズすることが可能です
    - [Cytoscape Styling / Edge Arrows](https://dash.plotly.com/cytoscape/styling)

- 矢印の位置を中央に

```python:app.py
        "style": {
            "mid-target-arrow-color": "#C5D3E2",
            "mid-target-arrow-shape": "triangle",
        }
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143058/d638fa6d-ab0b-9016-c423-eeac54ac219b.png)

- また、"selector"の中に条件を定義することで、特定のedge, nodeにstyleをあてることも可能です

```python:app.py
stylesheet  = [
    {
        "selector": 'node',
        'style': {
            "opacity": 0.9,
            "label": "data(label)",
            "background-color": "#07ABA0",
            "color": "#008B80"
        }
    },
    {
        "selector": '[label *= "alarm"]', # labelがalarmのnodeのみ"red"にする
        'style': {
            "opacity": 0.9,
            "label": "data(label)",
            "background-color": "red",
            "color": "#008B80"
        }
    },
    {
        "selector": 'edge',
        "style": {

            "target-arrow-color": "#C5D3E2",
            "target-arrow-shape": "triangle",
            "line-color": "#C5D3E2",
            'arrow-scale': 2,
            'curve-style': 'bezier'
        }
    }
]
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143058/171ee93a-3437-108d-3f3b-7a8dda7f2eca.png)





##### layoutを定義する

```python:app.py
app.layout = html.Div([
    dcc.Dropdown(
            id='dropdown-layout',
            options=[
                {'label': 'random',
                 'value': 'random'},
                {'label': 'grid',
                 'value': 'grid'},
                {'label': 'circle',
                 'value': 'circle'},
                {'label': 'concentric',
                 'value': 'concentric'},
                {'label': 'breadthfirst',
                 'value': 'breadthfirst'},
                {'label': 'cose',
                 'value': 'cose'}
            ], value='grid'
        ),
    html.Div(children=[
        cyto.Cytoscape(
            id='cytoscape',
            elements=cy_edges + cy_nodes,
            style={
                'height': '95vh',
                'width': '100%'
            },
            stylesheet=stylesheet # 先ほど定義したstyleを与える
        )
    ])

])
```

- 今回はグラフのレイアウトをDropdownから選択できる形にしています.

##### layoutを更新するcallbackを作成

```python:app.py
@app.callback(Output('cytoscape', 'layout'),
              [Input('dropdown-layout', 'value')])
def update_cytoscape_layout(layout):
    return {'name': layout}
```


## 完成

##### 全文

```python:app.py
import pandas as pd

import dash
from dash.dependencies import Input, Output
import dash_core_components as dcc
import dash_html_components as html

import dash_cytoscape as cyto

app = dash.Dash(__name__)
server = app.server

# prepare data
edges = pd.DataFrame.from_dict({'from':['earthquake', 'earthquake', 'burglary', 'alarm', 'alarm'],
                               'to': ['report', 'alarm', 'alarm','John Calls', 'Mary Calls']})
nodes = set()

cy_edges = []
cy_nodes = []

for index, row in edges.iterrows():
    source, target = row['from'], row['to']

    if source not in nodes:
        nodes.add(source)
        cy_nodes.append({"data": {"id": source, "label": source}})
    if target not in nodes:
        nodes.add(target)
        cy_nodes.append({"data": {"id": target, "label": target}})

    cy_edges.append({
        'data': {
            'source': source,
            'target': target
        }
    })

# define stylesheet
stylesheet = [
    {
        "selector": 'node', # すべてのnodeに対して
        'style': {
            "opacity": 0.9,
            "label": "data(label)", # 表示させるnodeのラベル
            "background-color": "#07ABA0", # nodeの色
            "color": "#008B80" # nodeのラベルの色
        }
    },
    {
        "selector": 'edge', # すべてのedgeに対して
        "style": {
            "target-arrow-color": "#C5D3E2", # 矢印の色
            "target-arrow-shape": "triangle", # 矢印の形
            "line-color": "#C5D3E2", # edgeのcolor
            'arrow-scale': 2, # 矢印のサイズ
            'curve-style': 'bezier' # デフォルトのcurve-styleだと矢印が表示されないため指定する
    }
}]

# define layout
app.layout = html.Div([
    dcc.Dropdown(
            id='dropdown-layout',
            options=[
                {'label': 'random',
                 'value': 'random'},
                {'label': 'grid',
                 'value': 'grid'},
                {'label': 'circle',
                 'value': 'circle'},
                {'label': 'concentric',
                 'value': 'concentric'},
                {'label': 'breadthfirst',
                 'value': 'breadthfirst'},
                {'label': 'cose',
                 'value': 'cose'}
            ], value='grid'
        ),
    html.Div(children=[
        cyto.Cytoscape(
            id='cytoscape',
            elements=cy_edges + cy_nodes,
            style={
                'height': '95vh',
                'width': '100%'
            },
            stylesheet=stylesheet
        )
    ])
])

@app.callback(Output('cytoscape', 'layout'),
              [Input('dropdown-layout', 'value')])
def update_cytoscape_layout(layout):
    return {'name': layout}

if __name__ == '__main__':
    app.run_server(debug=False)
```

##### 実行

```sh
$ python app.py
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143058/734a3dc7-38ce-e528-2b18-b7b9fc04838f.png)


RのvisNetworkのようなネットワークの可視化が実現できたと思います. 皆さんも是非お試しください.


## 参考

[有向グラフとは何？](https://www.weblio.jp/content/%E6%9C%89%E5%90%91%E3%82%B0%E3%83%A9%E3%83%95)
[Dash Cytoscape](https://dash.plotly.com/cytoscape/responsive)

- [NetworkX](https://networkx.github.io/documentation/stable/auto_examples/drawing/plot_directed.html)で描くと

```python:networkx_sample.py
# https://networkx.github.io/documentation/stable/auto_examples/drawing/plot_directed.html#sphx-glr-auto-examples-drawing-plot-directed-py
# Author: Rodrigo Dorantes-Gilardi (rodgdor@gmail.com)
import matplotlib as mpl
import matplotlib.pyplot as plt
import networkx as nx

G = nx.generators.directed.random_k_out_graph(10, 3, 0.5)
pos = nx.layout.spring_layout(G)

node_sizes = [3 + 10 * i for i in range(len(G))]
M = G.number_of_edges()
edge_colors = range(2, M + 2)
edge_alphas = [(5 + i) / (M + 4) for i in range(M)]

nodes = nx.draw_networkx_nodes(G, pos, node_size=node_sizes, node_color='blue')
edges = nx.draw_networkx_edges(G, pos, node_size=node_sizes, arrowstyle='->',
                               arrowsize=10, edge_color=edge_colors,
                               edge_cmap=plt.cm.Blues, width=2)
# set alpha value for each edge
for i in range(M):
    edges[i].set_alpha(edge_alphas[i])

pc = mpl.collections.PatchCollection(edges, cmap=plt.cm.Blues)
pc.set_array(edge_colors)
plt.colorbar(pc)

ax = plt.gca()
ax.set_axis_off()
plt.show()
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143058/58636fce-0cd1-6355-5f3e-84df1a724724.png)