【データビジュアライズソン】横浜市に在住する外国人の推移をビジュアライズ

こんにちは。山本です。

先日7月20日に、【LOCAL GOOD YOKOHAMA データビジュアライズソン Vol.1】に参加してきました。横浜市が推し進める「オープンデータ」プロジェクトの一環として公開されているデータを元に、いくつかのチームにわかれ「環境」や「経済」など横浜市に関するビジュアライズをおこないました。

2014-07-2890505

私のチームはテーマ設定からチームで行うことになり、決まったテーマは

「World In Yokohama」

横浜市に在住の外国人の推移をビジュアライズするというものになりました。まー、序盤はチーム崩壊か!?と思いましたが、結果的には各々、良いアウトプットが出せたのかなと思います。 私はD3.jsを利用したビジュアライズを担当作成しましたので、成果物として下記にソースと説明を載せておきます。

【World In Yokohama 〜横浜市に在住の外国人はどのくらい?〜】

 

2014-07-2891145

【メンバー】

イガさん

アオキさん

【使ったもの】

D3.js (ver 3系)

topojson.js (ver 0.0.3)

cartogram.js (バージョン情報無し)

bootstrap (ver 3.2)

【素材】

外国人の推移データ(リンクは横浜市オープンデータポータル)

マップデータ(world-110m.json)

2014-07-2891753

年代を指定すると、「カルトグラム」という手法により、人数が大きいほど「実際よりも大きく表示」され、人数が少ないほど「実際よりも小さく表示」されます。 年代をスライダー表示するようにしていたのですが、スライダー表示ライブラリとの相性が悪く、ドロップダウンリストでごめんなさい。

2014-07-2891806

このように、ぐんにゃりとアニメーションしながら歪みます。

2014-07-2891816

国名の部分にオンマウスで人数がわかるようになっています。国名は残念ながら英語表記のままです(時間切れ)。

また、結果的にはそれほど劇的な変化は起こっていないので、マップの変化は微々たるものです。。。もう少し作りこめば、スムーズな表現が可能になるかもしれませんが、ちょっとエネルギーが切れてしまいました。。。

 

特に問題が多かった部分

 

ライブラリのバージョン

D3.jsは最新のもので良かったのですが、topojson.jsのバージョンはいくつか試す必要がありました。最新のtopojson.jsのVer1.6.15では、cartogram.jsが動きません。cartogram.jsでtopojsonオブジェクトを直接扱っていますが、topojson.jsのVer1.6.15の仕様は考慮されていません。 今回使っているものは、topojson.jsのVer0.0.3となっています。(1.0.0でも動きませんでした。恐らくobject関数のcoordinatesの呼び出し方法がまずい) ライブラリ自体に手をいれるのは今回はちょっと。。。ということですいません。

データの差

0人の国もあれば、中国は3万人以上の在日外国人がいる状態だと、差分が大きすぎるせいかマップが崩れてしまいます。このあたりは2次元CADを触ったことのある人ならわかると思いますが、ノード(ポイント)間の差を自動で繋げることができず、図面が破綻してしまいます。 ですので、若干のパーセンテージを加味しています。


if(num >= 30000){
  opti_num = parseInt(num*0.1);
}else if(num >= 10000 && num < 30000){
	opti_num = parseInt(num*0.1); 
}else if(num >= 5000 && num < 10000){
  opti_num = parseInt(num*0.1);
}

この処理はもっとうまいメソッドがあるのかもしれませんが、追求していません。また、データ自体に手をいれていることにもなりますが、今回はビジュアライズを優先しています。正しい数値は、オンマウス時のポップアップで見ることができます。

データビジュアライズとD3.jsの可能性について

 

日々の作業や資料作成の中では、大きな意味でのデータビジュアライズという行為を行っているかと思います。しかし、より効果的なデータビジュアライズという意味で、Tube Graphics 木村博之さんの

 

  • 相手の立場にたった(相手が求める)ビジュアライズ
  • 無限にあるデータから何を選ぶか?フィルターと演出。例えば、横浜市がどんなビジュアライズを望むか?
  • 1つのソースを選ぶのはカンタンだが、複数のソースを組み合わせることで見えてくるものがある
  • 盛りだくさんはわかりづらい
  • 「キレイだけどわかりづらい」に陥りやすい
  • 美しさは、「わかりやすさ」「使いやすさ」

 

