Chapter3

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

KernelはPython2を使用します。

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

データの可視化

3章では、svgの図形の他にHTMLページや画像を表示する方法を説明しています。

W杯のデータ

例題に使用しているのは、以下のようなW杯のデータです。

In [2]:
%%writefile data/worldcup.csv
"team","region","win","loss","draw","points","gf","ga","cs","yc","rc"
"Netherlands","UEFA",6,0,1,18,12,6,2,23,1
"Spain","UEFA",6,0,1,18,8,2,5,8,0
"Germany","UEFA",5,0,2,15,16,5,3,10,1
"Argentina","CONMEBOL",4,0,1,12,10,6,2,8,0
"Uruguay","CONMEBOL",3,2,2,11,11,8,3,13,2
"Brazil","CONMEBOL",3,1,1,10,9,4,2,9,2
"Ghana","CAF",2,2,1,8,5,4,1,12,0
"Japan","AFC",2,1,1,7,4,2,2,4,0
Overwriting data/worldcup.csv

CSSの定義

最初に使用するCSSとHTMLを定義します。

In [3]:
%%writefile css/d3ia.css
text {
    font-size: 10px;
}
g > text.active {
    font-size: 30px;
}
circle {
    fill: pink;
    stroke: black;
    stroke-width: 1px;
}
circle.active {
    fill: red;
}
circle.inactive {
    fill: gray;
}
Overwriting css/d3ia.css
In [4]:
%%HTML
<!-- CSSの読み込み -->
<link type="text/css" rel="stylesheet" href="css/d3ia.css" />
<!-- 外部jsファイル -->
<script src="js/colorbrewer.js" type="text/javascript"></script>

<!-- 描画するSVGの定義 -->
<div id='ex1'>
    <div id='viz'>
        <svg style="width:500px;height:500px;border:1px lightgray solid;" />
    </div>
    <div id='controls' />
</div>
In [5]:
%%javascript
d3.csv("data/worldcup.csv", function(data) {
    overallTeamViz('#ex1', data);
});

function overallTeamViz(topTag, incomingData) {
    // 中央に表示用のキャンバス用g(teamG)と出場国用g(overallG)を作成
    createFrame(topTag, incomingData);
    
    var teamG = d3.select(topTag).selectAll("g.overallG");
    // 出場国のCircleとチーム名を表示
    createTeamCircle(teamG);
    // ボタンアクションをバインディング
    bindButtons(topTag, incomingData);
    // mouseアクションをバインディング
    bindMouseAction(topTag, teamG);
}


function createFrame(topTag, incomingData) {
    d3.select(topTag).select("svg")
    .append("g")
    .attr("id", "teamsG")
    .attr("transform", "translate(50,300)")
    .selectAll("g")
    .data(incomingData)
    .enter()
    .append("g")
    .attr("class", "overallG")
    .attr("transform", function (d,i) {return "translate(" + (i * 50) + ", 0)"});
}

function createTeamCircle(teamG) {
    // 各チームのCircleを作成
    teamG
        .append("circle").attr("r", 0)
           .transition()
           .delay(function(d,i) {return i * 100})
           .duration(500)
           .attr("r", 40)
           .transition()
           .duration(500)
           .attr("r", 20);    
    // チームタイトルを表示
    teamG
        .append("text")
        .style("text-anchor", "middle")
        .attr("y", 30)
        .style("font-size", "10px")
        .text(function(d) {return d.team});
}

function bindButtons(topTag, incomingData) {
    // 最初のデータからキーの配列を取り出し、不要なteamとregionを除いてボタンを生成
    var dataKeys = d3.keys(incomingData[0])
        .filter(function (el) {return el != "team" && el != "region"})    
    d3.select(topTag).select("#controls").selectAll("button.teams").data(dataKeys).enter().append("button")
        .on("click", buttonClick)
        .html(function(d) {return d});        
    
    // ボタンのアクションをバインディング
    function buttonClick(datapoint) {
        var maxValue = d3.max(incomingData, 
            function(d) {return parseFloat(d[datapoint])
        });
        var radiusScale = d3.scale.linear().domain([0,maxValue]).range([2,20]);
        d3.select(topTag).selectAll("g.overallG").select("circle").transition().duration(1000).attr("r", 
            function(d) {return radiusScale(d[datapoint])})
    }   
}

