1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【D3】バブルチャートを実装する

Last updated at Posted at 2024-11-23

はじめに

この記事では、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 で表現します。

data.ts
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 に追加します。
また、チャート全体の大きさを指定するため、widthheight も追加します。

page.tsx
"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 タグが表示されます。

image.png

チャートの実装

バブルチャートを作成する処理を実装していきます。
チャート作成処理は、renderChart という関数を作成し、その内部に実装していきます。renderChartuseEffect 内で初回レンダリング時に実行し、画面上にバブルチャートを描画します。

page.tsx
"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: domainrange をもとにX軸方向の表示範囲を定義
  • band.domain: X軸に表示する element の値を(入力値)
  • band.range: 入力値を画面上に表示する範囲(出力値)
page.tsx
const xScale = d3
 .scaleBand()
 .domain(data.map((d) => d.element))
 .range([0, width]);

Y軸方向

page.tsx
const yScale = d3
  .scaleLinear()
  .domain([0, d3.max(data, (d) => d.score) || 0])
  .range([height, 0]);

バブルの大きさ

page.tsx
const sizeScale = d3
  .scaleSqrt()
  .domain([0, d3.max(data, (d) => d.count) || 0])
  .range([0, 30]);

D3 による SVG の操作を有効化

以下のAPIを利用して先ほどレンダリングした SVG を D3 で操作できるようにします。

page.tsx
const svg = d3.select(svgRef.current);

SVG を初期化する

この後、実際に SVG を操作して、チャートを実装していきますが、その前に一度 SVG を初期化します。

page.tsx
svg.selectAll("*").remove();

今回のユースケースでは、表示するデータは常に固定で初回レンダリング時のみチャートの描画処理(renderChart)が実行されます。そのため、一見初期化処理は不要に思えます。ただ、今回の開発環境では、React の Strict Mode が有効になっているため、ローカル開発環境だと描画処理が2回実行されるため、初期化処理を追加しています。

初期化処理あり

データの配列の要素数 (9) 分の circle タグが表示される

image.png

初期化処理なし

データの配列の要素数 (9) × レンダリング回数 (2) 分の circle タグが表示される

image.png

ここまではある意味実際にチャートを作成するための準備処理です。ここから実際にチャートを作成する処理を実装していきます。

データを SVG に渡す

以下のAPIを利用してチャートに表示するデータを SVG に渡します。

page.tsx
const bubbles = svg.selectAll(".bubble").data(data);

チャートの描画

以下のAPIを利用して、バブルの形・色・大きさ・表示位置などを指定していきます。

circle タグ

バブルを表示するための circle タグを追加します。

page.tsx
bubbles.enter().append("circle");

データの配列の要素数分の circle タグがレンダリングされます。

image.png

クラス

page.tsx
bubbles.enter().append("circle").attr("class", "bubble");

circle タグに bubble クラスが追加されます。

image.png

page.tsx
bubbles
  .enter()
  .append("circle")
  .attr("class", "bubble")
  .attr("fill", "teal");

circle タグに fill="teal" が追加されます。

image.png

X軸方向の表示位置

page.tsx
bubbles
  .enter()
  .append("circle")
  .attr("class", "bubble")
  .attr("fill", "teal")
  .attr("cx", (d) => xScale(d.element)! + xScale.bandwidth() / 2);

circle タグごとにX軸方向の表示位置が変わります。

localhost_3000_charts_d3_buble-charts_render-function-Google-Chrome-2024-11-23-10-38-57.gif

Y軸方向の表示位置

page.tsx
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軸方向の表示位置が変わります。

localhost_3000_charts_d3_buble-charts_render-function-Google-Chrome-2024-11-23-10-42-00.gif

大きさ

page.tsx
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軸方向の表示位置、バブルの大きさが変わります。

image.png

これで完成です!

完成形

完成したコードは以下の通りです。

page.tsx
"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} />;
}

参考

1
2
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?