D3.js とは? †
- http://d3js.org/
- Data-Driven Documents
- データをグラフィカルに表現するための Javascript ライブラリ
- D3.js は、CSV を食わせるとグラフにしてくれる魔法の杖ではない
- D3.js は、jQuery などと同じ DOM 操作のためのライブラリ
- CSV や JSON などのデータを SVG に加工
- SVG を HTML5 文書に埋め込む
- D3.js は、そういったことに特化した Javascript の部品群
D3.js による DOM 操作 †
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>D3 Page Template</title>
<script type="text/javascript" src="https://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<script type="text/javascript" src="https://code.jquery.com/jquery-2.0.2.js" charset="utf-8"></script>
</head>
<body>
<ul id="list1">
<li>L1</li>
<li>L2</li>
</ul>
<ul id="list2">
<li>A</li>
<li>B</li>
<li>C</li>
<li>D</li>
<li>E</li>
<li>F</li>
<li>G</li>
<li>H</li>
</ul>
<button>D3.jsはじめの一歩</button>
<script type="text/javascript">
var dataset = [5, 10, 15, 20, 25];
$('button')
.attr('disabled', false)
.on('click', function() {
applyData('#list1', dataset);
applyData('#list2', dataset);
$(this).attr('disabled', true);
});
function applyData(selector) {
var update = d3.select(selector).selectAll("li").data(dataset);
var enter = update.enter();
var exit = update.exit();
// 既存の <li>
update
.text( function(d,i) {
return $(this).html() + "→" + d;
} )
.style("color", "gray")
.attr("type", "1");
// 追加された <li>
enter
.append("li")
.text( function(d) { return d; } )
.style("color", "red");
// 余った <li>
exit.remove();
}
</script>
</body>
</html>
- 最近は <body> の一番最後に <script> を書くのが流行っているみたい
- jQuery と組み合わせて使い分けると便利
- $() : jQuery の DOM セレクタ。jQuery は一つのタグに何かを設定するのに便利
- d3.select() / d3.selectAll() : d3.js の DOM セレクタ。データを DOM に展開するのに便利
- データを DOM に展開する
var update = d3.select(selector).selectAll("li").data(dataset);
- CSS セレクタで DOM を指定できる
tag | ul |
id | #list1 |
class | .mylist |
これらをスペース区切りで複数組み合わせることもできる
- data(dataset) で、選択した DOM 要素の配列に、dataset が展開される
- update, enter, exit
var update = d3.select(selector).selectAll("li").data(dataset);
var enter = update.enter();
var exit = update.exit();
update | | データが割り当てられた既存のDOM要素の集合 |
enter | データの個数 > 選択されたDOM要素のとき | 余ってしまったデータの集合 |
exit | データの個数 < 選択されたDOM要素のとき | 余ってしまったDOM要素の集合 |
- update
update
.text( function(d,i) {
return $(this).html() + "→" + d;
} )
.style("color", "gray")
.attr("type", "1");
- 既存のDOM要素に対して何かする。上のように書くと、update に含まれる DOM 要素それぞれに対して、テキスト設定、スタイル設定、属性設定が行われる
- text() テキスト
- style() CSS <li style="color:pink">もも<li> なら .style("color","pink")
- attr() タグ属性 <li type="1">牛</li> なら .attr("type",'1")
- text()、style()、attr() の引数には function(d, i) {} を与えることもできる
- d : データ、i : 連番 (引数は省略可能)
- 返り値が設定値になる
- function(d,i){} 内の this は、操作対象の DOM 要素になる。サンプルプログラムの場合、this は <li></li> 1つ分になる
- enter
enter
.append("li")
.text( function(d) { return d; } )
.style("color", "red");
- 余ってしまったデータに対して何かする。上のように書くと、update の次に <li></li> が追加される
- function(d){} 。iは省略可能
- remove
exit.remove();
- 余ってしまった DOM 要素に対して何かする。上のように書くと、exit に含まれる DOM 要素が削除される
D3.js によるデータの ajax 取得 †
- csv を ajax で取得
d3.csv(url, function(error, data) {
// データ取得後の処理
});
data には、url から読み取った csv が変換された配列が格納される。配列の要素は Map で、キーは見出し部の項目名、値は各行の値になる
- json を ajax で取得
d3.json(url, function(error, data) {
// データ取得後の処理
});
data には、url から読み取った json がそのまま格納される
- データ取得後の処理は、d3.csv() や d3.json() 呼び出しとは非同期で起きることに注意。取得後の処理は必ず第二引数の function で行う
- サンプルプログラム
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>D3 Page Template</title>
<script type="text/javascript" src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
</head>
<body>
<div style="float:left; padding: 10px">
CSV <table border="1" id="csvtbl"></table>
</div>
<div style="float:left; padding: 10px">
JSON <table border="1" id="jsontbl"></table>
</div>
<script type="text/javascript">
d3.csv("http://localhost:8080/JAX-RSExam/webresources/d3/text", function(error, data) {
if (error) {
console.log("ERROR RECV");
console.log(error);
return;
}
var row = d3.select('#csvtbl').selectAll('tr').data(data)
.enter()
.append('tr');
row.append('td').text(function(d){ return d.date; });
row.append('td').text(function(d){ return d.freq; });
return;
});
d3.json("http://localhost:8080/JAX-RSExam/webresources/d3/json", function(error, data) {
if (error) {
console.log("ERROR RECV");
console.log(error);
return;
}
var row = d3.select('#jsontbl').selectAll('tr').data(data)
.enter()
.append('tr');
row.append('td').text(function(d){ return d.date; });
row.append('td').text(function(d){ return d.freq; });
return;
});
</script>
</body>
</html>
- 読み込んだデータは、Glassfish JAX-RS で作った REST サービス
[~]$ curl http://localhost:8080/JAX-RSExam/webresources/d3/json
[{"date":1371995770259,"freq":0},
{"date":1372082170259,"freq":1},
{"date":1372168570259,"freq":2},
{"date":1372254970259,"freq":3},
{"date":1372341370259,"freq":4},
{"date":1372427770259,"freq":5},
{"date":1372514170259,"freq":6},
{"date":1372600570259,"freq":7},
{"date":1372686970259,"freq":8},
{"date":1372773370259,"freq":9}]
[~]$ curl http://localhost:8080/JAX-RSExam/webresources/d3/text
date,freq
2013-06-23 22:56:31,0
2013-06-24 22:56:31,1
2013-06-25 22:56:31,2
2013-06-26 22:56:31,3
2013-06-27 22:56:31,4
2013-06-28 22:56:31,5
2013-06-29 22:56:31,6
2013-06-30 22:56:31,7
2013-07-01 22:56:31,8
2013-07-02 22:56:31,9
- d3.json(url, function(error, data) {}) が、ローカルファイルの読み込みに使えないことについて
- url にローカルファイルを設定すると、ブラウザのセキュリティ例外で読み取れない
- 対処案1 : "chrome --allow-file-access-from-files" で chrome を起動する。事前にいったん全部の chrome を停止する必要あり
- 対処案2 : そもそも d3.json() を使わなくても良くね ?
d3.json('world-map.js', function(error, data) {
d3.select(...).data(data).enter()....
...
})
を
var data = [{....},{....},....]; // world-map.js の内容
d3.select(...).data(data).enter()....
...
にすれば動くんぢゃね?
D3.js で SVG を書く (1) †
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>SVG1</title>
<style>
.mycircle {
fill: yellow;
stroke: orange;
stroke-width: 3px;
}
</style>
</head>
<body>
<script src="https://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<script>
var dataset = [11, 25, 15, 20, 4, 8 , 16, 32];
var w = 400;
var h = 100;
var svg = d3.select("body").append("svg").attr({width:w, height:h});
var circles = svg.selectAll("circle")
.data(dataset)
.enter()
.append("circle");
circles
.classed("mycircle", true)
.attr("cx", function(d, i){
return (i * 50) + 25;
})
.attr("cy", function(d, i){
return h/2 - d;
})
.attr("r", function(d, i){
return d/4;
});
var labels = svg.selectAll("text")
.data(dataset)
.enter()
.append("text");
labels
.attr("x", function(d, i){
return (i * 50) + 25;
})
.attr("y", function(d, i){
return h/2 - d + 16;
})
.text(function(d, i){
return d;
});
svg.append("line")
.attr("x1", 0)
.attr("y1", function(d, i){
return h/2;
})
.attr("x2" , function(d, i) {
return w;
})
.attr("y2", function(d, i){
return h/2;
})
.attr("stroke", "black");
</script>
</body>
</html>
- body に svg を追加する定跡
var dataset = [11, 25, 15, 12, 4];
var w = 400;
var h = 100;
var svg = d3.select("body").append("svg").attr({width:w, height:h});
- <svg> に svg の図形を書いていく
- .classd('class name', boolean)
- クラスを適用する、クラスを外す
- 本来は <html><body><svg><defs><style> に書く必要があるけど <html><head><style> でもいいみたい
D3.js で SVG を書く (2) †
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>SVG2</title>
<style>
.myrect {
stroke : black;
stroke-width : 1;
}
</style>
</head>
<body>
<script src="https://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<script>
var dataset = [11, 25, 15, 20, 4, 8, 16, 32];
var w = 400;
var h = 100;
var margin = 20;
var svg = d3.select("body").append("svg").attr({width:w, height:h});
var rect = svg.selectAll("rect")
.data(dataset)
.enter()
.append("rect");
rect
.classed("myrect", true)
.attr({
"x" : function(d, i){
return (i * 50);
},
"y" : function(d, i){
return h - margin - d;
},
"height" : function(d, i){
return d;
},
"width" : 45,
"fill" : function(d, i) {
return "rgb(" + (d*10) + ",255,255)";
}
});
var labels = svg.selectAll("text")
.data(dataset)
.enter()
.append("text");
labels
.attr({
"x" : function(d, i){
return (i * 50);
},
"y" : function(d, i){
return h - margin - d;
}})
.text(function(d, i){
return d;
});
svg.append("line")
.attr({
"x1" : 0,
"y1" : function(){
return h - margin;
},
"x2" : function() {
return w;
},
"y2" : function(){
return h - margin;
},
"stroke" : "black"});
</script>
</body>
</html>
- attr() は、attr(key, val) の他に attr({key1:val1, key2:val2, key3:val3}) とも書ける
Scale (1) データ値→画面上のpx 変換 †
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Scale</title>
<script type="text/javascript" src="https://d3js.org/d3.v3.min.js" charset="utf-8"></script>
</head>
<body>
<script type="text/javascript">
// N225
var dataset = [
{"date":1372078863613,"value":13062},
{"date":1372165263613,"value":12969},
{"date":1372251663613,"value":12834},
{"date":1372338063613,"value":13213},
{"date":1372424463613,"value":13677},
{"date":1372683663613,"value":13852}
];
// min value
var min = d3.min(dataset, function(d) {
return d.value;
});
// max value
var max = d3.max(dataset, function(d) {
return d.value;
});
// margin
var margin = 1000;
var w = 400;
var h = 100;
// Input Data is (12834-1000)yen to (13852+1000)yen
// Output is 20px to 400px
var scale = d3.scale.linear()
.domain([min - margin, max + margin])
.range([20, h]);
var svg = d3.select("body").append("svg").attr({width:w, height:h});
svg.selectAll("rect")
.data(dataset)
.enter()
.append("rect")
.attr({
"x" : function(d, i){
return (i * 60);
},
"y" : function(d, i){
return h - scale(d.value);
},
"height" : function(d, i){
return scale(d.value);
},
"width" : 55,
"fill" : "blue"
});
svg.selectAll("text")
.data(dataset)
.enter()
.append("text")
.attr({
"x" : function(d, i){
return (i * 60);
},
"y" : function(d, i){
return h - scale(d.value) - 5;
}})
.text(function(d, i){
return d.value;
});
</script>
</body>
</html>
Scale (2) (date,value)→画面上のpx 変換 †
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Scale2</title>
<script type="text/javascript" src="https://d3js.org/d3.v3.min.js" charset="utf-8"></script>
</head>
<body>
<script type="text/javascript">
var WEEKDAY = ['日','月','火','水','木','金','土'];
var w = 400;
var h = 100;
// N225
var dataset = [
{"date":1372078863613,"value":13062},
{"date":1372165263613,"value":12969},
{"date":1372251663613,"value":12834},
{"date":1372338063613,"value":13213},
{"date":1372424463613,"value":13677},
{"date":1372683663613,"value":13852}
];
// min value
var minX = d3.min(dataset, function(d) {
return d.date;
});
// max value
var maxX = d3.max(dataset, function(d) {
return d.date;
});
var scaleX = d3.scale.linear()
.domain([minX, maxX])
.nice() // input domain を適当にマージンを取って拡大
.rangeRound([0, w]) // output range (画面上のピクセル位置) を整数に丸める
.clamp(true); // (val > max) ? (max) : (val < min ? min : val)
// min value
var minY = d3.min(dataset, function(d) {
return d.value;
});
// max value
var maxY = d3.max(dataset, function(d) {
return d.value;
});
// margin
var marginY = 1000;
var scaleY = d3.scale.linear()
.domain([minY - marginY, maxY + marginY])
.nice()
.rangeRound([h, 0]) // Technique! minY は (x, 100)にプロット、maxY は (x,0) にプロット
.clamp(true);
var svg = d3.select("body").append("svg").attr({width:w, height:h});
svg.selectAll("circle")
.data(dataset)
.enter()
.append("circle")
.attr("cx", function(d){
return scaleX(d.date);
})
.attr("cy", function(d){
return scaleY(d.value);
})
.attr("r", "2");
svg.append("g").selectAll("text")
.data(dataset)
.enter()
.append("text")
.attr({
"font-size" : 10,
"text-anchor" : function(d, i){
return (dataset.length == i+1) ? "end" : "start";
},
"x" : function(d){
return scaleX(d.date);
},
"y" : function(d){
return scaleY(d.value) + 10;
}})
.text(function(d){
var date = new Date();
date.setTime(d.date);
return (date.getMonth() + 1) + "-" + date.getDate() + "(" + WEEKDAY[date.getDay()] + ")";
});
svg.append("g").selectAll("text")
.data(dataset)
.enter()
.append("text")
.attr({
"font-size" : 10,
"text-anchor" : function(d, i){
return (dataset.length == i+1) ? "end" : "start";
},
"x" : function(d){
return scaleX(d.date);
},
"y" : function(d){
return scaleY(d.value) + 20;
}})
.text(function(d){
return d.value;
});
</script>
</body>
</html>
- scale は、y だけではなく x にも適用するとよろしい
- scale は、nice(), rangeRound(), clamp() などで値域/定義域を調整できる
var scaleX = d3.scale.linear()
.domain([minX, maxX])
.nice() // input domain を適当にマージンを取って拡大
.rangeRound([0, 400]) // output range (画面上のピクセル位置) を整数に丸める
.clamp(true); // (val > max) ? (max) : (val < min ? min : val)
- データの値をSVGの縦方向の座標に変換するときのテクニック。range を [100,0] に設定すると、最小の値に 100px 、最大の値に 0px が割り当てられる
var scaleY = d3.scale.linear()
.domain([minY - marginY, maxY + marginY])
.nice()
.rangeRound([100, 0]) // Technique! minY は (x, 100)にプロット、maxY は (x,0) にプロット
.clamp(true);
Scale (3) linear 以外の Scale †
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Scale3</title>
<script type="text/javascript" src="https://d3js.org/d3.v3.min.js" charset="utf-8"></script>
</head>
<body>
<script type="text/javascript">
var w = 300;
var h = 200;
var margin = {x:20, y:20};
var dataset = [0.1,0.2,0.4,1,2,4,10,20,40,100,200,400,1000];
var scaleX = d3.scale.linear()
.domain([1, dataset.length])
.range([0 + margin.x ,w - margin.x])
.nice();
var linearY = d3.scale.linear()
.domain([d3.min(dataset), d3.max(dataset)])
.range([h - margin.y , 0 + margin.y])
.nice();
var sqrtY = d3.scale.sqrt()
.domain([d3.min(dataset), d3.max(dataset)])
.range([h - margin.y , 0 + margin.y])
.nice();
var logY = d3.scale.log()
.domain([d3.min(dataset), d3.max(dataset)])
.range([h - margin.y , 0 + margin.y])
.nice();
var powY = d3.scale.pow().exponent(0.5)
.domain([d3.min(dataset), d3.max(dataset)])
.range([h - margin.y , 0 + margin.y])
.nice();
var pow1Y = d3.scale.pow()
.domain([d3.min(dataset), d3.max(dataset)])
.range([h - margin.y , 0 + margin.y])
.nice();
var pow2Y = d3.scale.pow().exponent(2)
.domain([d3.min(dataset), d3.max(dataset)])
.range([h - margin.y , 0 + margin.y])
.nice();
var quantizeY = d3.scale.quantize()
.domain([d3.min(dataset), d3.max(dataset)]) // Quantize's domain is range (D3.js use 1st and last number)
.range([h - margin.y , h/2, 0 + margin.y]); // we can specify ['red','yellow','blue'] also.
var quantileY = d3.scale.quantile()
.domain(dataset) // Quantize's domain is discrete values
.range([h - margin.y , h/2, 0 + margin.y]); // we can specify ['red','yellow','blue'] also.
var color = d3.scale.threshold()
.domain([1,10,100,1000])
.range(['cyan', 'blue','yellowgreen','orange','red'])
plot("linear scale", scaleX, linearY);
plot("sqrt scale", scaleX, sqrtY);
plot("log scale", scaleX, logY);
plot("pow(0.5) scale (=sqrt)", scaleX, powY);
plot("pow(1.0) scale (=linear)", scaleX, pow1Y);
plot("pow(2.0) scale", scaleX, pow2Y);
plot("quantize scale", scaleX, quantizeY);
plot("quantile scale", scaleX, quantileY);
function plot(explanation, scaleX, scaleY) {
var svg = d3.select("body").append("svg").attr({width:w, height:h}).style("padding","5px");
svg.append("rect").attr({x:0, y:0, width:w, height:h, stroke:"black", fill:"snow"});
svg.selectAll("circle")
.data(dataset)
.enter()
.append("circle")
.attr("cx", function(d,i){
return scaleX(i+1);
})
.attr("cy", function(d){
return scaleY(d);
})
.attr("r", "4")
.attr("fill" , function(d){
return color(d);
});
svg.selectAll("text")
.data(dataset)
.enter()
.append("text")
.attr({
"font-size" : 10,
"font-family" : "sans-serif",
"text-anchor" : "middle",
"x" : function(d,i){
return scaleX(i+1);
},
"y" : function(d,i){
return scaleY(d) - 10;
}})
.text(function(d){
return d;
});
svg.append("text")
.attr({
"font-size" : 15,
"text-anchor" : "start",
"x" : 15,
"y" : 15})
.text(explanation);
}
</script>
</body>
</html>
d3.scale.linear() | 線形 |
d3.scale.sqrt() | 平方根 |
d3.scale.log() | 対数 |
d3.scale.pow().exponent(e) | べき乗(e 未指定時は e=1 linear() と同じ, e=0.5 で sqrt() と同じ) |
d3.scale.quantize() | range([a,b,c]) で、データを値で 3 つに分割して a, b, c に変換 |
d3.scale.quantile() | range([a,b,c]) で、データを順番で 3 つに分割して a, b, c に変換 |
d3.scale.threshold() | domain([a,b,c]).range([p,q,r,s]) で、(-∞〜a)→p、(a〜b)→q、(b〜c)→r、(c〜∞)→s |
Axis †
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Axis</title>
<script type="text/javascript" src="https://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<style type="text/css">
.axis path,
.axis line {
fill: none;
stroke: black;
shape-rendering: crispEdges;
}
.axis text {
font-family: sans-serif;
font-size: 11px;
}
</style>
</head>
<body>
<script type="text/javascript">
var w = 400;
var h = 200;
var margin = {x:50, y:20};
// N225
var dataset = [
{"date":1371999600000,"value":13062},
{"date":1372086000000,"value":12969},
{"date":1372172400000,"value":12834},
{"date":1372258800000,"value":13213},
{"date":1372345200000,"value":13677},
{"date":1372604400000,"value":13852}
];
// min value
var minX = d3.min(dataset, function(d) {
return d.date;
});
// max value
var maxX = d3.max(dataset, function(d) {
return d.date;
});
var scaleX = d3.scale.linear()
.domain([minX - 86400000, maxX + 86400000])
.rangeRound([margin.x, w - margin.x])
.clamp(true);
var formatX = function(time){
var formatter = d3.time.format("%b%d");
return formatter(new Date(time));
};
var axisX = d3.svg.axis()
.scale(scaleX)
.orient("bottom")
.ticks(10) // That's only hint, d3.js will overwrite concise number !
.tickFormat(formatX);
// min value
var minY = d3.min(dataset, function(d) {
return d.value;
});
// max value
var maxY = d3.max(dataset, function(d) {
return d.value;
});
var scaleY = d3.scale.linear()
.domain([minY - 100, maxY + 100])
.nice()
.rangeRound([h - margin.y, margin.y])
.clamp(true);
var axisY = d3.svg.axis()
.scale(scaleY)
.orient("left")
.ticks(5);
var svg = d3.select("body").append("svg").attr({width: w, height: h});
svg.selectAll("circle")
.data(dataset)
.enter()
.append("circle")
.attr("cx", function(d){
return scaleX(d.date);
})
.attr("cy", function(d){
return scaleY(d.value);
})
.attr("r", "2");
svg.append("g").selectAll("text")
.data(dataset)
.enter()
.append("text")
.attr({
"font-size" : 10,
"x" : function(d){
return scaleX(d.date);
},
"y" : function(d){
return scaleY(d.value) + 10;
}})
.text(function(d){
return d.value;
});
svg.append("g")
.attr("class", "axis")
.attr("transform", "translate(0," + (h - margin.y) + ")")
.call(axisX);
svg.append("g")
.attr("class", "axis")
.attr("transform", "translate(" + margin.x + ",0)")
.call(axisY);
</script>
</body>
</html>
var scaleX = d3.scale.linear()
.domain([minX - 86400000, maxX + 86400000])
.rangeRound([margin.x, w - margin.x])
.clamp(true);
var formatX = function(time){
var formatter = d3.time.format("%b%d");
return formatter(new Date(time));
};
var axisX = d3.svg.axis()
.scale(scaleX)
.orient("bottom")
.ticks(10) // That's only hint, d3.js will overwrite concise number !
.tickFormat(formatX);
svg.append("g")
.attr("class", "axis")
.attr("transform", "translate(0," + (h - margin.y) + ")")
.call(axisX);
- d3.svg.axis() で軸を作る
- scale(scaleX) : scale を設定
- orient("bottom") : 横軸
- ticks(10) : 値ラベルを10個くらい表示。実際のラベル数は D3.js が勝手に決める
- tickFormat(func) : 値として表示する文字列を返す function
- formatter
- SVG に変換
- svg.append("g") で、SVG のグループを作る。(軸を構成する line や path や text を含むグループを作る)
- attr("class", "axis") : グループの要素にまるっとStyle を適用
- attr("transform", "translate(0," + (h - margin.y) + ")") : 無指定だと一番上に表示されるので、これを x 方向に 0px、y 方向に (h-margin.y) px だけずらす
- .call(axisX) : 軸の SVG を作る axisX 関数 を実行する
- 縦軸は axisY
D3.js で <path> : 折れ線グラフはどうやんだっけ? †
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Axis</title>
<script type="text/javascript" src="https://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<style type="text/css">
.axis path,
.axis line {
fill: none;
stroke: black;
shape-rendering: crispEdges;
}
.axis text {
font-family: sans-serif;
font-size: 8px;
}
</style>
</head>
<body>
<script type="text/javascript">
var w = 300;
var h = 150;
var margin = {x:50, y:20};
// N225
var dataset = [
{"date":1371999600000,"value":13062},
{"date":1372086000000,"value":12969},
{"date":1372172400000,"value":12834},
{"date":1372258800000,"value":13213},
{"date":1372345200000,"value":13677},
{"date":1372604400000,"value":13852}
];
// min value
var minX = d3.min(dataset, function(d) {
return d.date;
});
// max value
var maxX = d3.max(dataset, function(d) {
return d.date;
});
var scaleX = d3.scale.linear()
.domain([minX - 86400000, maxX + 86400000])
.rangeRound([margin.x, w - margin.x])
.clamp(true);
var formatX = function(time){
var formatter = d3.time.format("%b%d");
return formatter(new Date(time));
};
var axisX = d3.svg.axis()
.scale(scaleX)
.orient("bottom")
.ticks(5) // That's only hint, d3.js will overwrite concise number !
.tickFormat(formatX);
// min value
var minY = d3.min(dataset, function(d) {
return d.value;
});
// max value
var maxY = d3.max(dataset, function(d) {
return d.value;
});
var scaleY = d3.scale.linear()
.domain([minY - 100, maxY + 100])
.nice()
.rangeRound([h - margin.y, margin.y])
.clamp(true);
var axisY = d3.svg.axis()
.scale(scaleY)
.orient("left")
.ticks(5);
plot("linear");
plot("step-before");
plot("step-after");
plot("basis");
plot("basis-open");
plot("basis-closed");
plot("bundle");
plot("cardinal");
plot("cardinal-open");
plot("cardinal-closed");
plot("monotone");
function plot(linetype) {
var svg = d3.select("body").append("svg").attr({width: w, height: h}).style("padding","5px");
svg.append("rect").attr({x:0, y:0, width:w, height:h, stroke:"black", fill:"snow"});
var line = d3.svg.line()
.x(function(d) { return scaleX(d.date); })
.y(function(d) { return scaleY(d.value); })
.interpolate(linetype);
svg.append("path")
.attr("d", line(dataset))
.attr("stroke", "blue")
.attr("stroke-width", 2)
.attr("fill", "none");
svg.selectAll("circle")
.data(dataset)
.enter()
.append("circle")
.attr("cx", function(d){ return scaleX(d.date); })
.attr("cy", function(d){ return scaleY(d.value); })
.attr("r", "2");
svg.append("g").selectAll("text")
.data(dataset)
.enter()
.append("text")
.attr({
"font-size" : 10,
"x" : function(d){ return scaleX(d.date); },
"y" : function(d){ return scaleY(d.value) + 10; }
})
.text(function(d){
return d.value;
});
svg.append("g")
.attr("class", "axis")
.attr("transform", "translate(0," + (h - margin.y) + ")")
.call(axisX);
svg.append("g")
.attr("class", "axis")
.attr("transform", "translate(" + margin.x + ",0)")
.call(axisY);
svg.append("text")
.attr({
"font-size" : 15,
"text-anchor" : "start",
"x" : w/2,
"y" : 15})
.text(linetype);
}
</script>
</body>
</html>
var line = d3.svg.line()
.x(function(d) { return scaleX(d.date); })
.y(function(d) { return scaleY(d.value); })
.interpolate(linetype);
svg.append("path")
.attr("d", line(dataset))
.attr("stroke", "blue")
.attr("stroke-width", 2)
.attr("fill", "none");
- 見ればわかるわな
- linetype には、色々な補間アルゴリズムを使うことができる
塗りつぶし、目盛線 †
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Axis</title>
<script type="text/javascript" src="https://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<style type="text/css">
.axis path,
.axis line {
fill: none;
stroke: black;
shape-rendering: crispEdges;
}
.axis text {
font-family: sans-serif;
font-size: 8px;
}
.axis .major line {
fill: none;
stroke: cyan;
stroke-dasharray: 5,5;
shape-rendering: crispEdges;
}
.axis .major text {
font-family: sans-serif;
font-size: 12px;
stroke: yellowgreen;
}
.axis .minor {
fill: none;
stroke: cyan;
stroke-dasharray: 5,10;
shape-rendering: crispEdges;
}
</style>
</head>
<body>
<script type="text/javascript">
var w = 300;
var h = 150;
var margin = {x:50, y:20};
// N225
var dataset = [
{"date":1371999600000,"value":13062},
{"date":1372086000000,"value":12969},
{"date":1372172400000,"value":12834},
{"date":1372258800000,"value":13213},
{"date":1372345200000,"value":13677},
{"date":1372604400000,"value":13852}
];
// min value
var minX = d3.min(dataset, function(d) {
return d.date;
});
// max value
var maxX = d3.max(dataset, function(d) {
return d.date;
});
var scaleX = d3.scale.linear()
.domain([minX - 86400000, maxX + 86400000])
.rangeRound([margin.x, w - margin.x])
.clamp(true);
var formatX = function(time){
var formatter = d3.time.format("%b%d");
return formatter(new Date(time));
};
var axisX = d3.svg.axis()
.scale(scaleX)
.orient("bottom")
.ticks(5) // That's only hint, d3.js will overwrite concise number !
.tickFormat(formatX)
.tickSubdivide(true)
.tickSize(-(h - margin.y * 2), -(h - margin.y * 2), 0);
// min value
var minY = d3.min(dataset, function(d) {
return d.value;
});
// max value
var maxY = d3.max(dataset, function(d) {
return d.value;
});
var scaleY = d3.scale.linear()
.domain([minY - 100, maxY + 100])
.nice()
.rangeRound([h - margin.y, margin.y])
.clamp(true);
var axisY = d3.svg.axis()
.scale(scaleY)
.orient("left")
.ticks(5)
.tickSubdivide(true)
.tickSize(-(w - margin.x * 2), 3, 0);
plot("linear");
plot("step-before");
plot("step-after");
plot("basis");
plot("basis-open");
plot("basis-closed");
plot("bundle");
plot("cardinal");
plot("cardinal-open");
plot("cardinal-closed");
plot("monotone");
function plot(linetype) {
var svg = d3.select("body").append("svg").attr({width: w, height: h}).style("padding","5px");
svg.append("rect").attr({x:0, y:0, width:w, height:h, stroke:"black", fill:"snow"});
var line = d3.svg.line()
.x(function(d) { return scaleX(d.date); })
.y(function(d) { return scaleY(d.value); })
.interpolate(linetype);
var area = d3.svg.area()
.x(function(d) { return scaleX(d.date); })
.y0(function(d) { return scaleY(0); })
.y1(function(d) { return scaleY(d.value); })
.interpolate(linetype);
svg.append("path")
.attr("d", line(dataset))
.attr("stroke", "blue")
.attr("stroke-width", 2)
.attr("fill", "none");
svg.append("path")
.attr("d", area(dataset))
.attr("fill", "pink")
.attr("opacity", 0.2);
svg.selectAll("circle")
.data(dataset)
.enter()
.append("circle")
.attr("cx", function(d){ return scaleX(d.date); })
.attr("cy", function(d){ return scaleY(d.value); })
.attr("r", "2");
svg.append("g").selectAll("text")
.data(dataset)
.enter()
.append("text")
.attr({
"font-size" : 10,
"x" : function(d){ return scaleX(d.date); },
"y" : function(d){ return scaleY(d.value) + 10; }
})
.text(function(d){
return d.value;
});
svg.append("g")
.attr("class", "axis")
.attr("transform", "translate(0," + (h - margin.y) + ")")
.call(axisX);
svg.append("g")
.attr("class", "axis")
.attr("transform", "translate(" + margin.x + ",0)")
.call(axisY);
svg.append("text")
.attr({
"font-size" : 15,
"text-anchor" : "start",
"x" : w/2,
"y" : 15})
.text(linetype);
}
</script>
</body>
</html>
- 塗りつぶし
var area = d3.svg.area()
.x(function(d) { return scaleX(d.date); })
.y0(function(d) { return scaleY(0); })
.y1(function(d) { return scaleY(d.value); })
.interpolate(linetype);
svg.append("path")
.attr("d", area(dataset))
.attr("fill", "pink")
.attr("opacity", 0.2);
- 目盛線
var axisX = d3.svg.axis()
.scale(scaleX)
.orient("bottom")
.ticks(5) // That's only hint, d3.js will overwrite concise number !
.tickFormat(formatX)
.tickSubdivide(true)
.tickSize(-(h - margin.y * 2), -(h - margin.y * 2), 0);
- 軸から、値の文字列 (tick) に伸びている短いヒゲを、tick とは逆方向に目一杯伸ばしてやれば目盛線になる
- .tickSubdivide(ture) : tick と tick とのあいだに補助目盛線を入れるか
- .tickSize(major) , .tickSize(major, minor) , .tickSize(major, minor, end) : 目盛線の長さ
- major (tickへの目盛線)
- minor (補助目盛線)
- end (最後)
- デフォルト値は .tickSize(6,3,0)
- 目盛線のスタイル
.axis .major line {
fill: none;
stroke: cyan;
stroke-dasharray: 5,5;
shape-rendering: crispEdges;
}
.axis .major text {
font-family: sans-serif;
font-size: 12px;
stroke: yellowgreen;
}
.axis .minor {
fill: none;
stroke: cyan;
stroke-dasharray: 5,10;
shape-rendering: crispEdges;
}
- .major text : tick
- .major line : 目盛線
- .minor : 補助目盛線
- D3.jsの axis は、SVG上では <g class=".major"><text><line></g> <line class="minor> というように展開されている
棒グラフ ordinal scale †
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Bar</title>
<script type="text/javascript" src="https://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<style type="text/css">
.axis path,
.axis line {
fill: none;
stroke: black;
shape-rendering: crispEdges;
}
.axis text {
font-family: sans-serif;
font-size: 8px;
}
</style>
</head>
<body>
<script type="text/javascript">
var w = 300;
var h = 150;
var margin = {x:50, y:20, title:15};
// Browser Share (2012-05)
var dataset = [
{"browser":"IE","share":28.87},
{"browser":"Chrome","share":29.15},
{"browser":"Firefox","share":22.97},
{"browser":"Safari","share":8.68},
{"browser":"Opera","share":3.80},
{"browser":"Android","share":2.13}
];
var scaleX = d3.scale.ordinal()
.domain(dataset.map(function(d){return d.browser;}))
.rangeRoundBands([0,w],0.05);
var axisX = d3.svg.axis()
.scale(scaleX)
.orient("bottom")
.tickSize(0); // 棒グラフなので X 軸から、値ラベルまでの伸びるヒゲは無しにする
// min value
var minY = d3.min(dataset, function(d) {
return d.share;
});
// max value
var maxY = d3.max(dataset, function(d) {
return d.share;
});
var scaleY = d3.scale.linear()
.domain([minY, maxY])
.nice()
.rangeRound([margin.y, h - margin.y - margin.title])
.clamp(true);
var svg = d3.select("body").append("svg").attr({width: w, height: h});
svg.append("rect").attr({x:0, y:0, width:w, height:h, stroke:"black", fill:"snow"});
svg.append("g").selectAll("rect")
.data(dataset)
.enter()
.append("rect")
.attr({
"x" : function(d){ return scaleX(d.browser); },
"y" : function(d){ return h - margin.y - scaleY(d.share); },
"width" : scaleX.rangeBand(),
"height" : function(d){ return scaleY(d.share); },
"fill" : function(d){ return "rgb(64, 64, " + Math.floor(d.share * 255 / maxY) + ")";}
});
svg.append("g").selectAll("text")
.data(dataset)
.enter()
.append("text")
.text(function(d){ return d.share; })
.attr({
"x" : function(d){ return scaleX(d.browser) + scaleX.rangeBand() / 2; },
"y" : function(d){ return h - margin.y - scaleY(d.share) + 14; },
"fill" : "white",
"text-anchor" : "middle"
});
svg.append("g")
.attr("class", "axis")
.attr("transform", "translate(0," + (h - margin.y) + ")")
.call(axisX);
svg.append("text")
.attr({
"font-size" : 15,
"text-anchor" : "middle",
"x" : w/2,
"y" : 15})
.text("Browser Share");
</script>
</body>
</html>
- domain スケール
var dataset = [
{"browser":"IE","share":28.87},
{"browser":"Chrome","share":29.15},
{"browser":"Firefox","share":22.97},
{"browser":"Safari","share":8.68},
{"browser":"Opera","share":3.80},
{"browser":"Android","share":2.13}
];
var scaleX = d3.scale.ordinal()
.domain(dataset.map(function(d){return d.browser;}))
.rangeRoundBands([0,w],0.05);
var axisX = d3.svg.axis()
.scale(scaleX)
.orient("bottom")
.tickSize(0); // 棒グラフなので X 軸から、値ラベルまでの伸びるヒゲは無しにする
- 文字列などの値を定義域や値域に指定できる。(もちろん数値でも良い)
- domain スケールの場合には、変換後の値に rangeRoundBands?() を使うことができる。rangeRoundBands?([0,w],0.05) は、最小値 0 最大値 w マージン 5%
- 当然 nice() とかは使えない
- array.map( func )
- その他の Array の機能 + D3.js のヘルパーfunction https://github.com/mbostock/d3/wiki/Arrays
アニメーション(1) 値の変更 †
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Bar</title>
<script type="text/javascript" src="https://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<style type="text/css">
.axis path,
.axis line {
fill: none;
stroke: black;
shape-rendering: crispEdges;
}
.axis text {
font-family: sans-serif;
font-size: 8px;
}
</style>
</head>
<body>
<div id="chart"></div>
<button id="2011-06">2011-06</button>
<button id="2011-09">2011-09</button>
<button id="2012-01">2012-01</button>
<button id="2012-05">2012-05</button>
<select id="ease">
<option value="linear">linear</option>
<option value="quad">quad</option>
<option value="cubic-in-out" selected="">cubic</option>
<option value="sin">sin</option>
<option value="exp">exp</option>
<option value="bounce">bounce</option>
</select>
<script type="text/javascript">
var w = 300;
var h = 300;
var margin = {x:50, y:20, title:15};
// Browser Share (2011-06 to 2012-05)
var dataset = {
"2012-05" : [
{"browser":"IE","share":28.87},
{"browser":"Chrome","share":29.15},
{"browser":"Firefox","share":22.97},
{"browser":"Safari","share":8.68},
{"browser":"Opera","share":3.80},
{"browser":"Android","share":2.13}
],
"2012-01" : [
{"browser":"IE","share":34.27},
{"browser":"Chrome","share":25.08},
{"browser":"Firefox","share":23.24},
{"browser":"Safari","share":7.98},
{"browser":"Opera","share":3.84},
{"browser":"Android","share":1.71}
],
"2011-09" : [
{"browser":"IE","share":38.85},
{"browser":"Chrome","share":22.02},
{"browser":"Firefox","share":24.98},
{"browser":"Safari","share":6.64},
{"browser":"Opera","share":3.06},
{"browser":"Android","share":1.34}
],
"2011-06" : [
{"browser":"IE","share":40.73},
{"browser":"Chrome","share":19.30},
{"browser":"Firefox","share":26.49},
{"browser":"Safari","share":4.74},
{"browser":"Opera","share":1.63},
{"browser":"Android","share":1.28}
]
}
var scaleX = d3.scale.ordinal()
.domain(dataset["2011-06"].map(function(d){return d.browser;}))
.rangeRoundBands([0,w],0.05);
var axisX = d3.svg.axis()
.scale(scaleX)
.orient("bottom")
.tickSize(0); // 棒グラフなので X 軸から、値ラベルまでの伸びるヒゲは無しにする
var minY = Number.MAX_VALUE;
var maxY = Number.MIN_VALUE;
for (key in dataset) {
minY = Math.min( minY, d3.min(dataset[key], function(d) { return d.share; }));
maxY = Math.max( maxY, d3.max(dataset[key], function(d) { return d.share; }));
}
var scaleY = d3.scale.linear()
.domain([minY, maxY])
.nice()
.rangeRound([margin.y, h - margin.y - margin.title])
.clamp(true);
var svg = d3.select("#chart").append("svg").attr({width: w, height: h});
svg.append("rect").attr({x:0, y:0, width:w, height:h, stroke:"black", fill:"snow"});
var gBarRects = svg.append("g");
gBarRects.selectAll("rect")
.data(dataset["2011-06"])
.enter()
.append("rect")
.attr({
"x" : function(d){ return scaleX(d.browser); },
"y" : function(d){ return h - margin.y - scaleY(d.share); },
"width" : scaleX.rangeBand(),
"height" : function(d){ return scaleY(d.share); },
"fill" : function(d){ return "rgb(64, 64, " + Math.floor(d.share * 255 / maxY) + ")";}
});
var gBarTexts = svg.append("g");
gBarTexts.selectAll("text")
.data(dataset["2011-06"])
.enter()
.append("text")
.text(function(d){ return d.share; })
.attr({
"x" : function(d){ return scaleX(d.browser) + scaleX.rangeBand() / 2; },
"y" : function(d){ return h - margin.y - scaleY(d.share) + 14; },
"fill" : "white",
"text-anchor" : "middle"
});
svg.append("g")
.attr("class", "axis")
.attr("transform", "translate(0," + (h - margin.y) + ")")
.call(axisX);
var txtTitle = svg.append("text");
txtTitle
.attr({
"font-size" : 15,
"text-anchor" : "middle",
"x" : w/2,
"y" : 15})
.text("Browser Share 2011-06");
d3.selectAll("button").on("click", function() {
var term = this.id;
gBarTexts.selectAll("text")
.data(dataset[term])
.text(function(d){ return d.share; })
.transition()
.duration(100)
.delay(function(d,i){ return i*100; })
.attr({
"y" : function(d){ return h - margin.y - scaleY(d.share) + 14; },
});
gBarRects.selectAll("rect")
.data(dataset[term])
.transition()
.duration(1000)
.ease(document.getElementById("ease").value)
.delay(function(d,i){ return i*100; })
.attr({
"y" : function(d){ return h - margin.y - scaleY(d.share); },
"height" : function(d){ return scaleY(d.share); },
"fill" : function(d){ return "rgb(64, 64, " + Math.floor(d.share * 255 / maxY) + ")";}
});
txtTitle
.transition()
.text("Browser Share " + term);
});
</script>
</body>
</html>
- 最初は、定義域が変わらないアニメーション
- 時系列データすべての min, max を取得する
var minY = Number.MAX_VALUE;
var maxY = Number.MIN_VALUE;
for (key in dataset) {
minY = Math.min( minY, d3.min(dataset[key], function(d) { return d.share; }));
maxY = Math.max( maxY, d3.max(dataset[key], function(d) { return d.share; }));
}
- transition() 以下がアニメーションの設定になる
gBarRects.selectAll("rect")
.data(dataset[term])
.transition()
.duration(1000)
.ease(document.getElementById("ease").value)
.delay(function(d,i){ return i*100; })
.attr({
"y" : function(d){ return h - margin.y - scaleY(d.share); },
"height" : function(d){ return scaleY(d.share); },
"fill" : function(d){ return "rgb(64, 64, " + Math.floor(d.share * 255 / maxY) + ")";}
});
アニメーション(2) 軸の変更 †
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Axis</title>
<script type="text/javascript" src="https://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<style type="text/css">
.axisX path,
.axisX line {
fill: none;
stroke: gray;
shape-rendering: crispEdges;
}
.axisX text {
font-family: sans-serif;
fill: gray;
font-size: 9px;
}
.axisY path,
.axisY line {
fill: none;
stroke: gray;
shape-rendering: crispEdges;
}
.axisY text {
font-family: sans-serif;
fill: gray;
font-size: 9px;
}
</style>
</head>
<body>
<div id="chart"></div>
<button value="rotate">Rotate</button>
<button value="reverse">Reverse</button>
<button value="out">Zoom Out</button>
<button value="in">Zoom In</button>
<script type="text/javascript">
function fx(t) {
return fr(t) * Math.cos(t);
}
function fy(t) {
return fr(t) * Math.sin(t);
}
function fr(t) {
return Math.exp(0.01*t);
}
function rad2rgb(rad) {
var r,g,b;
var deg = (rad * 180 / Math.PI) % 360;
if (deg < 60) {
r = 255;
g = 255 * deg / 60;
b = 0;
} else if (deg < 120) {
r = 255 - ( 255 * (deg - 60) / 60 );
g = 255;
b = 0;
} else if (deg < 180) {
r = 0;
g = 255;
b = 255 * (deg -120) / 60;
} else if (deg < 240) {
r = 0;
g = 255 - ( 255 * (deg - 180) / 60 );
b = 255;
} else if (deg < 300) {
r = 255 * (deg -240) / 60;
g = 0;
b = 255;
} else {
r = 255;
g = 0;
b = 255 - ( 255 * (deg - 300) / 60 );
}
return "rgba(" + Math.floor(r) + "," + Math.floor(g) + "," + Math.floor(b) + ",0.8)";
}
var w = 400;
var h = 400;
var margin = {x:40, y:40};
var t = 0.0;
var dataset = new Array();
for (var p = 0; p < 255; p++) {
dataset.push([fx(t), fy(t), t]);
t += Math.PI * 0.13;
}
var minX = d3.min(dataset, function(d) {return d[0];});
var maxX = d3.max(dataset, function(d) {return d[0];});
var minY = d3.min(dataset, function(d) {return d[1];});
var maxY = d3.max(dataset, function(d) {return d[1];});
var scaleX = d3.scale.linear()
.domain([minX, maxX])
.rangeRound([margin.x, w - margin.x])
.nice();
var axisX = d3.svg.axis()
.scale(scaleX)
.orient("bottom");
var scaleY = d3.scale.linear()
.domain([minY, maxY])
.rangeRound([h - margin.y, margin.y])
.nice();
var axisY = d3.svg.axis()
.scale(scaleY)
.orient("left");
var svg = d3.select("#chart").append("svg").attr({width: w, height: h});
svg.selectAll("circle")
.data(dataset)
.enter()
.append("circle")
.attr("cx", function(d){
return scaleX(d[0]);
})
.attr("cy", function(d){
return scaleY(d[1]);
})
.attr("fill", function(d){return rad2rgb(d[2]); })
.attr("r", function(d){return scaleX(fr(d[2])) - scaleX(fr(d[2]-2*Math.PI)); });
svg.append("g")
.attr("class", "axisX")
.attr("transform", "translate(0," + scaleY(0) + ")")
.call(axisX);
svg.append("g")
.attr("class", "axisY")
.attr("transform", "translate(" + scaleX(0) + ",0)")
.call(axisY);
var rotate = 0;
d3.selectAll("button").on("click", function() {
var btn = this.value;
switch(btn) {
case 'rotate' :
rotate = (rotate + 1) % 4;
svg.selectAll("circle")
.data(dataset)
.transition()
.ease("bounce")
.duration(2000)
.attr("cx", function(d){
return scaleX((rotate == 0 || rotate == 1) ? d[0] : -1 * d[0]);
})
.attr("cy", function(d){
return scaleY((rotate == 0 || rotate == 3) ? d[1] : -1 * d[1]);
});
break;
case 'reverse' :
rotate = (rotate + 2) % 4;
svg.selectAll("circle")
.data(dataset)
.transition()
.delay(function(d,i){ return i; })
.ease("bounce")
.duration(2000)
.attr("cx", function(d){
return scaleX((rotate == 0 || rotate == 1) ? d[0] : -1 * d[0]);
})
.attr("cy", function(d){
return scaleY((rotate == 0 || rotate == 3) ? d[1] : -1 * d[1]);
});
break;
case 'in' :
t -= Math.PI * 0.13 * 255 * 2;
case 'out' :
for (var p = 0; p < 255; p++) {
dataset[p] = [fx(t), fy(t), t];
t += Math.PI * 0.13;
}
svg.selectAll("circle")
.data(dataset)
.transition()
.attr("cx", function(d){
return scaleX((rotate == 0 || rotate == 1) ? d[0] : -1 * d[0]);
})
.attr("cy", function(d){
return scaleY((rotate == 0 || rotate == 3) ? d[1] : -1 * d[1]);
});
minX = d3.min(dataset, function(d) {return d[0];});
maxX = d3.max(dataset, function(d) {return d[0];});
minY = d3.min(dataset, function(d) {return d[1];});
maxY = d3.max(dataset, function(d) {return d[1];});
scaleX.domain([minX,maxX]);
scaleY.domain([minY,maxY]);
svg.selectAll("circle")
.data(dataset)
.transition()
.delay(function(d,i){ return 500+i; })
.ease("bounce")
.duration(1000)
.attr("cx", function(d){
return scaleX((rotate == 0 || rotate == 1) ? d[0] : -1 * d[0]);
})
.attr("cy", function(d){
return scaleY((rotate == 0 || rotate == 3) ? d[1] : -1 * d[1]);
});
svg.select(".axisX")
.transition()
.delay(500)
.duration(1000)
.call(axisX);
svg.select(".axisY")
.transition()
.delay(500)
.duration(1000)
.call(axisY);
break;
}
});
</script>
</body>
</html>
- ただ軸のmin maxを変えるのも何なので、余計なアニメーションが付いてる
- 描画した図形は、ベルヌーイの螺旋 。縮小しても拡大しても同じフラクタル図形
- 色は、色相環 。cf. Basic 補色について
- 軸の min max を変えるのは簡単
scaleX.domain([minX,maxX]);
scaleY.domain([minY,maxY]);
svg.select(".axisX")
.transition()
.delay(500)
.duration(1000)
.call(axisX);
svg.select(".axisY")
.transition()
.delay(500)
.duration(1000)
.call(axisY);
- scale を変更する
- X軸、Y軸が格納されている <g></g> に対して、.transition().call(axisX) でアニメーション付きの再描画する
- 当然 XY 平面上のデータのプロット点も新しい scale に合わせて再描画する必要がある (今回の場合、フラクタル図形なので、再描画しても見た目上変化がない)
- 必要があれば、軸を描画する位置を再調整する必要がある。ただし人間の感覚的には、軸自体が動くのはよろしくない(わかりやすくするためにグラフにしているのに混乱させてどうするのさ)。
アニメーション(3) 描画範囲内に描画する (+each()) †
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Heart</title>
<script type="text/javascript" src="https://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<style type="text/css">
.axisX path,
.axisX line {
fill: none;
stroke: gray;
shape-rendering: crispEdges;
}
.axisX text {
font-family: sans-serif;
fill: gray;
font-size: 9px;
}
.axisY path,
.axisY line {
fill: none;
stroke: gray;
shape-rendering: crispEdges;
}
.axisY text {
font-family: sans-serif;
fill: gray;
font-size: 9px;
}
</style>
</head>
<body>
<div id="chart"></div>
<button value="in" disabled>Zoom In</button>
<button value="out">Zoom Out</button>
<script type="text/javascript">
function fx(t) {
return Math.pow(Math.sin(t), 3);
}
function fy(t) {
return 1.0 + 0.8125 * Math.cos(t) - 0.3125 * Math.cos(2 * t) - 0.125 * Math.cos(3*t) - 0.0625 * Math.cos(4*t);
}
var w = 300;
var h = 300;
var margin = {x:20, y:20};
var dataset = new Array();
for (var t = 0.0; t < 2*Math.PI; t+=0.1) {
dataset.push([fx(t), fy(t)]);
}
var scaleX = d3.scale.linear()
.domain([-0.2, 0.2])
.rangeRound([margin.x, w - margin.x])
.nice();
var axisX = d3.svg.axis()
.scale(scaleX)
.orient("bottom");
var scaleY = d3.scale.linear()
.domain([-0.2, 0.2])
.rangeRound([h - margin.y, margin.y])
.nice();
var axisY = d3.svg.axis()
.scale(scaleY)
.orient("left");
var line = d3.svg.area()
.x(function(d) { return scaleX(d[0]); })
.y(function(d) { return scaleY(d[1]); })
.interpolate("monotone");
var svg = d3.select("#chart").append("svg").attr({width: w, height: h});
var chartArea
= svg.append("clipPath")
.attr("id", "dataPanel")
.append("rect")
.attr({
x: margin.x,
y: margin.y,
width: w - margin.x * 2,
height: h - margin.y * 2
});
svg.append("g")
.attr("id", "circles")
.attr("clip-path", "url(#dataPanel)")
.selectAll("circle")
.data(dataset)
.enter()
.append("circle")
.attr("cx", function(d){
return scaleX(d[0]);
})
.attr("cy", function(d){
return scaleY(d[1]);
})
.attr("fill", "black")
.attr("r", 3);
svg.append("g")
.attr("id", "line")
.attr("clip-path", "url(#dataPanel)")
.append("path")
.attr("d", line(dataset))
.attr("stroke", "black");
svg.append("g")
.attr("class", "axisX")
.attr("transform", "translate(0," + scaleY(0) + ")")
.call(axisX);
svg.append("g")
.attr("class", "axisY")
.attr("transform", "translate(" + scaleX(0) + ",0)")
.call(axisY);
d3.selectAll("button").on("click", function() {
var btn = this.value;
d3.selectAll("button").attr("disabled", null);
switch(btn) {
case 'in' :
scaleX.domain([-0.2,0.2]);
scaleY.domain([-0.2,0.2]);
break;
case 'out' :
minX = d3.min(dataset, function(d) {return d[0];}) - 0.1;
maxX = d3.max(dataset, function(d) {return d[0];}) + 0.1;
minY = d3.min(dataset, function(d) {return d[1];});
maxY = d3.max(dataset, function(d) {return d[1];});
scaleX.domain([minX,maxX]).nice();
scaleY.domain([minY,maxY]).nice();
break;
}
var DULATION = 3000;
svg.select("#circles").selectAll("circle")
.data(dataset)
.transition()
.duration(DULATION)
.ease("bounce")
.each("start", function(){
d3.select(this)
.attr("fill","red");
})
.each("end", function(){
d3.select(this)
.attr("fill","pink")
})
.attr("cx", function(d){
return scaleX(d[0]);
})
.attr("cy", function(d){
return scaleY(d[1]);
})
.attr("r", 0);
svg.select("#line").selectAll("path")
.data(dataset)
.transition()
.duration(DULATION)
.ease("bounce")
.attr("d", line(dataset))
.attr("stroke", "pink")
.attr("stroke-width", 2);
svg.select(".axisX")
.transition()
.duration(DULATION)
.ease("bounce")
.call(axisX)
.attr("transform", "translate(0," + scaleY(0) + ")");
svg.select(".axisY")
.transition()
.duration(DULATION)
.ease("bounce")
.call(axisY);
});
</script>
</body>
</html>
- 今まで作ってきたのは、全データが画面に表示されるグラフ。一部を表示したいときにはどうするのさ?
- SVG の <clippath id="dataPanel"> を使う
- clippath は、画面上の領域を示す。
- 領域は <clippath>...</clippaath> 内に書かれた図形の和 (文字列もOK)
- ここでは x=20, y=20, width=260, height=260 の矩形を指定している
- <g clip-path="url(#dataPanel)">...</g>
- この g (group) 内に書かれた図形は、<clippath id="dataPanel"> で示される領域の中だけしか描画されない
- .transition().each("start", function(){...})
- transition() 開始時に実行されるイベントフック
- this は、現在処理中の SVG タグ。d3.select(this).attr(...) などのようにする
- each()内で、transition() を使わないこと! D3.js のアニメーションは、新しいアニメーションをしようとしたら実行中のアニメーションを強制終了する。(jQuery は、逆に Queue に積んで逐次実行)
- .transition().each("end", function(){...})
- transition() 終了後に実行されるイベントフック
- ここでも each()内で、transition() を使わない方が無難。一連の操作の最後でよばれる each() 内なら transition() を使えるけど、データのプロット点(circle)と軸(axis)をアニメーションで同時に動かしているときに、プロット点の each("end",funtion(){}) で transiont() を使うと、軸のアニメーションが止まる
- アニメーションと関係なく d3.selectAll().each(function(){}) とかもできる
インタラクション (データの増減 + イベント処理) †
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Bar</title>
<style type="text/css">
.axis path,
.axis line {
fill: none;
stroke: black;
shape-rendering: crispEdges;
}
.axis text {
font-family: sans-serif;
font-size: 8px;
}
#tooltips {
position: absolute;
padding: 10px;
color: black;
font-size: 12px;
background: white;
border-radius: 10px;
border-style:solid;
border-width:2px;
border-color: red;
opacity: 0.8
}
#tooltips.hidden {
display: none;
}
</style>
</head>
<body>
<div id="chart">
<div id="tooltips" class="hidden">TOOLTIPS</div>
</div>
<button value="prev">Prev</button>
<button value="next">Next</button>
<script type="text/javascript" src="https://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<script type="text/javascript" src="N225.js" charset="utf-8"></script>
<script type="text/javascript">
var w = 700;
var h = 150;
var margin = {x:50, y:20, title:15};
var minY = d3.min(N225, function(d) { return d.end; });
var maxY = d3.max(N225, function(d) { return d.end; });
// 表示するデータ
var dataset = new Array();
dataset.push(getData("2013-07"));
// X軸
var scaleX = d3.scale.ordinal()
.domain(dataset.map(function(d){return d.date;}))
.rangeRoundBands([0,w],0.05);
var axisX = d3.svg.axis()
.scale(scaleX)
.orient("bottom")
.tickSize(0);
// Y軸
var scaleY = d3.scale.linear()
.domain([minY, maxY])
.nice()
.rangeRound([margin.y, h - margin.y - margin.title])
.clamp(true);
// グラフ描画領域
var svg = d3.select("#chart").append("svg").attr({width: w, height: h});
// データ描画領域
var dataPane = svg.append("g");
// 棒グラフの集合 (棒グラフ一つ分が <g> になる)
dataPane.selectAll("g")
.data(dataset)
.enter()
.append("g")
.each(function(d,i){
addOrUpdateBar(
d3.select(this).append("rect"),
d3.select(this).append("rect"),
d3.select(this).append("text"),
d);
})
.on("mouseover", function(d,i){
showTooltips(d);
d3.select(this).select(".bar").transition().duration(500).attr("fill","orange");
})
.on("mouseout", function(d,i){
hideTooltips();
d3.select(this).select(".bar").transition().duration(500)
.attr("fill","rgb(64, 64, " + Math.floor(d.end * 255 / maxY) + ")");
});
// X軸を描画
svg.append("g")
.attr("class", "axis")
.attr("transform", "translate(0," + (h - margin.y) + ")")
.call(axisX);
// ボタンをクリックしたときのイベント処理
d3.selectAll("button").on("click", function() {
var btnName = this.value;
switch(this.value){
case "prev" :
var newData = getPrev(dataset[0].date);
if (newData) {
dataset.unshift(newData);
while(dataset.length > 10) {
dataset.pop();
}
} else {
if(dataset.length > 1) {
dataset.pop();
}
}
break;
case "next" :
var newData = getNext(dataset[dataset.length - 1].date);
if (newData) {
dataset.push(newData);
while(dataset.length > 10) {
dataset.shift();
}
} else {
if(dataset.length > 1) {
dataset.shift();
}
}
break;
}
scaleX
.domain(dataset.map(function(d){return d.date;}));
scaleY
.domain([minY, maxY])
.rangeRound([margin.y, h - margin.y - margin.title]);
var update = dataPane.selectAll("g").data(dataset);
update
.each(function(d,i){
addOrUpdateBar(
d3.select(this).select(".space").transition(),
d3.select(this).select(".bar").transition().duration(500).ease("bounce"),
d3.select(this).select("text").transition().duration(500).ease("bounce"),
d);
});
update.enter()
.append("g")
.each(function(d,i){
addOrUpdateBar(
d3.select(this).append("rect"),
d3.select(this).append("rect"),
d3.select(this).append("text"),
d);
})
.on("mouseover", function(d,i){
showTooltips(d);
d3.select(this).select(".bar").transition().duration(500).attr("fill","orange");
})
.on("mouseout", function(d,i){
hideTooltips();
d3.select(this).select(".bar").transition().duration(500)
.attr("fill","rgb(64, 64, " + Math.floor(d.end * 255 / maxY) + ")");
});
update.exit()
.each(function(d,i){
d3.select(this).select("rect").transition().duration(500).ease("bounce").attr("x",w);
d3.select(this).select("text").transition().duration(500).ease("bounce").attr("x",w);
})
.remove();
svg.select(".axis")
.transition()
.duration(500)
.ease("bounce")
.call(axisX)
.attr("transform", "translate(0," + (h - margin.y) + ")")
});
function addOrUpdateBar(spaceRect, barRect, text, d) {
spaceRect.attr({
"class" : "space",
"x" : scaleX(d.date),
"y" : 0,
"width" : scaleX.rangeBand(),
"height" : h,
"fill" : "rgba(0,0,0,0)"
});
barRect.attr({
"class" : "bar",
"x" : scaleX(d.date),
"y" : h - margin.y - scaleY(d.end),
"width" : scaleX.rangeBand(),
"height" : scaleY(d.end),
"fill" : "rgb(64, 64, " + Math.floor(d.end * 255 / maxY) + ")"
});
text.text(Math.floor(d.end)).attr({
"x" : scaleX(d.date) + scaleX.rangeBand() / 2,
"y" : h - margin.y - scaleY(d.end) + 14,
"font-size" : "10px",
"fill" : "white",
"text-anchor" : "middle"
});
}
function showTooltips(d) {
d3.select("#tooltips")
.style("left", (scaleX(d.date) + scaleX.rangeBand()) + "px")
.style("top", (margin.y) + "px")
.classed("hidden", false)
.html(d.date
+ "<br/>始値" + Math.floor(d.start)
+ "<br/>安値" + Math.floor(d.low)
+ "<br/>高値" + Math.floor(d.high)
+ "<br/>終値" + Math.floor(d.end));
}
function hideTooltips() {
d3.select("#tooltips")
.classed("hidden", true);
}
</script>
</body>
</html>
- インタラクション処理を行うための SVG の構造
- イベントの単位で <g> にまとめる
- <g> にイベントハンドラを貼り付ける
- もしも rect と text が別々なら、マウスが rect から text に移ったときに mouseover イベントが発生してしまう。それに対する対処が必要
- イベントが発火する範囲を定義するために、<g> に透明な rect を含ませる。(見えている棒グラフの上あたりにマウスが来ても選択されるようにしたい)
- 棒グラフ一つ分はこんな感じ
// 棒グラフの集合 (棒グラフ一つ分が <g> になる)
dataPane.selectAll("g")
.data(dataset)
.enter()
.append("g")
.each(function(d,i){
addOrUpdateBar(
d3.select(this).append("rect"),
d3.select(this).append("rect"),
d3.select(this).append("text"),
d);
})
function addOrUpdateBar(spaceRect, barRect, text, d) {
spaceRect.attr({
"class" : "space",
"x" : scaleX(d.date),
"y" : 0,
"width" : scaleX.rangeBand(),
"height" : h,
"fill" : "rgba(0,0,0,0)"
});
barRect.attr({
"class" : "bar",
"x" : scaleX(d.date),
"y" : h - margin.y - scaleY(d.end),
"width" : scaleX.rangeBand(),
"height" : scaleY(d.end),
"fill" : "rgb(64, 64, " + Math.floor(d.end * 255 / maxY) + ")"
});
text.text(Math.floor(d.end)).attr({
"x" : scaleX(d.date) + scaleX.rangeBand() / 2,
"y" : h - margin.y - scaleY(d.end) + 14,
"font-size" : "10px",
"fill" : "white",
"text-anchor" : "middle"
});
}
- データが増減した場合の既存の SVG 要素の変更
update
.each(function(d,i){
addOrUpdateBar(
d3.select(this).select(".space").transition(),
d3.select(this).select(".bar").transition().duration(500).ease("bounce"),
d3.select(this).select("text").transition().duration(500).ease("bounce"),
d);
});
- データが増えた場合の新たな SVG 要素の作成
update.enter()
.append("g")
.each(function(d,i){
addOrUpdateBar(
d3.select(this).append("rect"),
d3.select(this).append("rect"),
d3.select(this).append("text"),
d);
})
- データが減った場合に、アニメーションで SVG 要素を削除する
update.exit()
.each(function(d,i){
d3.select(this).select("rect").transition().duration(500).ease("bounce").attr("x",w);
d3.select(this).select("text").transition().duration(500).ease("bounce").attr("x",w);
})
.remove();
画面の左端までアニメーションで移動してから remove() する
- データが増減した場合の X 軸の書き換え
scaleX
.domain(dataset.map(function(d){return d.date;}));
svg.select(".axis")
.transition()
.duration(500)
.ease("bounce")
.call(axisX)
.attr("transform", "translate(0," + (h - margin.y) + ")")
- イベント処理の定義
- これまでもボタンクリックイベントなどで
d3.selectAll("button").on("click", function() {
とかやってきた
- データがバインドされているようその場合には function(d,i) とできる
dataPane.selectAll("g")
.data(dataset)
.enter()
.append("g")
.each(function(d,i){
addOrUpdateBar(
d3.select(this).append("rect"),
d3.select(this).append("rect"),
d3.select(this).append("text"),
d);
})
.on("mouseover", function(d,i){
showTooltips(d);
d3.select(this).select(".bar").transition().duration(500).attr("fill","orange");
})
.on("mouseout", function(d,i){
hideTooltips();
d3.select(this).select(".bar").transition().duration(500)
.attr("fill","rgb(64, 64, " + Math.floor(d.end * 255 / maxY) + ")");
});
- mouseover で、棒グラフをオレンジに変更して、mouseout で元に戻す
- イベントが張り付いているのは、棒グラフ一つ分の <g>
- 色変えるだけなら、CSSの :hover 疑似クラスでやっても良いけど、どうせほかにも何かやるし、transition() もやりたいだろう
- tooltips
- HTML の <div> を使うとよろしい
- SVG タグの title 属性を使っても良いけど格好悪い。<div> に簡単な CSS を適用するだけで、そこそこ見栄えの良い tooltips になる
event, mouse †
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Bar</title>
<style type="text/css">
.axis path,
.axis line {
fill: none;
stroke: black;
shape-rendering: crispEdges;
}
.axis text {
font-family: sans-serif;
font-size: 8px;
}
#tooltips {
position: absolute;
padding: 10px;
color: black;
font-size: 12px;
background: white;
border-radius: 10px;
border-style:solid;
border-width:2px;
border-color: red;
opacity: 0.8
}
#tooltips.hidden {
display: none;
}
</style>
</head>
<body>
D3.js EVENT EXAMPLE
<div id="tooltips" class="hidden">EVENT</div>
<div id="chart" style="border:1px solid">
</div>
<script type="text/javascript" src="https://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<script type="text/javascript">
var w = 300;
var h = 150;
var margin = {x:50, y:20, title:15};
// 表示するデータ
var dataset = [
{fruits:"りんご", amount:"13"},
{fruits:"みかん", amount:"4"},
{fruits:"ぶどう", amount:"28"},
];
// X軸
var scaleX = d3.scale.ordinal()
.domain(dataset.map(function(d){return d.fruits;}))
.rangeRoundBands([0,w],0.05);
var axisX = d3.svg.axis()
.scale(scaleX)
.orient("bottom")
.tickSize(0);
// Y軸
var scaleY = d3.scale.linear()
.domain([4, 28])
.nice()
.rangeRound([margin.y, h - margin.y - margin.title])
.clamp(true);
// グラフ描画領域
var svg = d3.select("#chart").append("svg").attr({width: w, height: h});
// データ描画領域
var dataPane = svg.append("g");
dataPane.selectAll("rect")
.data(dataset)
.enter()
.append("rect")
.attr("x" , function(d){ return scaleX(d.fruits); })
.attr("y" , function(d){ return h - margin.y - scaleY(d.amount); })
.attr("width" , function(d){ return scaleX.rangeBand(); })
.attr("height" , function(d){ return scaleY(d.amount); })
.attr("fill" , function(d){ return "りんご" === d.fruits ? "red" : "みかん" === d.fruits ? "orange" : "darkblue"; })
.on("mousemove", function(d) {
// d3.event は現在のイベント
d3.event.stopPropagation()
d3.event.preventDefault()
var mXY = d3.mouse(document.body);
var mXY2 = d3.mouse(document.getElementById("chart"));
d3.select("#tooltips")
.style("left", (mXY[0] + 10) + "px")
.style("top", (mXY[1] + 10) + "px")
.classed("hidden", false)
.html(d.fruits + " " + d.amount + "<br/>"
+ "body からの相対位置 = (" + mXY + ")<br/>"
+ "div からの相対位置 = (" + mXY2 + ")");
})
.on("mouseout", function(d) {
d3.select("#tooltips")
.classed("hidden", true);
});
// X軸を描画
svg.append("g")
.attr("class", "axis")
.attr("transform", "translate(0," + (h - margin.y) + ")")
.call(axisX);
</script>
</body>
</html>
- https://github.com/mbostock/d3/wiki/Selections#wiki-d3_event
- d3.event
- 最新のイベントオブジェクト
- d3.event.stopPropagation() とか d3.event.preventDefault() とか
- d3.mouse(container)
- container の左上に対するマウスポインタの相対位置
- container には、HTML の element (<div> や <svg> や <g>) を指定する必要がある。d3.mouse( d3.select("#container") ) とかは指定できない
- d3.touches(container)
- で、タッチ位置を取得できるはずだけど iPad mini (iOS6) では上手くいかなかった
- タッチ位置をマウスポインタの位置として取得することは可能
HTML