function bindMouseAction(topTag, teamG) {
    // マウスアクションをバインディング
    teamG.on("mouseover", highlightRegion);
    teamG.on("mouseout", function()  {
        d3.select(topTag).selectAll("g.overallG").select("circle").style("fill", "pink")
    });

    function highlightRegion(d) {
        d3.select(topTag).selectAll("g.overallG").select("circle").style("fill", function(p) 
            {return p.region == d.region ? "red" : "gray"})
    }    
}

コードのメモ

図3.3 を機能ごとに分けてみました。 この小さなコードで、すごい表現ができるものだと感心しました。

3章の説明を簡単にまとめてみます。

  • g要素:SVGの要素をグルーピングするタグ、g要素単位でアニメーションや座標変換を行います
  • 描画用のキャンバスフレームをセット(createFrame)
  • 各チームのCircleとネームを表示(createTeamCircle)
  • ボタンのアクションをバインディング(bindButtons)
  • マウスアクションをバインディング(bindMouseAction)

バイディングされたデータ

teamGの各g要素にバインディングされたデータは、以下のように data 属性に保持されます。

バインディングされたデータ

イベント処理

最初は、ボタンイベントをセットしているbindButtonsをみていきましょう。

dataKeys変数に、キー配列["team","region","win","loss","draw","points","gf","ga","cs","yc","rc"]からteamとregionを除いた配列を取り出します。

In [6]:
%%javascript
var dataKeys = ["team","region","win","loss","draw","points","gf","ga","cs","yc","rc"]
        .filter(function (el) {return el != "team" && el != "region"})   
element.text(dataKeys);

その後、配列の各要素を名前とするボタンを生成し、onClick時の関数としてbuttonClickをバインドします。

var dataKeys = d3.keys(incomingData[0])
        .filter(function (el) {return el != "team" && el != "region"})    
    d3.select("#controls").selectAll("button.teams").data(dataKeys).enter().append("button")
        .on("click", buttonClick)
        .html(function(d) {return d});

ボタンクリックのコールバック関数は、datapointで渡されたキーの値から最大値をmaxValueに保持し、各Circleの半径を値の割合に変化させています。

function buttonClick(datapoint) {
        var maxValue = d3.max(incomingData, 
            function(d) {return parseFloat(d[datapoint])
        });
        var radiusScale = d3.scale.linear().domain([0,maxValue]).range([2,20]);
        d3.select(topTag).selectAll("g.overallG").select("circle").transition().duration(1000).attr("r", 
            function(d) {return radiusScale(d[datapoint])})
    }

テンプレートを使ってコードを再利用

原因はわからないのですが、%%javascriptで定義した関数はjupyterのセルで共存することができないので、別のセルで定義した関数を再利用することができません。

そこで、Templateを使って各関数を定義することにします。

In [7]:
teamViz = '''
function overallTeamViz(topTag, incomingData) {
    createFrame(topTag, incomingData);
    
    var teamG = d3.select(topTag).selectAll("g.overallG");
    createTeamCircle(teamG);
    bindButtons(topTag, incomingData);
    bindMouseAction(topTag, teamG);
}

function createFrame(topTag, incomingData) {
    d3.select(topTag).select("svg")
    .append("g")
    .attr("id", "teamsG")
    .attr("transform", "translate(50,300)")
    .selectAll("g")
    .data(incomingData)
    .enter()
    .append("g")
    .attr("class", "overallG")
    .attr("transform", function (d,i) {return "translate(" + (i * 50) + ", 0)"});
}

function createTeamCircle(teamG) {
    teamG
        .append("circle").attr("r", 0)
           .transition()
           .delay(function(d,i) {return i * 100})
           .duration(500)
           .attr("r", 40)
           .transition()
           .duration(500)
           .attr("r", 20);    
    teamG
        .append("text")
        .style("text-anchor", "middle")
        .attr("y", 30)
        .style("font-size", "10px")
        .text(function(d) {return d.team});
}
'''

