D3.js

開発環境

Webサーバの起動

d3.jsではデータの読み込みにHTTPRequestを多用するため、ローカル環境に簡易なWebサーバが構築できると便利.

作業しているhtmlやjsの入っているディレクトリに移動してから、shell (MacOSXのTerminal)から

python -m SimpleHTTPServer

で、port:8000でWebサーバが起動する。

ブラウザからhttp://localhost:8000/ にリクエストすると、ディレクトリ以下のファイルにアクセスできる

d3を使う前に

はじめにd3.jsを使わずにHTMLを書いてcircleとrectを描画する

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title></title>
  </head>
  <body>
    <div class="container">
    	<svg width="600" height="400">
    	    <circle cx="200" cy="100" r="50" fill="red"/> <!-- 座標(200,100)に半径50px 赤の円-->
    	    <rect x="200" y="100" width="100" height="100" fill="blue"  style="opacity:0.6"/> <!-- 四角形 透明度60% -->
    	</svg>
    </div>
  </body>
</html>

d3特殊な描画環境を提供しているわけではない。Webの標準的な規格であるCSS, HTML5, SVGを操作することでデータを視覚化するためのライブラリである。そのため、例えばcircleやrectなどの描画についてどう書けばよいかは MDN SVG element reference などで調べることができる。

d3.jsの動作確認

index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script> <!--ライブラリd3.jsを読み込み-->
    <title></title>
  </head>
  <body>
    <div class="container">
    	<svg width="600" height="400">
    	    <circle cx="200" cy="100" r="50" fill="red"/>
    	    <rect x="200" y="100" width="100" height="100" fill="blue"  style="opacity:0.6"/>
    	</svg>
    </div>
    <script src="firstd3.js" charset="utf-8"></script> <!--これからプログラミングするjsファイルパス-->
  </body>
</html>

firstd3.js

先ほど描画した図形の属性(サイズ、色)を書き換えたり、新しい図形を追加する

console.log('hello world');
d3.selectAll('circle') // circle要素をすべて取得
	.attr('cx', 100) // それらの x座標を100に設定
	.attr('cy', 100)
	.attr('r', 100)
	.attr('fill', 'green')
	.style('opacity', 0.5) // 透明度 50%

d3.selectAll('rect') // rect要素をすべて取得
	.attr('x', 200)
	.attr('y', 100)
	.attr('width', 200)
	.attr('height', 100)
	.attr('fill', '#39c') // 色をRGB値で指定
	.style('opacity', 1.0)

// d3で新しくcircle要素を追加
d3.select('svg') // svg要素をひとつ取得
	.append('circle') // 取得したsvg要素の中にcircle要素を追加
	.attr('cx', 100)
	.attr('cy', 200)
	.attr('r', 50)
	.style('opacity', 0.5)

データセットからcircleを描画する

描画する図形の属性をデータによって決定する

// データをもとにcircle描画する
// <circle>の大きさをdatasetの個々のデータに対応させ, 一列に並べる
var dataset = [10, 70, 80, 40, 20, 50, 30]
var container = d3.select('.container')

var svg = container.append('svg')
    .attr('width', 600)
    .attr('height', 500)

svg.selectAll("circle")
    .data(dataset) // データを渡す
    .enter()
    .append('circle')
    .attr('cx', function(data, i) { return i*80} ) // dataに個々のデータ(10,70,80,40,20,50,30)、iにはデータの番号(0〜6)が渡される
    .attr('cy', 100 ) // y座標は全部のcircleで同じ100px
    .attr('r', function(data){ return data }) // 半径の大きさは個々データの値にする[10, 70, 80, 40, 20, 50, 30]
    .style('opacity', 0.3) //透明度


circleのx座標は0, 80, 160, 240, 320, 400, 480 と i * 80になっている
circleの半径はデータの10, 70, 80, 40, 20, 50, 30になっている

*複数のデータセットを描画する

// 2つのデータ(dataset_a, dataset_b)をもとにcircle描画する
// データの種類を区別するためにcircleにクラス名をつける

var dataset_a = [10, 70, 80, 40, 20, 50, 30]
var dataset_b = [30, 90, 100, 60, 40, 70, 50]

var container = d3.select('.container')

var svg = container.append('svg')
    .attr('width', 600)
    .attr('height', 500)
// dataset_a用
svg.selectAll("circle.data_a") // クラス名 data_a のcircleタグを取得
    .data(dataset_a)
    .enter()
    .append('circle')
    .attr('class', 'data_a') // クラス名 data_a を指定
    .attr('cx', function(data, i) { return i*80} )
    .attr('cy', 100 )
    .attr('r', function(data){ return data })
    .style('opacity', 0.3) //透明度
// dataset_b用
svg.selectAll("circle.data_b") クラス名 data_b のcircleタグを取得
    .data(dataset_b)
    .enter()
    .append('circle')
    .attr('class', 'data_b') // クラス名 data_b を指定
    .attr('cx', function(data, i) { return i*80} )
    .attr('cy', 200 )
    .attr('r', function(data){ return data })
    .attr('fill', 'red')
    .style('opacity', 0.3)

