JavaScriptでグラフ作ろう勉強会資料v0.01


# 勉強会情報

- 2015/12/04(金) 19:30  〜  20:30
- http://fukuoka-cam-study.connpass.com/event/23438/
- JavaScriptでグラフ作ろう勉強会 
- コードを書いてD3.jsでのグラフ作成を学ぶ勉強会

# 内容

## 勉強会の環境

- ブラウザはChromeで説明します

- Runstantを使ってブラウザのみで学習する
http://goo.gl/s2tfqa

- ライブラリをダウンロードしてローカル環境で試す  
https://d3js.org/  

例)d3.zipを解凍したフォルダにhtmlとjsファイルを置く

```html:index.html
<!DOCTYPE html>
<html lang="jp">

<head>
  <meta charset="utf-8">
  <title>D3 Test</title>
  <script type="text/javascript" src="d3.js"></script>
  <script type="text/javascript" src="index.js"></script>
  <style>

  </style>
</head>

<body>
  <h1>Hello, D3</h1>
  <div class="graph"></div>
</body>

</html>
```

```javascript:index.js
window.onload = function() {
  updateGraph();
};

function updateGraph () {
  // CSSセレクタで要素を選択します
  // 要素は配列[]に格納されます
  // 配列[]にD3.jsのメソッドをつけて様々な動きが出来るようになります。
  var graphPane = d3.select(".graph");

  console.log(graphPane);
}
```


## CSSセレクタで要素を取得

source: http://goo.gl/L7fOW7


```javascript
function updateGraph () {
  var graphPane = d3.select(".graph");
}
```
- CSSセレクタで要素を選択します
- 要素は配列[]に格納されます
- 配列[]にD3.jsのメソッドをつけて様々な動きが出来るようになります。

> mbostock/d3 Selections
> https://github.com/mbostock/d3/wiki/Selections

*☆課題☆コンソールにログを出してみましょう*

```javascript
console.log(graphPane);
```

result: http://goo.gl/45rY1X


## グラフデータと要素を紐づけ

source: http://goo.gl/PH55Ux

- div要素は存在しないので空配列[]を作成し空配列[]にD3.jsのメソッドを付ける
- データと紐づくと **enter** と **exit** という状態が増える

```javascript
window.onload = function() {
  updateGraph([200, 150, 300]);
};

function updateGraph (dataList) {
  var graphPane = d3.select(".graph");
  var divBindList = graphPane.selectAll("div").data(dataList);
}
```

☆課題☆ .data(dataList) を追加してください

result: http://goo.gl/lFzkEJ

## Enter(データが要素より多い)、Exit(データが要素より少ない)

source: http://goo.gl/hu6AJx

```javascript

function updateGraph (dataList) {
  var graphPane = d3.select(".graph");
  var divBindList = graphPane.selectAll("div").data(dataList);
  
  // enterは表示要素よりもデータが多いとデータが設定される
  var enter = divBindList.enter();
  console.log("enter=", enter);
  
  // exitは表示要素よりデータが少ないとデータが設定される
  var exit = divBindList.exit();
  console.log("exit=", exit);
}
```

※初回動作時はグラフ要素は一つもないので全てenter側に登録されます

☆課題☆console.logを追加して enterとexitの中をみてみましょう

result: http://goo.gl/1zBeY4

> d3.js - 三つの小円 [要素の生成]
> http://ja.d3js.node.ws/document/tutorial/circle.html

## Enter状態のデータを追加する

source: http://goo.gl/duCqyb

```javascript
function updateGraph (dataList) {
  var graphPane = d3.select(".graph");
  var divBindList = graphPane.selectAll("div").data(dataList);
  
  // データに対応する要素が足りないのでdiv追加
  divBindList
    .enter()
    .append("div")
    .classed("bar", true);

}
```

- append で要素の追加
- classed で追加した要素の CSSのクラス名を付与

```css
.bar { border: solid 1px;  width: 10px;  height: 10px; }
```

☆課題☆ .append("div").classed("bar", true); を追加しましょう

result: http://goo.gl/FPv6GT


## Exit状態を作ってみる

source: http://goo.gl/gu8dzS

