Streamlit+Cytoscape.jsでぐりぐり動かせるネットワークのグラフを簡単に描画する方法


本記事ではStreamlitとCytoscape.jsを使って、インタラクティブに動かせる**ネットワークグラフ**を簡単に描画できると方法を紹介します。
（ネットワークグラフに特化しており、棒グラフ、折れ線グラフの紹介はありませんmm）
どのようなことができるか言葉で説明するより、以下のデモを触ってもらったほうがイメージが持てるかと思います。マウス操作でノードの移動、Googleマップと同じ操作感で拡大/中心箇所の移動が可能ですので試してください。
【デモ】https://ts5129nk-streamlit-cyto-app-7duur9.streamlit.app/



Streamlitでネットワークのグラフを描画するには、標準で使えるgraphvizがありますが、描画後に自由にノードの位置をマウス操作で移動させることができないため、できる方法がないかなーと探すとありました！開発者には感謝です。

# Streamlitとは
以下公式より
>Streamlit turns data scripts into shareable web apps in minutes.
>All in pure Python. No front‑end experience required.

日本語で言うと「HTMLやCSS、javascriptといったフロントエンドの知識を必要とせず、pythonでWebアプリケーションを数分で作成できる」ライブラリです。
以下の画像のように数行で、右図のようなグラフのアプリケーションが作成できます。
![スクリーンショット (62).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1108240/609214d5-9cd0-8c67-0878-acbd9a6e5611.png)
(公式サイトより引用:https://streamlit.io/）

IBMやIntel、Teslaなどが利用しています。
pythonの小規模開発向けのフレームワークとしてFlaskがありますが、こちらはさらに少ない行数で済み、さらにフロントエンド側の実装が不要です。そのため学習コストが少なく、マニュアルもわかりやすく、私もすぐに簡単なことはできるようになりました。
（ライセンスはApache License 2.0）



# Cytoscape.jsとは
以下公式より

>Cytoscape.js is an open-source graph theory (a.k.a. network) library written in JS. 
>You can use Cytoscape.js for graph analysis and visualisation.
>Cytoscape.js allows you to easily display and manipulate rich, interactive graphs. 

日本語で言うと「javacriptでかかれたネットワークグラフの可視化ライブラリ。インタラクティブでリッチなグラフの表示を容易にします」とのこと。

下の図や[こちらのデモのリンク](https://js.cytoscape.org/#demos)を見てもらうとわかりやすいです。こちらもGoogleやAmazon、富士通など様々な企業/大学/研究機関等で使われているようです。
（ライセンスはMIT）
![スクリーンショット (63).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1108240/3c7bc016-1e6a-8687-5a11-a243e1ea7abd.png)
(公式サイトより引用:https://js.cytoscape.org/）

本記事で紹介する方法ではjavascriptの知識は不要で、wrapperしたpythonライブラリがあるためそちらを使います。そのためpythonだけで完結します。




# Get Started
StreamlitとStreamlit上でCtoscape.jsを動かすライブラリを取得。
~~~bash: ライブラリのインストール
pip install streamlit
pip install st-cytoscape
~~~
 以下のファイルを`app.py`というファイル名で作成（内容についてはあとで解説します）
~~~python: ノード2つのネットワーク
import streamlit as st
from st_cytoscape  import cytoscape

st.set_page_config(layout="wide")

elements = [
    {"data": {"id": "one"}},
    {"data": {"id": "two"}},
    {"data": {"id":"edge_1","source": "one", "target": "two"}},
]
stylesheet = [
    {"selector": "#one", "style": {"label":"data(id)","width": 20, "height": 20,}},
    {"selector": "#two", "style": {"label":"data(id)","width": 20, "height": 20,}},
    {"selector": "edge", "style": {"label":"data(id)","width": 3}},
]
layout ={"name":"cose","padding": 100}

st.title("Hello Cytoscape.js")

selected = cytoscape(elements, stylesheet,height="500px",layout=layout,key="graph",)

st.write(selected)

~~~
Streamlitのサーバ起動
~~~
streamlit run app.py
~~~
ページにアクセス
以下のURLにブラウザからアクセスします。
`http://localhost:8501/`

アクセスすると以下の画像のような2つのノードが連結しただけのネットワークグラフが描画されます。マウスいろいろいじってください。
![スクリーンショット (60).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1108240/46df39a0-e3a4-8bbd-7811-242a7ea3bd0d.png)

以上、簡単に描画できました。ただ、これだけではグラフの構造をどのように入力すればよいの？状態かと思いますので次節で解説します。

## 解説
グラフ描画の処理には以下の**3つの設定情報**と**1つの関数**から構成されています。
+ **(配列)　elements** 
    +  **ノードとエッジ情報**を格納する配列
    +  ノードを辞書形式`{"data": {"id": ノードID名}}`で記述し、elementsへ格納しています。上の例ではノードID名を`one`と`two`にしています
    +  エッジは`{"data": {"id":エッジのID名, "source": 接続元のノードID, "target": 接続先のノードID}`で記述し、elementsへ格納しています。上の例では接続元を`one`、接続先を`two`としています。
+  **（配列）stlyesheet**
    +  **ノード及びエッジのスタイル**（ラベル、大きさなど）を指定
    +  ` {"selector": 対象ID, "style": {"label":ラベル名,"width": ノードの幅, "height": ノードの高さ,}`と記述します。
    +  対象IDの前には`#`をつける必要があります。また`node`もしくは'edge'と書くと、全ノードもしくは全エッジに指定のスタイルが反映されます。
    +  ラベル名を`data(id)`にするとノードID名（エッジID名）がセットされます。
+  **（辞書）layout**
    +  **グラフのレイアウトの種類**等を指定しています。
    +  例では'cosa'を指定しています。最適な配置を自動で決めてくれます。ほかのレイアウトは後で紹介します。
+  **（関数）cytoscape**
    +  上記3つの情報を引数にcytoscapeのライブラリの関数へ渡します。
    +  グラフを描画する画面の高さを指定しています。
   

あとはStreamlitでおなじみの`st.write`で描画します。

`st.set_page_config(layout="wide")`はstreamlitの表示画面の幅を広くするコマンドです。デフォルトでは幅が狭く、グラフも小さくなるので設定をおすすめします。

# 他にもできること（カスタマイズ）
最小限のグラフの書き方を紹介しましたが、他にもできること、いろいろとカスタマイズできることがあるのでいくつか紹介します。

## クリックで選択されたノードID、エッジIDを取得
これができると例えば「選択されたノードIDのみを残して再描画する」、「ノードIDの特徴量で機械学習のモデルを学習する」など様々な処理ができるようになります。
特に新しいコードを書く必要はなく`cytoscape`関数の返り値として辞書形式で取得できます。例では`selected`変数に格納しています。
以下は例でノードIDがoneのノードをクリックした場合です。
`{'nodes': ['one'], 'edges': []}`

ノードをクリックするたびに格納される値が更新されます。

## レイアウトのカスタマイズ
`layout={'name':'cose'}`の'name'の値を変えると、違うレイアウトのグラフを描画できます。
特にこだわりがないなら`cose`がおすすめです。
+ random : 各ノードをランダムに配置
+ preset : 指定した座標にノードを配置（座標の指定は後述）
+ grid : 格子状にノードを配置
+ circle : 円形にノードを配置
+ breadthfirst : ツリー状にノードを配置
+ cose : 自動で最適な位置にノードを配置（多数のノードがある場合に処理が重くなります）

## ノードのカスタマイズ
### 配置の座標指定
+ `elements`の要素の中に` "position": {"x":x軸座標,"y": y軸座標}`を追加。
+ 例えば` {"data": {"id": "one"},"position":{"x":0 ,"y":100}}`
+ レイアウトは`preset`を指定。
### ノードの色
+ `style`の要素に`"background-color":色`を追加
+ カラーコードで指定可能
+ 例のノードの色を赤にした例
   + `"style": {"label":"data(id)","width": 20, "height": 20,"background-color":"red"},`
### ノードの形
+ `sytle`の要素に`"shape":形状名`を追加。
+ 例のノードの形状をrectangleにした場合は
    + `"style": {"label":"data(id)","width": 20, "height": 20, "shape":"rectangle"}`。結果は以下図の通り
    + ほかにもさまざまな形状が用意されており、[こちら](https://js.cytoscape.org/demos/node-types/)のデモで一覧が見れます。![スクリーンショット (61).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1108240/ac474a88-65f4-31b5-06e5-4d165a2afa86.png)

### ノードに画像を張り付ける
+ `stlye`に`'background-image': ファイルのパス,'background-fit':'cover'`を追加
+ 例のノードに画像を張り付けた場合。
    + `"style": {"label":"data(id)","width": 20, "height": 20,
                    'background-image': 'https://live.staticflickr.com/7272/7633179468_3e19e45a0c_b.jpg',
                    'background-fit':'cover'}`
+ アウトプット例
![スクリーンショット (64).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1108240/10ecd0a1-a957-a745-1dba-1150cbeec310.png)


## エッジのカスタマイズ
### エッジを矢印にする
+ `stlye`に`'target-arrow-shape': 'triangle','curve-style': 'straight'`を追加
+  ※`'target-arrow-shape': 'triangle'`だけでいいはずだが、`'curve-style': 'straight'`も追加しないと矢印が表示されなかった。
+ 例のエッジを矢印にした場合
   + `"style": {"label":"data(id)","width": 3,'target-arrow-shape': 'triangle','curve-style': 'straight'}`
  + 矢印の種類も多数あり。[デモ](https://js.cytoscape.org/demos/edge-arrows/)で一覧を確認可能
### 線の種類
+ `style`に`"line-style":種類`を追加
+ 種類には以下がある
  + solid : 実践
  + dashed : 破線
  + dotted : 点線
+ 例のedgeを点線にした場合
  + `"style": {"label":"data(id)","width": 3,"line-style":"dotted"}`
### エッジの色
+ `style`に`"line-color":色`を追加
+ カラーコード指定可能
+ 例のedgeを色を赤にした場合
    + ` "style": {"label":"data(id)","width": 3,"line-color":"red"}`

## ラベルのカスタマイズ
### ラベルの位置
+ `style`に`"text-valign":位置の種類`を追加
+ 例のラベルをノードの中心に配置した場合
    + `"style": {"label":"data(id)","width": 20, "height": 20, "text-valign":"center"}`
+ 位置の種類は[デモ](https://js.cytoscape.org/demos/labels/)で確認可能
### ラベルのサイズ
+ `style`に`"font-size":サイズ値`を追加
+ 例のノードのラベルのサイズを10にした場合
  + `"style": {"label":"data(id)","width": 10, "height": 10,"font-size":10}`

# 最後に
StreamlitでCytoscapeによるネットワークグラフの描画方法について紹介しました。
本家Cytoscape.jsの方の[マニュアル](https://js.cytoscape.org/)を見ていただくとわかりますが、紹介したこと以外にも本当にいろいろできます。まだ試せてないこともあるので、ニーズがあれば紹介したいと思います。

ページ冒頭のデモのコードも載せておきます。ノード数が少ないので1つ1つ入力していますが、多い場合はpandasなどのデータフレームで保持したものをループで処理したほうがよいですね。
~~~ python:デモのapp.py
import streamlit as st
from st_cytoscape  import cytoscape

st.set_page_config(layout="wide")

st.title("Hello Cytoscape.js with Streamlit")

layout_set = st.sidebar.selectbox('select layout',('cose','breadthfirst','circle','grid'))

node_size = 60

elements = [
    {"data": {"id": "bird","img_url":'https://live.staticflickr.com/7272/7633179468_3e19e45a0c_b.jpg'}},
    {"data": {"id": "cat","img_url":'https://live.staticflickr.com/1261/1413379559_412a540d29_b.jpg'}},
    {"data": {"id": "ladybug","img_url":'https://live.staticflickr.com/3063/2751740612_af11fb090b_b.jpg'}},
    {"data": {"id": "aphid","img_url":'https://live.staticflickr.com/8316/8003798443_32d01257c8_b.jpg'}},
    {"data": {"id": "rose","img_url":'https://live.staticflickr.com/5109/5817854163_eaccd688f5_b.jpg'}},
    {"data": {"id": "grasshopper","img_url":'https://live.staticflickr.com/6098/6224655456_f4c3c98589_b.jpg'}},
    {"data": {"id": "plant","img_url":'https://live.staticflickr.com/3866/14420309584_78bf471658_b.jpg'}},
    {"data": {"id": "wheat","img_url":'https://live.staticflickr.com/2660/3715569167_7e978e8319_b.jpg'}},
    { "data": { "source": 'cat', "target": 'bird' } },
    { "data": { "source": 'bird', "target": 'ladybug' } },
    { "data": { "source": 'bird', "target": 'grasshopper' } },
    { "data": { "source": 'grasshopper', "target": 'plant' } },
    { "data": { "source": 'grasshopper', "target": 'wheat' } },
    { "data": { "source": 'ladybug', "target": 'aphid' } },
    { "data": { "source": 'aphid', "target": 'rose' } }
]
stylesheet = [
    {"selector": "node", "style": {"label":"data(id)","width": node_size, "height": node_size,
                    'background-image': "data(img_url)",
                    'background-fit':'cover'}},
    {"selector": "edge", "style": {"width": 4,'target-arrow-shape': 'triangle','curve-style': 'straight'}},
]
layout ={"name":layout_set,"padding": 20}


selected = cytoscape(elements, stylesheet,height="600px",layout=layout,
        key="graph",)

st.write(selected)
for i in selected["nodes"]:
    st.sidebar.info(i + ' is selected')


~~~

いいところばかり挙げましたがBadポイントも挙げておきます。
+ Cytoscape.jsでは「ノードをクリックしたら別サイトに飛ぶ」「エッジをマウス操作で追加する」など複雑なことも実装次第でできますが、javascriptなどで開発する必要があります（その場合はjavascriptのほかにHTML,CSS,バックエンド側の知識が必要になり気軽な実装が難しくなります）
+ graphvizはエッジとノードが重複しないよう描画してくれますが、こちらはできません。