CSVファイルを読み込んで描画

data.csv

name,size
dog,84
cat,23
fish,5
orange,58
apple,53
var container = d3.select('.container')

d3.csv("http://localhost:8000/data.csv", function(error, dataset) {
    console.log(error) // csv受信にエラーがあればlogに
    console.log(dataset) // csv受信に成功すれば datasetの中身がlogに

    var svg = container.append('svg')
	.attr('width',500)
	.attr('height',500)

    // データを表現する円
    svg.selectAll("circle")
	.data(dataset)
	.enter()
	.append('circle')
	.attr('cx', 200)
	.attr('cy', function(data){ return data.size})
	.attr('r', function(data){ return data.size })
	.attr('fill', '#39c')
	.style('opacity', 0.2)

    // dog, cat などの文字ラベルを表示する
    svg.selectAll('text')
	.data(dataset)
	.enter()
	.append('text')
	.attr('x', 200)
	.attr('y', function(data) {return data.size*2})
	.attr('fill','#F31')
    	.text(function(data) {return data.name})
})

補足 読み込むファイルがJSONの場合

data.json

[{"name": "dog", "size": 84},
{"name": " cat", "size": 23},
{"name": " fish", "size": 5},
{"name": " orange", "size": 58},
{"name": " apple", "size": 53}]

上記data.jsonをHTTPリクエストを使って取得

var container = d3.select('.container')

d3.json("http://localhost:8000/data.json", function(error, dataset) {
    console.log(error) // json受信にエラーがあればlogに
    console.log(dataset) // json受信に成功すれば datasetの中身がlogに

    var svg = container.append('svg')
	.attr('width',500)
	.attr('height',500)

    // データを表現する円
    svg.selectAll("circle")
	.data(dataset)
	.enter()
	.append('circle')
	.attr('cx', 200)
	.attr('cy', function(data){ return data.size})
	.attr('r', function(data){ return data.size })
	.attr('fill', '#39c')
	.style('opacity', 0.2)

    // dog, cat などの文字ラベルを表示する
    svg.selectAll('text')
	.data(dataset)
	.enter()
	.append('text')
	.attr('x', 200)
	.attr('y', function(data) {return data.size*2})
	.attr('fill','#F31')
    	.text(function(data) {return data.name})
})

JSON取得の注意点

取得するjsonやcsvファイルは同じドメインのものでなければならない.
外部のjsonを取りに行こうとするとブラウザのセキュリティ機能によりエラーになる(詳しくはクロスドメインで検索)

例えば

外部ドメインにあるjsonやcsvファイルを扱いたい場合はJSONPを使用する

JSONP
http://ja.wikipedia.org/wiki/JSONP

d3.jsonp
https://github.com/d3/d3-plugins/tree/master/jsonp

Scale

1234から8983までの値をとるデータセットを、svgの幅500x高さ300の領域に表示する時
データセットの値をそのまま座標の値にすると、svgの領域からはみ出してしまう。(500<1234)
そのため1234〜8983までのデータを、500x300の領域に収まるように描画するための仕組みが必要

var dataset = [1234, 7000, 8983, 4000, 2120, 5000, 3100]
var container = d3.select('.container')

// datasetの中の1234〜8983までの範囲のデータを50〜450に収まるように変換する関数(scale_x)を作る
var scale_x = d3.scale.linear()
    .domain([1234, 8983]) // データのとる範囲を指定する
    .range([50, 450]) // 変換したい範囲を指定する

// scale_x(1234) --> 50
// scale_x(2000) --> 89.5405858820493
// scale_x(8983) --> 450
// scale_xを呼び出してみると1234〜8983を50〜450にスケールしているのがわかる

var svg = container.append('svg')
    .attr('width', 500)
    .attr('height', 300)
    .style('background', '#EEE')

svg.selectAll('circle')
    .data(dataset)
    .enter()
    .append('circle')
    .attr('cx', scale_x) // .attr('cx', function(data) { return scale_x(data)} ) と同じ
    .attr('cy', 100)
    .attr('r', 10)
    .style('opacity', 0.3)

svg.selectAll('text')
    .data(dataset)
    .enter()
    .append('text')
    .attr('x', scale_x)
    .attr('y', 100)
    .attr('fill', '#F31')
    .text(function(data){return data})

circleとtextのx座標が50〜449.99999にスケールされ、500pxの中に収まっている

Transition

描画した要素をアニメーションさせる

var dataset = [1234, 7000, 8983, 4000, 2120, 5000, 3100]
var container = d3.select('.container')

var width = 500
var height = 300
var padding = 20

var scale_x = d3.scale.linear()
    .domain(d3.extent(dataset))
    .range([0+padding, width-padding*2])

