HoloViewsのChordからbokehのGraphRendererを学ぶ



Pythonに[HoloViews](https://holoviews.org/index.html)というライブラリがある。bokehやmatplotlibといったグラフ描画ライブラリを簡単かつ共通のコードで使えるようにしてくれる便利なもので、その中にChordというエレメントがある。[Reference Gallery](https://holoviews.org/reference/index.html)の[該当ページ](https://holoviews.org/reference/elements/bokeh/Chord.html#elements-bokeh-gallery-chord)に2つの例が載っていて、それをbokehで再現しようとしたら`GraphRenderer`の勉強になったのでまとめておく。

# bokehについて

ブラウザやjupyter notebook上でデータのインタラクティブな可視化ができるライブラリ。pythonだけでグリグリ動かせるグラフをつくり、データを差し替えたりできる。2021年9月にバージョン2.4.0がリリースされた。公式ドキュメントのReferenceが見やすくなってうれしい。

# 参考サイト

- [bokeh](https://docs.bokeh.org/en/2.4.0/)
- [holoviews](https://holoviews.org/index.html)
- [pandas](https://pandas.pydata.org/docs/)

# バージョン情報

- python 3.8.8
- bokeh 2.3.3
- holoviews 1.14.5
- pandas 1.3.2
- jupyterlab 3.1.7

# 使用するデータ

- [bokeh.sampledata.les_mis](https://docs.bokeh.org/en/latest/docs/reference/sampledata.html#module-bokeh.sampledata.les_mis)

```python
import pandas as pd
from bokeh.sampledata.les_mis import data

print(data.keys())

nodes = pd.DataFrame(data['nodes'])
links = pd.DataFrame(data['links'])

print(len(nodes), len(links))
print(nodes.head(), end='\n\n')
print(links.head())
```

    dict_keys(['nodes', 'links'])
    77 254
                  name  group
    0           Myriel      1
    1         Napoleon      1
    2  Mlle.Baptistine      1
    3     Mme.Magloire      1
    4     CountessdeLo      1
    
       source  target  value
    0       1       0      1
    1       2       0      8
    2       3       0     10
    3       3       2      6
    4       4       0      1
    
# Chord

```python
import holoviews as hv

hv.extension('bokeh')
hv.output(size=200)
```

```python
chord = hv.Chord(links)
chord
```

![chord_1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1714033/5c8a8d05-4d8b-798f-e8d6-5b449e984367.jpeg)

![chord_1_hover.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1714033/6a44e5ed-cffc-5dce-cfff-fbf6f7bcefcd.jpeg)
![chord_1_select.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1714033/0f4fdd1e-69bd-2271-9c94-a976f43f426a.jpeg)

一行書いただけで何だかすごいグラフが出てくる。ホバーやタップにも対応している。でも、これのベースはbokehなので、bokehで描こうと思えば描けるはず。とはいうものの、どうやればいいのか全く分からない。公式の[ギャラリー](https://docs.bokeh.org/en/2.4.0/docs/gallery.html)をのぞいてみても、それらしいものはない。

仕方がないので、エレメントからbokehのFigureを取り出して参照する。

```python
from bokeh.io import show, output_notebook
from bokeh.plotting import figure

output_notebook()
```

```python
p = hv.render(chord)  # bokehの Figureを取り出す
p
```

    Figure(	id = '1248', …)

GlyphRendererとGraphRendererで構成されているのがわかる。

```python
p.renderers
```

    [GlyphRenderer(id='1314', ...), GraphRenderer(id='1295', ...)]

# GlyphRenderer

先ずは`GlyphRenderer`を見てみる。`GlyphRenderer`は描画するグリフ(図形)とデータを持っている。`GlyphRenderer.glyph`に何らかのグリフ(図形)、`GlyphRenderer.data_source`にデータである`ColumnDataSource`が入っている。

## MultiLine

取り出したglyph_rendererの`glyph`属性を確認。グリフは`MultiLine`。
描画してみると、輪っか部分だった。幾つもの弧を組み合わせてひとつの円を作っているのが、ホバーツールでわかる。


```python
glyph_renderer = p.renderers[0]
print(glyph_renderer.glyph)
print(glyph_renderer.data_source)

p2 = figure(frame_height=300,
            frame_width=300,
            tooltips=[('', '$index')])
p2.renderers.append(glyph_renderer)

show(p2)
```

    MultiLine(id='1310', ...)
    ColumnDataSource(id='1280', ...)
    
![multiline_circle.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1714033/95514c39-7d20-078b-634e-3971566d84d0.jpeg)
![multiline_circle_2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1714033/22a82431-15eb-8444-1975-22a34391b32b.jpeg)

MultiLineは複数の線を描画できる。xsにxの座標、ysにyの座標をそれぞれ渡す。データの形は"list of list"で、`np.nan`で区切って複数の線をひとまとまりにすることもできる。


```python
import numpy as np

xs = [[1, 2, 3, 4, 4, np.nan, 7, 8, 9],
      [1, 2, 3, 4, 5, 6]]
ys = [[3, 1, 2, 4, 4, np.nan, 7, 8, 9],
      [6, 8, 8, 6, 7, 2]]
colors = ['firebrick', 'navy']
alpha = [0.8, 0.6]

p3 = figure(frame_height=200,  # 描画スペースの高さ指定
           frame_width=200,   # 描画スペースの横幅指定 
           tooltips='$index'
           )
p3.multi_line(xs, ys,
             color=colors,
             alpha=alpha,
             line_width=10
             )
show(p3)
```

![multiline_sample.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1714033/197e51eb-9c6c-ba43-d8c7-82613591a83f.jpeg)

## ColumnDataSource

`GlyphRenderer.data_source`属性には`ColumnDataSource`が入っている。

`ColumnDataSource`はglyphの描画やホバーツールに必要なデータを保持している。`data`属性でデータの参照、`to_df()`メソッドでデータをデータフレームに変換できる。他にもデータの追加や削除のメソッドがある。詳しくは[公式](https://docs.bokeh.org/en/2.4.0/docs/reference/models/sources.html?highlight=columndatasource#bokeh.models.ColumnDataSource)を参照。

glyph_rendererのデータをデータフレーム化してみる。`'index'`, `'index_hover'`, `'arc_xs'`, `'arc_ys'`の列がある。

- 'index' - 頂点のインデックス、固有値。  
- 'index_hover' - HoverTool用(?)の'index'のstr型
- 'arc_xs' - x座標
- 'arc_ys' - y座標


```python
multi_cds = glyph_renderer.data_source

df = multi_cds.to_df()

print(multi_cds)
print(len(df))
df.head()
```

    ColumnDataSource(id='1280', ...)
    77
    
<div>
<style scoped>
    .dataframe tbody tr th:only-of-type {
        vertical-align: middle;
    }

    .dataframe tbody tr th {
        vertical-align: top;
    }

    .dataframe thead th {
        text-align: right;
    }
</style>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>index</th>
      <th>index_hover</th>
      <th>arc_xs</th>
      <th>arc_ys</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>0</th>
      <td>0</td>
      <td>0</td>
      <td>[1.0, 0.9999799714965371, 0.9999198867884302, ...</td>
      <td>[0.0, 0.006329028818457834, 0.0126578041149644...</td>
    </tr>
    <tr>
      <th>1</th>
      <td>1</td>
      <td>1</td>
      <td>[0.9927783948222082, 0.9927467247726159, 0.992...</td>
      <td>[0.11996273910777294, 0.12022454180924975, 0.1...</td>
    </tr>
    <tr>
      <th>2</th>
      <td>2</td>
      <td>2</td>
      <td>[0.9921648603295138, 0.9917982703635422, 0.991...</td>
      <td>[0.12493554309049267, 0.12781310928025358, 0.1...</td>
    </tr>
    <tr>
      <th>3</th>
      <td>3</td>
      <td>3</td>
      <td>[0.9837758550359634, 0.9831550409112764, 0.982...</td>
      <td>[0.17940197058075802, 0.1827735361882195, 0.18...</td>
    </tr>
    <tr>
      <th>4</th>
      <td>4</td>
      <td>4</td>
      <td>[0.9700122274758824, 0.9699480971028605, 0.969...</td>
      <td>[0.24305612221722958, 0.24331191694312868, 0.2...</td>
    </tr>
  </tbody>
</table>
</div>

```python
print(*df['index'])
```

    0 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
    
```python
for v in df['index_hover']:
    print(f'{v!r}', end=' ')
```

    '0' '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' 

```python
multi_cds.data['arc_xs'][:2]
```

    [array([1.        , 0.99997997, 0.99991989, 0.99981975, 0.99967956,
            0.99949933, 0.99927906, 0.99901876, 0.99871845, 0.99837812,
            0.99799781, 0.99757752, 0.99711727, 0.99661708, 0.99607697,
            0.99549696, 0.99487707, 0.99421733, 0.99351776, 0.99277839]),
     array([0.99277839, 0.99274672, 0.99271499, 0.99268318, 0.9926513 ,
            0.99261935, 0.99258734, 0.99255525, 0.9925231 , 0.99249088,
            0.99245859, 0.99242623, 0.9923938 , 0.9923613 , 0.99232873,
            0.9922961 , 0.99226339, 0.99223062, 0.99219777, 0.99216486])]

```python
multi_cds.data['arc_ys'][:2]
```

    [array([0.        , 0.00632903, 0.0126578 , 0.01898607, 0.02531358,
            0.03164007, 0.0379653 , 0.04428901, 0.05061094, 0.05693084,
            0.06324847, 0.06956356, 0.07587586, 0.08218512, 0.0884911 ,
            0.09479352, 0.10109215, 0.10738673, 0.11367701, 0.11996274]),
     array([0.11996274, 0.12022454, 0.12048634, 0.12074812, 0.1210099 ,
            0.12127167, 0.12153343, 0.12179518, 0.12205693, 0.12231866,
            0.12258039, 0.12284211, 0.12310382, 0.12336552, 0.12362721,
            0.12388889, 0.12415057, 0.12441224, 0.12467389, 0.12493554])]

# GraphRenderer

グラフを描画するためのレンダラー。頂点と辺、2つの`GlyphRenderer`を持つ。頂点のレンダラーは`node_renderer`属性、辺は`edge_renderer`属性にある。公式の[Reference](https://docs.bokeh.org/en/2.4.0/docs/reference/models/renderers.html?highlight=graphrenderer#graphrenderer)と[User guide](https://docs.bokeh.org/en/2.4.0/docs/user_guide/graph.html)も参照。グラフの描画にかかわる属性は以下。

- 頂点 : `node_renderer`と`layout_provider`
- 辺 : `edge_renderer`
- ホバー : `inspection_policy`
- セレクト : `selection_policy`


chordのgraph_rendererを描画してみる。円状に並んでいる青い丸が頂点、黒い線が辺。


```python
graph_renderer = p.renderers[1]

p4 = figure(frame_height=350,
            frame_width=350,
            tooltips='$index')
p4.renderers.append(graph_renderer)

show(p4)
```

![gr.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1714033/f6acdd12-e70f-b8b2-09f6-d85e360ca599.jpeg)

```python
print(graph_renderer)
print()
vars(graph_renderer)
```

    GraphRenderer(id='1295', ...)

    {'_id': '1295',
     '_document': <bokeh.document.document.Document at 0x1af1e43c670>,
     '_temp_document': None,
     '_event_callbacks': {},
     '_callbacks': {},
     '_property_values': {'layout_provider': StaticLayoutProvider(id='1282', ...),
      'node_renderer': GlyphRenderer(id='1287', ...),
      'edge_renderer': GlyphRenderer(id='1293', ...),
      'selection_policy': NodesAndLinkedEdges(id='1304', ...),
      'inspection_policy': NodesAndLinkedEdges(id='1306', ...)},
     '_unstable_default_values': {'js_property_callbacks': {},
      'tags': [],
      'js_event_callbacks': {}},
     '_unstable_themed_values': {},
     '_initialized': True}

## 頂点(ノード)

### GraphRenderer.node_renderer

中身は`GlyphRenderer`。デフォルトのグリフは`Circle`。`Scatter`、`Rect`のようにx, yを指定するグリフに置き換え可能。データには頂点のユニーク値となる`'index'`列が必要。

```python
node_renderer = graph_renderer.node_renderer
node_cds = node_renderer.data_source

print(node_renderer)
print(node_renderer.glyph)
print(node_cds)
```

    GlyphRenderer(id='1287', ...)
    Circle(id='1283', ...)
    ColumnDataSource(id='1280', ...)

データを見てみると輪っかのデータとキーが同じ。
確認してみると、同一の`ColumnDataSource`を使用していることがわかる。となると、データにはそれぞれの頂点の位置を指定する列が存在しないことになる。`arc_xs`, `arc_ys`は輪っかのMultiLine用。試しに描画してみても何も表示されない。

```python
print(node_cds.data.keys())
print(multi_cds.data.keys())
node_cds is multi_cds
```

    dict_keys(['index', 'index_hover', 'arc_xs', 'arc_ys'])
    dict_keys(['index', 'index_hover', 'arc_xs', 'arc_ys'])
    True

```python
p5 = figure(plot_width=250, plot_height=250)
p5.renderers.append(node_renderer)

show(p5)
```

![white.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1714033/e304d2d0-40e4-f041-58a4-c5bf6bc518a2.jpeg)

### GraphRenderer.layout_provider

ノードの位置を指定するデータは`layout_provider`の中に入っている。中身は`StaticLayoutProvider`。`graph_layout`キーワード引数にノードの位置を指定する辞書を渡す。キーが頂点のindex、値が座標(x, y)。以下のように使う。

```python
from bokeh.models import GraphRenderer, StaticLayoutProvider

gr = GraphRenderer()

graph_layout = {0: [1, 0],
                1: [0, 1],
                2: [-1, 0],
                3: [0, -1]}
gr.layout_provider = StaticLayoutProvider(graph_layout=graph_layout)
```
    
これで頂点の描画ができる。

```python
from bokeh.models import Scatter

# GraphRendererのインスタンス化
gr = GraphRenderer()

# 頂点                                       
index = [0, 1, 2, 3]
gr.node_renderer.glyph = Scatter(size=20, fill_color='orange')
gr.node_renderer.data_source.add(index, 'index')  # data_source.data['index'] = index

# layout_provider
graph_layout = {0: [1, 0], 1: [0, 1], 2: [-1, 0], 3: [0, -1]}
gr.layout_provider = StaticLayoutProvider(graph_layout=graph_layout)

print(gr.node_renderer.glyph, gr.node_renderer.data_source.data)
print(gr.layout_provider.graph_layout)

p6 = figure(frame_width=250,
            frame_height=250,
            x_range=(-1.3, 1.3),
            y_range=(-1.3, 1.3))
p6.renderers.append(gr)

show(p6)
```

    Scatter(id='1805', ...) {'index': [0, 1, 2, 3]}
    {0: [1, 0], 1: [0, 1], 2: [-1, 0], 3: [0, -1]}

![node_sample.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1714033/63104788-1d82-f15a-33c5-bded5a1c0b34.jpeg)

## 辺(エッジ)

### GraphRenderer.edge_renderer

中身は`GlyphRenderer`。グリフは`MultiLine`。データには始点と終点をあらわす`'start'`と`'end'`列が必要。

```python
edge_renderer = graph_renderer.edge_renderer
edge_cds = edge_renderer.data_source

print(edge_renderer)
print(edge_renderer.glyph)
print(edge_cds)
```

    GlyphRenderer(id='1293', ...)
    MultiLine(id='1289', ...)
    ColumnDataSource(id='1281', ...)

```python
df = edge_cds.to_df()

print(len(df))
df.head()
```

    254

<div>
<style scoped>
    .dataframe tbody tr th:only-of-type {
        vertical-align: middle;
    }

    .dataframe tbody tr th {
        vertical-align: top;
    }

    .dataframe thead th {
        text-align: right;
    }
</style>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>start</th>
      <th>end</th>
      <th>xs</th>
      <th>ys</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>0</th>
      <td>1</td>
      <td>0</td>
      <td>[0.9927783948222082, 0.9630074475389099, 0.934...</td>
      <td>[0.11996273910777294, 0.11636535584423288, 0.1...</td>
    </tr>
    <tr>
      <th>1</th>
      <td>2</td>
      <td>0</td>
      <td>[0.9837758550359634, 0.9542808382004578, 0.926...</td>
      <td>[0.17940197058075802, 0.17398204989887306, 0.1...</td>
    </tr>
    <tr>
      <th>2</th>
      <td>3</td>
      <td>0</td>
      <td>[0.9700122274758824, 0.9409401385262388, 0.913...</td>
      <td>[0.24305612221722958, 0.23567173954270657, 0.2...</td>
    </tr>
    <tr>
      <th>3</th>
      <td>3</td>
      <td>2</td>
      <td>[0.978545131672936, 0.9492070855485545, 0.9211...</td>
      <td>[0.20603258305228409, 0.19982079380656642, 0.1...</td>
    </tr>
    <tr>
      <th>4</th>
      <td>4</td>
      <td>0</td>
      <td>[0.9700122274758824, 0.9409417399748237, 0.913...</td>
      <td>[0.24305612221722958, 0.23564908833075593, 0.2...</td>
    </tr>
  </tbody>
</table>
</div>

データを見てみると、`'start'`、`'end'`、 `'xs'`、 `'ys'`の列がある。このうち、`'start'`と`'end'`は必須。値は頂点のインデックスのシーケンス。これだけで頂点と頂点を結ぶ直線を描画してくれる。`'xs'`と`'ys'`が辺の座標になる。`'xs'`、`'ys'`はMultiLine用なので、値はx, yそれぞれの"list of list"。

```python
# nanで区切られているのを確認できる。
print('始点', df['start'][8], '終点', df['end'][8])
print('辺:\n', df['xs'][8])
```

    始点 8 終点 0
    辺:
     [0.962268   0.93343509 0.9058501  0.87951207 0.85442005 0.83057311
     0.80797028 0.78661061 0.76649317 0.74761699 0.72998113 0.71358464
     0.69842656 0.68450596 0.67182188 0.66037336 0.65015947 0.64117925
     0.63343175 0.62691602 0.62163111 0.61757608 0.61474997 0.61315183
     0.61278072 0.61363568 0.61571576 0.61902002 0.62354751 0.62929727
     0.63626836 0.64445982 0.65387071 0.66450008 0.67634697 0.68941045
     0.70368955 0.71918333 0.73589084 0.75381113 0.77294325 0.79328625
     0.81483918 0.83760109 0.86157104 0.88674806 0.91313122 0.94071956
     0.96951214 0.999508          nan 0.9649464  0.9360316  0.90836497
     0.88194562 0.85677267 0.83284523 0.81016242 0.78872335 0.76852714
     0.7495729  0.73185974 0.71538679 0.70015315 0.68615794 0.67340028
     0.66187928 0.65159405 0.64254371 0.63472738 0.62814417 0.6227932
     0.61867357 0.61578441 0.61412482 0.61369393 0.61449086 0.6165147
     0.61976458 0.62423962 0.62993893 0.63686162 0.6450068  0.6543736
     0.66496113 0.6767685  0.68979483 0.70403923 0.71950082 0.73617871
     0.75407202 0.77317986 0.79350135 0.8150356  0.83778173 0.86173884
     0.88690607 0.91328251 0.94086729 0.96965952 0.99965832]

これで頂点と辺の両方を描画できる。始点と終点を一本の直線で結ぶ場合、`'xs'`、`'ys'`は必要ない。

```python
from bokeh.models import MultiLine

gr = GraphRenderer()

# 頂点
index = [0, 1, 2, 3]
gr.node_renderer.glyph = Scatter(marker='hex', size=16, fill_color='orange')
gr.node_renderer.data_source.add(index, 'index')

# 頂点の位置
graph_layout = {0: [1, 0], 1: [0, 1], 2: [-1, 0], 3: [0, -1]}
gr.layout_provider = StaticLayoutProvider(graph_layout=graph_layout)

# 辺
gr.edge_renderer.glyph = MultiLine(line_width=5, line_color='colors')

edge_source = dict(
    start=[0, 1],
    end=[2, 3],
    xs=[[1, 0, -1], [0, -0.2, 0, np.nan, 0.2, 0, 0.2]],
    ys=[[0, 0.2, 0], [1, 0, -1, np.nan, 1, 0, -1]],
    colors=['tomato', 'lightblue']
)
gr.edge_renderer.data_source.data = edge_source

p7 = figure(frame_width=250,
            frame_height=250,
            x_range=(-1.3, 1.3),
            y_range=(-1.3, 1.3),
            )
p7.renderers.append(gr)

show(p7)
```

![edge_sample.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1714033/2ceceb0a-1fc7-af1d-a37d-35b2207777ad.jpeg)

## ホバーツール

### GraphRenderer.inspection_policy

ホバーツールがどこでどこに働くかを決めるもの。中身は`EdgesOnly`, `NodesOnly`, `NodesAndLinkedEdges`, `EdgesAndLinkedNodes`の4つのインスタンスのいずれか。デフォルトは`NodesOnly`。
`graph_renderer.inspection_policy = NodesAndLinkedEdges()`のように使う。
カーソル位置(どこで)　対象(どこに)は以下の表。

|クラス名|カーソル位置|対象|
|-|-|-|
|NodesOnly|頂点|頂点|
|EdgesOnly|辺|辺|
|NodesAndLinkedEdges|頂点|頂点・辺|
|EdgesAndLinkedNodes|辺|辺・両端の頂点|

Chordで描画されるグラフにマウスカーソルを乗っけると、頂点で、頂点と辺に働いているので、`NodesAndLinkedEdges`だとわかる。

```python
graph_renderer.inspection_policy  # Chordの graph_renderer
```

```
NodesAndLinkedEdges(id='1306', ...)
```

Chordのようにカーソルを乗っけると色が変わるようにするには、もう一手間必要。`GlyphRenderer`の`hover_glyph`属性に、`graph_renderer.node_renderer.hover_glyph = Circle(fill_color='green', line_color='green')`のように色違いのグリフを設定する必要がある。違う種類のグリフを設定すると挙動が怪しくなるので避けた方がいいのかもしれない。

```python
from bokeh.models import (Circle,
                          NodesOnly,
                          EdgesOnly,
                          EdgesAndLinkedNodes,
                          NodesAndLinkedEdges)

gr = GraphRenderer()

# 頂点
node_source = dict(
    index=[0, 1, 2, 3],
    node_text=['zero', 'one', 'two', 'three']
)
gr.node_renderer.glyph = Circle(size=20,
                                fill_color='orange',
                                fill_alpha=0.5,
                                )
gr.node_renderer.data_source.data = node_source

# 頂点の位置
graph_layout = {0: [1, 0], 1: [0, 1], 2: [-1, 0], 3: [0, -1]}
gr.layout_provider = StaticLayoutProvider(graph_layout=graph_layout)

# 辺
edge_source = dict(
    start=[0, 1],
    end=[2, 3],
    xs=[[1, 0, -1], [0, -0.2, 0, np.nan, 0.2, 0, 0.2]],
    ys=[[0, 0.2, 0], [1, 0, -1, np.nan, 1, 0, -1]],
    colors=['tomato', 'lightblue'],
    edge_text=['辺(0, 2)', '辺(1, 3)']
)
gr.edge_renderer.glyph = MultiLine(line_width=5, line_color='colors')
gr.edge_renderer.data_source.data = edge_source

# 頂点と辺の hover_glyphの設定
gr.node_renderer.hover_glyph = Circle(fill_alpha=1,
                                      line_color='firebrick',
                                      fill_color='darkgreen',
                                      )
gr.edge_renderer.hover_glyph = MultiLine(line_alpha=0.5,
                                         line_color='darkblue',
                                         line_width=10
                                         )

# Figureを作成してGraphRendererを追加
p8 = figure(frame_width=250,
            frame_height=250,
            x_range=(-1.3, 1.3),
            y_range=(-1.3, 1.3),
            tooltips=[('node_text', '@node_text'), ('edge_text', '@edge_text')]
            )

p8.renderers.append(gr)
```

```python
# NodesOnly(デフォルト)
print(gr.inspection_policy)

show(p8)
```

    NodesOnly(id='2155', ...)
    
![hover_1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1714033/62aea4f7-1d4a-1c44-80f6-09d2014dafd2.jpeg)

```python
# EdgesOnly
gr.inspection_policy = EdgesOnly()
print(gr.inspection_policy)

show(p8)
```

    EdgesOnly(id='2375', ...)
    
![hover_2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1714033/26255706-5561-3106-a6db-0d64dd3d6e82.jpeg)

```python
# NodesAndLinkedEdges
gr.inspection_policy = NodesAndLinkedEdges()
print(gr.inspection_policy)

show(p8)
```

    NodesAndLinkedEdges(id='2590', ...)
    
![hover_3.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1714033/c9048a93-1e96-9947-fdd0-c492e0f11f12.jpeg)

```python
# EdgesAndLinkedNodes
gr.inspection_policy = EdgesAndLinkedNodes()
print(gr.inspection_policy)

show(p8)
```

    EdgesAndLinkedNodes(id='2804', ...)
    
![hover_4.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1714033/207968dc-001d-96c8-c1a0-9abed135603d.jpeg)

## タップツール

### GraphRenderer.selection_policy

タップツールやボックスセレクトなど選択用ツールに関しての設定。中身や挙動は`inspection_policy`と同じで、`NodesOnly`, `EdgesOnly`, `NodesAndLinkedEdges`, `EdgesAndLinkedNodes`となる。デフォルトは`NodesOnly`。辺に関してはタップツールのみ有効っぽい。  

選択した部分の色を変えたりするのもホバーツールと同様。`selection_glyph`属性に別のグリフを設定する。違うのは、選択していない部分のグリフも設定できること。`nonselection_glyph`属性にグリフを設定する。

```python
gr = GraphRenderer()

# 頂点
node_source = dict(
    index=[0, 1, 2, 3],
    node_text=['zero', 'one', 'two', 'three']
)
gr.node_renderer.glyph = Circle(size=20,
                                fill_color='orange',
                                fill_alpha=0.5,
                                )
gr.node_renderer.data_source.data = node_source

# 頂点の位置
graph_layout = {0: [1, 0], 1: [0, 1], 2: [-1, 0], 3: [0, -1]}
gr.layout_provider = StaticLayoutProvider(graph_layout=graph_layout)

# 辺
edge_source = dict(
    start=[0, 1],
    end=[2, 3],
    xs=[[1, 0, -1], [0, -0.2, 0, np.nan, 0.2, 0, 0.2]],
    ys=[[0, 0.2, 0], [1, 0, -1, np.nan, 1, 0, -1]],
    colors=['tomato', 'lightblue'],
    edge_text=['辺(0, 2)', '辺(1, 3)']
)
gr.edge_renderer.glyph = MultiLine(line_width=5, line_color='colors')
gr.edge_renderer.data_source.data = edge_source

# 頂点と辺の selection_glyph, nonselection_glyphの設定
gr.node_renderer.selection_glyph = Circle(fill_alpha=1,
                                      line_color='firebrick',
                                      fill_color='darkgreen',
                                      )
gr.node_renderer.nonselection_glyph = Circle(fill_alpha=0.2,
                                             line_alpha=0.2,
                                             fill_color='orange'
                                             )

gr.edge_renderer.selection_glyph = MultiLine(line_alpha=0.5,
                                         line_color='darkblue',
                                         line_width=10
                                         )
gr.edge_renderer.nonselection_glyph = MultiLine(line_alpha=0.2,
                                                line_color='grey',
                                                line_width=3
                                                )

# Figureを作成してGraphRendererを追加
p9 = figure(frame_width=250,
            frame_height=250,
            x_range=(-1.3, 1.3),
            y_range=(-1.3, 1.3),
            tools='pan, wheel_zoom, tap, box_select, reset',
            tooltips=[('node_text', '@node_text'), ('edge_text', '@edge_text')]
            )

p9.renderers.append(gr)
```


```python
# NodesOnly(デフォルト)
print(gr.selection_policy)

show(p9)
```

    NodesOnly(id='3089', ...)
    
![select_1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1714033/f510fe4b-9085-0b07-7e5f-72dc3637d0bd.jpeg)

```python
# EdgesOnly
gr.selection_policy = EdgesOnly()
print(gr.selection_policy)

show(p9)
```

    EdgesOnly(id='3326', ...)
    
![select_2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1714033/98f1c99d-949b-1904-f29b-da68d65df7e3.jpeg)

```python
# NodesAndLinkedEdges
gr.selection_policy = NodesAndLinkedEdges()
print(gr.selection_policy)

show(p9)
```

    NodesAndLinkedEdges(id='3562', ...)
    
![select_3.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1714033/e6e3ee60-8bc2-d079-3873-4e5e1bc3bc50.jpeg)

```python
# EdgesAndLinkedNodes
gr.selection_policy = EdgesAndLinkedNodes()
print(gr.selection_policy)

show(p9)
```

    EdgesAndLinkedNodes(id='3798', ...)
    
![select_4.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1714033/36a62bfa-f87f-06c7-aea2-e4ea328078dd.jpeg)

これでChordと同様のグラフを描画できる。上のグラフに輪っかにあたる部分を加えてみる。`Figure.multi_line`メソッドを使うと、ホバー、セレクトの設定が楽になる。

```python
gr = GraphRenderer()

# 頂点
# 頂点のソースは輪っか部分と共通なので、ここに輪っかの座標 xs, ysを加える
node_source = dict(
    index=[0, 1, 2, 3],
    node_text=['zero', 'one', 'two', 'three'],
    xs=[[0.5, 1, 0.5],[0.5, 0, -0.5], [-0.5, -1, -0.5], [-0.5, 0, 0.5]],
    ys=[[-0.5, 0, 0.5], [0.5, 1, 0.5], [0.5, 0, -0.5], [-0.5, -1, -0.5]]
)
gr.node_renderer.glyph = Circle(size=20,
                                fill_color='orange',
                                fill_alpha=0.5,
                                )
gr.node_renderer.data_source.data = node_source

# 頂点の位置
graph_layout = {0: [1, 0], 1: [0, 1], 2: [-1, 0], 3: [0, -1]}
gr.layout_provider = StaticLayoutProvider(graph_layout=graph_layout)

# 辺
edge_source = dict(
    start=[0, 1],
    end=[2, 3],
    xs=[[1, 0, -1], [0, -0.2, 0, np.nan, 0.2, 0, 0.2]],
    ys=[[0, 0.2, 0], [1, 0, -1, np.nan, 1, 0, -1]],
    colors=['tomato', 'lightblue'],
    edge_text=['zero to one', 'two to three']
)
gr.edge_renderer.glyph = MultiLine(line_width=5, line_color='colors')
gr.edge_renderer.data_source.data = edge_source

# 頂点と辺の hover_glyph, selection_glyph, nonselection_glyphの設定
gr.node_renderer.hover_glyph = Circle(fill_color='limegreen')
gr.node_renderer.selection_glyph = Circle(line_color='firebrick',
                                          fill_color='darkgreen',
                                          )
gr.node_renderer.nonselection_glyph = Circle(fill_alpha=0.2,
                                             line_alpha=0.2,
                                             fill_color='grey',
                                             line_color='grey'
                                             )
gr.edge_renderer.hover_glyph = MultiLine(line_color='limegreen',
                                         line_width=10
                                         )
gr.edge_renderer.selection_glyph = MultiLine(line_alpha=0.5,
                                             line_color='darkblue',
                                             line_width=10
                                             )
gr.edge_renderer.nonselection_glyph = MultiLine(line_alpha=0.2,
                                                line_color='grey',
                                                line_width=3
                                                )
# inspection_policy, selection_policyの設定
gr.inspection_policy = gr.selection_policy = NodesAndLinkedEdges()

# Figureを作成してGraphRendererを追加
p10 = figure(frame_width=250,
             frame_height=250,
             x_range=(-1.3, 1.3),
             y_range=(-1.3, 1.3),
             tools='box_select, tap, wheel_zoom, reset',
             tooltips=[('node_text', '@node_text'), ('edge_text', '@edge_text')]
             )

# MultiLine(輪っかにあたる部分)
# Figure.multi_lineを使えば、少し面倒が省ける
p10.multi_line(source=gr.node_renderer.data_source,
               line_width=10,
               hover_color='limegreen'
               )

p10.renderers.append(gr)

show(p10)
```

![comp_1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1714033/d735f465-6ef6-5b78-23d4-85091ee981df.jpeg)
![comp_2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1714033/c342f9bd-87a5-ab76-5879-f2151ff65d03.jpeg)
![comp_3.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1714033/7d0168d8-fa33-3858-4587-277ab07f183f.jpeg)

形は別として、頂点と複数の線、それに輪っかをもつChordと同様のグラフが描けた。ホバーとタップの動きも同じ。

# 配置

後は頂点の位置や辺の数の決め方、辺の曲線化が分かればChordを再現できるはず。と言っても全く分からないので[Github](https://github.com/)の[HoloViews](https://github.com/holoviz/holoviews)から[Chord](https://github.com/holoviz/holoviews/blob/b1421c41aca821ee23daff376e44f42b208ce9cf/holoviews/element/graphs.py#L745)のコードを見てみる。__init__の中にいかにもという感じの[layout_chords](https://github.com/holoviz/holoviews/blob/b1421c41aca821ee23daff376e44f42b208ce9cf/holoviews/element/graphs.py#L615)という名前を発見。Chordのすぐ上にあるので眺めてみると、numpyを使って何かやってるなあというのはわかった。理解し得た範囲での流れは次のような感じ。

- 全体の辺の数の調整。
- 頂点に属する辺の数(重み)に応じて、円周上で占める範囲を求める。
- 頂点の座標を求める。
- 辺の端点の位置を求める。
- 辺の曲線の座標を求める。

以下、`layout_chords`の処理を見ていくが、コードは同一ではない。また、自分がわかりやすいのでpandasを使っている。

## 全体の辺の数の調整

`layout_chords`に`max_chords`という属性がある。デフォルトは500。データの重みの合計がこの値を超えたら調整するようだ。

```python
# 最初に importしたデータ
edges = links.copy()
print(edges.head())
print(edges['value'].sum())

max_chords = 500

edges['value'] = edges['value'] / edges['value'].sum() * max_chords
edges['value'] = np.ceil(edges['value']).astype(int)  # 小数点以下は切り上げ

edges.head()
```

       source  target  value
    0       1       0      1
    1       2       0      8
    2       3       0     10
    3       3       2      6
    4       4       0      1
    820
    
<div>
<style scoped>
    .dataframe tbody tr th:only-of-type {
        vertical-align: middle;
    }

    .dataframe tbody tr th {
        vertical-align: top;
    }

    .dataframe thead th {
        text-align: right;
    }
</style>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>source</th>
      <th>target</th>
      <th>value</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>0</th>
      <td>1</td>
      <td>0</td>
      <td>1</td>
    </tr>
    <tr>
      <th>1</th>
      <td>2</td>
      <td>0</td>
      <td>5</td>
    </tr>
    <tr>
      <th>2</th>
      <td>3</td>
      <td>0</td>
      <td>7</td>
    </tr>
    <tr>
      <th>3</th>
      <td>3</td>
      <td>2</td>
      <td>4</td>
    </tr>
    <tr>
      <th>4</th>
      <td>4</td>
      <td>0</td>
      <td>1</td>
    </tr>
  </tbody>
</table>
</div>

## 頂点の辺の数に応じた円周上の範囲を求める

ラジアンと言われてもピンとこない自分は次のことだけ覚えた。

- 半径1、中心が(0, 0)の円で考える。
- ラジアンは角度。180° = π、360° = 2π。
- 円周は直径×円周率で2π。
- 360° = 2π = 円周。
- 頂点が円周上で占める範囲は弧の長さで、ラジアン(角度)と同じになる。

`layout_chords`の処理は以下のような感じ。

```python
# 辺が存在する頂点一覧
nodes = np.union1d(edges['source'], edges['target'])
nodes = pd.DataFrame({'index': nodes})

# edges['value']から頂点ごとの辺の数(重み)を計算
values = edges.pivot('source', 'target', 'value')
nodes['weights_of_areas'] = values.sum().add(values.sum(axis=1), fill_value=0)

# 辺の数を範囲(弧の長さ)に変換
nodes['areas_in_radians'] = (
    nodes['weights_of_areas'] / nodes['weights_of_areas'].sum() * 2*np.pi
)
nodes.head()
```

<div>
<style scoped>
    .dataframe tbody tr th:only-of-type {
        vertical-align: middle;
    }

    .dataframe tbody tr th {
        vertical-align: top;
    }

    .dataframe thead th {
        text-align: right;
    }
</style>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>index</th>
      <th>weights_of_areas</th>
      <th>areas_in_radians</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>0</th>
      <td>0</td>
      <td>24.0</td>
      <td>0.120252</td>
    </tr>
    <tr>
      <th>1</th>
      <td>1</td>
      <td>1.0</td>
      <td>0.005011</td>
    </tr>
    <tr>
      <th>2</th>
      <td>2</td>
      <td>11.0</td>
      <td>0.055116</td>
    </tr>
    <tr>
      <th>3</th>
      <td>3</td>
      <td>13.0</td>
      <td>0.065137</td>
    </tr>
    <tr>
      <th>4</th>
      <td>4</td>
      <td>1.0</td>
      <td>0.005011</td>
    </tr>
  </tbody>
</table>
</div>

## 頂点の座標を求める

中心(0, 0)、半径1の円の円周上の座標は `x = cos(rad)`、 `y = sin(rad)`。

```python
# 各弧の長さを累積和で1つの円の上での位置に
points = nodes['areas_in_radians'].cumsum()
points.loc[-1] = 0
points = points.sort_index()
points = pd.cut(points, bins=points, precision=20)
points = pd.IntervalIndex(points[1:]) # インターバルインデックス

print(points[:5])

# 各範囲の下限、上限
nodes['points_min'] = points.left
nodes['points_max'] = points.right

nodes.head()
```

    IntervalIndex([(0.0, 0.12025235037664278], (0.12025235037664278, 0.12526286497566957], (0.12526286497566957, 0.1803785255649642], (0.1803785255649642, 0.24551521535231238], (0.24551521535231238, 0.25052572995133915]], dtype='interval[float64, right]', name='areas_in_radians')
    
<div>
<style scoped>
    .dataframe tbody tr th:only-of-type {
        vertical-align: middle;
    }

    .dataframe tbody tr th {
        vertical-align: top;
    }

    .dataframe thead th {
        text-align: right;
    }
</style>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>index</th>
      <th>weights_of_areas</th>
      <th>areas_in_radians</th>
      <th>points_min</th>
      <th>points_max</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>0</th>
      <td>0</td>
      <td>24.0</td>
      <td>0.120252</td>
      <td>0.000000</td>
      <td>0.120252</td>
    </tr>
    <tr>
      <th>1</th>
      <td>1</td>
      <td>1.0</td>
      <td>0.005011</td>
      <td>0.120252</td>
      <td>0.125263</td>
    </tr>
    <tr>
      <th>2</th>
      <td>2</td>
      <td>11.0</td>
      <td>0.055116</td>
      <td>0.125263</td>
      <td>0.180379</td>
    </tr>
    <tr>
      <th>3</th>
      <td>3</td>
      <td>13.0</td>
      <td>0.065137</td>
      <td>0.180379</td>
      <td>0.245515</td>
    </tr>
    <tr>
      <th>4</th>
      <td>4</td>
      <td>1.0</td>
      <td>0.005011</td>
      <td>0.245515</td>
      <td>0.250526</td>
    </tr>
  </tbody>
</table>
</div>

```python
# 範囲の中間点が頂点の位置
midpoints = points.mid

# 必要なのは(x, y)の座標
nodes['node_x'] = np.cos(midpoints)
nodes['node_y'] = np.sin(midpoints)

nodes.head()
```

<div>
<style scoped>
    .dataframe tbody tr th:only-of-type {
        vertical-align: middle;
    }

    .dataframe tbody tr th {
        vertical-align: top;
    }

    .dataframe thead th {
        text-align: right;
    }
</style>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>index</th>
      <th>weights_of_areas</th>
      <th>areas_in_radians</th>
      <th>points_min</th>
      <th>points_max</th>
      <th>node_x</th>
      <th>node_y</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>0</th>
      <td>0</td>
      <td>24.0</td>
      <td>0.120252</td>
      <td>0.000000</td>
      <td>0.120252</td>
      <td>0.998193</td>
      <td>0.060090</td>
    </tr>
    <tr>
      <th>1</th>
      <td>1</td>
      <td>1.0</td>
      <td>0.005011</td>
      <td>0.120252</td>
      <td>0.125263</td>
      <td>0.992475</td>
      <td>0.122450</td>
    </tr>
    <tr>
      <th>2</th>
      <td>2</td>
      <td>11.0</td>
      <td>0.055116</td>
      <td>0.125263</td>
      <td>0.180379</td>
      <td>0.988346</td>
      <td>0.152227</td>
    </tr>
    <tr>
      <th>3</th>
      <td>3</td>
      <td>13.0</td>
      <td>0.065137</td>
      <td>0.180379</td>
      <td>0.245515</td>
      <td>0.977412</td>
      <td>0.211341</td>
    </tr>
    <tr>
      <th>4</th>
      <td>4</td>
      <td>1.0</td>
      <td>0.005011</td>
      <td>0.245515</td>
      <td>0.250526</td>
      <td>0.969400</td>
      <td>0.245485</td>
    </tr>
  </tbody>
</table>
</div>

## 辺の端点の位置を求める

各頂点の範囲を辺の数で分割する。

```python
def get_end_points(x):
    p0, p1 = x['points_min'], x['points_max']
    angles = np.linspace(p0, p1, int(x['weights_of_areas']))

    return angles.tolist()

nodes['edges_end_points'] = nodes.apply(get_end_points, axis=1)

nodes.head()
```

<div>
<style scoped>
    .dataframe tbody tr th:only-of-type {
        vertical-align: middle;
    }

    .dataframe tbody tr th {
        vertical-align: top;
    }

    .dataframe thead th {
        text-align: right;
    }
</style>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>index</th>
      <th>weights_of_areas</th>
      <th>areas_in_radians</th>
      <th>points_min</th>
      <th>points_max</th>
      <th>node_x</th>
      <th>node_y</th>
      <th>edges_end_points</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>0</th>
      <td>0</td>
      <td>24.0</td>
      <td>0.120252</td>
      <td>0.000000</td>
      <td>0.120252</td>
      <td>0.998193</td>
      <td>0.060090</td>
      <td>[0.0, 0.005228363059854034, 0.0104567261197080...</td>
    </tr>
    <tr>
      <th>1</th>
      <td>1</td>
      <td>1.0</td>
      <td>0.005011</td>
      <td>0.120252</td>
      <td>0.125263</td>
      <td>0.992475</td>
      <td>0.122450</td>
      <td>[0.12025235037664278]</td>
    </tr>
    <tr>
      <th>2</th>
      <td>2</td>
      <td>11.0</td>
      <td>0.055116</td>
      <td>0.125263</td>
      <td>0.180379</td>
      <td>0.988346</td>
      <td>0.152227</td>
      <td>[0.12526286497566957, 0.13077443103459904, 0.1...</td>
    </tr>
    <tr>
      <th>3</th>
      <td>3</td>
      <td>13.0</td>
      <td>0.065137</td>
      <td>0.180379</td>
      <td>0.245515</td>
      <td>0.977412</td>
      <td>0.211341</td>
      <td>[0.1803785255649642, 0.1858065830472432, 0.191...</td>
    </tr>
    <tr>
      <th>4</th>
      <td>4</td>
      <td>1.0</td>
      <td>0.005011</td>
      <td>0.245515</td>
      <td>0.250526</td>
      <td>0.969400</td>
      <td>0.245485</td>
      <td>[0.24551521535231238]</td>
    </tr>
  </tbody>
</table>
</div>

## 辺の曲線の座標を求める

`layout_chords`では`quadratic_bezier`という関数を使っている。[ベジェ曲線](https://ja.javascript.info/bezier-curve)というものらしい。中には何かの式が書いてある。理解はできないので`quadratic_bezier`をそのまま使う。とりあえず、辺の両端の座標と制御点の座標を使って計算するといい感じの曲線ができるらしい。

#### quadratic_bezier(start, end, c0=(0, 0), c1=(0, 0), steps=50)

quadratiq bezierは2次ベジェ曲線らしいが、ベジェ曲線の説明を読む限り、4点を使うのは3次ベジェ曲線ではないかと疑問に思いつつ、

引数
- start : (x0, y0) 辺の始点の座標。  
- end : (x1, y1) 辺の終点の座標。  
- c0 : (cx0, cy0) 制御点の座標。  
- c1 : (cx1, cy1) もう一つの制御点の座標。  
- steps : 曲線の座標の数。デフォルトは50。  

戻り値

- -> numpy.ndarray (shapeは(steps, 2))

```python
from holoviews.element.util import quadratic_bezier

def get_path(x):
    xs = np.empty(0)
    ys = np.empty(0)
    for i in range(x['value']):
        # 2つの頂点から辺の端点を取得
        s = nodes.loc[x['source'], 'edges_end_points'].pop()
        t = nodes.loc[x['target'], 'edges_end_points'].pop()
        # 座標に変換
        x0, y0 = np.cos(s), np.sin(s)
        x1, y1 = np.cos(t), np.sin(t)
        # ベジェ曲線
        b = quadratic_bezier((x0, y0), (x1, y1), (x0/2, y0/2),
                             (x1/2, y1/2), steps=50)
        # MultiLine用のデータのために、xs, ysそれぞれのリスト
        xs = np.append(xs, b[:, 0].flatten())
        ys = np.append(ys, b[:, 1].flatten())
        # np.nanで区切る
        xs =  np.append(xs, np.nan)
        ys =  np.append(ys, np.nan)
    # 余分な np.nanを削除
    xs = np.delete(xs, -1)
    ys = np.delete(ys, -1)
    
    return xs, ys

edges[['xs', 'ys']] = edges.apply(get_path,
                                  axis=1, result_type='expand')

edges.head()
```

<div>
<style scoped>
    .dataframe tbody tr th:only-of-type {
        vertical-align: middle;
    }

    .dataframe tbody tr th {
        vertical-align: top;
    }

    .dataframe thead th {
        text-align: right;
    }
</style>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>source</th>
      <th>target</th>
      <th>value</th>
      <th>xs</th>
      <th>ys</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>0</th>
      <td>1</td>
      <td>0</td>
      <td>1</td>
      <td>[0.9927783948222082, 0.9630074475389099, 0.934...</td>
      <td>[0.11996273910777293, 0.11636535584423287, 0.1...</td>
    </tr>
    <tr>
      <th>1</th>
      <td>2</td>
      <td>0</td>
      <td>5</td>
      <td>[0.9837758550359634, 0.9542808382004578, 0.926...</td>
      <td>[0.17940197058075802, 0.17398204989887306, 0.1...</td>
    </tr>
    <tr>
      <th>2</th>
      <td>3</td>
      <td>0</td>
      <td>7</td>
      <td>[0.9700122274758824, 0.9409401385262388, 0.913...</td>
      <td>[0.24305612221722958, 0.23567173954270657, 0.2...</td>
    </tr>
    <tr>
      <th>3</th>
      <td>3</td>
      <td>2</td>
      <td>4</td>
      <td>[0.978545131672936, 0.9492070855485545, 0.9211...</td>
      <td>[0.20603258305228409, 0.19982079380656642, 0.1...</td>
    </tr>
    <tr>
      <th>4</th>
      <td>4</td>
      <td>0</td>
      <td>1</td>
      <td>[0.9700122274758824, 0.9409417399748237, 0.913...</td>
      <td>[0.24305612221722958, 0.23564908833075593, 0.2...</td>
    </tr>
  </tbody>
</table>
</div>

これで必要なデータはほぼ出揃った。残るは輪っかの座標だが、どこで処理をしているか見つけられなかった。Chordのデータを見直すと、各頂点の弧の長さに関係なく点の数は20で共通しているので、適当に処理する。

```python
def arc_coords(x):
    p0, p1 = x['points_min'], x['points_max']
    angles = np.linspace(p0, p1, 20)
    xs = np.cos(angles)
    ys = np.sin(angles)

    return xs, ys

nodes[['arc_xs', 'arc_ys']] = nodes.apply(arc_coords,
                                          axis=1, result_type='expand')

nodes.head()
```

<div>
<style scoped>
    .dataframe tbody tr th:only-of-type {
        vertical-align: middle;
    }

    .dataframe tbody tr th {
        vertical-align: top;
    }

    .dataframe thead th {
        text-align: right;
    }
</style>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>index</th>
      <th>weights_of_areas</th>
      <th>areas_in_radians</th>
      <th>points_min</th>
      <th>points_max</th>
      <th>node_x</th>
      <th>node_y</th>
      <th>edges_end_points</th>
      <th>arc_xs</th>
      <th>arc_ys</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>0</th>
      <td>0</td>
      <td>24.0</td>
      <td>0.120252</td>
      <td>0.000000</td>
      <td>0.120252</td>
      <td>0.998193</td>
      <td>0.060090</td>
      <td>[]</td>
      <td>[1.0, 0.9999799714965371, 0.9999198867884302, ...</td>
      <td>[0.0, 0.006329028818457834, 0.0126578041149644...</td>
    </tr>
    <tr>
      <th>1</th>
      <td>1</td>
      <td>1.0</td>
      <td>0.005011</td>
      <td>0.120252</td>
      <td>0.125263</td>
      <td>0.992475</td>
      <td>0.122450</td>
      <td>[]</td>
      <td>[0.9927783948222082, 0.9927467247726159, 0.992...</td>
      <td>[0.11996273910777293, 0.12022454180924974, 0.1...</td>
    </tr>
    <tr>
      <th>2</th>
      <td>2</td>
      <td>11.0</td>
      <td>0.055116</td>
      <td>0.125263</td>
      <td>0.180379</td>
      <td>0.988346</td>
      <td>0.152227</td>
      <td>[]</td>
      <td>[0.9921648603295138, 0.9917982703635422, 0.991...</td>
      <td>[0.12493554309049267, 0.12781310928025358, 0.1...</td>
    </tr>
    <tr>
      <th>3</th>
      <td>3</td>
      <td>13.0</td>
      <td>0.065137</td>
      <td>0.180379</td>
      <td>0.245515</td>
      <td>0.977412</td>
      <td>0.211341</td>
      <td>[]</td>
      <td>[0.9837758550359634, 0.9831550409112764, 0.982...</td>
      <td>[0.17940197058075802, 0.1827735361882195, 0.18...</td>
    </tr>
    <tr>
      <th>4</th>
      <td>4</td>
      <td>1.0</td>
      <td>0.005011</td>
      <td>0.245515</td>
      <td>0.250526</td>
      <td>0.969400</td>
      <td>0.245485</td>
      <td>[]</td>
      <td>[0.9700122274758824, 0.9699480971028605, 0.969...</td>
      <td>[0.24305612221722958, 0.24331191694312868, 0.2...</td>
    </tr>
  </tbody>
</table>
</div>

後はデータの形を整え、GraphRendererを作るだけ

```python
# StaticLayoutProvider用 頂点の座標
graph_layout = {
    int(idx): (x, y) for idx, (x, y) in nodes[['node_x', 'node_y']].iterrows()
}
```

```python
nodes = nodes.reindex(columns=['index', 'arc_xs', 'arc_ys'])  # 'index'列は必須
edges = edges.reindex(columns=['source', 'target', 'xs', 'ys'])
edges.columns = ['start', 'end', 'xs', 'ys']  # 'start', 'end'列は必須
```

```python
gr = GraphRenderer()

# 頂点
# node_rendererのデフォルトの Glyphは Circleなので、属性の値だけ変更
gr.node_renderer.glyph.update(fill_color='#30a2da', size=15)
gr.node_renderer.data_source.data = nodes

# 辺
gr.edge_renderer.data_source.data = edges

# 頂点の位置
gr.layout_provider = StaticLayoutProvider(graph_layout=graph_layout)

# ホバー用
gr.node_renderer.hover_glyph = Circle(fill_color='limegreen')
gr.edge_renderer.hover_glyph = MultiLine(line_color='limegreen')

# タップ用
gr.node_renderer.selection_glyph = Circle(fill_color='limegreen')
gr.node_renderer.nonselection_glyph = Circle(fill_color='#30a2da',
                                             fill_alpha=0.2,
                                             line_alpha=0.2
                                             )
gr.edge_renderer.selection_glyph = MultiLine(line_color='limegreen')
gr.edge_renderer.nonselection_glyph = MultiLine(line_alpha=0.2)

# 挙動設定
gr.inspection_policy = gr.selection_policy = NodesAndLinkedEdges()

# Figure作成
my_chord = figure(x_range=(-1.1, 1.1), y_range=(-1.1, 1.1),
                  tools='pan,wheel_zoom,tap,reset',
                  tooltips='@index'
                  )
# 輪っか部分
my_chord.multi_line('arc_xs', 'arc_ys',
                    source=gr.node_renderer.data_source,
                    line_color='#30a2da', 
                    line_width=10,
                    hover_color='limegreen',
                    selection_color='limegreen',
                    nonselection_alpha=0.2
                    )

my_chord.renderers.append(gr)
```

ようやく完成!

```python
show(my_chord)
```

![bokeh_chord_1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1714033/fff1ab45-05a7-b4e1-8840-c5785fccabca.jpeg)

![bokeh_chord_2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1714033/0cee6972-7382-46d7-851c-1bfd46d6c042.jpeg)
![bokeh_chord_3.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1714033/16fefc41-6d57-03fd-c2d7-2075a1be9c4c.jpeg)


# 色の塗り分け、テキストありのChord

HoloViewsの[Reference Gallery](https://holoviews.org/reference/index.html)の[Chord](https://holoviews.org/reference/elements/bokeh/Chord.html#elements-bokeh-gallery-chord)のページにはもう一つの例が載っている。

```python
nodes = hv.Dataset(pd.DataFrame(data['nodes']), 'index')
nodes.data.head()
```

<div>
<style scoped>
    .dataframe tbody tr th:only-of-type {
        vertical-align: middle;
    }

    .dataframe tbody tr th {
        vertical-align: top;
    }

    .dataframe thead th {
        text-align: right;
    }
</style>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>index</th>
      <th>name</th>
      <th>group</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>0</th>
      <td>0</td>
      <td>Myriel</td>
      <td>1</td>
    </tr>
    <tr>
      <th>1</th>
      <td>1</td>
      <td>Napoleon</td>
      <td>1</td>
    </tr>
    <tr>
      <th>2</th>
      <td>2</td>
      <td>Mlle.Baptistine</td>
      <td>1</td>
    </tr>
    <tr>
      <th>3</th>
      <td>3</td>
      <td>Mme.Magloire</td>
      <td>1</td>
    </tr>
    <tr>
      <th>4</th>
      <td>4</td>
      <td>CountessdeLo</td>
      <td>1</td>
    </tr>
  </tbody>
</table>
</div>

色とテキストがついてより豪華なグラフに。

```python
chord = hv.Chord((links, nodes)).select(value=(5, None))
chord.opts(
    hv.opts.Chord(cmap='Category20', edge_cmap='Category20',
                  edge_color=hv.dim('source').str(),
                  labels='name', node_color=hv.dim('index').str()))
```

![chord_2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1714033/921b1c36-b69d-dde2-f133-a72ba32db3fc.jpeg)

![chord_2_hover.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1714033/ee8fbcf0-ddfe-7983-52db-ece9c866f76e.jpeg)
![chord_2_select.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1714033/358c4dc7-b12b-b2eb-70d0-49a69d28bc5f.jpeg)

でも、よく見るとグラフ自体は上とほぼ同じ、違うのは頂点の数と色とテキストがついたこと。なので、違うところを眺めてみたい。`Figure`を取り出して`renderers`を参照すると、レンダラーが1つ増えている。これがテキスト部分だろう。

```python
# Figureの取り出し
p = hv.render(chord)
p.renderers
```




    [GlyphRenderer(id='5080', ...),
     GraphRenderer(id='5061', ...),
     GlyphRenderer(id='5086', ...)]




```python
text = p.renderers[2]
print(text.glyph)
print(text.data_source.data.keys())

p11 = figure(frame_height=350, frame_width=350,
            x_range=(-2, 2), y_range=(-2, 2))
p11.renderers.append(text)

show(p11)
```

    Text(id='5084', ...)
    dict_keys(['x', 'y', 'text', 'angle'])
    
![text.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1714033/d0606ad9-853b-89a9-5555-17d711e3d801.jpeg)

## Text

`'x'`にx座標、`'y'`にy座標、`'text'`に表示する文字列、`'angle'`に回転角度を渡す。


```python
text = ['node 0', 'node 1', 'node 2', 'node 3']
x = [1, 0.5, 0, -0.5]
y = [0, 0.5, 1, 0.5]
angle = [0, np.pi/4, np.pi/2, np.pi*0.75]

p12 = figure(frame_width=250, frame_height=250,
            x_range=(-2, 2), y_range=(-2, 2))

p12.text(x=x, y=y, text=text, angle=angle,
        text_font_size='20px', text_baseline='middle',
        )

show(p12)
```

![text_sample.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1714033/39a5ab4c-ca85-2e57-fc5e-ddc6fe86b76f.jpeg)

## CategoricalColorMapper

次に色の部分。

`GraphRenderer`から頂点と辺の`GlyphRenderer`を取り出し、頂点は`glyph.fill_color`、辺は`glyph.line_color`属性を見てみる。どちらも`'field'`と`'transform'`のキーを持った辞書。`'transform'`の値が`CategoricalColorMapper`で、これが色の設定になる。

```python
graph_renderer = p.renderers[1]
node_renderer = graph_renderer.node_renderer
edge_renderer = graph_renderer.edge_renderer

node_renderer.glyph.fill_color, edge_renderer.glyph.line_color
```

    ({'field': 'node_color', 'transform': CategoricalColorMapper(id='5042', ...)},
     {'field': 'edge_color', 'transform': CategoricalColorMapper(id='5044', ...)})

`CategoricalColorMapper`は`factors`の値と`palette`の色をマッピングするクラス。`factors`は色を適用する値のリスト、`palette`は色のリストになる。`bokeh.transform.factor_cmap`を使うと、記述が少しだけ楽になる。`factors`ではint型は使えない。str型、またはstr型のタプル。タプルに関しては[公式](https://docs.bokeh.org/en/latest/docs/reference/models/mappers.html?highlight=categoricalcolormapper#bokeh.models.mappers.CategoricalColorMapper)の`factors`, `start`, `end`を参照。


```python
from bokeh.models import ColumnDataSource, CategoricalColorMapper

palette = ['navy', 'orange', 'olive']
factors = list('abc')

mapper = CategoricalColorMapper(palette=palette,
                                factors=factors,
                                nan_color='firebrick'  # factorsにない時の色
                                )
# factorsにない'd'の値もある
source = dict(
    x=[1, 2, 3, 4],
    y=[3, 5, 2, 4],
    value=['c', 'b', 'a', 'd']
)    
source = ColumnDataSource(source)

p13 = figure(plot_height=250, plot_width=250)

p13.vbar(x='x', width=0.8, top='y',
         source=source,
         color={'field': 'value', 'transform': mapper})  # 'field'に適用する列名をいれる

show(p13)
```

![cmap.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1714033/58b62424-9a18-f220-bb48-3b46ee79ba2b.jpeg)

```python
from bokeh.transform import factor_cmap

# 違いはフィールド名も渡すところ。戻り値は辞書。
cmap = factor_cmap('value', palette, factors, nan_color='firebrick')
print(type(cmap))
print(cmap)

p14 = figure(plot_height=250, plot_width=250)

p14.vbar(x='x', width=0.8, top='y',
         source=source,
         color=cmap)

show(p14)
```

    <class 'dict'>
    {'field': 'value', 'transform': CategoricalColorMapper(id='5893', ...)}
    
![cmap.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1714033/58b62424-9a18-f220-bb48-3b46ee79ba2b.jpeg)

Chordに使われている2つの`CategoricalColorMapper`を見てみると、`factors`と`palette`の値は同じだとわかる。idは別だが、==で比較するとTrueになる。

```python
node_mapper = node_renderer.glyph.fill_color['transform']
edge_mapper = edge_renderer.glyph.line_color['transform']

print(node_mapper.palette[:5])
print(edge_mapper.palette[:5])
print(node_mapper.palette == edge_mapper.palette)

print(node_mapper.factors[:5])
print(edge_mapper.factors[:5])
print(node_mapper.factors == edge_mapper.factors)
```

    ['#1f77b4', '#aec7e8', '#ff7f0e', '#ffbb78', '#2ca02c']
    ['#1f77b4', '#aec7e8', '#ff7f0e', '#ffbb78', '#2ca02c']
    True
    ['0', '2', '3', '11', '20']
    ['0', '2', '3', '11', '20']
    True

# bokehでChordを再現する

ここまで来たら、後は実際にデータを加工して必要な部品を揃えるだけだ。

作業を始める前に、Chordで使われているデータを確認する。

node_rendererのデータには最初の例で見た列名に加えて、`'name'`、`'group'`、`'node_color'`列が追加されている。`'name'`、`'group'`は `data['nodes']`。`'node_color'`は`'index'`列をstr型にしたもの。`CategoricalColorMapper`の`factors`はint型ではエラーになる。

edge_rendererのデータは`'edge_color'`列だけ追加されている。こちらは`'start'`列をstr型にしたもの。

```python
df_nodes = node_renderer.data_source.to_df()  # データフレームに
df_nodes.head()
```

<div>
<style scoped>
    .dataframe tbody tr th:only-of-type {
        vertical-align: middle;
    }

    .dataframe tbody tr th {
        vertical-align: top;
    }

    .dataframe thead th {
        text-align: right;
    }
</style>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>index</th>
      <th>index_hover</th>
      <th>name</th>
      <th>group</th>
      <th>arc_xs</th>
      <th>arc_ys</th>
      <th>node_color</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>0</th>
      <td>0</td>
      <td>0</td>
      <td>Myriel</td>
      <td>1</td>
      <td>[1.0, 0.9999616084203429, 0.9998464366291983, ...</td>
      <td>[0.0, 0.008762515928708449, 0.0175243590437603...</td>
      <td>0</td>
    </tr>
    <tr>
      <th>1</th>
      <td>2</td>
      <td>2</td>
      <td>Mlle.Baptistine</td>
      <td>1</td>
      <td>[0.9861725355513823, 0.9852745890889341, 0.984...</td>
      <td>[0.1657218456455203, 0.1709794844290746, 0.176...</td>
      <td>2</td>
    </tr>
    <tr>
      <th>2</th>
      <td>3</td>
      <td>3</td>
      <td>Mme.Magloire</td>
      <td>1</td>
      <td>[0.9643470021510558, 0.9627159124942113, 0.961...</td>
      <td>[0.26464100106043953, 0.27051445771055954, 0.2...</td>
      <td>3</td>
    </tr>
    <tr>
      <th>3</th>
      <td>11</td>
      <td>11</td>
      <td>Valjean</td>
      <td>2</td>
      <td>[0.9273043248938287, 0.9111224279579819, 0.893...</td>
      <td>[0.3743082807435611, 0.4121358044042667, 0.449...</td>
      <td>11</td>
    </tr>
    <tr>
      <th>4</th>
      <td>20</td>
      <td>20</td>
      <td>Favourite</td>
      <td>3</td>
      <td>[0.39435585511331855, 0.39260460034073885, 0.3...</td>
      <td>[0.9189578116202306, 0.9197073598657829, 0.920...</td>
      <td>20</td>
    </tr>
  </tbody>
</table>
</div>

```python
df_edges = edge_renderer.data_source.to_df()
df_edges.head()
```

<div>
<style scoped>
    .dataframe tbody tr th:only-of-type {
        vertical-align: middle;
    }

    .dataframe tbody tr th {
        vertical-align: top;
    }

    .dataframe thead th {
        text-align: right;
    }
</style>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>start</th>
      <th>end</th>
      <th>xs</th>
      <th>ys</th>
      <th>edge_color</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>0</th>
      <td>2</td>
      <td>0</td>
      <td>[0.9643470021510558, 0.9354421839235255, 0.907...</td>
      <td>[0.26464100106043953, 0.2566436993401825, 0.24...</td>
      <td>2</td>
    </tr>
    <tr>
      <th>1</th>
      <td>3</td>
      <td>0</td>
      <td>[0.9273043248938287, 0.8995384093005471, 0.873...</td>
      <td>[0.3743082807435611, 0.36291707158663716, 0.35...</td>
      <td>3</td>
    </tr>
    <tr>
      <th>2</th>
      <td>3</td>
      <td>2</td>
      <td>[0.953414147011291, 0.9248394897961085, 0.8974...</td>
      <td>[0.3016644895885698, 0.2925577397447924, 0.283...</td>
      <td>3</td>
    </tr>
    <tr>
      <th>3</th>
      <td>11</td>
      <td>0</td>
      <td>[0.39435585511331855, 0.38290561858217864, 0.3...</td>
      <td>[0.9189578116202306, 0.8908491354595254, 0.862...</td>
      <td>11</td>
    </tr>
    <tr>
      <th>4</th>
      <td>21</td>
      <td>20</td>
      <td>[0.3268606654329953, 0.3170800161987392, 0.307...</td>
      <td>[0.9450725397516846, 0.9167244490725669, 0.889...</td>
      <td>21</td>
    </tr>
  </tbody>
</table>
</div>

## データの加工

辺の`'value'`が5以上のデータを使う。`'value'`の合計は500を超えないので全体の辺の数は調整しない。

```python
# 辺用データの作成
edges = links[links['value'] >= 5].copy()  # valueが 5以上の辺
edges.columns = ['start', 'end', 'value']
edges['color'] = edges['start'].astype(str)  # color列

print(edges['value'].sum())
edges.head()
```

    434
    
<div>
<style scoped>
    .dataframe tbody tr th:only-of-type {
        vertical-align: middle;
    }

    .dataframe tbody tr th {
        vertical-align: top;
    }

    .dataframe thead th {
        text-align: right;
    }
</style>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>start</th>
      <th>end</th>
      <th>value</th>
      <th>color</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>1</th>
      <td>2</td>
      <td>0</td>
      <td>8</td>
      <td>2</td>
    </tr>
    <tr>
      <th>2</th>
      <td>3</td>
      <td>0</td>
      <td>10</td>
      <td>3</td>
    </tr>
    <tr>
      <th>3</th>
      <td>3</td>
      <td>2</td>
      <td>6</td>
      <td>3</td>
    </tr>
    <tr>
      <th>13</th>
      <td>11</td>
      <td>0</td>
      <td>5</td>
      <td>11</td>
    </tr>
    <tr>
      <th>32</th>
      <td>21</td>
      <td>20</td>
      <td>5</td>
      <td>21</td>
    </tr>
  </tbody>
</table>
</div>

```python
# 頂点、輪っか用データの作成
nodes = pd.DataFrame(data['nodes'])
nodes['index'] = nodes.index
nodes['color'] = nodes['index'].astype(str)  # color列

# 辺の'start', 'end'に存在する頂点を抽出
filter_1 = nodes['index'].isin(edges['start'])
filter_2 = nodes['index'].isin(edges['end'])
nodes = nodes[filter_1 | filter_2]

print(len(nodes))  # 26個に減っている
nodes.head()
```

    26

<div>
<style scoped>
    .dataframe tbody tr th:only-of-type {
        vertical-align: middle;
    }

    .dataframe tbody tr th {
        vertical-align: top;
    }

    .dataframe thead th {
        text-align: right;
    }
</style>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>name</th>
      <th>group</th>
      <th>index</th>
      <th>color</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>0</th>
      <td>Myriel</td>
      <td>1</td>
      <td>0</td>
      <td>0</td>
    </tr>
    <tr>
      <th>2</th>
      <td>Mlle.Baptistine</td>
      <td>1</td>
      <td>2</td>
      <td>2</td>
    </tr>
    <tr>
      <th>3</th>
      <td>Mme.Magloire</td>
      <td>1</td>
      <td>3</td>
      <td>3</td>
    </tr>
    <tr>
      <th>11</th>
      <td>Valjean</td>
      <td>2</td>
      <td>11</td>
      <td>11</td>
    </tr>
    <tr>
      <th>20</th>
      <td>Favourite</td>
      <td>3</td>
      <td>20</td>
      <td>20</td>
    </tr>
  </tbody>
</table>
</div>

```python
# 各頂点の辺の数
values = edges.pivot('start', 'end', 'value')
nodes['weights_of_areas'] = values.sum().add(values.sum(axis=1), fill_value=0).astype(int)

# 弧の長さ
nodes['areas_in_radians'] = (
    nodes['weights_of_areas'] / nodes['weights_of_areas'].sum() * 2*np.pi
)

# 弧の境界
points = nodes['areas_in_radians'].cumsum()
points.loc[-1] = 0
points = points.sort_index()
points = pd.cut(points, bins=points, precision=20)
points = pd.IntervalIndex(points[1:])

nodes['points_min'] = points.left
nodes['points_max'] = points.right

# 頂点の座標
midpoints = points.mid

nodes['node_x'] = np.cos(midpoints)
nodes['node_y'] = np.sin(midpoints)

# 辺の端点
def get_end_points(x):
    p0, p1 = x['points_min'], x['points_max']
    end_points = np.linspace(p0, p1, int(x['weights_of_areas']))
    return list(end_points)

nodes['edges_end_points'] = nodes.apply(get_end_points, axis=1)

# 各辺のベジェ曲線
def get_path(x):
    s, t, v = x['start'], x['end'], x['value']
    xs = np.empty(0)
    ys = np.empty(0)
    for j in range(v):
        s_point = nodes.loc[s, 'edges_end_points'].pop()
        t_point = nodes.loc[t, 'edges_end_points'].pop()
        
        x0, y0 = np.cos(s_point), np.sin(s_point)
        x1, y1 = np.cos(t_point), np.sin(t_point)

        ts = quadratic_bezier((x0, y0), (x1, y1),
                              (x0/2, y0/2), (x1/2, y1/2))
        
        xs = np.append(xs, ts[:, 0])
        ys = np.append(ys, ts[:, 1])
        xs = np.append(xs, np.nan)
        ys = np.append(ys, np.nan)
        
    xs = np.delete(xs, -1)
    ys = np.delete(ys, -1)
    
    return xs, ys
    
edges[['xs', 'ys']] = edges.apply(get_path,
                                  axis=1, result_type='expand')

# 輪っか用の座標
def arc_coords(x):
    p0, p1 = x['points_min'], x['points_max']
    angles = np.linspace(p0, p1, 20)
    xs = np.cos(angles)
    ys = np.sin(angles)

    return xs, ys

nodes[['arc_xs', 'arc_ys']] = nodes.apply(arc_coords,
                                          axis=1, result_type='expand')
```

ここまでは上の例と同じ処理。

次にText用のデータ。回転の角度はmidpointをそのまま使える。(x, y)は頂点の(x, y)から少し外側に設定。

```python
# Text用データ
# 頂点の位置(ラジアン)がそのままテキストの回転角度になる
texts = pd.DataFrame(midpoints, columns=['angle'], index=nodes.index)

# 位置調整
texts[['x', 'y']] = nodes[['node_x', 'node_y']] * 1.06

texts['name'] = nodes['name']

texts.head()
```

<div>
<style scoped>
    .dataframe tbody tr th:only-of-type {
        vertical-align: middle;
    }

    .dataframe tbody tr th {
        vertical-align: top;
    }

    .dataframe thead th {
        text-align: right;
    }
</style>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>angle</th>
      <th>x</th>
      <th>y</th>
      <th>name</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>0</th>
      <td>0.083245</td>
      <td>1.056329</td>
      <td>0.088138</td>
      <td>Myriel</td>
    </tr>
    <tr>
      <th>2</th>
      <td>0.217161</td>
      <td>1.035104</td>
      <td>0.228385</td>
      <td>Mlle.Baptistine</td>
    </tr>
    <tr>
      <th>3</th>
      <td>0.325741</td>
      <td>1.004259</td>
      <td>0.339212</td>
      <td>Mme.Magloire</td>
    </tr>
    <tr>
      <th>11</th>
      <td>0.774540</td>
      <td>0.757627</td>
      <td>0.741351</td>
      <td>Valjean</td>
    </tr>
    <tr>
      <th>20</th>
      <td>1.183526</td>
      <td>0.400322</td>
      <td>0.981500</td>
      <td>Favourite</td>
    </tr>
  </tbody>
</table>
</div>

```python
graph_layout = {
    idx: (x, y) for idx, (x, y) in nodes[['node_x', 'node_y']].iterrows()
}
```

```python
nodes = nodes.reindex(columns=['index', 'name', 'group', 'color', 'arc_xs', 'arc_ys'])
nodes.head()
```

<div>
<style scoped>
    .dataframe tbody tr th:only-of-type {
        vertical-align: middle;
    }

    .dataframe tbody tr th {
        vertical-align: top;
    }

    .dataframe thead th {
        text-align: right;
    }
</style>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>index</th>
      <th>name</th>
      <th>group</th>
      <th>color</th>
      <th>arc_xs</th>
      <th>arc_ys</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>0</th>
      <td>0</td>
      <td>Myriel</td>
      <td>1</td>
      <td>0</td>
      <td>[1.0, 0.9999616084203429, 0.9998464366291983, ...</td>
      <td>[0.0, 0.008762515928708449, 0.0175243590437603...</td>
    </tr>
    <tr>
      <th>2</th>
      <td>2</td>
      <td>Mlle.Baptistine</td>
      <td>1</td>
      <td>2</td>
      <td>[0.9861725355513823, 0.9852745890889341, 0.984...</td>
      <td>[0.1657218456455203, 0.1709794844290746, 0.176...</td>
    </tr>
    <tr>
      <th>3</th>
      <td>3</td>
      <td>Mme.Magloire</td>
      <td>1</td>
      <td>3</td>
      <td>[0.9643470021510558, 0.9627159124942113, 0.961...</td>
      <td>[0.26464100106043953, 0.27051445771055954, 0.2...</td>
    </tr>
    <tr>
      <th>11</th>
      <td>11</td>
      <td>Valjean</td>
      <td>2</td>
      <td>11</td>
      <td>[0.9273043248938287, 0.9111224279579819, 0.893...</td>
      <td>[0.3743082807435611, 0.4121358044042667, 0.449...</td>
    </tr>
    <tr>
      <th>20</th>
      <td>20</td>
      <td>Favourite</td>
      <td>3</td>
      <td>20</td>
      <td>[0.39435585511331855, 0.39260460034073885, 0.3...</td>
      <td>[0.9189578116202306, 0.9197073598657829, 0.920...</td>
    </tr>
  </tbody>
</table>
</div>

```python
edges = edges.reindex(columns=['start', 'end', 'color', 'xs', 'ys'])
edges.head()
```

<div>
<style scoped>
    .dataframe tbody tr th:only-of-type {
        vertical-align: middle;
    }

    .dataframe tbody tr th {
        vertical-align: top;
    }

    .dataframe thead th {
        text-align: right;
    }
</style>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>start</th>
      <th>end</th>
      <th>color</th>
      <th>xs</th>
      <th>ys</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>1</th>
      <td>2</td>
      <td>0</td>
      <td>2</td>
      <td>[0.9643470021510558, 0.9354421839235255, 0.907...</td>
      <td>[0.26464100106043953, 0.2566436993401825, 0.24...</td>
    </tr>
    <tr>
      <th>2</th>
      <td>3</td>
      <td>0</td>
      <td>3</td>
      <td>[0.9273043248938287, 0.8995384093005471, 0.873...</td>
      <td>[0.3743082807435611, 0.36291707158663716, 0.35...</td>
    </tr>
    <tr>
      <th>3</th>
      <td>3</td>
      <td>2</td>
      <td>3</td>
      <td>[0.953414147011291, 0.9248394897961085, 0.8974...</td>
      <td>[0.3016644895885698, 0.2925577397447924, 0.283...</td>
    </tr>
    <tr>
      <th>13</th>
      <td>11</td>
      <td>0</td>
      <td>11</td>
      <td>[0.39435585511331855, 0.38290561858217864, 0.3...</td>
      <td>[0.9189578116202306, 0.8908491354595254, 0.862...</td>
    </tr>
    <tr>
      <th>32</th>
      <td>21</td>
      <td>20</td>
      <td>21</td>
      <td>[0.3268606654329953, 0.3170800161987392, 0.307...</td>
      <td>[0.9450725397516846, 0.9167244490725669, 0.889...</td>
    </tr>
  </tbody>
</table>
</div>

nodesとedgesの色用列は同じcmapを使えるように`'color'`で統一している。データの用意ができたので、あとは描画するだけ。

## 描画

```python
from bokeh.palettes import Category20

# カラーマッパー
palette = (Category20[20] * 2)[:len(nodes)]
cmap = factor_cmap('color', palette, nodes['color'])

# グラフレンダラー
gr = GraphRenderer()

# 頂点
gr.node_renderer.glyph.update(fill_color=cmap, size=15)
gr.node_renderer.hover_glyph = Circle(fill_color='limegreen')
gr.node_renderer.nonselection_glyph = Circle(fill_color=cmap,
                                             fill_alpha=0.2,
                                             line_alpha=0.2
                                             )
gr.node_renderer.data_source.data = nodes

gr.layout_provider = StaticLayoutProvider(graph_layout=graph_layout)

# 辺
gr.edge_renderer.glyph.update(line_color=cmap)
gr.edge_renderer.hover_glyph = MultiLine(line_color='limegreen')
gr.edge_renderer.nonselection_glyph = MultiLine(line_color=cmap,
                                                line_alpha=0.2,)

gr.edge_renderer.data_source.data = edges

# ホバー、タップ
gr.inspection_policy = gr.selection_policy = NodesAndLinkedEdges()

# Figure
my_chord_color = figure(x_range=(-1.55, 1.55), y_range=(-1.55, 1.55),
                        frame_width=600, frame_height=600,
                        tools='pan,wheel_zoom,tap,reset',
                        tooltips=[('index', '@index'),
                                  ('name', '@name'),
                                  ('group', '@group')]
                        )
my_chord_color.axis.visible = False
my_chord_color.grid.visible = False

# 輪っか
my_chord_color.multi_line('arc_xs', 'arc_ys',
                          source=gr.node_renderer.data_source,
                          line_color=cmap,
                          hover_color='limegreen',
                          nonselection_line_alpha=0.2,
                          line_width=10
                          )

my_chord_color.renderers.append(gr)

# テキスト
my_chord_color.text('x', 'y', 'name', 'angle',
                    source=ColumnDataSource(texts),
                    text_baseline='middle',
                    text_font_size='11px',
                    text_color='black',
                    )
```

これで完成! Chordをほぼ再現できた。

```python
show(p)
```

![bokeh_chord_color.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1714033/b2d708b6-f0e5-f509-6f3d-90f3256b536c.jpeg)

![bokeh_chord_color_hover.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1714033/b9b40308-36e4-777b-bef9-70eba71ea395.jpeg)
![bokeh_chord_color_select.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1714033/41da23f9-3f93-eb60-4634-759c997bd8e4.jpeg)