はじめに
この記事では、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
チャートに表示するデータ
これから作成するバブルチャートでは、X軸に element
、Y軸に score
を表示します。
バブルの大きさは count
で表現します。
export const data = [
{ element: "A", score: 1, count: 2 },
{ element: "A", score: 2, count: 2 },
{ element: "A", score: 3, count: 4 },
{ element: "B", score: 1, count: 0 },
{ element: "B", score: 2, count: 5 },
{ element: "B", score: 3, count: 3 },
{ element: "C", score: 1, count: 7 },
{ element: "C", score: 2, count: 7 },
{ element: "C", score: 3, count: 1 },
];
SVG のレンダリング
まずはチャートを表示するための SVG をレンダリングします。
今回は実装したチャートをこちらの SVG に ref
を通して渡します。そのため、ref
を SVG の props
に追加します。
また、チャート全体の大きさを指定するため、width
と height
も追加します。
"use client";
import { useRef } from "react";
export default function Page() {
const svgRef = useRef<SVGSVGElement | null>(null);
const width = 600;
const height = 400;
return <svg ref={svgRef} width={width} height={height} />;
}
この時点では、画面上には 600 × 400 の SVG タグが表示されます。
チャートの実装
バブルチャートを作成する処理を実装していきます。
チャート作成処理は、renderChart
という関数を作成し、その内部に実装していきます。renderChart
を useEffect
内で初回レンダリング時に実行し、画面上にバブルチャートを描画します。
"use client";
import { useEffect, useRef } from "react";
export default function Page() {
const svgRef = useRef<SVGSVGElement | null>(null);
const width = 600;
const height = 400;
const renderChart = () => {};
useEffect(() => {
renderChart();
}, []);
return <svg ref={svgRef} width={width} height={height} />;
}
データの出力範囲の定義
それぞれ後述するAPIを利用して、X軸(element
)方向・Y軸(score
)方向・バブル(count
)の大きさを定義します。
X軸方向
-
scaleBand:
domain
、range
をもとにX軸方向の表示範囲を定義 -
band.domain: X軸に表示する
element
の値を(入力値) - band.range: 入力値を画面上に表示する範囲(出力値)
const xScale = d3
.scaleBand()
.domain(data.map((d) => d.element))
.range([0, width]);
Y軸方向
const yScale = d3
.scaleLinear()
.domain([0, d3.max(data, (d) => d.score) || 0])
.range([height, 0]);
バブルの大きさ
const sizeScale = d3
.scaleSqrt()
.domain([0, d3.max(data, (d) => d.count) || 0])
.range([0, 30]);
D3 による SVG の操作を有効化
以下のAPIを利用して先ほどレンダリングした SVG を D3 で操作できるようにします。
const svg = d3.select(svgRef.current);
SVG を初期化する
この後、実際に SVG を操作して、チャートを実装していきますが、その前に一度 SVG を初期化します。
svg.selectAll("*").remove();
今回のユースケースでは、表示するデータは常に固定で初回レンダリング時のみチャートの描画処理(renderChart
)が実行されます。そのため、一見初期化処理は不要に思えます。ただ、今回の開発環境では、React の Strict Mode が有効になっているため、ローカル開発環境だと描画処理が2回実行されるため、初期化処理を追加しています。
初期化処理あり
データの配列の要素数 (9) 分の circle
タグが表示される
初期化処理なし
データの配列の要素数 (9) × レンダリング回数 (2) 分の circle
タグが表示される
ここまではある意味実際にチャートを作成するための準備処理です。ここから実際にチャートを作成する処理を実装していきます。
データを SVG に渡す
以下のAPIを利用してチャートに表示するデータを SVG に渡します。
const bubbles = svg.selectAll(".bubble").data(data);
チャートの描画
以下のAPIを利用して、バブルの形・色・大きさ・表示位置などを指定していきます。
circle
タグ
バブルを表示するための circle
タグを追加します。
bubbles.enter().append("circle");
データの配列の要素数分の circle
タグがレンダリングされます。
クラス
bubbles.enter().append("circle").attr("class", "bubble");
circle
タグに bubble クラスが追加されます。
色
bubbles
.enter()
.append("circle")
.attr("class", "bubble")
.attr("fill", "teal");
circle
タグに fill="teal"
が追加されます。
X軸方向の表示位置
bubbles
.enter()
.append("circle")
.attr("class", "bubble")
.attr("fill", "teal")
.attr("cx", (d) => xScale(d.element)! + xScale.bandwidth() / 2);
circle
タグごとにX軸方向の表示位置が変わります。
Y軸方向の表示位置
bubbles
.enter()
.append("circle")
.attr("class", "bubble")
.attr("fill", "teal")
.attr("cx", (d) => xScale(d.element)! + xScale.bandwidth() / 2)
.attr("cy", (d) => yScale(d.score));
circle
タグごとにX軸・Y軸方向の表示位置が変わります。
大きさ
bubbles
.enter()
.append("circle")
.attr("class", "bubble")
.attr("fill", "teal")
.attr("cx", (d) => xScale(d.element)! + xScale.bandwidth() / 2)
.attr("cy", (d) => yScale(d.score))
.attr("r", (d) => sizeScale(d.count));
circle
タグごとにX軸・Y軸方向の表示位置、バブルの大きさが変わります。
これで完成です!
完成形
完成したコードは以下の通りです。
"use client";
import { useEffect, useRef } from "react";
import * as d3 from "d3";
import { data } from "../data";
export default function Page() {
const svgRef = useRef<SVGSVGElement | null>(null);
const width = 600;
const height = 400;
const renderChart = () => {
// Define scales
const xScale = d3
.scaleBand()
.domain(data.map((d) => d.element))
.range([0, width]);
const yScale = d3
.scaleLinear()
.domain([0, d3.max(data, (d) => d.score) || 0])
.range([height, 0]);
const sizeScale = d3
.scaleSqrt()
.domain([0, d3.max(data, (d) => d.count) || 0])
.range([0, 30]);
// Enable D3 API manipulate svg
const svg = d3.select(svgRef.current);
// Clear everything before appending new elements
svg.selectAll("*").remove();
// Bind data to bubbles
const bubbles = svg.selectAll(".bubble").data(data);
// Enter
bubbles
.enter()
.append("circle")
.attr("class", "bubble")
.attr("fill", "teal")
.attr("cx", (d) => xScale(d.element)! + xScale.bandwidth() / 2)
.attr("cy", (d) => yScale(d.score))
.attr("r", (d) => sizeScale(d.count));
};
useEffect(() => {
renderChart();
}, []);
return <svg ref={svgRef} width={width} height={height} />;
}