Chapter2

D3.js in Actionの2章の勉強ノートです。

In [1]:
%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では様々なデータをサポートしています。

  • TEXT: d3.text()
  • XML: d3.xml()
  • CSV: d3.csv()
  • JSON: d3.json()
  • HTML: d3.html()

pythonとのインタフェースを取ることを考えると、一般的に構造を保持できるJSONとCSVがデータの受け渡しに使われるます。

例として以下のようなcities.csvを読み込んでみましょう。

In [2]:
!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)形式のコールバックで与えられます。 このコールバックの中で実行したい処理を記述する方式になります。

In [3]:
%%javascript
d3.csv("data/cities.csv",function(error,data) {console.log(error,data)});

javascriptのコンソールに以下のようにデータの内容が出力されます。

cities console log

これを見るとCSVのデータがヘッダのカラム名をキーとする辞書の配列として渡されていることがわかります。

Pythonデータの受け渡し

jupyterの計算結果をD3に渡す方法を以下に紹介します。

jsonとTemplateライブラリを使用します。

In [5]:
from string import Template
import json
import pandas as pd

pandasを使ってcities.csvを読み込み、データフレームdfにセットします。

In [6]:
df = pd.read_csv("data/cities.csv")
df.head()
Out[6]:
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')を使用するのがポイントです。

In [7]:
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で読み込んだ時と同じログが出力されています。

In [8]:
HTML(data_text)
Out[8]:

スケールマッピング

D3のスケール処理はとても良くできており、データに応じて選べるようになっています。

  • d3.scale.linear(): 線形補間
  • d3.scale.quantile(): 分位数(区間で分けられた値)

最初に、線形補間を見てみましょう。 domain関数では、問題領域(ここではデータの分布領域)をrange関数で指定された範囲にマッピングします。

console.logの代わりにelement.textを使うとjupyterのノートブックに出力できます(ただし1回だけ指定可能みたい)。

In [9]:
%%javascript
var newRamp = d3.scale.linear().domain([500000,13000000]).range([0, 500]);
element.text(newRamp(1000000) + ", " +newRamp(9000000)+", "+newRamp.invert(313));

D3のscaleの凄いのは、数値だけでなくカラーにもマッピングできることです。

In [10]:
%%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にマッピングしています。

In [11]:
%%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に解説されているので、引用します。

Fig. 2.24

バインディングする要素よりもデータが多い場合のenterの動作を見てみましょう。

In [12]:
%%HTML
<div id="ex1">
    <div></div>
</div>
In [13]:
%%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の処理を追加してみましょう。

In [14]:
%%HTML
<div id="ex2">
    <div></div>
</div>
In [15]:
%%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を削除してから処理をします。

In [16]:
%%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; });

exitによる削除

次に要素よりもデータが少ない場合に使用するexitを試してみます。

4個のdiv要素にa, b, c, dがセットされているところに、1, 2の2個の要素をバインディングします。

In [17]:
%%HTML
<div id="ex3">
    <div>a</div>
    <div>b</div>
    <div>c</div>
    <div>d</div>
</div>
a
b
c
d
In [18]:
%%javascript
var sampleData = [1, 2];

d3.select('#ex3').selectAll('div')
    .data(sampleData)
    .html(function (d) { return d; })
    .exit()
    .remove();

棒グラフの表示

svgタグに棒グラフを描いてみましょう。

  • rect要素を追加し、xを10pxずつ移動し、yをデータの値*10pxで表示
In [19]:
%%HTML
<div id='ex4'>
    <svg/>
</div>
In [20]:
%%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;});

CSVデータから棒グラフを作る

2章のメインテーマは、CSVファイルから棒グラフを作成することです。

例題にしたがって、cities.csvから棒グラフを作ってみます。

In [21]:
%%HTML
<div id='ex5'>
    <svg/>
</div>
In [22]:
%%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);
}
In [ ]: