はじめに
この記事では、JavaScript 向けのデータ可視化ライブラリである D3 を使って、棒グラフを実装する手順を記載します。
開発環境
開発環境は以下の通りです。
- Windows 11
- Next.js 14.2.4
- React 18.3.1
- TypeScript 5.5.2
- D3 7.9.0
- @types/d3 7.4.3
実装手順
以下の流れで実装を進めています。
- 棒グラフに表示するデータの定義
- 棒グラフを表示する SVG の描画
- データの表示範囲(座標)の定義
- SVG 操作の有効化・初期化
- 棒グラフの表示
- その他関連要素の表示
1. 棒グラフに表示するデータの定義
X軸に month
、Y軸に income
を表示します。
export const data = [
{ month: "1月", income: 80 },
{ month: "2月", income: 10 },
{ month: "3月", income: 40 },
{ month: "4月", income: 30 },
{ month: "5月", income: 40 },
{ month: "6月", income: 50 },
{ month: "7月", income: 60 },
{ month: "8月", income: 70 },
{ month: "9月", income: 80 },
{ month: "10月", income: 40 },
{ month: "11月", income: 60 },
{ month: "12月", income: 90 },
];
2. 棒グラフを表示する SVG の描画
棒グラフを表示する SVG を描画します。
const WIDTH = 640;
const HEIGHT = 360;
export default function Page() {
return <svg width={WIDTH} height={HEIGHT} />;
}
現時点では、横 640px、縦 360 px の SVG が表示されるだけです。
今後、この SVG 内に棒グラフを実装していきます。
3. データの表示範囲(座標)の定義
1.で用意したデータの表示範囲(座標)定義します。
X軸
D3 の以下のAPIを利用して、X軸の表示範囲を定義します。
-
scaleBand:
domain
、range
をもとにX軸方向の表示範囲を定義 -
band.domain: X軸に表示する
month
の値の範囲(入力値) - band.range: 入力値を画面上に表示する範囲(出力値)
- band.padding: 各棒グラフ間の余白
const xScale = d3
.scaleBand()
.domain(data.map((d) => d.month))
.range([0, WIDTH]);
Y軸
Y軸も D3 の API を利用して、表示範囲を定義します。
X軸の入力値が文字(離散値、カテゴリ)であるのに対し、Y軸の入力値が数値(連続値)なので、scaleBand
の代わりに scaleLinear
という API を利用します。
-
scaleLinear:
domain
、range
をもとにY軸方向の表示範囲を定義 -
scaleLinear.domain: Y軸に表示する
income
の値の範囲(入力値) - scaleLinear.range: 入力値を画面上に表示する範囲(出力値)
const yScale = d3
.scaleLinear()
.domain([0, 100])
.range([0, HEIGHT]);
4. SVG 操作の有効化・初期化
- で描画した SVG を操作して棒グラフを表示できるようにするため、SVG 操作の有効化・初期化します。
- select: D3 による SVG 操作の有効化
- selection.remove: SVG の初期化(過去の値の削除)
import * as d3 from "d3";
import { useEffect, useRef } from "react";
const WIDTH = 640;
const HEIGHT = 360;
export default function Page() {
const ref = useRef<SVGSVGElement>(null);
useEffect(() => {
const svg = d3.select(ref.current); // SVG の操作を有効化
svg.selectAll("*").remove(); // SVG を初期化
}, []);
return <svg ref={ref} width={WIDTH} height={HEIGHT} />;
}
5. 棒グラフの表示
今まで定義した値をもとにして SVG 内に棒グラフを表示します。
"use client";
import * as d3 from "d3";
import { data } from "./data";
import { useEffect, useRef } from "react";
const WIDTH = 640;
const HEIGHT = 360;
export default function Page() {
const ref = useRef<SVGSVGElement>(null);
useEffect(() => {
const svg = d3.select(ref.current); // SVG の操作を有効化
svg.selectAll("*").remove(); // SVG を初期化
const xScale = d3
.scaleBand()
.domain(data.map((d) => d.month))
.range([0, WIDTH])
.padding(0.2);
const yScale = d3.scaleLinear().domain([0, 100]).range([0, HEIGHT]);
svg
.selectAll("rect") // 棒グラフを表示するための SVG の要素を選択
.data(data) // 要素とデータの紐づけ
.join("rect") // データに対応する分の要素を追加
.attr("x", (d) => xScale(d.month) || 0) // X軸の表示位置
.attr("y", (d) => yScale(d.income)) // Y軸の表示位置
.attr("width", xScale.bandwidth()) // 横幅
.attr("height", (d) => yScale(100) - yScale(d.income)) // 高さ
.attr("fill", "steelblue"); // 色
}, []);
return <svg ref={ref} width={WIDTH} height={HEIGHT} />;
}
棒グラフが表示されます。
また、開発者ツールで実装した要素やプロパティが追加されていることを確認できます。
6. その他関連要素の表示
X軸 / Y軸の目盛りやツールチップを表示することも可能です。
"use client";
import * as d3 from "d3";
import { data } from "./data";
import { useEffect, useRef } from "react";
const WIDTH = 640;
const HEIGHT = 360;
const MARGIN = { top: 20, right: 30, bottom: 30, left: 40 };
export default function Page() {
const ref = useRef<SVGSVGElement>(null);
useEffect(() => {
const svg = d3.select(ref.current);
svg.selectAll("*").remove();
const xScale = d3
.scaleBand()
.domain(data.map((d) => d.month))
.range([MARGIN.left, WIDTH - MARGIN.right])
.padding(0.2);
const yScale = d3
.scaleLinear()
.domain([0, 100])
.range([HEIGHT - MARGIN.bottom, MARGIN.top]);
// ツールチップ
const tooltip = d3
.select("body")
.append("div")
.style("position", "absolute")
.style("background", "white")
.style("border", "1px solid gray")
.style("border-radius", "4px")
.style("padding", "4px 8px")
.style("font-size", "12px")
.style("visibility", "hidden")
.style("pointer-events", "none");
svg
.selectAll("rect")
.data(data)
.join("rect")
.attr("x", (d) => xScale(d.month) || 0)
.attr("y", (d) => yScale(d.income))
.attr("width", xScale.bandwidth())
.attr("height", (d) => HEIGHT - MARGIN.bottom - yScale(d.income))
.attr("fill", "steelblue")
.on("mouseover", (event, d) => {
tooltip
.style("visibility", "visible")
.html(`<strong>${d.month}</strong><br/>Income: ${d.income}`);
})
.on("mousemove", (event) => {
tooltip
.style("top", `${event.pageY + 10}px`)
.style("left", `${event.pageX + 10}px`);
})
.on("mouseout", () => {
tooltip.style("visibility", "hidden");
});
// X軸の目盛り
svg
.append("g")
.attr("transform", `translate(0, ${HEIGHT - MARGIN.bottom})`)
.call(d3.axisBottom(xScale));
// Y軸の目盛り
svg
.append("g")
.attr("transform", `translate(${MARGIN.left}, 0)`)
.call(d3.axisLeft(yScale));
}, []);
return <svg ref={ref} width={WIDTH} height={HEIGHT} />;
}
おまけ1
React であれば、JSX にレンダリングする実装方法もあります。
詳しくは以下の記事に記載しています。
おまけ2
MantineCharts という D3.js、Recharts ベースの React 向けチャートライブラリを利用すると、だいぶコード量を減らして実装できます。
import { BarChart } from "@mantine/charts";
import { data } from "./data";
export default function Page() {
return (
<BarChart
w={640}
h={360}
data={data}
dataKey="month"
series={[{ name: "income", color: "blue.6" }]}
/>
);
}