この記事は Data Visualization Advent Calendar 2014 の 3 日目の記事です。
記事の概要
この記事では、E2D3 チームの活動として、朝日新聞社の衆議院選の得票数に関する記事に寄稿したヴィジュアライゼーションの技術的な解説をしていきます。(リンク先の記事は過去の衆院選を、得票の絶対数、棄権・無効票の数の増減という観点で、わかりやすい解説とともに振り返った良記事です)
得票率1割台で政権取る可能性 グラフで見る衆院選 - withnews(ウィズニュース)
ポイントといきさつ
今回のヴィジュアライゼーションのポイントは
- 選挙のある年に注目して、各党の得票と棄権・無効票の比較がしやすいこと
- ある党(または棄権・無効票)に注目して、前後の回との得票数の増減がわかりやすいこと
の二つです。時間変化に伴う各党の得票数の変化を表現するなら、線グラフが第一の選択肢になりますが、例えば得票数の合計(の10万分の一の高さ)で実際に作ってみるとこうなります。
…若干手抜き感はありますが、まぁ、こんな感じになります。確かに二つのポイントを押さえていて、合計の推移という点ではわかりやすいのですが、実際の記事に寄稿するものでは得票数三種類(小選挙区、比例区、合計)と各政党の議席数を同時に出すということを考え、さらに映像コンテンツとして創り上げることを考えると、別の選択肢を検討する価値は十分にありそうでした。
一点を10万票として、積み上げ、動かすという表現は、2014年11月21日の最初のミーティングで、記者の古田氏から出たアイデアで、それを全面的に採用し、プロト実装が始まりました。データの準備や各回の解説は朝日新聞社様にやっていただき、自分はインタラクティブなヴィジュアライゼーションを提供するという形のコラボレーションでした。
点の移動は、実際の投票者の移動ではなく、票の増減に合わせている、ということは、音声と文字の両方で注意してもらうようにお願いしました。
ここからは技術の話
ヴィジュアライゼーションはデータ可視化によく使われる JavaScript ライブラリの D3.js を使って創り上げました。
寄稿したものを非常に単純化したものをbl.ocks.orgにアップしてあります。全体のソースはこちらを参照してください。
D3.js は、画面上の要素一つひとつをデータの一つひとつと対応させ、データの変化を画面上の変化とリンクさせて見せるのに適しています。今回のデータは、年×各党の得票数(あるいは無効・棄権票数)という二次元配列のデータでしたが、それをたくさんの点として動かすため、一つひとつの点に対応したデータを、プログラム内で生成しています。
var displaydata = d3.range(summax).map(function(d){return [];});
summax
は年ごとの和の最大値です。displaydata
変数に、最も沢山必要なときの点の数と同じ要素数の配列を用意します。各要素は最初空の配列にしておきます。
次は各種スケールの設定です。
var xScale = d3.scale.ordinal().rangeBands([0,dim.graphWidth],0.05);
var xLocalScale = d3.scale.ordinal();
var yScale = d3.scale.ordinal().rangePoints([dim.graphHeight, 0]);
var colorScale = d3.scale.category10();
d3.csv('data.csv', function(err,raw)
{
/* ...<中略>... */
var max = d3.max(years.map(function(d){return d3.max(data[d]);}));
var nrow = Math.ceil(dim.graphHeight/(2*(radius+mar)));
barWidth = Math.ceil(max/nrow);
yScale.domain(d3.range(nrow));
xScale.domain(parties.map(function(d,i){return i;}));
xLocalScale.rangeBands([0,xScale.rangeBand()]).domain(d3.range(barWidth));
colorScale.domain(d3.range(parties.length));
各点には
- どのグループに属するか (
label
) - グループ内で何番目か (
idx
)
という情報を持たせ、それらによって実際の位置を決めます。xScale
がlabel
を元に、グループの位置を決め、xLocalScale
とyScale
がidx
を元に、グループに与えられた幅の中での各点の位置を決めます。
単年なら{label,idx}
の配列でいいのですが、実際は、年ごとの変化を追うので、各データは各年ごとに label
と idx
を持ちます。先ほど用意したdisplaydata
の、空で初期化した配列に、各年ごとの label
と idx
を push
していきます。例えば最初の年の分は下記の通りです。
var indexMargin = 0;
parties.forEach(function(party,partyidx)
{
for (var i=0;i<data[years[0]][partyidx];++i)
{
displaydata[indexMargin+i].push({label:partyidx,idx:i});
}
indexMargin += data[years[0]][partyidx];
});
あとはこれを繰り返せばいいような気もしますが、動く必要のない点は動かさず、動く点は移動先に均等にわけるとか考えると少し複雑になります。自分は、各点を動かす必要があるか判定し、動かす必要があるものは一旦プールして、シャッフルした後必要なところに順番に割り当てていく作戦で解決しました。(ソースは長いので省略… blocksにあります; ここが肝という気もします)
点を最初に並べているのはこの部分です。座標計算がやたらと複雑に見えるかもしれませんが、ほとんどはどこにも属さなかった場合に点を画面中央上で透明にするのに使っています。
time = 0;
var votes = graphLayer.selectAll('.vote').data(displaydata).enter().append('circle')
.attr('class','vote')
.attr('r',radius)
.attr('cx',function(d){return ((d[time].label!=null)?(xScale(d[time].label)+xLocalScale(d[time].idx%barWidth)+radius+mar):(dim.graphWidth/2));})
.attr('cy',function(d){return ((d[time].label!=null)?(yScale(Math.floor((d[time].idx+0.1)/barWidth))-radius-mar):0);})
.style('opacity',function(d){return (d[time].label!=null)?0.8:0.0;})
.style('fill',function(d){return colorScale(d[time].label);});
最後に点の移動ですが、下記のソースになります。time
という、配列のインデックスを表す変数に、移動するべき年を入れておいて、上述のコードと同じ座標計算を行っているだけです。
var votes = graphLayer.selectAll('.vote')
.filter(function(d){return d[current].label!=d[time].label || d[current].idx!=d[time].idx;})
.transition()
.duration(duration)
.delay(function(d){return Math.random()*delayMax;})
.attr('cx',function(d){return ((d[time].label!=null)?(xScale(d[time].label)+xLocalScale(d[time].idx%barWidth)+radius+mar):(dim.graphWidth/2));})
.attr('cy',function(d){return ((d[time].label!=null)?(yScale(Math.floor((d[time].idx+0.1)/barWidth))-radius-mar):0);})
.style('opacity',function(d){return (d[time].label!=null)?0.8:0.0;})
.style('fill',function(d){return colorScale(d[time].label);});
移動のコードで、一点だけ特筆したいのが、.delay(function(d){return Math.random()*delayMax;})
の部分です。これがなくても成り立つのですが、これを入れると点がバラバラに飛び立ちます。ないと一斉に飛び立つのですが、これがあるのとないのでは感じ方に差があり、自分はバラバラに飛ばした方が面白く感じました。
感想
元のデータが単純な二次元配列でも、そこに人が想いとストーリィを吹き込むと、こんなにも面白いものに仕上がるのだというのを再確認できました。特に、2Dの表現に動きを加えることで、もう一次元加えた以上の何かを込められたような気がします。技術の話と言いながら、感覚の話も沢山書いてしまいましたが、データジャーナリズム、データヴィジュアライゼーションの可能性を少しでも感じていただければ幸いです。