1
0

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 v7 棒グラフのいろいろ

Last updated at Posted at 2024-03-26

D3 では棒グラフを描くことが多いと思います。今回は都道府県罰人口の棒グラフを描くことで、様々なテクニックを見ていきたいと思います。

データは以下のサイトからCSVをダウンロードして使います。
社会・人口統計体系 - e-Stat(政府統計の総合窓口)

【関連記事】
React 再入門 (React hook API) - Qiita
D3 v7 入門 - Enter / Update / Exit - Qiita
D3 v7 応用 - Enter / Update / Exit - Qiita
D3 v7 グラフ - d3-scale、d3-axis、d3-shape - Qiita
React+D3 アプリ作成入門 - Qiita
D3 v7 棒グラフのいろいろ - Qiita
D3 v7 都道府県別人口の treemap - Qiita

D3.js では様々なグラフや軸を描くことができますが、それなりに細かいテクニックが必要となりますので少し見ていきたいと思います。
Drawing axis in d3.js - 数パタンの軸のサンプル

1.都道府県の人口(X軸に都道府県名)

1-1.人口棒グラフ

都道府県別の人口の棒グラフですが、X軸に都道府県名を縦書きで書くことがポイントの一つです。

image.png

1-2.取得データ(CSV)

社会・人口統計体系 - e-Stat(政府統計の総合窓口) のサイトから以下のようなCSVをダウンロードします。public/test2.csv というファイル名にします。d3 の d3-fetch モジュールを使って CSV ファイルを読み込みます。

public/test2.csv
year,area,item,pop
2022年度,北海道,,"5,140,000"
2022年度,青森県,,"1,204,000"
2022年度,岩手県,,"1,181,000"
2022年度,宮城県,,"2,280,000"
2022年度,秋田県,,"930,000"
2022年度,山形県,,"1,041,000"
2022年度,福島県,,"1,790,000"
2022年度,茨城県,,"2,840,000"
2022年度,栃木県,,"1,909,000"
2022年度,群馬県,,"1,913,000"
2022年度,埼玉県,,"7,337,000"
2022年度,千葉県,,"6,266,000"
2022年度,東京都,,"14,038,000"
2022年度,神奈川県,,"9,232,000"
2022年度,新潟県,,"2,153,000"
---

d3-fetch
d3 の d3-fetch モジュールを使って,以下のように CSV ファイルをパースして読み込むことができます。

const data = await d3.csv("hello-world.csv"); // [{"Hello": "world"}, …]

d3-fetch 公式ドキュメント

1-3. プログラムソース

1-3-1.React App の作成

このグラフ表示のプログラムは、より大規模なアプリの中の1画面であることを想定しています。React プログラムの中で D3.js を使うことを前提にします。

まずは React のプロジェクトを作成します。

create-react-app react-fetch
cd react-fetch
npm start

1-3-2. public/index.html

index.html に x_test という 縦書き のためのスタイルを追加します。

public/index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>React App</title>
    <style>
      .x_text {
        /* 縦書き */
        writing-mode: vertical-rl;
        font-size: 14px;
      }
    </style>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

writing-mode
writing-mode は CSS のプロパティで、テキストの行のレイアウトを 横書き にするか 縦書き にするか、ブロックのフロー方向を左向きにするか右向きにするかを設定します。文書全体に設定する場合は、ルート要素 (HTML 文書の場合は html 要素) に設定してください。

writing-mode: horizontal-tb;
writing-mode: vertical-rl;
writing-mode: vertical-lr;

writing-mode - CSS: カスケーディングスタイルシート | MDN

ちなみに、このスタイルは動的に D3 メッソドで追加することも可能です。

3-1-3.src/App.js

グラフ的には棒グラフとX軸、Y軸を描きます。プログラムの詳細を以下に見ていきたいと思います。スケールとマージンの調整が少し面倒です。

src/App.js
import { useRef, useEffect } from 'react';
import * as d3 from "https://cdn.jsdelivr.net/npm/d3@7/+esm";

export default function App() {
  
  return (
    <div>
      <h2>React D3.js SVG Bar Chart</h2>
      <Chart />
    </div>
  )
}

