React + react-chartjs-2 + Chart.js を使って、デフォルトの Legend の代わりに HTML Legend を表示してみた

以前、React + react-chartjs-2 + Chart.js を使って、Pie chart を表示してみました。
React + react-chartjs-2 + Chart.js を使って、Pie chart を表示してみた - メモ的な思考的な

その時は凡例 (Legend)のカスタマイズとして「凡例をPie chartの右隣に表示する」をためしました。

 
そんな中、Pie chartの元データが多い場合、凡例にすべてを表示しきれないことがありました。

 
例えば、以下のPie chartの凡例は、ラベル1~ラベル8まで並んでいます。

凡例にはスクロールバーも表示されておらず、これを見ただけではラベル8までのデータに見えます。

 
しかし、実装を見てみると、本当はラベル10まで存在しています。

import { createLazyRoute } from '@tanstack/react-router'
import { ArcElement, Legend, Tooltip, Chart as chartJs } from 'chart.js'
import { Pie } from 'react-chartjs-2'

const Component = () => {
  chartJs.register(ArcElement, Tooltip, Legend)
  chartJs.overrides.pie.plugins.legend.position = 'right'

  const data = {
    labels: [
      'ラベル1',
      'ラベル2',
      'ラベル3',
      'ラベル4',
      'ラベル5',
      'ラベル6',
      'ラベル7',
      'ラベル8',
      'ラベル9',  // 凡例に表示されないラベル
      'ラベル10',  // 凡例に表示されないラベル
    ],
    datasets: [
      {
        label: '購入数',
        data: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
        backgroundColor: ['mediumseagreen'],
      },
    ],
  }

  return (
    <div style={{ width: 300, height: 200 }}>
      <Pie data={data} />
    </div>
  )
}

export const Route = createLazyRoute(
  '/static_pie_chart_with_part_of_legend_missing',
)({
  component: Component,
})

 
このように、全ての凡例を見れないのは不便なことから、対応してみたときのメモを残します。

なお、今回は凡例の表示をメインにしていることもあり、CSSの調整は最低限にしています。

 
目次

 

環境

  • Windows11 WSL2
  • React 18.2.0
  • Chart.js 4.4.2
  • react-chartjs-2 5.2.0
  • Hono 4.2.7
  • TanStack Router 1.30.1
  • TanStack Query 5.32.0

 
なお、今回はPie chartのデータソースについて

  • 固定値
  • バックエンドのAPIから取得した値

の2つを試してみることから、Honoなども使っています。

Honoなどの詳細については、以前の記事を参照ください。

 

デフォルトの Legend の代わりに HTML Legend を表示する

調査

まずは、Chart.jsのドキュメントにて、デフォルトの Legend をカスタマイズする方法を見てみます。
Legend | Chart.js

 
しかし、デフォルトで用意されている Legend では

  • スクロールバーを表示する
  • 凡例をすべて表示する

などの設定が見当たりませんでした。

 
次に、デフォルトの Legend の代わりとなる機能を探したところ、 HTML Legend がありました。plugin として実装すれば良さそうです。
HTML Legend | Chart.js

 
ただ、今回はReactでChart.jsを扱えるようにする react-chartjs-2 を使っています。そこで、 react-chartjs-2 で HTML Legend を使う方法を調べたところ、以下の stackoverflow がありました。

 
そのため、Pie chart コンポーネントplugin props に、 HTML Legend 向けの plugin を指定すれば良さそうです。

 

実装

最初に、Chart.js ドキュメントにある通り、レンダリングする前に呼ばれるプラグインafterUpdate イベントに plugin形式の HTML Legend を実装します。

 
なお、ひとまず動作すればよいとして、ここの実装では所々で any 型を使っています。

また、 afterUpdate が複数回呼ばれても良いようにするため、 document.getElementById('custom-ul') を使い element が存在していれば afterUpdate が実行されないようにしています。

const htmlLegendPlugin = {
  id: 'htmlLegend',
  afterUpdate(chart: any) {
    // Even if called multiple times, draw only once.
    const customUl = document.getElementById('custom-ul')
    if (customUl) return

    const items = chart.options.plugins.legend.labels.generateLabels(chart)
    const ul = document.createElement('ul')
    ul.id = 'custom-ul'

    items.forEach((item: any) => {
      const li = document.createElement('li')
      const boxSpan = document.createElement('span')
      boxSpan.style.background = item.fillStyle
      li.appendChild(boxSpan)
      li.appendChild(document.createTextNode(item.text))
      ul.appendChild(li)
    })

    const customLegend = document.getElementById('custom-legend')
    customLegend?.appendChild(ul)
  },
}

 
続いて、コンポーネントを実装します。

デフォルトの Legend を使ったときとの違いとしては

  • Pie コンポーネントplugin props に、上記で作成した plugin を指定
  • HTML Legend を表示するためのタグを用意し、idに custom-legend も定義
  • デフォルトの Legend を表示しないよう、 chartJs.overrides.pie.plugins.legend.display = false を設定

です。

