【D3】棒グラフを実装する

# はじめに
この記事では、JavaScript 向けのデータ可視化ライブラリである D3 を使って、棒グラフを実装する手順を記載します。

https://d3js.org/

# 開発環境
開発環境は以下の通りです。
- Windows 11
- Next.js 14.2.4
- React 18.3.1
- TypeScript 5.5.2
- D3 7.9.0
- @types/d3 7.4.3

# 実装手順
以下の流れで実装を進めています。

1. 棒グラフに表示するデータの定義
1. 棒グラフを表示する SVG の描画
1. データの表示範囲（座標）の定義
1. SVG 操作の有効化・初期化
1. 棒グラフの表示
1. その他関連要素の表示

# 1. 棒グラフに表示するデータの定義
X軸に `month`、Y軸に `income` を表示します。

```data.ts
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 を描画します。

```page.tsx
const WIDTH = 640;
const HEIGHT = 360;

export default function Page() {
  return <svg width={WIDTH} height={HEIGHT} />;
}
```

現時点では、横 640px、縦 360 px の SVG が表示されるだけです。
今後、この SVG 内に棒グラフを実装していきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/9d3acad8-940b-becd-42f2-b3c8be57c00c.png)

# 3. データの表示範囲（座標）の定義
1.で用意したデータの表示範囲（座標）定義します。

## X軸
D3 の以下のAPIを利用して、X軸の表示範囲を定義します。

- [scaleBand](https://d3js.org/d3-scale/band#scaleBand): `domain`、`range` をもとにX軸方向の表示範囲を定義
- [band.domain](https://d3js.org/d3-scale/band#band_domain): X軸に表示する `month` の値の範囲（入力値）
- [band.range](https://d3js.org/d3-scale/band#band_range): 入力値を画面上に表示する範囲（出力値）
- [band.padding](https://d3js.org/d3-scale/band#band_padding): 各棒グラフ間の余白

```page.tsx
const xScale = d3
  .scaleBand()
  .domain(data.map((d) => d.month))
  .range([0, WIDTH]);
```

## Y軸
Y軸も D3 の API を利用して、表示範囲を定義します。
X軸の入力値が文字（離散値、カテゴリ）であるのに対し、Y軸の入力値が数値（連続値）なので、`scaleBand` の代わりに `scaleLinear` という API を利用します。

- [scaleLinear](https://d3js.org/d3-scale/linear#scaleLinear): `domain`、`range` をもとにY軸方向の表示範囲を定義
- [scaleLinear.domain](https://d3js.org/d3-scale/linear#linear_domain): Y軸に表示する `income` の値の範囲（入力値）
- [scaleLinear.range](https://d3js.org/d3-scale/linear#linear_range): 入力値を画面上に表示する範囲（出力値）

```page.tsx
const yScale = d3
  .scaleLinear()
  .domain([0, 100])
  .range([0, HEIGHT]);
```

# 4. SVG 操作の有効化・初期化
2. で描画した SVG を操作して棒グラフを表示できるようにするため、SVG 操作の有効化・初期化します。

- [select](https://d3js.org/d3-selection/selecting#select): D3 による SVG 操作の有効化
- [selection.remove](https://d3js.org/d3-selection/modifying#selection_remove): SVG の初期化（過去の値の削除）

```page.tsx
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 内に棒グラフを表示します。

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

```

棒グラフが表示されます。
また、開発者ツールで実装した要素やプロパティが追加されていることを確認できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/8d733cfc-4967-3f90-bb70-374cca892cc6.png)

# 6. その他関連要素の表示
X軸 / Y軸の目盛りやツールチップを表示することも可能です。

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

```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/ecbcafbd-7552-a15d-603e-c446772981e1.png)

# おまけ1
React であれば、JSX にレンダリングする実装方法もあります。
詳しくは以下の記事に記載しています。

https://qiita.com/Yasushi-Mo/items/6c307652674ae2853b58

# おまけ2
MantineCharts という D3.js、Recharts ベースの React 向けチャートライブラリを利用すると、だいぶコード量を減らして実装できます。

https://mantine.dev/charts/bar-chart/

```paget.tsx
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" }]}
    />
  );
}

```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/48a30de5-81cd-f17c-69a4-ff7cd3e1b60c.png)


# 参照
- [BarChart](https://mantine.dev/charts/bar-chart/)
- [Most basic barplot in d3.js](https://d3-graph-gallery.com/graph/barplot_basic.html)