bindButtons = '''
function bindButtons(topTag, incomingData) {
    var dataKeys = d3.keys(incomingData[0])
        .filter(function (el) {return el != "team" && el != "region"})    
    d3.select(topTag).select("#controls").selectAll("button.teams").data(dataKeys).enter().append("button")
        .on("click", buttonClick)
        .html(function(d) {return d});        
    
    function buttonClick(datapoint) {
        var maxValue = d3.max(incomingData, 
            function(d) {return parseFloat(d[datapoint])
        });
        var radiusScale = d3.scale.linear().domain([0,maxValue]).range([2,20]);
        d3.select(topTag).selectAll("g.overallG").select("circle").transition().duration(1000).attr("r", 
            function(d) {return radiusScale(d[datapoint])})
    }   
}
'''

bindMouseAction = '''
function bindMouseAction(topTag, teamG) {
    teamG.on("mouseover", highlightRegion);
    teamG.on("mouseout", function()  {
        d3.select(topTag).selectAll("g.overallG").select("circle").style("fill", "pink")
    });

    function highlightRegion(d) {
        d3.select(topTag).selectAll("g.overallG").select("circle").style("fill", function(p) 
            {return p.region == d.region ? "red" : "gray"})
    }    
}
'''
js_text = Template('''
<link type="text/css" rel="stylesheet" href="css/d3ia.css" />
<script src="js/colorbrewer.js" type="text/javascript"></script>

<div id='$example'>
    <div id='viz'>
        <svg style="width:500px;height:500px;border:1px lightgray solid;" />
    </div>
    <div id='controls' />
</div>

<script>
    d3.csv("data/worldcup.csv", function(data) {
        overallTeamViz('#$example', data);
    });
    $teamViz
    $bindButtons
    $bindMouseAction
</script>
''')

example='ex2'
html_text = js_text.substitute({'example': example, 'teamViz': teamViz, 
                                'bindButtons': bindButtons, 'bindMouseAction': bindMouseAction, 'extention': ""})
In [8]:
HTML(html_text)
Out[8]:

DOMの操作

バイディングされたデータで見たとおりバインディングされたデータや可視化された要素全てが、DOMの中に保持されています。

javascriptでこれらのDOM要素にアクセスする方法を見てみましょう。

In [9]:
%%javascript
d3.select('#ex2').select("circle").each(function(d,i) {
    console.log(d);console.log(i);console.log(this);
});

デベロッパーツールのConsoleには、以下のように表示されます。

DOM_this

thisは、選択された要素のDOMオブジェクトを参照していることがわかりました。

また、node()メソッドで選択された要素を取り出すこともできます。

In [10]:
%%javascript
console.log(d3.select('#ex2').select("circle").node());

カラーマッピング

ボタンをクリックした時に、値によってCircleの半径だけでなく、色も変える例が紹介されています。

最初にLAB ramp関数を使って数値で色を黄色から青色の間を線形補間して表示する方法を示します。

var ybRamp = d3.scale.linear()
            .interpolate(d3.interpolateLab)
            .domain([0,maxValue]).range(["yellow", "blue"]);
In [11]:
bindButtons = '''
function bindButtons(topTag, incomingData) {
    var dataKeys = d3.keys(incomingData[0])
        .filter(function (el) {return el != "team" && el != "region"})    
    d3.select(topTag).select("#controls").selectAll("button.teams").data(dataKeys).enter().append("button")
        .on("click", buttonClick)
        .html(function(d) {return d});        
    
    function buttonClick(datapoint) {
        var maxValue = d3.max(incomingData, function(el) {
            return parseFloat(el[datapoint ]); 
        });
        var ybRamp = d3.scale.linear()
            .interpolate(d3.interpolateLab)
            .domain([0,maxValue]).range(["yellow", "blue"]);

        var radiusScale = d3.scale.linear().domain([0,maxValue]).range([2,20]); 
        d3.selectAll("g.overallG").select("circle").transition().duration(1000)
            .style("fill", function(p) {
                return ybRamp(p[datapoint ])}
            ) .attr("r", function(p) {
                return radiusScale(p[datapoint ])
            });
    }   
}
'''
In [12]:
example='ex3'
html_text = js_text.substitute({'example': example, 'teamViz': teamViz, 
                                'bindButtons': bindButtons, 'bindMouseAction': bindMouseAction, 'extention': ""})
HTML(html_text)
Out[12]:

次にcolorbrewerライブラリを使って、赤系統の3色でグルーピングする例を示します。

var colorQuantize = d3.scale.quantize()
            .domain([0,maxValue]).range(colorbrewer.Reds[3]);
