D3.jsを使ってスキルマップを作ってみる

#はじめに

Data Visualization Advent Calendarの15日目の記事です
（投稿が遅れました申し訳ありません）

データビジュアライゼーションの話をすると、よく出てくるD3.js。
Webサイト上でインタラクティブなグラフを表示するにはうってつけなものです。

いちエンジニアとして、これは習得しなければならない！
という謎の衝動に駆られ、最近手を出し始めました。

今日はそのD3.jsを使って、ちょっとしたツールを作ったお話です。

#d3.jsについて

正式名称はData-Driven Documentsといいます。
Javascriptのライブラリファイルになります。

Dが頭文字の単語が３つなのでD3ですね。
ちなみにAmazonが展開しているクラウドサービスの一つ、「Simple Storage Service」はS3と呼ばれていますね。関係ないですね。

公式サイトは[こちら](http://d3js.org/)
![Data-Driven Documentsの画像](http://d3js.org/preview.png)

[ギャラリー](https://github.com/mbostock/d3/wiki/Gallery)にはたくさんの実例が上がっているので、是非目を通してみてください。

個人的には[ボストンの地下鉄の運行を可視化したサイト](http://mbtaviz.github.io/)が非常に面白かったです。
![Preview画像](http://mbtaviz.github.io/media/wide-preview-collage.png)

そんな便利でナウいJavascript、D3.jsを使ってスキルマップを今回作ってみようと思います。

#スキルマップを作る


##経緯

自分はWebのエンジニアなんですが、よく「君は（スキルとして）何ができるの？」と聞かれて答えに困ることがあります。例えばPHPができます！といっても、PHP単体だけじゃなくて、MySQLとApacheの設定もしますし、Shellでバッチも組みますし、それらを一言で表そうとすると苦労しますし、その場でパッと出てこなかったりします。

逆に、「これとあれとあれとあれができます！」というのをあらかじめ準備出来ていて、可視化できていれば、第三者にわかりやすく伝えられるわけですね。

なおかつ、Webに関わる人が気軽に自分からその「スキルを見せるツール」を使えるようなことができればいいなと思いました。
ということで、自分の持っているスキルを可視化するスキルマップを作りました。
※めちゃめちゃプロトタイプでまだ製作途中です。

##完成予想

自分が持っているスキルをある一種の「タグ」として見た場合、
そのタグを自分にベタベタと貼っていき、そのタグがどのジャンルにわかれていてどのタグの頻度が多いのか、というのを円グラフで表せたらいいなと思いました。

なので、画面に現れたタグ（ボタン）で自分がスキルとして持っているものをクリックしていき、その結果、自分のスキル傾向がどれによっているのかといったことを再認識するようなツールです。

##デモ

実際に作ったものが[こちら](http://shinya.github.io/demos/skill_map.html)になります。
実際に用意するタグの種類の精査は必要ですが、とりあえずプロトタイプということでいくつか列挙しました。ボタンを押すと、円グラフで傾向がわかるようになっています。

#実際に作ったソースファイル

実装にはbootstrapとjQuery、そして勿論d3.jsを使用しています。

[Github](https://github.com/saitas/saitas.github.io/blob/master/demos/skill_map.html)でソースも公開しております。
（クオリティは低いですが・・・）

```html:HTMLファイル
<!DOCTYPE html>
<html lang="ja">
<head>
	<meta charset="utf-8">
	<meta name="viewport" content="width=device-width,initial-scale=1.0,user-scalable=no,minimum-scale=0.25,maximum-scale=2" />

	<title>Skill Map Sample</title>

	<link rel="stylesheet" href="/css/bootstrap.min.css">
	<link rel="stylesheet" href="/css/bootstrap-responsive.min.css">
	<link rel="stylesheet" href="/css/todc-bootstrap.css">
	<link rel="stylesheet" href="/css/join.css">
	<link rel="stylesheet" href="/css/sample/skill_map.css">

	<script src="/js/jquery-1.9.1.min.js"></script>
	<script src="/js/bootstrap.min.js"></script>
	<script src="/js/d3.min.js"></script>
	<script src="/js/sample/skill_data.js"></script>
	<script src="/js/sample/skill_map.js"></script>
</head>
<body>

	<div class="container-fluid root">
		<div id="content">

		<h2>Skill Map Demo</h2>

			<div class="row-fluid">
				<div class="span8" id="canvas"></div>
				<div class="span4" id="selected"></div>
			</div>

			<div class="row-fluid">
				<div class="span8" id="skills"></div>
			</div>

		</div>
	</div>

</body>
</html>
```

```css:skill_map.css
body {
	margin-top: 20px;
}

#skills a , #selected div {
	margin-right: 10px;
	margin-bottom: 10px;
}
```

```js:skill_data.js
var skills  = [
	{
		category : 'Language',
		name : 'Java'
	},
	{
		category : 'Language',
		name : 'Python'
	},
	{
		category : 'Language',
		name : 'C'
	},
	{
		category : 'Language',
		name : 'C++'
	},
	{
		category : 'Language',
		name : 'PHP'
	},
	{
		category : 'Language',
		name : 'C#'
	},
	{
		category : 'Language',
		name : 'COBOL'
	},
	{
		category : 'Language',
		name : 'Perl'
	},
	{
		category : 'Language',
		name : 'Go'
	},

	// Web関係
	{
		category : 'Web',
		name : 'HTML'
	},
	{
		category : 'Web',
		name : 'CSS'
	},
	{
		category : 'Web',
		name : 'Apache'
	},
	{
		category : 'Web',
		name : 'jQuery'
	},
	{
		category : 'Web',
		name : 'IIS'
	},

	// サーバー関係
	{
		category : 'Server',
		name : 'MySQL'
	},
	{
		category : 'Server',
		name : 'Mongo'
	},
	{
		category : 'Server',
		name : 'shell'
	},
	{
		category : 'Server',
		name : 'Vim'
	},


	// OS
	{
		category : 'OS',
		name : 'Windows'
	},
	{
		category : 'OS',
		name : 'Mac OS X'
	},
	{
		category : 'OS',
		name : 'Linux'
	},
	{
		category : 'OS',
		name : 'Android'
	},
	{
		category : 'OS',
		name : 'iOS'
	},


	// 製品関連
	{
		category : 'Product',
		name : 'Photoshop'
	},
	{
		category : 'Product',
		name : 'Illustrator'
	},
	{
		category : 'Product',
		name : 'Dreamweaver'
	},
	{
		category : 'Product',
		name : 'Eclipse'
	},
	{
		category : 'Product',
		name : 'Microsoft Office'
	},


	// その他ツールなど
	{
		category : 'Other',
		name : 'Subversion'
	},
	{
		category : 'Other',
		name : 'Git'
	},
	{
		category : 'Other',
		name : 'Redmine'
	},
];

var labelClass = {
	Web      : {
		className : 'primary',
		labelName : 'info'
	},
	Server   : {
		className : 'warning',
		labelName : 'warning'
	},
	Language : {
		className : 'success',
		labelName : 'success'
	},
	Product  : {
		className : 'danger',
		labelName : 'important'
	},
	OS  : {
		className : 'inverse',
		labelName : 'inverse'
	},
	Other  : {
		className : 'other',
		labelName : 'other'
	}
};
```

```js:skill_map.js
(function () {

	var width = 640;
	var height = 480;
	var canvas;
	var svg;
	var data;
	var colors;
	var pie;
	var arc, arc2;

	var current = [];

	function init(){

		canvas = d3.select('#canvas');

		svg = canvas.append('svg')
				.attr('width', width)
				.attr('height', height)
				.attr('style', 'background: #eeeeee')
				.append('g')
					.attr('transform', 'translate(' + width/2 + ', ' + height/2 + ')')
					.attr('id', 'pie')
		;

		data = [
			{
				name : 'Web',
				score : 0
			},
			{
				name : 'Server',
				score : 0
			},
			{
				name : 'Language',
				score : 0
			},
			{
				name : 'Product',
				score : 0
			},
			{
				name : 'OS',
				score : 0
			},
			{
				name : 'Other',
				score : 0
			},
		];

		colors = d3.scale.category10().range();

		pie = d3.layout.pie()
			.value(function(d){
				return d.score
			})
			.sort(function(a, b){
				return a.score < b.score;
			});

		arc = d3.svg
			.arc()
			.innerRadius(0)
			.outerRadius(height/2 * 0.8);

	}

	function render(){

		var g = svg.selectAll('path')
			.data(pie(data))
			.enter()
			.append('g')
				.attr("class", "arc")
			;


		g.append('path')
			.attr('fill', function(d, i){
				return colors[i];
			})
			.attr('d', arc)
			.attr('stroke', '#ffffff')
			.each(function(d, i) {
				current[i] = d;
			})
		;

		g.append('text')
			.attr('transform', function(d){ return 'translate(' + arc.centroid(d) + ')'; })
			.style('text-anchor', 'middle')
			.attr("font-size", "14")
			.attr('fill', '#ffffff')
			.text(function(d){

				if(d.data.score <= 0){
					return '';
				}else{
					return d.data.name;
				}

			})
			.each(function(d) {
			})
		;
	}

	function animation(target, type, args){

		target
			.selectAll(type)
			.transition()
			.duration(800)
			.attrTween(args, function(d, i) {

				if(type == 'path'){
					var interpolate = d3.interpolate(current[i], d);
					current[i] = interpolate(0);
					return function(t) {
						return arc(interpolate(t));
					};
				}else if(type == 'text'){
					var interpolate = d3.interpolate(arc.centroid(current[i]), arc.centroid(d));
					current[i] = d;
					return function(t) {
						return "translate(" + interpolate(t) + ")";
					};

				}

			})
		;
	}

	function update(newData){

		svg
			.selectAll("path")
			.data(pie(newData))
		;

		animation(svg, 'path', 'd');

		svg
			.selectAll("text")
			.data(pie(newData))
			.text(function(d) {
				console.log('name: ' + d.data.name);
				if(d.data.score <= 0){
					return '';
				}else{
					return d.data.name;
				}
			})
		;

		animation(svg, 'text', 'transform');
	}



	$(document).ready(function () {
		// 初期処理
		init();

		// 生成
		render();

		var className = '';
		var labelName = '';

		for(var i in skills){
			console.log(skills[i].name);

			if(skills[i].category == 'Language') {
				className = 'success';
				labelName = 'success';
			}else if(skills[i].category == 'Web'){
				className = 'danger';
				labelName = 'success';
			}else if(skills[i].category == 'Server'){
				className = 'primary';
				labelName = 'success';
			}else if(skills[i].category == 'Product'){
				className = 'warning';
				labelName = 'success';
			}else{
				className = 'foo';
				labelName = 'foo';
			}

			$('#skills').append(
				$('<a>')
					.addClass('btn btn-' + labelClass[skills[i].category].className)
					.text(skills[i].name)
					.attr('category', skills[i].category)
					.addClass('skill')
			);
		}

		$('.skill').click(function(){
			var category = $(this).attr('category');
			var name = $(this).text();

			for(var i = 0; i < data.length; i++){
				if(data[i].name == category){
					data[i].score += 1;
				}
			}

			$('#selected').append(
				$('<div></div>')
					.addClass('label label-' + labelClass[category].labelName)
					.text(name)
			);

			update(data);

			$(this).remove();
		});

	});

})();
```

#最後に

今回作ったものは本当にプロトタイプのプロトタイプで、実際にはもっと細かいカテゴリー分けや、「二階層以上に円グラフが別れる」「マウスオーバーで詳細がポップアップ表示される」「スキルだけでなく習熟度も表せる」・・・など色々な課題が山積みになっています。

それらを整理して、もっと実用的なスキルマップを作りたいと思います！