ys memos

Blog

reactでd3を使って線グラフを描く


react

2022/05/02


D3(D3.js)とは,JS 製のデータ可視化ライブラリである.TS で利用するための型定義は,サードパーティの@types/d3でなされている.

react-d3-libraryという OSS はあるものの,リポジトリの最終更新が 4 年前,npm パッケージの最終公開が 5 年前となっていたため,D3 本体と比較すると古くなってしまっているため,今回は React から D3 を直接扱って線グラフを描くことにした.

本記事では,「React で D3 を扱う」という事に注力し,D3 の扱いについては特に言及しないものとする.


CRA を使って React App を作る.

私の場合,自前のテンプレートを公開しているので,それを用いて TS 用のミニマムな App を準備した(通常の--tempalte typescriptでも問題なく本記事と同じことはできる).

$ npx create-react-app@latest --template @ysuzuki19/ts-min react-d3-line-chart
$ cd react-d3-line-chart

データは,参考に掲載した線形グラフのサンプル用いられているデータを JSON 形式に変換したものを用いた.

また,データ型については,実際に API から取得したデータを描画することを視野に入れ,RowDataというstringとして受けるものとDataという実際に扱うための型を準備し,それらの変換もソースコードとした.

元データ
date value
2013-04-28 135.98
2013-04-29 147.49
2013-04-30 146.93
...
加工後
const rowData: RowData[] = [
  { date: '2013-04-28', value: '135.98' },
  { date: '2013-04-29', value: '147.49' },
  { date: '2013-04-30', value: '146.93' },
  ...
];

D3にはd3.csv()d3.json()のように,REST API から直接データを受け取って描画を助けるメソッドもあるが,今回は敢えてそちらを使わないサンプルとした.これは,GraphQL を使いたい場合や,REST API の結果の形式を束縛されないためである.


基本的には,src/App.tsxのみを書き換える.

また,使用しているデータについては外部から使用しているものなので,形式を参考にしてもらう前提で,先頭・末尾の3行ずつのみの掲載とした(データの取得先についてはリンクにあるとおり).

今回,決まったデータから線形グラフを描画するためだけのプログラムであるという点,ソースコードの複雑さを減少させたいという点から,実際の描画と関係が薄い部分はApp()の外に書いているが,データの取得・描画をアプリ内で行う場合は,そのために調整する必要があるということは念頭に入れておいてほしい.

App.tsx
import React, { useRef } from 'react';
import * as d3 from 'd3';

const parseTime = d3.timeParse('%Y-%m-%d');

interface RowData {
  date: string;
  value: string;
}

/** create data from https://raw.githubusercontent.com/holtzy/data_to_viz/master/Example_dataset/3_TwoNumOrdered.csv */
const rowData: RowData[] = [
  { date: '2013-04-28', value: '135.98' },
  { date: '2013-04-29', value: '147.49' },
  { date: '2013-04-30', value: '146.93' },
  // ...,
  { date: '2018-04-21', value: '8997.57' },
  { date: '2018-04-22', value: '9001.64' },
  { date: '2018-04-23', value: '8958.55' },
];

interface Data {
  date: Date;
  value: number;
}

const data: Data[] = rowData.map((e) => ({
  date: parseTime(e.date) as Date,
  value: +e.value,
}));

const maxValue = Math.max(...data.map((e) => e.value));

const margin = {
  top: 20,
  right: 20,
  bottom: 50,
  left: 70,
};

const svgSize = {
  width: 500,
  height: 500,
};

const size = {
  width: svgSize.width - margin.left - margin.right,
  height: svgSize.height - margin.top - margin.bottom,
};