```javascript
window.onload = function() {
  updateGraph([200, 150, 300]);
  
  setTimeout(function() {
    updateGraph([200, 150]);
  }, 2000);
};

function updateGraph (dataList) {
  var graphPane = d3.select(".graph");
  var divBindList = graphPane.selectAll("div").data(dataList);
  
  // データに対する要素が足りない時～
  divBindList.enter()
    .append("div")
    .classed("bar", true);

  // 要素に対応するデータが足りない時～
  divBindList
    .exit()
    .classed("red", true);
}

```

- setTimeoutで 2秒後に一つすくないデータを渡す
- Exit状態の要素に classed で CSSクラス名を付与する

```css
.red { background-color: red; }
```

☆課題☆exit状態のdivに.redクラスをつけて赤くしましょう

source: http://goo.gl/BhZJAh



## データと要素が一致する状態

source: http://goo.gl/1L3Zi4

```javascript
function updateGraph (dataList) {
  var graphPane = d3.select(".graph");
  var divBindList = graphPane.selectAll("div").data(dataList);
  
  // データに対する要素が足りない時～
  divBindList
    .enter()
    .append("div")
    .classed("bar", true);

  // 要素に対応するデータが足りない時～
  divBindList
    .exit()
    .remove();
    
  // 要素とデータが一致するとき～
  divBindList
    .style("width", function(d, i){
      console.log(d, i);
      return d + "px";
    });
}
```

- remove()は削除 (補足)
- style()で CSSのwidthを **関数で** 設定しています
- 引数d は配列の値、iは配列の添え字

☆課題☆ return d+ "px"; に修正しましょう

result: http://goo.gl/1hZFgK


## SVGで描く

source: http://goo.gl/fVBq9S

```html
    <svg class="graph" ></svg>
```

```javascript
function updateGraph (dataList) {
  var graphPane = d3.select(".graph");
  var barBindList = graphPane.selectAll("rect").data(dataList);
  
  // データに対する要素が足りない時～
  barBindList
    .enter()
    .append("rect")
    .classed("bar", true);

  // 要素に対応するデータが足りない時～
  barBindList
    .exit()
    .remove();
    
  // 要素とデータが一致するとき～
  barBindList
    .attr({
      "width": function(d, i){ return d; },
      "height": 8,
      "x": 0,
      "y": function(d, i){ return i*10; }
    });
}
```

- .graphのタグを svgに変更
- div要素をrect要素に変更
- styleで幅指定していたものを attr指定に変更

☆課題☆"y"のy座標計算を i*10 に修正しましょう

result: http://goo.gl/08WtTZ

## 息抜き（アニメーション)

source: http://goo.gl/BpIAs3

```javascript
function updateGraph (dataList) {
  var graphPane = d3.select(".graph");
  var barBindList = graphPane.selectAll("rect").data(dataList);
  
  // データに対する要素が足りない時～
  barBindList
    .enter()
    .append("rect")
    .classed("bar", true)
    .attr({
      "width": 0,
      "height": 8,
      "x": 0,
      "y": function(d, i){ return i*10; }
    });

  // 要素に対応するデータが足りない時～
  barBindList
    .exit()
    .transition().duration(1000)
    .attr("width", 0)
    .remove();
    
  // 要素とデータが一致するとき～
  barBindList
    .transition().duration(1000)
    .attr("width", function(d, i){ return d; });
    
}
```

- transition() でアニメーションする
- duration()で時間を設定する
- widthの幅を初期値０でappendしてから幅を指定する

☆課題☆transition()を追加しましょう

result: http://goo.gl/9u65oQ

## スケール

### とても大きな値をいれるとグラフをはみ出す...(ToT)

source: http://goo.gl/XmpmIr

```javascript
  updateGraph([2000, 1500, 3000]);
```

### 画面表示幅に合わせ縮小拡大

source: http://goo.gl/LZM4xR

画面表示幅に合わせて縮小拡大を行うX座標のスケール関数を作る

> d3.jsな日々 スケーリング：domainとrange
> http://d3js.hatenadiary.jp/entry/2013/04/22/074725


