はじめに
この記事では、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} />;
}