//色もscale.category10を使って適切に設定
var scale_color = d3.scale.category10(dataset)
// scale_color(1234) --> "#1f77b4"
// scale_color(7000) --> "#ff7f0e"

var svg = container.append('svg')
    .attr('width', width)
    .attr('height', height)
    .style('background', '#EEE')

svg.selectAll('circle')
    .data(dataset)
    .enter()
    .append('circle')
    .attr('cx', 0) // 最初はすべてのcircleが x=0
    .attr('cy', 100)
    .attr('r', 10)
    .style('opacity', 0.8)
    .transition()
    .duration(2000) // 2000ミリ秒かけてアニメーション
    .attr('cx', scale_x) // scale_xで計算されたxへ
    .attr('fill', scale_color)  //scale_colorで計算された色へ

svg.selectAll('text')
    .data(dataset)
    .enter()
    .append('text')
    .attr('x', 0) // 最初すべてのtextが x=0
    .transition()
    .duration(2000) // 2000ミリ秒かけてアニメーション
    .attr('x', scale_x) // scale_xで計算されたxへ
    .attr('y', 100-padding)
    .attr('fill', scale_color) //scale_colorで計算された色へ
    .text(function(data){return data})



グループ化

Scaleのコードを修正 circle,textなどを個別に座標指定するのは大変なのでグループ化する
グループの中の要素はtransformで座標を指定する

var dataset = [1234, 7000, 8983, 4000, 2120, 5000, 3100]
var container = d3.select('.container')

var width = 500
var height = 300
var padding = 20

var scale_x = d3.scale.linear()
    .domain(d3.extent(dataset))
    .range([0+padding, width-padding*2])

var svg = container.append('svg')
    .attr('width', width)
    .attr('height', height)
    .style('background', '#EEE')

var group = svg.selectAll('g')
    .data(dataset)
    .enter()
    .append('g')
    .attr('transform', // 座標は transformで指定する
            function(data) { return 'translate('+scale_x(data)+','+100+')'})

group.append('circle')
    .attr('r', 10)
    .style('opacity', 0.3)

group.append('text')
    .attr('fill', '#F31')
    .attr('x', -15) // <g>の座標からの相対的な位置を指定する
    .attr('y', 30)  // <g>の座標からの相対的な位置を指定する
    .text(function(data){return data})

CoffeeScript

D3.jsをCoffeeScriptで書く利点いくつか


# square = function(x) {
#   return x * x;
# };
# square(10)

square = (x) -> x * x
square 10 # 100

# list comprehension
value = (square num for num in [1,2,3,4]) # [1, 4, 9, 16]

# string interpolation
console.log "result is #{square 10} !!" # "result is 100 !!"

group.jsをcoffeescriptで書きなおすと

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
    <title>1st coffee</title>
  </head>
  <body>
    <div class="container"></div>
    <script src="http://coffeescript.org/extras/coffee-script.js" charset="utf-8"></script>
    <script type="text/coffeescript" src="group.coffee"></script>
  </body>
</html>
dataset = [1234, 7000, 8983, 4000, 2120, 5000, 3100]
container = d3.select('.container')

width = 500
height = 300
padding = 20

scale_x = d3.scale.linear()
  .domain(d3.extent dataset) # coffee
  #.domain(d3.extent(dataset)) ... js
  .range([0+padding, width-padding*2])

svg = container.append('svg')
  .attr('width', width)
  .attr('height', height)
  .style('background', '#EEE')

group = svg.selectAll('g')
  .data(dataset)
  .enter()
  .append('g')
  .attr('transform', (data)->"translate(#{scale_x data}, 100)") # coffee
 #.attr('transform',
           # function(data) { return 'translate('+scale_x(data)+','+100+')'})  ... js

group.append('circle')
  .attr('r', 10)
  .style('opacity', 0.3)

group.append('text')
  .attr('fill', '#F31')
  .attr('x', -15)
  .attr('y', 30)
  .text((data)->data) # coffee
 #.text(function(data){return data}) ... js

CoffeeScriptをJavaScriptにコンパイル

coffee -c group.coffee # group.jsが出力される

group.js 通常はこのjsをアップして使う

// Generated by CoffeeScript 1.6.3
(function() {
  var container, dataset, group, height, padding, scale_x, svg, width;
  dataset = [1234, 7000, 8983, 4000, 2120, 5000, 3100];
  container = d3.select('.container');
  width = 500;
  height = 300;
  padding = 20;
  scale_x = d3.scale.linear().domain(d3.extent(dataset)).range([0 + padding, width - padding * 2]);
  svg = container.append('svg').attr('width', width).attr('height', height).style('background', '#EEE');
  group = svg.selectAll('g').data(dataset).enter().append('g').attr('transform', function(data) {
    return "translate(" + (scale_x(data)) + ", 100)";
  });
  group.append('circle').attr('r', 10).style('opacity', 0.3);
  group.append('text').attr('fill', '#F31').attr('x', -15).attr('y', 30).text(function(data) {
    return data;
  });
}).call(this);