```javascript
var CANVAS_WIDTH = 300;

function updateGraph (dataList) {
  var graphPane = d3.select(".graph").attr("width", CANVAS_WIDTH);
  var barBindList = graphPane.selectAll("rect").data(dataList);
  
  var xScale = d3.scale.linear();
  xScale.domain([0, d3.max(dataList, function(d){ return d;})]);
  xScale.range([0, CANVAS_WIDTH]);
  
  barBindList.enter()
    .append("rect")
    .classed("bar", true)
    .attr({
      "width": 0,
      "height": 8,
      "x": 0,
      "y": function(d, i){ return i*10; }
    });

  barBindList.exit()
    .transition().duration(1000)
    .attr("width", 0)
    .remove();
    
  barBindList
    .transition().duration(1000)
    .attr("width", function(d, i){ return xScale(d); }); // <- POINT!
}
```
- d3.scale.linear()で線形スケール関数が出来る
- domain が入力値の幅
- rangeが出力値の幅
- スケール関数に値を渡すと画面表示位置座標を計算してくれる

☆課題☆一番下のwidthの計算をreturn xScale(d);　に修正する

result: http://goo.gl/ZKtppI

## 息抜き ちょっとソースを整理

source: http://goo.gl/qgcQE2

```javascript
var CANVAS_WIDTH = 300;

window.onload = function() {
  var barGraph = new BarGraph();
  
  barGraph.updateGraph([2000, 1500, 3000]);
  
  setTimeout(function() {
    barGraph.updateGraph([2000, 1500]);
  }, 2000);
};

var BarGraph = function (){
  this.graphPane = d3.select(".graph").attr("width", CANVAS_WIDTH);
  this.xScale = d3.scale.linear();
};

BarGraph.prototype.updateGraph = function (dataList) {
  var graphPane = this.graphPane,
      xScale = this.xScale;
  
  var barBindList = graphPane.selectAll("rect").data(dataList);
  
  xScale.domain([0, d3.max(dataList, function(d){ return d;})]);
  xScale.range([0, CANVAS_WIDTH]);
  
  barBindList.enter()
    .append("rect")
    .classed("bar", true)
    .attr({
      "width": 0,
      "height": 8,
      "x": 0,
      "y": function(d, i){ return i*10; }
    });

  barBindList.exit()
    .transition().duration(1000)
    .attr("width", 0)
    .remove();
    
  barBindList
    .transition().duration(1000)
    .attr("width", function(d, i){ return xScale(d); });
};
```

