Cytoscape.js: The style value of `label` is deprecated for `width`

[Cytoscape.js](https://js.cytoscape.org/)でノードの横幅をラベル文字の横幅に合わせたい場合に使えた`width: 'label'`オプションが、v3.16から非推薦となっている[^1]。

[^1]: [Deprecate `width:label` & `height:label`](https://github.com/cytoscape/cytoscape.js/commit/977515325eb6a53898e6af1bc3ce6dbfe9863b52)

```ts
cytoscape({
  style: [
    {
      selector: 'node',
      style: {
        shape: 'rectangle',
        width: 'label' // The style value of `label` is deprecated for `width`
      }
    }
  ]
})
```

さらに、このオプションでは図のようにラベルの前後にパディングがなく、見た目が悪い。

![width: 'label'](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1129415/f26f94ea-39d5-60cd-28f9-e431a9fe8f7e.png)

## マッパーの作成

ドキュメントには「プロパティの値を直接指定する代わりにマッパーを用いて動的にプロパティ値を指定することもできる[^2]」とあるので、ラベル文字からキャンバスに描画された文字の横幅を取得するマッパーを作成する。

[^2]: [Mappers](https://js.cytoscape.org/#style/mappers)

```ts:labelWidth.ts
import type { NodeCollection } from 'cytoscape'

const labelWidth = (node: NodeCollection): number => {
  const canvas = document.createElement('canvas')
  const context = canvas.getContext('2d')!
  context.font = ['style', 'weight', 'size', 'family']
    .map(style => node.style(`font-${style}`))
    .join(' ')
  const { width } = context.measureText(node.data('label'))
  const padding = 10

  return width + padding
}

export { labelWidth }
```

<details><summary>JavaScriptの場合</summary><div>

```js:labelWidth.js
const labelWidth = node => {
  const canvas = document.createElement('canvas')
  const context = canvas.getContext('2d')
  context.font = ['style', 'weight', 'size', 'family']
    .map(style => node.style(`font-${style}`))
    .join(' ')
  const { width } = context.measureText(node.data('label'))
  const padding = 10

  return width + padding
}
```
</div></details>

:::note warn
`node.data('label')`の`label`の値は、`elements`で定義したノードのラベルのプロパティ名に合わせて変更してください。

```ts
cytoscape({
  elements: [
    {
      data: {
        id: '1',
        label: 'node1' // ← このプロパティ名に合わせる
    }
  }]
})
```
:::

## マッパーの使用

`width`オプションに作成したマッパーを指定する。

```diff
+ import { labelWidth } from 'labelWidth.ts'

  cytoscape({
    style: [
      {
        selector: 'node',
        style: {
          shape: 'rectangle',
-         width: 'label' // The style value of `label` is deprecated for `width`
+         width: labelWidth
        }
      }
    ]
  })
```

いい感じになった。

![width: labelWidth](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1129415/92e652df-75c7-5f3e-e2f2-95c33d7e9ffc.png)

## 余談

ワーニングの文字列「The style value of `label` is deprecated for `width`」でググると、おそらく[cytoscape/cytoscape.js#2713](https://github.com/cytoscape/cytoscape.js/issues/2713#issuecomment-1062746258)を参考にしたてあろう下記のようなマッパーがいくつかヒットしたが、自分で試したところ上手くいかなかった。

```js
const canvas = document.createElement('canvas')
const context = canvas.getContext('2d')
const fontStyle = node.style('font-style')
const fontSize = node.style('font-size')
const fontFamily = node.style('font-family')
const fontWeight = node.style('font-weight')
context.font = fontStyle + ' ' + fontWeight + ' ' + fontSize + ' ' + fontFamily
```

調査したところ、原因は **`node.style()`を適用する順番** にあることがわかった。
MDN Web docsによると、CSSの`font`プロパティの値は次の順番で指定しなければならない[^3]。

[^3]: [font - CSS: カスケーディングスタイルシート | MDN](https://developer.mozilla.org/ja/docs/Web/CSS/font#%E6%A7%8B%E6%96%87)

```css
font: font-style font-weight font-size font-family
```

上記コードにおいて`context.font`の指定順は問題ないが、その前の`node.style()`の適用においてもこの順番を守る必要があったようである。
つまり、次のように適用順番を修正すれば、上記コードでも正しく機能する。

```diff
  const canvas = document.createElement('canvas')
  const context = canvas.getContext('2d')
  const fontStyle = node.style('font-style')
+ const fontWeight = node.style('font-weight')
  const fontSize = node.style('font-size')
  const fontFamily = node.style('font-family')
- const fontWeight = node.style('font-weight')
  context.font = fontStyle + ' ' + fontWeight + ' ' + fontSize + ' ' + fontFamily
```