や、visualizing.jp の矢崎裕一さんの

 

  • 1つの変数(ソース)=1つの表示スタイル
  • 似て非なるものは使わない。
  • 誇張はせずに違いを端的に表す。

 

といったお話はナルホドと、今後に活かせる内容でした。 横浜市職員の方の人口推移の話では、

  • 今の単身高齢者は過去に子供がいてその後、単身になった世代
  • これからの高齢者は、独身のまま引き続き高齢者になる世代が出てくる。

というものが、興味深いですね。 第2回以降があれば、また参加してみたいですね。終わり。
 

ソース

サンプルのダウンロードはこちら

<!DOCTYPE html>
<html>
<head>
    <title>World in Yokohama</title>
    <meta charset="utf-8">
    <script src="js/d3.min.js"></script>
    <script src="js/topojson_vx.js"></script>
    <script src="js/cartogram.js"></script>
    <link href='http://fonts.googleapis.com/css?family=Open+Sans:300' rel='stylesheet' type='text/css'>
    <link href='css/bootstrap.css' rel='stylesheet' type='text/css'>
    <style type="text/css">
        body {
            font-family:'Open Sans', sans-serif;
            font-weight: 300;
            font-size: 14px;
            line-height: 1.4em;
            padding-top: 20px;
            margin: 0 auto;
        }
        #map-container {
            height: 600px;
            text-align: center;
            position: relative;
            margin: 30px 0;
            width: 100%;
        }
        #progress {
            color:#ccc;
            font-size: 1.2em;
        }

        .map_tooltip {
            border: 1px solid #ccc;
            background-color: white;
            padding: 5px 8px 4px 8px;
            border-radius: 4px;
            opacity: 0.8;
            min-width: 200px;
            text-align: center;
            -moz-border-radius: 4px;
            -webkit-border-radius: 4px;
        }
        .map_tooltip dt {
            padding: 6px 0;
            border-bottom: 1px solid #ccc;
            margin-bottom: 6px;
        }
        .map_tooltip dd {
            padding: 4px 0;
            margin-bottom: 10px;
            font-weight: bold;
        }
        #progress_dark {
            background-color: #000;
            opacity: 0.3;
            z-index: 10000;
            position: fixed;
            left: 0;
            top: 0;
            width: 100%;
            height: 1000px;
            visibility: hidden;
        }
    </style>
</head>

<body>
<div id="progress_dark"></div>
<h1 class="text-center">The World In Yokohama</h1>
<div class="container text-center">
横浜市に在住の外国人の推移をビジュアライズ
</div>
<hr />
<div id="change_slider" class="text-center" style="margin: 20px">
    年代を選びましょう
    <select id="age_slider" name="age_slider">
        <option value="">--</option>
        <option value="2003">2003年</option>
        <option value="2004">2004年</option>
        <option value="2005">2005年</option>
        <option value="2006">2006年</option>
        <option value="2007">2007年</option>
        <option value="2008">2008年</option>
        <option value="2009">2009年</option>
        <option value="2010">2010年</option>
        <option value="2011">2011年</option>
        <option value="2012">2012年</option>
        <option value="2013">2013年</option>
    </select>
</div>
<hr />
<div id="map-container">
    <svg id="map" style="width: 100%; height: 800px"></svg>