const Chart = () => {
  const ref = useRef()
  const width = 1500
  const height = 700

  function createChart (data) {
  
    // (1) データの準備
    const data2 = data.map( (d) => { return {...d, val:parseInt(d.pop.replace(/,/g, '')) }} )



    // (2) SVG領域の DOM 取得
    const svg = d3.select(ref.current);


    // (3) 軸スケールの設定
    const   margin = 80,
            x = d3.scaleBand()
                  .padding(0.1)
                  .domain(data2.map( (d) => d.area )) //[筆界同、青森県、岩手県...]
                  .range([margin, width-margin]),     //[margin, width-margin]
            y = d3.scaleLinear()
                  .domain([0, 15000000])              //[0, 15000000]
                  .range([height - margin, margin]);  //[height - margin, margin]
        
    // (4) 軸の表示  
    let xAxis = d3.axisBottom(x.range([0, width-2*margin])); // width - 左右の margin
    let yAxis = d3.axisLeft(y.range([height-2*margin, 0])).tickFormat(d3.format(",.0f"));
                                                             // height - 上下の margin

    svg.append("g")        
          .attr("class", "axis")
          .attr("transform", () => { // X軸原点 (margin, height - margin)
              return "translate(" + margin + "," + (height - margin) + ")";
          })
          .call(xAxis)
            .selectAll("text")
            .attr("transform", "translate(0,15)")
            .attr("class", "x_text")
    

    svg.append("g")        
          .attr("class", "axis")
          .attr("transform", () => { // Y軸原点 (margin, margin)
              return "translate(" + margin + "," + margin + ")";
          })
          .call(yAxis);


    // (5) 棒グラフの表示
    const selection = svg.selectAll('rect').data(data2);
    selection.enter()
            .append('rect')
            .attr("x", (d) => x(d.area) + margin) // 原点X
            .attr("y", (d) => y(d.val) + margin)  // 原点Y
            .attr("width", x.bandwidth())         // 幅は固定
                                                  // 原点Y + 高さ = height - margin
            .attr("height", (d) =>  (height - 2*margin) - y(d.val)  ) 
            .attr("fill", '#6fbadd')
  }

  useEffect(() => {
    const fetchData = async () => {
      const data = await d3.csv("test2.csv");
      createChart(data);
    }
    fetchData();
  }, [])

  return (
    <svg width={width} height={height}
      ref={ref}
    />
  )
}

(1) データの準備

render 終了後に 実 DOM が作成され、useEffect が実行されます。タイミング的に、ここで副作用を伴う data fetch と、 DOM 取得と操作が行われます。

 useEffect(() => {
    const fetchData = async () => {
      const data = await d3.csv("test2.csv");
      createChart(data);
    }
    fetchData();
  }, [])

fetchData 関数の定義で async を先頭につけていることに注意してください。これは d3.csv() の呼び出しで await しているので必要になります。fetch したデータは createChart() に渡し、そこで D3 のDOM取得、操作を行いグラフを描きます。

createChart() に渡されたデータは、扱いやすいように加工されます。人口データ pop を文字列から整数変換し、それを値とする val 属性を追加します。整数変換のために JavaScript 関数の replace や parseInt を使っていて見辛いですが、内容は簡単です。

// (1) データの準備
const data2 = data.map( (d) => { return {...d, val:parseInt(d.pop.replace(/,/g, '')) }} )

(2) SVG領域のDOM取得

React で DOM ノードを参照するための方法を示しておきます。

DOM ノードへの ref の取得
React が管理する DOM ノードにアクセスするには、まず useRef フックをインポートします。

import { useRef } from 'react';

次に、それを使ってコンポーネント内で ref を宣言します。

const myRef = useRef(null);

最後に、参照を得たい DOM ノードに対応する JSX タグの ref 属性にこの ref を渡します。

<div ref={myRef}>

useRef フックは、current という単一のプロパティを持つオブジェクトを返します。最初は myRef.current は null になっています。React がこの <div> に対応する DOM ノードを作成すると、React はこのノードへの参照を myRef.current に入れます。その後、イベントハンドラからこの DOM ノードにアクセスし、ノードに定義されている組み込みのブラウザ API を使用できるようになります。

// You can use any browser APIs, for example:
myRef.current.scrollIntoView();

「DOM ノードへの ref の取得」に従って今回のソースコードを確認していきます。

Chart コンポーネント関数の先頭で useRef() で、 DOM ノード参照のための ref 変数を宣言します。

  const ref = useRef()

Chart コンポーネントの return JSX の svg マークアップのref 属性に ref変数をセットします。

  return (
    <svg width={width} height={height}
      ref={ref}
    />
  )

svg マークアップのDOM要素の selection オブジェクトを取得します。

    // (2) SVG領域の DOM 取得
    const svg = d3.select(ref.current);

(3) 軸スケールの設定

スケールとは

  • Scale は抽象的データをビジュアル表現にマップするものです。ほとんどの場合、データを平面に描くために使われます。
  • 例えば scale は、ビジュアルエンコーディング(カラー、線幅、またはシンボル文字など)表現を用い、時間データを水平座標に、気温データを垂直座標にマップし、散布図を描きます。
  • またカテゴリデータや適切な距離を必要とする離散データを表現するためにも使われます。
X 軸の説明です
            x = d3.scaleBand()
                  .padding(0.1)
                  .domain(data2.map( (d) => d.area )) //[筆界同、青森県、岩手県...]
                  .range([margin, width-margin]),     //[margin, width-margin]

Scale x は、 domain 集合 [北海道、青森県、岩手県...] を range 区間 [margin, width-margin] にマップし配置します。

Band scales

  • Band scales は ordinal(序数) scales に似てますが、マップされる range が連続で数値である点が異なります。
  • Band scales は連続的な範囲を、均等なバンド(幅)に分割します。
  • Band scales は主に、序数またはカテゴリカルな次元を持つ棒グラフに適しています。

scaleBand(domain, range)
指定された domain と range を持つ、新しい band scale を構築します。padding無し、rounding無し、center alignment です。

const x = d3.scaleBand(["a", "b", "c"], [0, 960]);

band.padding(padding)
inner と outer padding を設定する便利なメソッド。inner と outer は同じ padding の値になります。

const x = d3.scaleBand(["a", "b", "c"], [0, 960]).padding(0.1);
Y 軸の説明です
            y = d3.scaleLinear()
                  .domain([0, 15000000])              //[0, 15000000]
                  .range([height - margin, margin]);  //[height - margin, margin]

Scale y は、 domain 区間 [0, 15000000] を range 区間 [height - margin, margin] にマップし配置します。このマップは比例ではなく、反比例であることに注意してください。

Linear scales

  • Linear scales は連続した定量的な定義域(domain) を、連続的な値域(range) へ、線形変換を使ってマップするものです。
  • つまり y = f(x) で言えば、x は定量的で連続な値、y も連続した値。
  • Linear scales は連続した定量的なデータに対しての、デフォルトの選択です。何故なら比例差を保持するからです。y = ax + b ですから、x が増加したら比例して y も増加します。もちろん a < 0 なら反比例。

scaleLinear(domain, range)
引数の domain と range で構成される linear scale を作成します。
(デフォルトの interpolator と clamping は無効。)
引数が1つの場合は range が指定されたと解釈します。domain または range が省略されたときは、デフォルトの [0, 1] が指定されたと解釈します。

linear.domain(domain)
Linear scales の domain を引数で指定されたものに設定します。引数の domain 配列は 2 つ以上の要素を持ちます。この要素が数値でない場合は数値変換されます。 新 domain 設定後の scale が返されます。

linear.range(range)
Linear scales の range を引数で指定されたものに設定します。引数の range 配列は2つ以上の要素を持ちます。domain と違って range の要素は数値以外でも、補正がサポートされている範囲で、任意の値で構いません。新 range 設定後の scale が返されます。

(4) 軸の表示

    // (4) 軸の表示  
    let xAxis = d3.axisBottom(x.range([0, width-2*margin])); // width - 左右の margin
    let yAxis = d3.axisLeft(y.range([height-2*margin, 0])).tickFormat(d3.format(",.0f"));
                                                             // height - 上下の margin

    svg.append("g")        
          .attr("class", "axis")
          .attr("transform", () => {
              return "translate(" + margin 
                  + "," + (height - margin) + ")";
          })
          .call(xAxis)
            .selectAll("text")
            .attr("transform", "translate(0,15)")
            .attr("class", "x_text")
    

    svg.append("g")        
          .attr("class", "axis")
          .attr("transform", () => {
              return "translate(" + margin 
                  + "," + margin + ")";
          })
          .call(yAxis);

axis component は scale の位置の目安となる見易いマークを描いてくれます。それは、linear, log, band, や time など、ほとんどの scale タイプで使えます。

X 軸の説明です

以下のような、X 軸の表示を目指します。

image.png

scale x の range 変更して、以下のように xAxis を定義します。

    let xAxis = d3.axisBottom(x.range([0, width-2*margin])); // width - 左右の margin

X 軸は、左右に margin が2つあるので、X 軸 0 から width-2*margin までの X 軸の数直線にマップされます。軸は g タグの原点から相対的な位置に表示されます。

軸の原点は translate(80, 620)
g 要素は SVG 画面の (80, 620) を原点とします。X 軸は g 要素の中に描かれますから、相対的な数値としては、[0, width-2*margin] の区間となります。

image.png

