React + D3 で Pie を描く

7 min read読了の目安(約6500字

はじめに

公式ドキュメントや技術書でインプットした内容をアウトプットとして記事を作成させてもらいました。もし、不適切な箇所があればご指摘よろしくお願いします。

最終的な成果物

こちらが成果物になります。
f6ab435be017c354ed270db3a11c7d47

Pieグラフのコード
Pie.js
import React, { useEffect, useRef } from "react";
import * as d3 from "d3";

const Pie = props => {
  const ref = useRef(null);
  const cache = useRef(props.data);
  const createPie = d3
    .pie()
    .value(d => d.value)
    .sort(null);
  const createArc = d3
    .arc()
    .innerRadius(props.innerRadius)
    .outerRadius(props.outerRadius);
  const colors = d3.scaleOrdinal(d3.schemeCategory10);
  const format = d3.format(".2f");

  useEffect(
    () => {
      const data = createPie(props.data);
      const prevData = createPie(cache.current);
      const group = d3.select(ref.current);
      const groupWithData = group.selectAll("g.arc").data(data);

      groupWithData.exit().remove();

      const groupWithUpdate = groupWithData
        .enter()
        .append("g")
        .attr("class", "arc");

      const path = groupWithUpdate
        .append("path")
        .merge(groupWithData.select("path.arc"));

      const arcTween = (d, i) => {
        const interpolator = d3.interpolate(prevData[i], d);

        return t => createArc(interpolator(t));
      };

      path
        .attr("class", "arc")
        .attr("fill", (d, i) => colors(i))
        .transition()
        .attrTween("d", arcTween);

      const text = groupWithUpdate
        .append("text")
        .merge(groupWithData.select("text"));

      text
        .attr("text-anchor", "middle")
        .attr("alignment-baseline", "middle")
        .style("fill", "white")
        .style("font-size", 10)
        .transition()
        .attr("transform", d => `translate(${createArc.centroid(d)})`)
        .tween("text", (d, i, nodes) => {
          const interpolator = d3.interpolate(prevData[i], d);

          return t => d3.select(nodes[i]).text(format(interpolator(t).value));
        });

      cache.current = props.data;
    },
    [props.data]
  );

  return (
    <svg width={props.width} height={props.height}>
      <g
        ref={ref}
        transform={`translate(${props.outerRadius} ${props.outerRadius})`}
      />
    </svg>
  );
};

export default Pie;
index.jsのコード
index.js
import React, { useState } from "react";
import ReactDOM from "react-dom";
import * as d3 from "d3";

// 作成したPieグラフ
import Pie from "./Pie";

function App() {

  const [width, height, innerRadius, outerRadius] = [200, 200, 60, 100];

  // ボタンをクリックした時、ランダムなデータを生成する
  const generateData = (value, length = 5) =>
    d3.range(length).map((item, index) => ({
      date: index,
      value: value === null || value === undefined ? Math.random() * 100 : value
    }));
  const [data, setData] = useState(generateData());
  const changeData = () => {
    setData(generateData());
  };

  return (
    <div className="App">
      <div>
        <button onClick={changeData}>Click</button>
      </div>
      <div>
        <h2 className="label">React Hook</h2>
        <Pie
          data={data}
          width={width}
          height={height}
          innerRadius={innerRadius}
          outerRadius={outerRadius}
        />
      </div>
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

コードの解説

では早速、コードの内容を説明させていただきます。

Pieグラフの設定を書く

script.js
 // useRefを設定します
  const ref = useRef(null);
  const cache = useRef(props.data);

  // d3.pieで 円弧の始点/終点計算を設定します
  const createPie = d3
    .pie()
    .value(d => d.value)
    .sort(null);

  // d3.arcで 円弧の外側の半径、内側の半径を設定します
  const createArc = d3
    .arc()
    .innerRadius(props.innerRadius)
    .outerRadius(props.outerRadius);

  // 項目の色を設定します
  const colors = d3.scaleOrdinal(d3.schemeCategory10);

  // フォーマットを設定します
  const format = d3.format(".2f");

"useRef" は 箱という認識です。ぶっちゃけ、雰囲気で使ってます...

余談はさておき、

  • d3.pie() にPieグラフのデータをセットする。
  • svg の path を作成して、Pieグラフ の円弧を描画します
  • Pieグラフ の内側にあるテキストにフォーマットを指定します

Pieグラフのデータ処理を書く

script.js
useEffect(
    () => {
      const data = createPie(props.data);
      const prevData = createPie(cache.current);
      const group = d3.select(ref.current);

      // 各円弧に data をセットします
      const groupWithData = group.selectAll("g.arc").data(data);

      // 不要な要素を削除します
      groupWithData.exit().remove();

      // データをセットされた新しい element にバインドする
      const groupWithUpdate = groupWithData
        .enter()
        .append("g") // group container を追加する
        .attr("class", "arc");

      const path = groupWithUpdate
        .append("path") // path を追加する
        .merge(groupWithData.select("path.arc")); // merge() は enter() と update()を組み合わせた関数

      const arcTween = (d, i) => {
        const interpolator = d3.interpolate(prevData[i], d);

        return t => createArc(interpolator(t));
      };

      // Pieグラフ をオシャレにする
      path
        .attr("class", "arc")
        .attr("fill", (d, i) => colors(i))
        .transition()
        .attrTween("d", arcTween);

      const text = groupWithUpdate
        .append("text") // text を追加する
        .merge(groupWithData.select("text"));

      // text をオシャレにする
      text
        .attr("text-anchor", "middle")
        .attr("alignment-baseline", "middle")
        .style("fill", "white")
        .style("font-size", 10)
        .transition()
        .attr("transform", d => `translate(${createArc.centroid(d)})`)
        .tween("text", (d, i, nodes) => {
          const interpolator = d3.interpolate(prevData[i], d);

          return t => d3.select(nodes[i]).text(format(interpolator(t).value));
        });

      cache.current = props.data;
    },
    [props.data]
  );

主にやってることは 2つ のみです

  • data をセットした g 要素 "<g class="arc">" を作成します。
  • attr(属性)を追加して、見た目を整えます。

D3を触る上で知って損はない関数です

  • "enter()" : 新しい要素を生成する
  • "update()" : 既存の要素を更新する
  • "exit()" : 不要な要素を扱う

データ描画を書く

script.js
return (
    <svg width={props.width} height={props.height}>
      <g
        ref={ref}
        transform={`translate(${props.outerRadius} ${props.outerRadius})`}
      />
    </svg>
  );
  • html に変数を書き込んだら、完成になります。

完成

f6ab435be017c354ed270db3a11c7d47

おわりに

読んで頂きありがとうございます。ボク自身、 JavaScript でデータ可視化するのは楽しいなと実感し始めたばかりです。今度はより分かりやすく、よりインタラクティブでデザインも良いデータ可視化できるように精進させていただきます!