In [13]:
bindButtons = '''
function bindButtons(topTag, incomingData) {
    var dataKeys = d3.keys(incomingData[0])
        .filter(function (el) {return el != "team" && el != "region"})    
    d3.select(topTag).select("#controls").selectAll("button.teams").data(dataKeys).enter().append("button")
        .on("click", buttonClick)
        .html(function(d) {return d});        
    
    function buttonClick(datapoint) {
        var maxValue = d3.max(incomingData, function(el) {
            return parseFloat(el[datapoint ]); 
        });
        var colorQuantize = d3.scale.quantize()
            .domain([0,maxValue]).range(colorbrewer.Reds[3]);

        var radiusScale = d3.scale.linear().domain([0,maxValue]).range([2,20]); 
        d3.selectAll("g.overallG").select("circle").transition().duration(1000)
            .style("fill", function(p) {
                return colorQuantize(p[datapoint ])}
            ) .attr("r", function(p) {
                return radiusScale(p[datapoint ])
            });
    }   
}
'''
In [14]:
example='ex4'
html_text = js_text.substitute({'example': example, 'teamViz': teamViz, 
                                'bindButtons': bindButtons, 'bindMouseAction': bindMouseAction, 'extention': ""})
HTML(html_text)
Out[14]:

画像を図形に表示する

次に画像を図形に表示する例を試してみます。

画像の表示には、"xlink:ref"を使用します。以下の例では、imageタグをtextの前に挿入し、 属性hrefに画像のURIをセットしています。

xlink_image

In [15]:
%%javascript
d3.select('#ex4').selectAll("g.overallG").insert("image", "text")
  .attr("xlink:href", function(d) {
      return "images/" + d.team + ".png";
  })
  .attr("width", "45px").attr("height", "20px").attr("x", "-22")
  .attr("y", "-10");

HTMLを表示する

最後に画像にHTMLを表示する例を試してみます。このほかにも3章にはとても興味深い例題が紹介されています。

ダイアログとして表示するために、以下のCSSを追加します。

In [16]:
%%writefile -a css/d3ia.css
#modal {
    position:fixed;
    left:150px;
    top:100px;
    z-index:1;
    background: white;
    border: 1px black solid;
    box-shadow: 10px 10px 5px #888888;
}
  tr {
    border: 1px gray solid;
}
  td {
    font-size: 10px;
}
td.data {
    font-weight: 900;
  }
Appending to css/d3ia.css

表示するダイアログのHTMLは、以下のようになります。

In [17]:
%%writefile resources/modal.html
<table> <tr>
        <th>Statistics</th>
    </tr>
    <tr><td>Team Name</td><td class="data"></td></tr>
    <tr><td>Region</td><td class="data"></td></tr>
    <tr><td>Wins</td><td class="data"></td></tr>
    <tr><td>Losses</td><td class="data"></td></tr>
    <tr><td>Draws</td><td class="data"></td></tr>
    <tr><td>Points</td><td class="data"></td></tr>
    <tr><td>Goals For</td><td class="data"></td></tr>
    <tr><td>Goals Against</td><td class="data"></td></tr>
    <tr><td>Clean Sheets</td><td class="data"></td></tr>
    <tr><td>Yellow Cards</td><td class="data"></td></tr>
    <tr><td>Red Cards</td><td class="data"></td></tr>
</table>
Overwriting resources/modal.html
In [18]:
example='ex5'
html_text = js_text.substitute({'example': example, 'teamViz': teamViz, 
                                'bindButtons': bindButtons, 'bindMouseAction': bindMouseAction, 
                                'extention': ""})
HTML(html_text)
Out[18]:
In [19]:
%%javascript
var teamG = d3.select('#ex5').selectAll("g.overallG");
teamG.on("click", teamClick);

d3.text("resources/modal.html", function(data) {
    d3.select('#ex5').append("div").attr("id", "modal").html(data);
});

function teamClick(d) {
  d3.select('#ex5').selectAll("td.data").data(d3.values(d)).html(function(p) {return p});
}
In [ ]:
 

This website does not host notebooks, it only renders notebooks available on other websites.

Delivered by Fastly, Rendered by Rackspace

nbviewer GitHub repository.

nbviewer version: f697053

nbconvert version: 5.4.1

Rendered (Thu, 23 May 2019 13:58:35 UTC)