axis component が、SVG の g 要素 の selection オブジェクト上で call されることで、axis(軸)が描かれます。axis は原点に描かれますが、その位置を変更したいときは、selection オブジェクトの transform 属性を指定します。

    // X 軸の表示
    svg.append("g")        
          .attr("class", "axis")
          .attr("transform", () => {
              return "translate(" + margin 
                  + "," + (height - margin) + ")";
          })
          .call(xAxis)
            .selectAll("text") // text タグを選択
            .attr("transform", "translate(0,15)")  // ラベルと軸の距離を調整
            .attr("class", "x_text") // ラベルの縦書き、フォントサイズ
    

ラベルは text タグとして描かれるので、適切な属性を指定します。ここでは特にスタイル x_text を追加して都道府県名を縦書きにしています。

          .call(xAxis)
            .selectAll("text") // text タグを選択
            .attr("transform", "translate(0,15)")  // ラベルと軸の距離を調整
            .attr("class", "x_text") // ラベルの縦書き、フォントサイズ

ここでの text タグの選択は、 X 軸の g タグ配下の text タグに限定されることに注意してください。この text タグに対して、transform 属性でラベルと軸の距離を調整し、スタイル x_text 追加しています。

HTML ソースとしては以下のようになります。textタグの行に注目してください。

image.png

Y 軸の説明です

scale y の range 変更して、以下のように yAxis を定義します。

    let yAxis = d3.axisLeft(y.range([height-2*margin, 0])).tickFormat(d3.format(",.0f"));
                                                             // height - 上下の margin

Y 軸は height-2*margin から 0 までの数直線上にマップされます。軸は g タグの原点から相対的な位置に表示されます。

    svg.append("g")        
          .attr("class", "axis")
          .attr("transform", () => {
              return "translate(" + margin 
                  + "," + margin + ")";
          })
          .call(yAxis);

(5) 棒グラフの表示

    // (5) 棒グラフの表示
    const selection = svg.selectAll('rect').data(data2);
    selection.enter()
            .append('rect')
            .attr("x", (d) => x(d.area) + margin) // 原点X
            .attr("y", (d) => y(d.val) + margin)  // 原点Y
            .attr("width", x.bandwidth())         // 幅は固定
                                                  // 原点Y + 高さ = height - margin
            .attr("height", (d) =>  (height - 2*margin) - y(d.val)  ) 
            .attr("fill", '#6fbadd')

まず最初の行に注目します。

    const selection = svg.selectAll('rect').data(data2);

rectはまだないのでデータは全て _enter に入っているのが確認できます。

image.png
image.png

次に以下の手順で棒グラフを描いています。

  • enter() メソッドで _enter を _groups に移動(再マウント時は_enter は空で最初から_groupsが存在)
  • append('rect') で _groups の要素に子要素 rect を追加
  • rect の属性を attr() メソッドで設定
    selection.enter()
            .append('rect')
            .attr("x", (d) => x(d.area) + margin) // 原点X
            .attr("y", (d) => y(d.val) + margin)  // 原点Y
            .attr("width", x.bandwidth())         // 幅は固定
                                                  // 原点Y + 高さ = height - margin
            .attr("height", (d) =>  (height - 2*margin) - y(d.val)  ) 
            .attr("fill", '#6fbadd')

rect タグ
SVGで四角を描画するにはrectタグを使用します。
rectの開始位置、原点は左上です。

rectタグ属性 説明
x 開始x座標
y 開始y座標
width 横幅
height 縦幅
fill 長方形の中の色
stroke-width 線の太さ
storke 線の色

rect の Y 方向の頂点ですが、 y(d.val) + margin が開始位置で、高さが (height - 2*margin) - y(d.val) ですので、足し算で height - margin となります。これはちょうど X 軸の g タグの高さの位置になることに注目してください。

再マウント時
React での開発時はデフォルトで必ず再マウントが行われます。
再マウント時は、svg.selectAll('rect') が空ではなくデータが全て rect にバインドされます。つまり selection オブジェクトの _enter は空で、最初から _groups に要素が入っています。結論として再マウント時は、selection.enter() が空なのでそれ以降の操作は生じませんが、最初からある DOM 要素によって棒グラフが描画されます。

2.都道府県の人口(ソート)

人口が多い順に棒グラフを描くようにソースを修正することは簡単です

image.png

ソースの修正は (1) データの準備 のところにソート分を一行追加するだけです。

src/App.js
// (1) データの準備
const data2 = data.map( (d) => { return {...d, val:parseInt(d.pop.replace(/,/g, '')) }} )
data2.sort( (a,b) => b.val - a.val )  // <-- 追加

JavaScript関数 sort は配列を破壊的にソートしてくれます。

