Learn to Live and Live to Learn

IT(たまにビジネス)に関する記事を読んで、考えて、使ってみたことをまとめる場。

D3.jsでヒートマップっぽいものを作ってみた

概要

地域データを扱うことが多いので,可視化の方法としてD3.jsを勉強しました. (可視化は手段なんで,RでもTablueauでも状況に合わせて使い分ければいいと思います)

D3.js(Data Driven Documents)は

ウェブブラウザで動的コンテンツを描画するJavaScriptライブラリ(Wikipediaより)

です.

dotinstallをざっと見とくと理解が早いと思います.

今回は以下の記事をベースに地図に「都道府県別 100万人あたりの常設映画館数」をプロットしてみました. qiita.com

完成図はこちら. f:id:A_01:20170219212803p:plain 情報源:政府統計の総合窓口 GL01010101 (東京が一番かと思いきや福岡が多いんですね.スクリーンの数とか,映画館の来場回数とか,DVDやテレビなど配給方法の割合とか色々気になりますが,今回は関係ないので触れません)

準備

ツール

# ShapeかGeoJSONへの変換に利用する
$ brew install gdal

# GeoJSONからTopoJSONへの変換に利用する
$ npm install -g topojson

# 最新版のV2系では,Qiitaで使用されているtopojsonというコマンドが4つのコマンドに分割されている.
# V2ではgeo2topoが該当するが,今回はV1を指定してインストールする
$ npm install -g topojson@1.6.27

# kokoじゃなくてもいいんだけど
# 今回,外部ファイルからデータを読み込むんで,直接htmlファイルを開くだけだと外部ファイルを読み込めなず
# サーバにファイルを置いてアクセスする必要がある.kokoはそのための手段.
$ brew install node
$ npm install -g koko

データ

Natural Earthから都道府県レベルの地図データをダウンロードします.Shapeファイルなんで,GeoJSON -> TopoJSONと変換して使います.

http://www.naturalearthdata.com/http//www.naturalearthdata.com/download/10m/cultural/ne_10m_admin_1_states_provinces.zip

# ne_10m_admin_1_states_provinces.shpというShapeファイルが入っているので,pref.jsonというGeoJSONに変換する
$ ogr2ogr -f GeoJSON -where "adm0_a3 = 'JPN'" pref.json ne_10m_admin_1_states_provinces.shp

# GeoJSONからjapan.jsonというTopoJSONに変換する
$ topojson -p name -p name_local -p latitude -p longitude -o japan.json pref.json

実装

こんな感じです. ちなみに2017/02/19時点でD3.jsの最新版はv4なんですが,アニメーションを設定しようとするとエラーになったのでhttps://d3js.org/d3.v3.min.jsを指定しています.

<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="utr-8">
        <title></title>
       <style type="text/css">
        .pref {
            fill: #fff;
            stroke: #aaa;
        }
        </style>
    </head>
    <body>
        <script src="https://d3js.org/d3.v3.min.js"></script>
        <script src="http://d3js.org/topojson.v0.min.js"></script>
        <script>
            var w = 1300;
            var h = 960;

            var svg = d3.select("body")
                .append("svg")
                .attr("width", w)
                .attr("height", h);

            // 地図の投影図法を設定する.
            var projection = d3.geo.mercator()
                .center([136, 35.5])
                .scale(2000)
                .translate([w / 2, h / 2]);

            // GeoJSONからpath要素を作る.
            var path = d3.geo.path()
                .projection(projection);

            var pref = {
                    "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":"沖縄県"
                    };

            // TopoJSONを読み込む.
            d3.json("japan.json", function(error, japan) {
                console.log(japan);
                var topo = topojson.object(japan, japan.objects.pref).geometries;
                svg.selectAll(".pref")
                    .data(topo)
                    .enter()
                    .append("path")
                    // キーとバリューを逆にしてしまったので回避策.
                    .attr("class", function(d) {
                        var id =  Object.keys(pref).filter((key) => {
                            return pref[key] === d.properties.name_local
                            });
                        return "pref pref" + id;
                    })
                    .attr("d", path);
            });    
    
            var category = [{"name":"常設映画館数(人口100万人当たり)(館)",
                "data":{"1":10.4,
                    "2":13.6,
                    "3":14.0,
                    "4":6.0,
                    "5":13.5,
                    "6":9.7,
                    "7":7.2,
                    "8":10.3,
                    "9":8.1,
                    "10":10.6,
                    "11":4.1,
                    "12":5.6,
                    "13":21.4,
                    "14":6.5,
                    "15":5.6,
                    "16":4.7,
                    "17":6.9,
                    "18":16.5,
                    "19":5.9,
                    "20":13.3,
                    "21":6.4,
                    "22":10.8,
                    "23":7.5,
                    "24":13.7,
                    "25":9.2,
                    "26":7.7,
                    "27":6.6,
                    "28":13.7,
                    "29":3.6,
                    "30":8.2,
                    "31":24.4,
                    "32":5.7,
                    "33":5.2,
                    "34":20.1,
                    "35":13.5,
                    "36":6.5,
                    "37":18.3,
                    "38":15.1,
                    "39":13.6,
                    "40":36.7,
                    "41":6.0,
                    "42":20.2,
                    "43":31.2,
                    "44":12.0,
                    "45":12.6,
                    "46":7.8,
                    "47":9.1}}];
            
            var max = 40;
            var hoge = Math.floor(255 / 40);

            // 右上にあった常設映画館数というテキストをマウスオーバーすると
            // 映画館数によって都道府県の色が濃くなるよう施す.
            svg.selectAll("text")
                .data(category)
                .enter()
                .append("text")
                .attr("class", "category-label")
                .attr("x", w - 360)
                .attr("y", function(d, i) {
                    return i * 20 + 200;
                })
                .text(function(d) {
                    return d.name;
                })
                .on("mouseover", function(d) {
                    for (var key in d.data) {
                        var color = 255 - hoge * d.data[key];
                        if (color < 0) {
                            color = 0;
                        }
                        d3.select(".pref" + key)
                            .transition()
                            .style("fill", "rgb(" + color + ", " + color + " ,255)");
                    }
                    d3.select(this).style("fill", "#66f");
                })
                .on("mouseout", function(d) {
                    for (var i = 1; i < 48; i++) {
                        d3.select(".pref" + i)
                            .transition()
                            .style("fill", "#fff");
                    }
                d3.select(this).style("fill", "gray");
                });
        </script>
    </body>
</html>

動作確認

$ koko -o

参考URL