const Component = () => {
  chartJs.register(ArcElement, Tooltip, Legend)
  chartJs.overrides.pie.plugins.legend.display = false

  const data = {
    labels: [
      'ラベル1',
      'ラベル2',
      'ラベル3',
      'ラベル4',
      'ラベル5',
      'ラベル6',
      'ラベル7',
      'ラベル8',
      'ラベル9',
      'ラベル10',
    ],
    datasets: [
      {
        label: '購入数',
        data: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
        backgroundColor: ['mediumseagreen'],
      },
    ],
  }

  return (
    <div
      style={{
        width: 400,
        height: 200,
        // Use flexbox to arrange pie charts and legends side by side
        display: 'flex', // justifyContent: 'space-between', flexBasis: '50%'
      }}
    >
      <Pie data={data} plugins={[htmlLegendPlugin]} />
      <div
        id={'custom-legend'}
        style={{
          maxHeight: '100%',
          overflowY: 'auto',
          width: '200px',
          padding: '10px',
          boxSizing: 'border-box',
          border: '1px solid #ccc',
          backgroundColor: '#f9f9f9',
        }}
      />
    </div>
  )
}

 

動作確認

HTML Legend 版を表示してみたところ、凡例にスクロールバーが表示されました。

また、スクロールすることで、ラベル10まで表示できました。

 

凡例をクリックすることで、Pie chart 上の表示を ON / OFF できるようにする

デフォルトの Legend の場合、凡例をクリックすると、 Pie chart 上の表示を ON / OFF できます。

例えば以下の場合は、一番面積の広いラベル8を非表示にしています。

 
一方、先ほど作成した HTML Legend の場合は、凡例をクリックしても何も変化はありません。

そこで、Chart.jsの公式ドキュメントにある通り、items.forEach の中で onclick を追加し、表示の ON / OFF ができるようにします。
HTML Legend | Chart.js

 

実装

onclick の中では

をします。

items.forEach((item: LegendItem) => {
  const li = document.createElement('li')
  li.onclick = () => {
    if (item.index !== undefined) {
      chart.toggleDataVisibility(item.index)
      chart.update()
    }
  }

  // あとは同じ
}

 
また、 afterUpdate の直後にて、2回以上更新しないよう

const customUl = document.getElementById('custom-ul')
if (customUl) return

とした部分について、そのままだと以下のようなエラーになります。

chunk-XWX6J6U2.js?v=126d9035:2367 Uncaught TypeError: Cannot read properties of null (reading 'parentNode')
at _getParentNode (chunk-XWX6J6U2.js?v=126d9035:2367:24)
at DomPlatform.isAttached (chunk-XWX6J6U2.js?v=126d9035:6353:23)
at Chart.bindResponsiveEvents (chunk-XWX6J6U2.js?v=126d9035:9181:18)
at Chart.bindEvents (chunk-XWX6J6U2.js?v=126d9035:9126:12)
at Chart._checkEventBindings (chunk-XWX6J6U2.js?v=126d9035:8822:12)
at Chart.update (chunk-XWX6J6U2.js?v=126d9035:8771:10)
at li.onclick (static_pie_chart_with_html_legend.lazy.tsx:20:15)

 
そこで、こちらも Chart.js のドキュメントの実装に差し替えます。

const ul = getOrCreateLegendList()

// Remove old legend items
while (ul.firstChild) {
  ul.firstChild.remove()
}

 
他にも、 react-chartjs-2 のドキュメントを参考に、Chart.js まわりの型定義を any から適切な型へと変更しています。
How to use react-chartjs-2 with TypeScript? | react-chartjs-2

 
全体像は以下のコミットになります。
https://github.com/thinkAmi-sandbox/react_chartjs_with_hono-example/pull/2/commits/932253314049884a46da393fe7fd6b209127c0fe

   

動作確認

凡例のラベルをクリックすると、Pie chart から該当部分が非表示になりました。

 

バックエンドのAPIから取得して表示する場合の実装

ここまで Pie chart のデータソースは固定値でした。

そんな中、Webアプリケーションの場合など、バックエンドのAPIからデータソースを取得・表示する場合の実装はどうなるのかが気になりました。

 
実際にためしてみたところ、Pie コンポーネントの plugin props を使うことで、固定値の場合と同じ実装で実現できました。

同じ実装だったため、この記事では省略します。詳細は以下のコミットを見てください。
https://github.com/thinkAmi-sandbox/react_chartjs_with_hono-example/pull/2/commits/3278e5d5885784e3862fa94c0d0a96e6b6e9f9bb

 
上記のコミットを使って動かしてみた場合のスクリーンショットはこちらです。

 

ソースコード

Githubに上げました。
https://github.com/thinkAmi-sandbox/react_chartjs_with_hono-example

今回のプルリクはこちら。
https://github.com/thinkAmi-sandbox/react_chartjs_with_hono-example/pull/2

 
ちなみに、今回のサンプルコードでは、 Linter / Formatter として Biome を使いました。そのため、プルリクには Biome の設定も含まれています。