</div>
<script>
    //年代判定用
    var ageHash = [];
    //年代変更時の処理
    d3.select("#age_slider")
            .on("change", function() {
                location.hash = "#age=" + this.value;
            });
    window.onhashchange = function() {
        init();
    };
    //人数リストを読み込み
    var change_human;
    d3.json("./js/human.json", function(data){
        change_human = data;
    });

    //画像エリア
    var stage = d3.select("#map");
    var tooltip = d3.select("body")
            .append("div")
            .attr("class", "map_tooltip")
            .style("position", "absolute")
            .style("z-index", "1000")
            .style("visibility", "hidden")
            .text("年代を指定して下さい");

    //onLoad時の処理
    init();

    //マップ描画
    function init(){
        //一度マップを削除
        stage.text('');
        //マップ初期化
        var map = stage.append("g").selectAll("path");
        var proj,topology,geometries,carto,carto_features;
        proj = d3.geo.mercator()
                .scale(300)
                .translate([600, 400]);

        carto = d3.cartogram()
                .projection(proj)
                .properties(function (d) {
                    return d.properties;
                });
        //その都度、マップファイルを再読み込み
        d3.json("./js/world-topo-110m.json", function (topo) {
            //年代の値を取得
            ageHash = location.hash.split("=");
            var age = ageHash[1];

            //
            topology = topo;
            geometries = topology.objects.countries.geometries;

            var features    = carto.features(topology, geometries);
            var path        = d3.geo.path().projection(proj);
            map = map.data(features)
                    .enter()
                    .append("path")
                    .attr({
                        stroke: "#666",
                        "stroke-width": 1
                    })
                    .attr("class", function(d){  return d.properties.name})
                    .style("fill", function(d) {
                        return "#fff";// 塗りつぶしの色
                    })
                    .attr("d", path)
                    .on("mouseover", function(d){
                        //マウスオーバー時のTooltip
                        d3.select(this).style({"stroke":"#0098FF"});
                        return tooltip.style("visibility", "visible");
                    })
                    .on("mousemove", function(d){
                        //人数取得
                        var num = 0;
                        if(change_human[d.properties.name]){
                            if(change_human[d.properties.name][age]){
                                num = change_human[d.properties.name][age];
                            }
                        }
                        //年代が指定されている場合のみ
                        if(age){
                            return tooltip
                                    .style("top", (d3.event.pageY-10)+"px")
                                    .style("left",(d3.event.pageX+10)+"px")
                                    .html("<dl><dt>国名</dt><dd>" + d.properties.name + "</dd><dt>人数</dt><dd id=\"tooltip_num_box_"+removeSpace(d.properties.name)+"\">"+num+"人</dd></dl>");
                        }
                    })
                    .on("mouseout", function(){
                        d3.select(this).style({"stroke":"#666"});
                        return tooltip.style("visibility", "hidden");
                    });

            if(age){
                //処理中
                d3.select("#progress_dark").style("visibility","visible");

                setTimeout(function () {
                    carto.value(function (d) {
                        //Cartogram用人数取得
                        var opti_num = 1;
                        var num = 0;
                        if(change_human[d.properties.name]){
                            if(change_human[d.properties.name][age]){
                                num = change_human[d.properties.name][age];
                            }
                        }
                        //人数差が大きい場合、マップが崩れるための対策
                        if(num >= 30000){
                            opti_num = parseInt(num*0.1);
                            console.log("over 30000" + d.properties.name);
                        }else if(num >= 10000 && num < 30000){
                            opti_num = parseInt(num*0.1);
                            console.log("over 10000" + d.properties.name);
                        }else if(num >= 5000 && num < 10000){
                            opti_num = parseInt(num*0.1);
                            console.log("over 5000" + d.properties.name);
                        }
                        //最適化済みの人数を返す
                        return opti_num;
                    });
                    carto_features = carto(topology, geometries).features;
                    //人数によってfillカラーを調整
                    map.data(carto_features).style("fill", function(d) {
                        var num = 0;
                        if(change_human[d.properties.name]){
                            if(change_human[d.properties.name][age]){
                                num = change_human[d.properties.name][age];
                            }
                        }
                        var color = "fff";
                        if(num == 0){
                            color = "#fff";
                        }else if(num >= 1 && num < 500){
                            color = "#FFDDFF"
                        }else if(num >= 500 && num < 1000){
                            color = "#FFAAFF"
                        }else if(num >= 1000 && num < 10000){
                            color = "#FF99FF"
                        }else {
                            color = "#FF33FF"
                        }
                        d3.select(this).attr({"fill":color});
                    });
                    map.transition()
                            .duration(700)
                            .each("end", function () {
                                d3.select("#progress_dark").style("visibility","hidden");
                            })
                            .attr("d", carto.path);
                }, 10);

            }

        });
        //国名からスペースやクォーテーションを削除(ID指定用)
        function removeSpace(str){
            return str.replace(/[\'\s]+/g,"");
        }
    }
</script>

</body>

</html>

ピタリ株式会社

「ピッタリのITを。」
ピタリ株式会社は、企業の基幹を支えるシンプルな管理ソフト「セールスノート」を提供しています。
オフィスでMacを活用する場合には、クラウド管理ツールが最適です。

会社概要セールスノートセミオーダー
プライバシーポリシー