React+D3.jsアプリ作成の基本

　例えばダッシュボードとかのページを作るときに、複数のチャート Componentを組み合わせて、１画面を作る必要があります。このような時はReact+D3.jsの組み合わせが第一候補となるでしょう。しかしReactはVirtual DOMを扱うライブラリであり、D3.jsもDOM（SVG要素）を直接扱うライブラリです。この2つを組み合わせて使うには、バッディングしないように明確な仕切りを行う必要があります。大きな流れとしてはReactで作ったDOM要素を、D3.js関数に渡してチャートを描きます。DOM要素を作り出すまではReactの仕事で、そのDOM要素をもらってチャート描く関数はD3.jsの仕事です。以下が今回作ったサンプルです。
http://react-d3-app.s3-website-ap-northeast-1.amazonaws.com/



　まず以下のコマンドで環境を作ります。ここではReact開発環境構築ツールの定番であるcreate-react-appを使ってプロジェクトディレクトリを作成しています。説明の都合上、d3全体をインストールしていますが、実際はd3-arrayとかd3-selection、d3-axisなどのサブライブラリーを個別にインストールして、importも必要なものだけを選んで行うほうが良いです。特にモバイル環境ではロード時間とかリソースの節約になりますので。

```sh:
create-react-app react-d3
cd react-d3
rm src/*
npm install d3 --save
```


　次にpublic/index.htmlを編集して、チャートに必要なstyleを指定して、Reactのマウントポイント id="root" を定義します。

```html:public/index.html
<!DOCTYPE html>
<html lang="jp">
  <head>
    <meta charset="utf-8">
    <style>
        .line{
            fill: none;    
            stroke: steelblue;
            stroke-width: 2;
        }
    </style>
    <title>React D3.js App</title>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>
```


　次にメイン関数のApp.jsを作成します。dataを設定して子ComponentのLineChartを呼び出します。dataはデータセットの配列で、ひとつのデータセットがひとつのラインチャートに対応します。ここではひとつのデータセットのみを設定していますので、ひとつのラインチャートのみが描かれます。ここまでは通常のReactアプリの作成過程です。

```js:src/App.js
import React, { Component } from 'react'
import LineChart from './LineChart'
const data =[
        [{x: 0, y: 6},{x: 1, y: 9},{x: 2, y: 6},
        {x: 3, y: 5},{x: 4, y: 2},{x: 6, y: 4},
        {x: 7, y: 2},{x: 8, y: 5},{x: 9, y: 2}]
    ];


class App extends Component {
  constructor(props){
    super(props)
  }

  render() {
    return (
      <div>
          <h2>React D3.js line chart</h2>
        <LineChart  data={data} />
      </div>
    )
  }
}

export default App
```



　以下は今回のメインのLineChart.jsです。ここでは少し考慮が必要です。このComponentの中でSVG要素を作り出し、D3.jsの関数にわたし、チャートを描いています。技術的なポイントを以下に行います。


```js:src/LineChart.js
import React, { Component } from 'react'
import * as d3 from 'd3';

class LineChart extends Component {
  constructor(props){
    super(props)
    this.createLineChart = this.createLineChart.bind(this)
  }

  componentDidMount() {
    this.createLineChart()
  }
  componentDidUpdate() {
    this.createLineChart()
  }

  createLineChart() {
    const _self = this;
    const node = this.node
    var width = 500,
        height = 500,
        margin = 50,
        x = d3.scaleLinear()
            .domain([0, 10])
            .range([margin, width - margin]),
        y = d3.scaleLinear()
            .domain([0, 10])
            .range([height - margin, margin]);
        
        d3.range(10).map(function(i){
            return {x: i, y: Math.sin(i) + 5};
        })
    
    var line = d3.line()
            .x(function(d){return x(d.x);})
            .y(function(d){return y(d.y);});
    
    var svg = d3.select(node)
    
    svg.attr("height", height)
        .attr("width", width);
        
     svg.selectAll("path")
            .data(_self.props.data)
        .enter()
            .append("path")
            .attr("class", "line")            
            .attr("d", function(d){return line(d);});
            
    renderAxes(svg);
        
    function renderAxes(svg){
        var xAxis = d3.axisBottom()
                .scale(x.range([0, quadrantWidth()]))
                .scale(x); 

        var yAxis = d3.axisLeft()
                .scale(y.range([quadrantHeight(), 0]))
                .scale(y);

        svg.append("g")        
            .attr("class", "axis")
            .attr("transform", function(){
                return "translate(" + xStart() 
                    + "," + yStart() + ")";
            })
            .call(xAxis);
            
        svg.append("g")        
            .attr("class", "axis")
            .attr("transform", function(){
                return "translate(" + xStart() 
                    + "," + yEnd() + ")";
            })
            .call(yAxis);
    }
        
    function xStart(){ return margin;}        
    function yStart(){ return height - margin;}
    function xEnd(){ return width - margin;}
    function yEnd(){ return margin;}
    function quadrantWidth(){ return width - 2 * margin;}
    function quadrantHeight(){ return height - 2 * margin;} 
  }

  render() {
    return <svg ref={node => this.node = node}>
    </svg>
  }
}

export default LineChart
```


　まず以下のコードですが、refはReactからDOM要素を得るためのものです。JSXが創り出すDOM要素をgetします。
https://reactjs.org/docs/refs-and-the-dom.html

```
<svg ref={node => this.node = node}>
```

　このJSXで、refで参照するDOM要素(SVG要素)をthis.nodeに保存しています。重要なことですが、refはReactが実DOMを作成した後に利用できるようになります。つまりVirtual DOMの間は存在しないものです。ですからrefを利用する関数は、以下のように適切なライフサイクルフックから呼ばれる必要があります。

```
  componentDidMount() {
    this.createLineChart()
  }
  componentDidUpdate() {
    this.createLineChart()
  }
```

　componentDidMount()はComponentの作成後にマウントされた時点で呼ばれます。componentDidUpdate()はComonentデータの変更がDOMに反映された後に呼ばれます（今回のアプリはupdateが起こらないので不要ですが、一般的には必要となります）。

　createLineChart()関数でDOMを参照し、チャートを描いています。 上で述べたように、DOMのSVG要素にはthis.nodeでアクセスできます。Reactはthis.nodeというSVG要素を作り、createLineChart()というD3.jsの関数に渡しています。ここが、ReactとD3.jsの境界線になります。


```
 var svg = d3.select(node)
```


　LineChartのようなComponentを複数作って、App.jsに貼り付ければ、簡単にダッシュボード画面を作り出すことができます。再利用可能なComponentはアプリ全体に見通しのよい構造を与えてくれます。


　蛇足ですが、最後にAWSのS3にdeployします。S3にバケットreact-d3-appを作り、静的ホスティングを有効にし、バケットポリシーにパブリックなReadアクセス権を与えます。aws-cliが設定されていれば、以下のコマンド一発でdeployできます。

```
npm run build && aws s3 sync build/ s3://react-d3-app
```

　AWSについては以下の記事も参考にしてください。aws-cliの設定の仕方も書いてあります。
[AWS+Reactアプリ作成入門(S3編）](https://qiita.com/sand/items/94cd85c3c0426dc768bd "AWS+Reactアプリ作成入門(S3編）")