【CytoscapeJS】複合ノード内にラベルを配置する

# はじめに

[CYtoscapeJS](https://js.cytoscape.org/)でのlabelの付け方は以下の種類が用意されています。

- [ラベルデモ](https://js.cytoscape.org/demos/labels/)

そして、Styleには[このよう](https://js.cytoscape.org/#style/labels)にプロパティ定義をすることで実現できます。

ではもし、例えば下記画像のように複合ノード内にタイトルを含めたい場合はどうすればいいのでしょうか？

![スクリーンショット 2022-08-20 20.15.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299962/7618e18b-720f-0808-8239-2bbf348ad560.png)

以下、本記事ではノードを包括する親ノードを複合ノードと呼びます。

> indicates the compound node parent id
> [Elements JSON](https://js.cytoscape.org/#notation/elements-json)

より。

# 結論

```ts
/** TextMetricsの取得 */
const getMeasureText = (target: cytoscape.NodeCollection | cytoscape.EdgeCollection): TextMetrics => {
  const canvas = document.createElement('canvas');
  const ctx = canvas.getContext( '2d' )!;
  const fontStyle = target.style('font-style');
  const fontSize = target.style('font-size');
  const fontFamily = target.style('font-family');
  const fontWeight = target.style('font-weight');

  ctx.font = fontStyle + ' ' + fontWeight + ' ' + fontSize + ' ' + fontFamily;

  return ctx.measureText(target.data('name'));
}

/** 横幅の取得 */
const getCharacterWidthInCanvas = (target: cytoscape.NodeCollection | cytoscape.EdgeCollection): number => {
  const labelWidthMargin = 10;

  const measure = getMeasureText(target);

  return measure.width + labelWidthMargin;
}

/** 高さの取得 */
const getCharacterHeightInCanvas = (target: cytoscape.NodeCollection | cytoscape.EdgeCollection): number => {
  const labelHeightMargin = 10;

  const measure = getMeasureText(target);

  return measure.actualBoundingBoxAscent + measure.actualBoundingBoxDescent + labelHeightMargin;
}

/** --- generate cytoscape --- */

cytoscape({
  container: /** target dom */,
  layout: /** layout */,
  elements: {
    nodes: /** nodes */,
    edges: /** edges */,
  },
  style: [
    {
      selector: 'node',
      css: {
        content: 'data(name)',
        width: getCharacterWidthInCanvas,
        shape: 'round-rectangle',
        'text-valign': 'center',
        'text-halign': 'center',
      },
    },
    {
      selector: ':parent',
      css: {
        padding: getCharacterHeightInCanvas,
        'text-valign': 'bottom',
        'text-halign': 'center',
        // @ts-ignore
        'text-margin-y': (node: cytoscape.NodeCollection) => -getCharacterHeightInCanvas(node),
      },
    },
  ],
});
```

複合ノードに `padding` と `text-margin-y` を与えてやります。

# 説明

`getCharacterWidthInCanvas` と `getCharacterHeightInCanvas` で与えられた文字列の横幅と高さを計算しています。([【CytoscapeJS】The style value of label is deprecated for width](https://qiita.com/masayasviel/items/b9d351f3900e9dd2f314)を参考にしてください)
また、見栄えも気にして+αで固定パディング値も与えてやりましょう。

すでに `node` に対してcssで `content: 'data(name)'` を与えているため、複合ノードも囲っているとはいえノードの一種、ラベルが適用されます。

CytoscapeJSのつまづきポイントとして、ノードを囲う、いわゆるグループの役割を果たすノードが親ノードとして `parent` で定義します。木構造を知っていると親ノードはより上位のノードを指すため、parentでエッジを貼るのかと思えますが、実際は複合ノードをparentで定義します。私はここでつまづきました。
さて、CSSの擬似クラスチックに `selector: ':parent'` と定義してやり複合ノードを指定します。
その中にパディングでラベルを入れられるだけのパディングを作成します。これは用いるレイアウトエンジンによりますが、ラベルとノードがかぶる危険を排除するためです。
そして空いたスペースに `text-margin-y` でラベルを動かして挿入します。負の値を与えることでマージンがめり込むので、それを活用しています。
[@types/cytoscape](https://www.npmjs.com/package/@types/cytoscape)を利用しているのですが、型定義が上手く聞いてくれなかったため、 `@ts-ignore` をつけています。

これで複合ノード内にラベルを配置することができました！

![スクリーンショット 2022-08-20 20.40.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299962/afca3398-b317-d450-2699-2440b0d4116b.png)

# おわりに

2022年8月現在、cytoscapeJSの型定義ファイルでは型定義が間違っているように思える部分がいくつかあります。そういうときはあまり好ましく無いですが、ts-ignoreや型アサーションなどを活用しましょう。