3.都道府県の人口(Y軸に都道府県名)

上ではX軸に都道府県名を取りましたが、これをY軸に変更しようと思います。このようにすると棒グラフの先頭にスペースができるので、人口の数値を描くことが可能になります。

3-1.人口棒グラフ

image.png

3-2. プログラムソース

3-2-1.React App の作成

このグラフ表示のプログラムは、より大規模なアプリの中の1画面であることを想定しています。React プログラムの中で D3.js を使うことを前提にします。

src/App.js
import { useRef, useEffect } from 'react';
import * as d3 from "https://cdn.jsdelivr.net/npm/d3@7/+esm";

export default function App() {
  
  return (
    <div>
      <h2>React D3.js SVG Bar Chart</h2>
      <Chart />
    </div>
  )
}

const Chart = () => {
  const ref = useRef()
  const width = 1300
  const height = 900

  function createChart (data) {
  
    // (1) データの準備
    const data2 = data.map( (d) => { return {...d, val:parseInt(d.pop.replace(/,/g, '')) }} )



    // (2) SVG領域の DOM 取得
    const svg = d3.select(ref.current);


    // (3) 軸スケールの設定
   const    margin = 80,
            x = d3.scaleLinear()
                  .domain([0, 15000000])
                  .range([margin, width-margin]),
            y = d3.scaleBand()
                  .padding(0.3)
                  .domain(data2.map( (d) => d.area ))
                  .range([height - margin, margin]); // 逆順に上からに、他移動、青森,岩手...
        
    // (4) 軸の表示  
    let xAxis = d3.axisBottom(x.range([0, width-2*margin])).tickFormat(d3.format(",.0f"));
                                                             // width - 左右の margin
    let yAxis = d3.axisLeft(y.range([0, height-2*margin]));  // height - 上下の margin
  
    svg.append("g")        
          .attr("class", "axis")
          .attr("transform", () => {
              return "translate(" + margin + "," + (height - margin) + ")";
          })
          .call(xAxis)
            .selectAll("text")
            .style("font-size", 14)  

    svg.append("g")        
          .attr("class", "axis")
          .attr("transform", () => {
              return "translate(" + margin + "," + margin + ")";
          })
          .call(yAxis)
            .selectAll("text")
            .style("font-size", 12)



    // (5) 棒グラフの表示
    const selection = svg.selectAll('rect').data(data2);
    selection.enter()
            .append('rect')
            .attr("x", (d) => margin)
            .attr("y", (d) => y(d.area) + margin)
            .attr("width", (d) =>  x(d.val) ) //- margin
            .attr("height", y.bandwidth())
            .attr("fill", '#6fbadd')

    // (6) 棒グラフのの数値を表示
    const selection2 = svg.selectAll('.label').data(data2);
    selection2.enter()
              .append('text')
              .attr("class", "label")
              .attr("x", (d) => 10 + x(d.val) + margin)
              .attr("y", (d) => 10 + y(d.area) + margin)
              .attr("font-size", "10px")
              .text((d) => d.pop)        
  }

  useEffect(() => {
    const fetchData = async () => {
      const data = await d3.csv("test2.csv");
      createChart(data);
    }
    fetchData();
  }, [])

  return (
    <svg width={width} height={height}
      ref={ref}
    />
  )
}

基本的には X 軸と Y 軸の定義を交換します。

また今回は X 軸のテキストラベルのスタイルを、以下のように直接変更しています。フォントサイズを 14 に変更しています。

    svg.append("g")        
          .attr("class", "axis")
          .attr("transform", () => {
              return "translate(" + margin + "," + (height - margin) + ")";
          })
          .call(xAxis)
            .selectAll("text")
            .style("font-size", 14)  

棒グラフの数値を表示するために、棒グラフ自信を描くコードと同じようなコードを書きます。selection は text.label というタグを選びます。 これは単にtextというタグを選ぶようにすると、軸ラベルのtextも選ばれてしまい、望む動作にならないからです。識別の意味でクラス .label を加えます。数値は rect の近く、重ならないように x, y を決めて書きます。

    // (6) 棒グラフのの数値を表示
    const selection2 = svg.selectAll('.label').data(data2);
    selection2.enter()
              .append('text')
              .attr("class", "label")
              .attr("x", (d) => 10 + x(d.val) + margin)
              .attr("y", (d) => 10 + y(d.area) + margin)
              .attr("font-size", "10px")
              .text((d) => d.pop)        

e-Stat(政府統計の総合窓口)にはいろいろなデータがあるので、それらを使って D3 の表現力を確認していきたいと思います。

今回は以上です。

1
0
0

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
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?