以前、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の調整は最低限にしています。
目次
- 環境
- デフォルトの Legend の代わりに HTML Legend を表示する
- 凡例をクリックすることで、Pie chart 上の表示を ON / OFF できるようにする
- バックエンドのAPIから取得して表示する場合の実装
- ソースコード
環境
- 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 がありました。
- javascript - Show custom html code in react-chartjs-2 legend - Stack Overflow
- javascript - How to custom legend with react-chartjs-2 - Stack Overflow
そのため、Pie chart コンポーネントの plugin
props に、 HTML Legend 向けの plugin を指定すれば良さそうです。
実装
最初に、Chart.js ドキュメントにある通り、レンダリングする前に呼ばれるプラグインの afterUpdate
イベントに plugin形式の HTML Legend を実装します。
- HTML Legend | Chart.js
- Plugins | Chart.js
- https://www.chartjs.org/docs/latest/api/interfaces/Plugin.html#afterupdate
なお、ひとまず動作すればよいとして、ここの実装では所々で 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
の中では
toggleDataVisibility
で、表示・非表示をトグルupdate
で更新
をします。
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 の設定も含まれています。