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()
の外に書いているが,データの取得・描画をアプリ内で行う場合は,そのために調整する必要があるということは念頭に入れておいてほしい.
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' },
];
データの加工
全てstring
のRowData
から,描画するための型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 の扱いの解説については,参考を見てもらうとよいだろう.