D3.js in Actionの2章の勉強ノートです。
%load_ext sage
from IPython.core.display import HTML
from string import Template
import json
import nvd3
nvd3.ipynb.initialize_javascript(use_remote=True)
loaded nvd3 IPython extension run nvd3.ipynb.initialize_javascript() to set up the notebook help(nvd3.ipynb.initialize_javascript) for options
D3では様々なデータをサポートしています。
pythonとのインタフェースを取ることを考えると、一般的に構造を保持できるJSONとCSVがデータの受け渡しに使われるます。
例として以下のようなcities.csvを読み込んでみましょう。
!cat data/cities.csv
"label","population","country","x","y" "San Francisco", 750000,"USA",37,-122 "Fresno", 500000,"USA",36,-119 "Lahore",12500000,"Pakistan",31,74 "Karachi",13000000,"Pakistan",24,67 "Rome",2500000,"Italy",41,12 "Naples",1000000,"Italy",40,14 "Rio",12300000,"Brazil",-22,-43 "Sao Paolo",12300000,"Brazil",-23,-46
読み込まれたデータは、function(error, data)形式のコールバックで与えられます。 このコールバックの中で実行したい処理を記述する方式になります。
%%javascript
d3.csv("data/cities.csv",function(error,data) {console.log(error,data)});
javascriptのコンソールに以下のようにデータの内容が出力されます。
これを見るとCSVのデータがヘッダのカラム名をキーとする辞書の配列として渡されていることがわかります。
from string import Template
import json
import pandas as pd
pandasを使ってcities.csvを読み込み、データフレームdfにセットします。
df = pd.read_csv("data/cities.csv")
df.head()
label | population | country | x | y | |
---|---|---|---|---|---|
0 | San Francisco | 750000 | USA | 37 | -122 |
1 | Fresno | 500000 | USA | 36 | -119 |
2 | Lahore | 12500000 | Pakistan | 31 | 74 |
3 | Karachi | 13000000 | Pakistan | 24 | 67 |
4 | Rome | 2500000 | Italy | 41 | 12 |
Templateを使ってscriptタグにdfを変数dataに代入します。
$python_dataの置換で、json.dumpsとto_dict(orient='records')を使用するのがポイントです。
data_text = Template('''
<script>
var data = $python_data ;
console.log(data);
</script>
''')
data_text = data_text.substitute({'python_data': json.dumps(df.to_dict(orient='records'))})
print data_text
<script> var data = [{"y": -122, "country": "USA", "population": 750000, "x": 37, "label": "San Francisco"}, {"y": -119, "country": "USA", "population": 500000, "x": 36, "label": "Fresno"}, {"y": 74, "country": "Pakistan", "population": 12500000, "x": 31, "label": "Lahore"}, {"y": 67, "country": "Pakistan", "population": 13000000, "x": 24, "label": "Karachi"}, {"y": 12, "country": "Italy", "population": 2500000, "x": 41, "label": "Rome"}, {"y": 14, "country": "Italy", "population": 1000000, "x": 40, "label": "Naples"}, {"y": -43, "country": "Brazil", "population": 12300000, "x": -22, "label": "Rio"}, {"y": -46, "country": "Brazil", "population": 12300000, "x": -23, "label": "Sao Paolo"}] ; console.log(data); </script>
以下のコマンドを実行して、javascriptのコンソールをみてください。d3で読み込んだ時と同じログが出力されています。
HTML(data_text)
D3のスケール処理はとても良くできており、データに応じて選べるようになっています。
最初に、線形補間を見てみましょう。 domain関数では、問題領域(ここではデータの分布領域)をrange関数で指定された範囲にマッピングします。
console.logの代わりにelement.textを使うとjupyterのノートブックに出力できます(ただし1回だけ指定可能みたい)。
%%javascript
var newRamp = d3.scale.linear().domain([500000,13000000]).range([0, 500]);
element.text(newRamp(1000000) + ", " +newRamp(9000000)+", "+newRamp.invert(313));
D3のscaleの凄いのは、数値だけでなくカラーにもマッピングできることです。
%%javascript
var newRamp = d3.scale.linear().domain([500000,13000000]).range(["blue","red"]);
element.text(newRamp(1000000) + ", " +newRamp(9000000));
次にquantile関数を使っていくつかのカテゴリに分けてみます。
以下の例では、sampleArrayのデータを3つのグループに分けて、small, medium, largetとします。 値ではなく、ソートした後にデータ数が等分になるようにカテゴリわけしているみたいです。
[1,10,44], [58,66,124], [423, 524, 900]から
(-∞..44], (44..124], (124..∞)の区間をsmall, medium, largeにマッピングしています。
%%javascript
var sampleArray = [423,124,66,424,58,10,900,44,1];
var qScaleName =
d3.scale.quantile().domain(sampleArray).range(["small", "medium","large"]);
element.text(qScaleName(68) + ", " +qScaleName(20)+", "+qScaleName(10000));
D3.jsの最も重要な機能は、データバインディングだと思います。
1章で見たのはenterメソッドでしたが、exit, updateについてその動きを確認してみましょう。
update, enter, exitの違いが、Fig. 2.24に解説されているので、引用します。
バインディングする要素よりもデータが多い場合のenterの動作を見てみましょう。
%%HTML
<div id="ex1">
<div></div>
</div>
%%javascript
var sampleData = [1, 2, 3, 4];
d3.select('#ex1').selectAll('div')
.data(sampleData)
.enter()
.append("div")
.html(function (d) { return d; })
この例では、1個のdivに対してデータの1がバインディングされて、残りの2, 3, 4に対しては新たにdiv要素を追加し、そこにデータの値をhtmlでセットします。
結果は期待に反し、2, 3, 4だけが表示されてました。最初のdivに対しては何も処理をしていないため、このようになります。
それでは、最初の1に対してもhtmlの処理を追加してみましょう。
%%HTML
<div id="ex2">
<div></div>
</div>
%%javascript
var sampleData = [1, 2, 3, 4];
d3.select('#ex2').selectAll('div')
.data(sampleData)
.html(function (d) { return d; })
.enter()
.append("div")
.html(function (d) { return d; });
これでは同じ処理を2度書かなくてはなりません。 そこで、最初にdivを削除してから処理をします。
%%javascript
var sampleData = [1, 2, 3, 4];
// remove all divs under #ex2
d3.select('#ex2').selectAll('div').remove();
d3.select('#ex2').selectAll('div')
.data(sampleData)
.enter()
.append("div")
.html(function (d) { return d; });
%%HTML
<div id="ex3">
<div>a</div>
<div>b</div>
<div>c</div>
<div>d</div>
</div>
%%javascript
var sampleData = [1, 2];
d3.select('#ex3').selectAll('div')
.data(sampleData)
.html(function (d) { return d; })
.exit()
.remove();
%%HTML
<div id='ex4'>
<svg/>
</div>
%%javascript
d3.select('#ex4').select("svg")
.selectAll("rect")
.data([15, 50, 22, 8, 100, 10])
.enter()
.append("rect")
.attr("width", 10)
.attr("height", function(d) {return d;})
.style("fill", "blue")
.style("stroke", "red")
.style("stroke-width", "1px")
.style("opacity", .25)
.attr("x", function(d, i) {return i * 10})
.attr("y", function(d) {return 100 - d;});
%%HTML
<div id='ex5'>
<svg/>
</div>
%%javascript
// dataフォルダのcities.csvを読み込み、dataViz関数を呼び出す
d3.csv("data/cities.csv",function(error,data) {dataViz(data);});
function dataViz(incomingData) {
var maxPopulation = d3.max(incomingData, function(el) {
// 人口のデータを文字列から数値に変換
return parseInt(el.population);}
);
// 人口の最大値を0-230の範囲にスケーリングするyScaleを作成
var yScale = d3.scale.linear().domain([0,maxPopulation]).range([0,230]);
// 棒グラフの作成
d3.select('#ex5').select("svg").attr("style","height: 240px; width: 300px;");
d3.select("#ex5 svg")
.selectAll("rect")
.data(incomingData)
.enter()
.append("rect")
.attr("width", 25)
.attr("height", function(d) {return yScale(parseInt(d.population));})
.attr("x", function(d,i) {return i * 30;})
.attr("y", function(d) {return 240 - yScale(parseInt(d.population));})
.style("fill", "blue")
.style("stroke", "red")
.style("stroke-width", "1px")
.style("opacity", .25);
}