【D3.js】Gunma.webのソーシャルグラフ作ってみた。
ソーシャルグラフを作ってみたかったので、Gunma.webに参加されたユーザーのグラフを作ってみました。
ユーザー間のリンクは、同じイベントに参加している回数が多いほど太く表示されます。
アイコンやユーザー名をクリックするとリンクが強調(赤)されます。
(アイコン画像はATNDから直で読み込んでいるので、そのうち弾かれるかも)
Gunma.web Social Graph
激重です。できればChromeで見てやってください。
Gunma.webについてはこちら
やったこと
- ATNDからイベント参加ユーザの取得
- イベントに参加したユーザーの組み合わせ(Combination)リスト作成
- 重複する組み合わせをカウント(同じイベントに参加している回数)
- 上記のデータをCSVで出力
- CSVファイルをD3.jsでJSONに変換
- Force Layoutdで表示
データセットの作成(手作業多め)
ATNDからデータを取得するスクリプトをnode.jsで作成します。
(APIを使えばよかったということに後で気付きましたが、後の祭り)
まずは、必要なライブラリをインストールします。
1 2 |
$ npm install request $ npm install cheerio |
Atndスクレイピング用スクリプト(atnd.js)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
var request = require("request"); var cheerio = require("cheerio"); var atndId = process.argv[2]; if(!atndId){ console.log('Not Found atndId') return ; } request({ uri: "http://atnd.org/events/" + atndId, }, function(error, response, body) { var $ = cheerio.load(body); var user =[]; $(".a-b > li > span").each(function() { var link = $(this).find('a'); var text = link.text(); user.push(text); }); var n = user.length; var i, j; for(i = 0; i < n; i++){ for(j = i + 1; j < n; j++){ console.log(user[i] + ", " + user[j]); } } }); |
実行
1 |
$ node atnd.js [ATNDページのID] > gunmaweb1.txt |
実行すると対象のATNDページから参加ユーザーを取得し、全ての組み合わせを出力します。
これをgunma.web~gunma.web#12まで繰り返し、全てのユーザー組み合わせを取得。
出力したテキストファイルをcatコマンドで一つにまとめます。
1 |
$ cat * > user.txt |
重複している組み合わせをカウントし、ユニークなユーザー組み合わせのみのリストに変換します
1 |
cat user.txt|sort|uniq -c > ulist.txt |
(これで計算あっているはず……たぶん)
作成したファイルをExcelで読み込み「データ→区切り位置」機能を使用して「重複カウント(value),ユーザー(source)、ユーザー(target)」のリストに変換しcsvで保存します。
出来上がったのが下記csvファイルです。
data.csv
このcsvをForce Layoutで使用できるようにJSONに変換します。
d3.jsを使って変換スクリプトを作成しました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
d3.csv('data.csv', function(data){ graph = {'nodes': [], 'links': [] }; data.forEach(function(d){ graph.nodes.push({'name': d.source }); graph.nodes.push({'name': d.target }); graph.links.push({ 'source': d.source, 'target': d.target, 'value': d.value }); }) graph.nodes = d3.keys( d3.nest() .key(function(d) { return d.name; }) .map(graph.nodes) ); graph.links.forEach(function(d, i){ graph.links[i].source = graph.nodes.indexOf(graph.links[i].source); graph.links[i].target = graph.nodes.indexOf(graph.links[i].target); }); graph.nodes.forEach(function(d,i){ graph.nodes[i] = {'name': d }; }); var jsonData = JSON.stringify(graph); //テキストエリアにエクスポート d3.select('body').append('textarea').text(jsonData); }); |
example convert.html
上記のデータをjsonファイルとして保存して、データ作成は完了です。
source,targetのリストからnodeとlinkを含むオブジェクトを作る作業については、D3 Tips and Tricks「Sankey Diagramsn」の章に詳しく掲載されていますので興味ある方は参照ください。
Force Layout 表示
Force Layoutについてはこちらを
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 |
d3.json('data.json', function(data){ var w = d3.select('body').style('width').replace('px',''); var h = 1000; //dataSet valueの最大値取得 var valueMax = d3.max(data.links, function(d){ return d.value }); var opacityScale = d3.scale.linear().domain([0, valueMax]).range([0.4, 0.8]); //valueの値によって透明度を変化させる var colorScale = d3.scale.linear().domain([0, valueMax]).range(["white", "blue"]); //valueの値によってカラーを変化させる //グラフを描画するステージ(svgタグ)を追加 var svg = d3.select("svg").attr("width", w).attr("height", h); //グラフタイトル追加 svg.append('text') .attr({ x:10, y:80, fill: "white", "font-size":60 }) .text("Gunma.web Social Graph"); //グラフの初期設定 var force = d3.layout.force() .nodes(data.nodes) .links(data.links) .gravity(.05) //重力 .distance(500) //ノード間の距離 .charge(-300) //各ノードの引き合うor反発しあう力 .size([w, h]); //図のサイズ //ユーザー間のリンク作成 var link = svg.selectAll("line.link") .data(force.links(), function(d, i) { return d.source + '-' + d.target; }) //linksデータを要素にバインド .enter().append("svg:line") .attr({ "class":function(d){ return "link " + "l"+data.nodes[d.source].name.replace(/./g,'') + " " + "l"+data.nodes[d.target].name.replace(/./g,'') }, "stroke": 'blue', "stroke-opacity":function(d){ return opacityScale(d.value) }, "stroke-width":function(d){ return d.value } }); //nodeデータをバインディング var node = svg.selectAll("g.node").data(force.nodes(), function(d) { return d.name;} ); //ユーザーグループ var nodeEnter = node.enter().append("g") .attr("class", "node") .attr("id", function(d){ return d.name.replace(/./g,'') }) .on('click', function(){ d3.selectAll(".link").attr("stroke", "blue") d3.selectAll(".l"+d3.select(this).attr('id')) .attr("stroke", "red") }) .call(force.drag); //ノードをドラッグできるように設定 //ユーザーアイコン追加 nodeEnter.append("image") .attr("class", "user") .attr({ "xlink:href":function(d){ return d.img }, //ノード用画像の設定 "x":"-16px", "y":"-16px", "width":"32px", "height":"32px" }); //ユーザー名追加 nodeEnter.append("text") .attr("class", "nodetext") .attr({ "dx":30, "dy":".35em", "fill":"white" }) .text(function(d) { return d.name }); //フォースレイアウトのアニメーションを開始 force.start(); //アニメーションループ force.on("tick", function() { node.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; }); link.attr({ x1: function(d) { return d.source.x; }, y1: function(d) { return d.source.y; }, x2: function(d) { return d.target.x; }, y2: function(d) { return d.target.y; } }); }); }); |
やってみてわかったこと
データセットを作るのが一番大変。
特に今回はwindows上で作業したのでSJIS-UTF8の変換を繰り返す羽目になってしまった。
nkfはやっぱり便利。
Force Layouは、うまく動かなかったときのデバッグが難しい。
本当は、過去群馬で開催されたイベント全てからデータを取ろうと思っていたのですが、力付きました。
まぁ、これ以上データが多くなると見づらくなるし。
愚直にデータを全て表示するのではなく、程よく省略してフォーカスが当たったときに詳細を表示する(ズーム/パン)処理を行いたいのですが、なかなかうまくいきません orz
その辺の処理がちゃんとできるようになったら再挑戦したいと思います。