- コンストラクタ関数で初期処理
- インスタンスメソッド(BarGraph#updateGraph)で実行

## 軸を作る

X座標のスケールを使ってグラフにX座標軸を作ることが出来ます。

source: http://goo.gl/A4U7JE

```javascript
var BarGraph = function (){
  this.graphPane = d3.select(".graph").attr("width", CANVAS_WIDTH);
  this.xScale = d3.scale.linear();
  // X軸追加！
  this.xAxis = d3.svg.axis().scale(this.xScale);
  this.graphPane.append("g")
                .classed("x-axis-pane", true)
                .attr("transform", "translate(0, 100)");
};

BarGraph.prototype.updateGraph = function (dataList) {
  var graphPane = this.graphPane,
      xScale = this.xScale,
      xAxis  = this.xAxis;
  
  var barBindList = graphPane.selectAll("rect").data(dataList);
  
  xScale.domain([0, d3.max(dataList, function(d){ return d;})]);
  xScale.range([0, CANVAS_WIDTH]);
  
  // X軸を計算して描画
  graphPane.select("g.x-axis-pane").call(xAxis);
  
  barBindList.enter()
    .append("rect")
    .classed("bar", true)
    .attr({
      "width": 0,
      "height": 8,
      "x": 0,
      "y": function(d, i){ return i*10; }
    });

  barBindList.exit()
    .transition().duration(1000)
    .attr("width", 0)
    .remove();
    
  barBindList
    .transition().duration(1000)
    .attr("width", function(d, i){ return xScale(d); });
};
```

☆課題☆graphPane.select("g.x-axis-pane")の後ろに.call(xAxis);をつける

result: http://goo.gl/h3TRW1

## 軸が入りきれないので余白をつける

source: http://goo.gl/lxPj6y

グラフはSVGの表示領域よりも小さくする

```javascript
var CANVAS_WIDTH = 440,
    PADDING = 20; // <- 余白

window.onload = function() {
  var barGraph = new BarGraph();
  
  barGraph.updateGraph([2000, 1500, 3000]);
  
  setTimeout(function() {
    barGraph.updateGraph([2000, 1500]);
  }, 2000);
};

var BarGraph = function (){
  this.graphPane = d3.select(".graph")
                     .attr("width", CANVAS_WIDTH)
                     .append("g")
                     .attr("transform", "translate(" + PADDING + ", 0)");  // <- グラフ左余白
  this.xScale = d3.scale.linear();
  // X軸追加！
  this.xAxis = d3.svg.axis().scale(this.xScale);
  this.graphPane.append("g")
                .classed("x-axis-pane", true)
                .attr("transform", "translate(0, 100)");
};

BarGraph.prototype.updateGraph = function (dataList) {
  var graphPane = this.graphPane,
      xScale = this.xScale,
      xAxis  = this.xAxis,
      graphWidth = CANVAS_WIDTH - PADDING*2; // <- 描画幅
  
  var barBindList = graphPane.selectAll("rect").data(dataList);
  
  xScale.domain([0, d3.max(dataList, function(d){ return d;})]);
  xScale.range([0, graphWidth]); // <- スケールも余白に合わせて設定
  
  // X軸を計算して描画
  graphPane.select("g.x-axis-pane").call(xAxis);
  
  barBindList.enter()
    .append("rect")
    .classed("bar", true)
    .attr({
      "width": 0,
      "height": 8,
      "x": 0,
      "y": function(d, i){ return i*10; }
    });

  barBindList.exit()
    .transition().duration(1000)
    .attr("width", 0)
    .remove();
    
  barBindList
    .transition().duration(1000)
    .attr("width", function(d, i){ return xScale(d); });
};
```

☆課題☆PADDING = 0から20に修正しましょう

result: http://goo.gl/epr7ov


## 色

source: http://goo.gl/yAF3Th

### スケールに合わせて色付け

青から値が大きいほど赤にする

```javascript
  // 幅に合わせて色をつける
  var colorScale = d3.scale.linear()
                        .domain([0, d3.max(dataList, function(d){ return d;})])
                        .range(["blue","red"]);
```

svgの属性 fillで色を塗る

```javascript
  barBindList
    .transition().duration(1000)
    .attr("width", function(d, i){ return xScale(d); })
    .attr("fill", function(d){ return colorScale(d); });
```

### お任せの色

☆課題☆ colorScaleをお任せのd3.scale.category10に変更する

```javascript
var colorScale = d3.scale.category10();
```

result: http://goo.gl/I5fbF0


## 軸を装飾する

source: http://goo.gl/rFeiKr

スタイルシートを整える

```css
.x-axis-pane path,
.x-axis-pane line {
  /*塗りつぶさない*/
  fill: none;
  /*線は黒*/
  stroke: black;
  /*細いシャープな線で*/
  shape-rendering: crispEdges;
}

.tick line{
  /*グラフ中の線は薄めの色*/
  opacity: 0.2;
}
```

描画領域の高さを定数に用意

```javascript
var CANVAS_WIDTH = 440,
    CANVAS_HEIGHT = 150, // <- 縦線を入れる為グラフの高さを設定
    PADDING = 20; 
```

SVGサイズに高さを指定

```javascript
  this.graphPane = d3.select(".graph")
                     .attr("width", CANVAS_WIDTH)
                     .attr("height", CANVAS_HEIGHT) // <- 描画SVG全体の高さ
                     .append("g")
                     .attr("transform", "translate(" + PADDING + ", 0)"); 

```

高さ分上に線を引く

```javascript
  this.xAxis = d3.svg.axis().scale(this.xScale)
                            .orient("bottom") // 文字を下に
                            .innerTickSize(-CANVAS_HEIGHT) // グラフ内線を上に引く
                            .outerTickSize(-CANVAS_HEIGHT) // グラフ外線を上に引く
                            .tickPadding(10); // グラフ軸と文字との距離

```

☆課題☆ innerTickSizeとouterTickSizeをマイナス値に変更しましょう

result: http://goo.gl/7ncu65


## イベント処理

### onでイベントを登録できます。

thisに自身の要素が入るのでd3.select(this)で操作が出来ます。

```javascript
  barBindList
    .on("mouseover", function(d, i){
      d3.select(this).attr("stroke", "black");
    })
    .on("mouseout", function(d, i){
      d3.select(this).attr("stroke", null);
    });
```
source : http://goo.gl/i2Qzb5

☆課題☆ console.logを d3.select(this).attr("stroke", "black");

result : http://goo.gl/RKdkOM

> selection.attr(name[, value])
> A null value will remove the specified attribute.
> https://github.com/mbostock/d3/wiki/Selections#attr