function App() {
  const ref = useRef(null);

  React.useEffect(() => {
    const svg = d3
      .select(ref.current)
      .append('svg')
      .attr('width', size.width + margin.left + margin.right)
      .attr('height', size.height + margin.top + margin.bottom)
      .append('g')
      .attr('transform', `translate(${margin.left}, ${margin.top})`);

    const x_range = d3.extent(data, (d: any) => d.date) as [Date, Date];
    const x = d3.scaleTime().range([0, size.height]).domain(x_range);

    const y_range = [0, maxValue];
    const y = d3.scaleLinear().range([size.height, 0]).domain(y_range);

    svg
      .append('g')
      .attr('transform', `translate(0, ${size.height})`)
      .call(d3.axisBottom(x));

    svg.append('g').call(d3.axisLeft(y));

    const dateValueLine = d3
      .line()
      .x((d: any) => x(d.date))
      .y((d: any) => y(d.value));

    svg
      .append('path')
      .data([data])
      .attr('class', 'line')
      .attr('fill', 'none')
      .attr('stroke', 'steelblue')
      .attr('stroke-width', 1.5)
      .attr('d', dateValueLine as any);
  }, []);

  return (
    <>
      <svg {...svgSize}>
        <g ref={ref} />
      </svg>
    </>
  );
}

export default App;


インストールコマンドは以下.

$ npm i d3
$ npm i -D @types/d3
import React, { useRef } from 'react';
import * as d3 from 'd3';

d3のメソッドで,時間の形式を指定したパーサを定義できる.

const parseTime = d3.timeParse('%Y-%m-%d');

ここでは,RowData型と名付けているが,これは元データの一列を意味するものとなっている.

API 等から直接取得する事を考慮し,全てstringで準備

interface RowData {
  date: string;
  value: string;
}

/** create data from https://raw.githubusercontent.com/holtzy/data_to_viz/master/Example_dataset/3_TwoNumOrdered.csv */
const rowData: RowData[] = [
  { date: '2013-04-28', value: '135.98' },
  { date: '2013-04-29', value: '147.49' },
  { date: '2013-04-30', value: '146.93' },
  // ...,
  { date: '2018-04-21', value: '8997.57' },
  { date: '2018-04-22', value: '9001.64' },
  { date: '2018-04-23', value: '8958.55' },
];

全てstringRowDataから,描画するための型Dataに変換する.

また,プロット範囲の特定のためにvalueの最大値も取り出しておく.(データの性質によっては最小値も必要)

interface Data {
  date: Date;
  value: number;
}

const data: Data[] = rowData.map((e) => ({
  date: parseTime(e.date) as Date,
  value: +e.value,
}));

const maxValue = Math.max(...data.map((e) => e.value));

SVG のサイズや描画のマージン等を定義.

描画サイズは SVG サイズおよび内部のマージンによって決定される.

const margin = {
  top: 20,
  right: 20,
  bottom: 50,
  left: 70,
};

const svgSize = {
  width: 500,
  height: 500,
};

const size = {
  width: svgSize.width - margin.left - margin.right,
  height: svgSize.height - margin.top - margin.bottom,
};

function App() {
  const ref = useRef(null);

  React.useEffect(() => {
    const svg = d3
      .select(ref.current)
      .append('svg')
      .attr('width', size.width + margin.left + margin.right)
      .attr('height', size.height + margin.top + margin.bottom)
      .append('g')
      .attr('transform', `translate(${margin.left}, ${margin.top})`);

    const x_range = d3.extent(data, (d: any) => d.date) as [Date, Date];
    const x = d3.scaleTime().range([0, size.height]).domain(x_range);

    const y_range = [0, maxValue];
    const y = d3.scaleLinear().range([size.height, 0]).domain(y_range);

    svg
      .append('g')
      .attr('transform', `translate(0, ${size.height})`)
      .call(d3.axisBottom(x));

    svg.append('g').call(d3.axisLeft(y));

    const dateValueLine = d3
      .line()
      .x((d: any) => x(d.date))
      .y((d: any) => y(d.value));

    svg
      .append('path')
      .data([data])
      .attr('class', 'line')
      .attr('fill', 'none')
      .attr('stroke', 'steelblue')
      .attr('stroke-width', 1.5)
      .attr('d', dateValueLine as any);
  }, []);

  return (
    <>
      <svg {...svgSize}>
        <g ref={ref} />
      </svg>
    </>
  );
}

export default App;

useRef()は,よく見るものの自分では活用できていなかったので,この機会に扱えたのは良い機会であった.

useEffect()内の D3 の扱いの解説については,参考を見てもらうとよいだろう.


関連タグを探す