react-chartjs-2でクリックした要素を取得する方法について


概要

 Reactについて細かい説明は必要ないでしょう。今流行りのWebフレームワークです。
 Chart.jsも有名ですね。JavaScriptで書かれたグラフ描画用ライブラリです。
 react-chartjs-2は、Chart.jsの各種グラフ要素を、ReactのComponentとして書ける便利なライブラリです。

 さて、react-chartjs-2で私は散布図を描画していました。

 で、Chart.jsは「凡例の表示をクリックした時、デフォルトではそのグラフを非表示にする」挙動をします。
 その際、「どのグラフの凡例をクリックしたか」を調べたいと思いました。
(下の画像では、「Верный+バルジ4」のグラフが非表示になっています)

 Chart.jsのドキュメントによると、凡例をクリックした際の標準の処理が載っています。その際、クリックしたグラフの情報が引数に乗るので、これを読み取れれば目的が達成されるのではないかと考えました。

 しかし、 そのコード中のthisってどうやって参照するんだよ 問題が目の前に立ちはだかります。Reactでは「Class ComponentやFunctional Component内にJSXを書いて仮想DOMを表現する」ので、thisの意味が「class本体」や「グローバルのthis」のようになります。つまり、「JSX内のタグの参照(目的とするthis)」をどうやって取得するのかが重要なのです。

解決策

 react-chartjs-2において散布図は、<Scatter>という独自タグで表現されています。その属性(Props)の中にはrefというものがあり、これが当該タグの参照となります。参照を保持するには、React Hooks的に考えるとuseRefを使うべきです。

const OutputGraph: React.FC<{params: IGraphParam[]}> = ({params}) => {
    // <Scatter>への参照情報
    const scatterElement = React.useRef<Scatter>(null);

    ()

    return (
        // 参照を利用される当該タグ
        <Scatter width={450} height={450} data={graphData}
        ref={scatterElement} onElementsClick={onClickGraph}
        options={{
            elements: { line: { tension: 0 } },
            scales: {
                xAxes: [{ scaleLabel: { display: true, labelString: '最終攻撃力' }, }],
                yAxes: [{ scaleLabel: { display: true, labelString: '大破率(%)' }, }]
            },
            showLines: true
        }} />
    );
}

 こうしてオブジェクト自体の参照を取ってしまえば、後はそこから辿っていけば、目的とする情報を取得できます。

    const onClickGraph = () => {
        // 参照を取得する
        const scatterObject = scatterElement.current;
        // ここで丁寧にnullチェックしているのは、scatterObjectが↑の段階だと
        // Scatter | null型なので、スマートキャストする必要があることから
        if (scatterObject == null) {
            return;
        }

        // ここで一旦anyにキャストしているのは、scatterObject.chartInstanceは
        // Chart型と判断されているのに、@types/chart.jsの定義によると、
        // なぜかlegendプロパティが生えていなかったから
        // (キャストしないとエディタがエラーを出してコンパイルできない)
        const temp: any = scatterObject.chartInstance;

        // 強引にanyにキャストしたご利益で、legendプロパティ以下を取得できている
        // temp.legend.legendItemsの中身はもっと複雑な型の配列(AoS)だが、
        // 必要な部分だけ読み取ることで記述量を減らしている
        const legendItems: Array<{text: string, hidden: boolean}> = temp.legend.legendItems;

        // 読み取りは終わったので後はデータ加工のみ
        const ignoreNames = legendItems.filter(item => item.hidden).map(item => item.text);

        // useState()から引っ張ってきた上書き用メソッドを利用する
        // (今回の記事とは無関係)
        setGraphData(createGraphData(ignoreNames));
    };

 ちなみに<Scatter>にはonElementsClick属性があり、名前からしてクリックしたグラフの情報を引数から渡してくれる……そんなふうに考えていた時期が私にもありました。実際にはクリックしても[]しか引数のe: anyに渡されないので、「クリックした」ということしか読み取れないというね! どうしてこうなった!!!

備考

package.jsonによると、

  • react: 16.8.2
  • chart.js": 2.7.3
  • react-chartjs-2: 2.7.4
  • @types/chart.js: 2.7.45

といった環境でした。