動かせる有向グラフを描きたい
RのvisNetworkのように、ブラウザ上で動かせる有向グラフをPythonでも描きたいと思いました.
Pythonのgraph libraryとしてはNetworkXが有名なようですが、今回RのvisNetworkに近いイメージで使えるのはDash Cytoscapeかなと思ったので、サンプルを書き残してみます.
ローカルでサーバーを立ち上げて、ブラウザからグラフを確認します.
目標
環境
- 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を用意します
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の内容をリストに格納します
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
}
})
node・edgeのstyleを定義する
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だと矢印が表示されないため指定する
}
}]
-
矢印のスタイルなど、以下を参考にカスタマイズすることが可能です
-
矢印の位置を中央に
app.py
"style": {
"mid-target-arrow-color": "#C5D3E2",
"mid-target-arrow-shape": "triangle",
}
- また、"selector"の中に条件を定義することで、特定のedge, nodeにstyleをあてることも可能です
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'
}
}
]
layoutを定義する
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を作成
app.py
@app.callback(Output('cytoscape', 'layout'),
[Input('dropdown-layout', 'value')])
def update_cytoscape_layout(layout):
return {'name': layout}
完成
全文
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)
実行
$ python app.py
RのvisNetworkのようなネットワークの可視化が実現できたと思います. 皆さんも是非お試しください.
参考
- NetworkXで描くと
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()