RACCOON TECH BLOG

株式会社ラクーンホールディングスのエンジニア/デザイナーから技術情報をはじめ、世の中のためになることや社内のことなどを発信してます。

sails.js + d3.jsでデータ可視化

こんにちは、開発チームのさわむらです。

こちら弊社エントランスです。クリスマス仕様。

entrance
visualizer

そのエントランスに置かれているのがこちら。
私の所属するユニットが作成した「SD export visualizer」というものです。

SD export visualizerとは

弊社の運営するサービスのひとつにファッション・雑貨業界のメーカーと国内の小売店を繋ぐ卸・仕入れサイト「SUPER DELIVERY」というものがあります。
そのSUPER DELIVERYの海外版のサービス「SD export」の開発に私が所属するユニットは携わっていました。
SD exportは国内のメーカーが世界各国の小売店と取引を行えるサービスなのですが、SD exportでの世界各国の受注状況や世界各国の小売店の数を可視化したいということで作成したのがSD export visualizerです。
世界地図上にデータを円で描画して可視化をしています。

このSD export visualizerですが、サーバー側をsails.js、地図、データの描画をd3.jsを使って作られています。

今回はそのsails.jsとd3.jsを使ってのデータの可視化の部分をご紹介したいと思います。

sails.jsとd3.jsについて

sails.js
      Node.jsのMVCフレームワーク。 Railsライク 。
     公式: http://sailsjs.com/
d3.js
     データをブラウザで可視化するためのライブラリ。
     データに基づいてSVGを操作してデータを可視化します。
     公式: https://d3js.org/
     

sail.jsの導入

今回は以下の環境で進めていきます。

Ubuntu 16.04 LTS
Node.js v4.2.6

まずはsails.jsのインストールから

$ 
sudo npm install sails -g
早速sailsプロジェクトを作成してみましょう。
$ sails new visualizer_techBlog

liftコマンドでHTTPサーバーを起動

sails_list

ブラウザで http://localhost:1337/ にアクセスするとデフォルトのプロジェクトページが表示されます。

sails_default

controller、viewの作成

サーバーの稼働確認をしたところで、controllerの作成からはじめます。
今回はRetailerCountというcontrollerとindexアクションを作成しています。

$ sails generate controller RetailerCount index

ここで、サーバーを再起動してhttp://localhost:1337/retailerCount/にアクセスすると以下のように表示されます。

retailerCountIndex


次にRetailerCountControllerのindexアクションに紐づくviewを作っていきます。

まず、indexアクションがHTMLページでレスポンスするように変更します。

api/controller/RetailerCountController.js

module.exports = {
    /**
     * `RetailerCountController.index()`
     */
    index: function (req, res) {
        return res.view();
    }
};

res.view()は常に小文字でviewファイルを探しにいくので、今回の場合だとviews/retailercount/index.ejsを見つけにいきます。(sailsのデフォルトのビューエンジンはEJSです。

views/retailercount/index.ejsを作成してhttp://localhost:1337/retailerCount/にアクセスしてみましょう。今回はサーバーの再起動は必要ありません。

views/retailercount/index.ejs

hello sails.js
index

表示されました。

世界地図をd3.jsで描画する

■世界地図データの入手
世界地図データはNatural Earth(http://www.naturalearthdata.com/)から入手できます。

http://www.naturalearthdata.com/downloads/110m-cultural-vectors/
上記のURLの Admin 0 – Countries の中のDownload countries から地図データをダウンロードします。

■地図データの変換
ここで、d3.jsで地図を描画できるのはGeoJSON(またはTopoJSON)形式となるので、先ほどダウンロードした地図データをGeoJSON形式へ変換する必要があります。

今回はGDALというライブラリのogr2ogrというコマンドでGeoJSONへ変換します。


$ sudo apt-get install gdal-bin

$ ogr2ogr -lco "ENCODING=UTF-8" -f geoJSON world.json ne_110m_admin_0_countries.shp

Natural Earthからダウンロードしたshape形式(.shp)のデータをworld.jsonという名前のGeoJSONに変換しています。

■地図描画
次にworld.jsonをassets/data/ など適当な場所に配置します。

そのファイルをd3.jsに読み込ませて世界地図を描画していきましょう。

d3.jsでは様々な投影法で地形を描画できます。
https://github.com/d3/d3-geo/blob/master/README.md

今回はその中からd3.geo.mercatorで描画します。

assets/js/ 下に地図描画を行うjsを作成します。

assets/js/draw_map.js

function drawMap() {
    return new Promise(function (resolve, reject) {
   
    // マップの表示サイズを設定
        var w = 1250;
        var h = 700;
        var svg = d3.select('#map').append('svg')
                .attr('width', w)cd ../
                .attr('height', h);

    // jsonファイルの読み込み
        d3.json('/data/world.json', function(err, wJson){
            if (err) {
               reject(err);
               return;
            }
            // 投影法や地図の大きさの設定
            var projection = d3.geo.mercator()
                                .rotate([210,0])
                                .translate([w/2, h/1.5])
                                .scale(w / 2.0 / Math.PI);
           
     // GeoJsonの緯度経度情報を投影法にしたがってブラウザのx座標、y座標に変換するための定義
            var path = d3.geo.path().projection(projection);

            // SVGに地図描画
            svg.selectAll('path')
               .data(wJson.features)
               .enter()
               .append('path')
               .attr('d', path)
               .attr('stroke', '#00ac97')
               .style('fill', '#a3d6cc');

           resolve(projection);
        });
    });
}

views/retailercount/index.ejs にマップの表示領域の作成、d3.js等の読み込み、先ほど作成したdrawMap()の呼び出しを記述します。

views/retailercount/index.ejs

<!--マップの表示領域-->
<div id="map"></div>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.0.0/jquery.min.js"></script>
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<script src="/js/draw_map.js" charset="utf-8"></script>
<script type="text/javascript">
    $(function(){
        drawMap().then(function(projection) {
          //後でこの中でデータの描画を行う  
        }).catch(function(err) {
            console.error(err);
        });
    });
</script>

http://localhost:1337/retailerCount/ にアクセスして地図を確認してみましょう。
世界地図が描画されるはずです。

world_map

データ描画

■DB接続

oracleが公開しているnode-oracledbドライバを使ってoracleに繋ぎます。
http://www.oracle.com/technetwork/jp/database/database-technologies/scripting-languages/node_js/index.html

下記URLからOracle Instant ClientのbasicとSDKのrpmをダウンロードしてきます。
http://www.oracle.com/technetwork/topics/linuxx86-64soft-092277.html

ダウンロードしてきたら、Debianパッケージに変換してインストールします。

$ sudo alien -d oracle-instantclient12.1-basic-12.1.0.2.0-1.x86_64.rpm
$ sudo alien -d oracle-instantclient12.1-devel-12.1.0.2.0-1.x86_64.rpm
$ sudo dpkg -i oracle-instantclient12.1-basic_12.1.0.2.0-2_amd64.deb
$ sudo dpkg -i oracle-instantclient12.1-devel_12.1.0.2.0-2_amd64.deb

下準備が整ったとこで、node-oracledbをインストールします。

$ sudo npm install oracledb

次にsailsプロジェクトのconfig下に接続情報のdbconfig.jsとapi/service下にDBに接続してsqlを実行するOracleService.jsを作成します。

config/dbconfig.js

module.exports = {
      user          : "user",
      password      : "password",
      connectString : "connectString"
};

api/service/OracleService.js

module.exports = {
    execute: function(sql, params) {
        var Promise = require('es6-promise').Promise;
        return new Promise(function(resolve, reject) {
                var oracledb = require('oracledb');
                var config = require('../../config/dbconfig.js');
                if (params == null) {
                    params = {};
                }
                oracledb.getConnection({
                        user: config.user,
                        password: config.password,
                        connectString: config.connectString
                    }).then(function(connection) {
                        connection.execute(sql, params, {outFormat: oracledb.OBJECT})
                            .then(function(result) {
                                connection.close().catch(function(err) {
                                    reject(err.message);
                                });
                                resolve(result.rows);
                            }).catch(function(err) {
                                connection.close().catch(function(err) {
                                    console.error(err.message);
                                });    
                                reject(err.message);
                            });
                    }).catch(function(err) {
                        reject(err.message);
                    });
        });
    }
}

RetailerCountController.jsにsqlを実行してデータを取得するgetCountアクションを追加して取得したデータを確認してみます。

api/controller/RetailerCountController.js

 module.exports = {
    /**
     * `RetailerCountController.index()`
     */
    index: function (req, res) {
        return res.view();
    },
   
    getCount: function(req, res) {
        var oracleService = require ('../services/OracleService');
        var retailerCountSql = "国ごとの小売店数を取得するsql";
        oracleService.execute(retailerCountSql, null).then(function(result) {
            res.send(result);
        }).catch(function(err) {
            console.error(err);
        });
    }
};

サーバーを再起動して、http://localhost:1337/retailerCount/getCount にアクセス。
getCount

■世界の首都の緯度経度のデータ作成
先ほど取得したデータのISO 3166-1 alpha-2の国名コードを元に、その国の首都に会員数を円で描画します。そのためにはそれぞれの国の首都の緯度経度情報が必要になります。

以下のアマノ技研さんのサイトからCSV形式でダウンロードできます。
http://www.amano-tec.com/download/world.html

入手したCSVをjsonに変換して、assets/data/ 下に配置します。(今回はcountries.jsonという名前で作成しています。)

■データの描画
それではgetCountアクションに追記していきます。
getCountアクションで取得した会員数を国名コードをもとにcountries.jsonに追加したものを返すようにしています。

api/controller/RetailerCountController.js

module.exports = {
    /**
     * `RetailerCountController.index()`
     */
    index: function (req, res) {
        return res.view();
    },
   
    getCount: function(req, res) {
        var oracleService = require ('../services/OracleService');
        var retailerCountSql = "国ごとの小売店数を取得するsql";                    

        oracleService.execute(retailerCountSql, null).then(function(result) {
            var countryDetail = require('../../assets/data/countries.json');
            var retailerCounts = Array();
           
            for (var i= 0; i < result.length; i++) {
                for (var j = 0; j<countryDetail.length; j++) {
                    if (result[i].COUNTRY_CODE == countryDetail[j].code) {
                        countryDetail[j].count = result[i].COUNT;
                        retailerCounts.push(countryDetail[j]);
                    }
                }
            }
            res.send(retailerCounts);  
        }).catch(function(err) {
            console.error(err);
        });
    }
};


view側でgetCountアクションを呼び出して、結果のjsonをd3.jsに渡して円を描画します。

views/retailerCount/index.ejs

    <!--マップの表示領域-->
    <div id="map"></div>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.0.0/jquery.min.js"></script>
    <script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
    <script src="/js/draw_map.js" charset="utf-8"></script>
    <script type="text/javascript">
        $(function(){
            drawMap().then(function(projection) {
                d3.json('/retailerCount/getCount', function(data) {
                    drawDataLayer(data, projection);
                });
            }).catch(function(err) {
                console.error(err);
            });
        });
    </script>

assets/js/draw_map.js

function drawMap() {
    ...
}

function drawDataLayer(data, projection) {

    // 数に応じた丸の半径の大きさを計算するための設定
    // rangeの範囲とデータのmin-maxから丸の大きさを勝手に計算してくれる便利なやつ

    var rScale = d3.scale.linear()
            .domain([d3.min(data, function(d) {return d.count;}), d3.max(data, function(d) {return d.count;})])
            .range([10, 30]);

    // 色の調整
    // 丸が小さい場合は大きい丸に重なると見えづらくなるので、小さい丸を少し濃くしてみる

    var colorScale = d3.scale.linear()
            .domain([d3.min(data, function(d) {return d.count;}), d3.max(data, function(d) {return d.count;})])
            .range(['#ffa500', '#f6ae54']);

    // データレイヤーの表示
    // SVGにcircleタグを追加
    // 丸の表示位置は緯度経度をx,y座標に変換した値

    d3.select('#map').select('svg').selectAll('circle')
        .data(data)
    .enter()
    .append('circle')
    // x座標への変換
    .attr('cx', function(d) {
      return projection([d.lon, d.lat])[0];
    })
    // y座標への変換
    .attr('cy', function(d) {
      return projection([d.lon, d.lat])[1];
    })
    .attr('r', function(d) {
      // 丸の大きさをスケールを使って決める
      return rScale(d.count);
    })
    .style('fill', function(d) {
      // 丸の色をスケールを使って決める
      return colorScale(d.count);}
    )
    .style('opacity', 0.75)
    .style('stroke', '#F6F6F6')
    .style('stroke-width', 1);
}

サーバーを再起動して、http://localhost:1337/retailerCount/ にアクセスして地図を確認してみます。

data_world_map

データが描画できました。

おわりに

今回はsails.jsとd3.jsを使ってのデータの可視化のご紹介でした。
もともとSD export visualizerはjsの知識を深めようということで、実務以外でユニットの目標として作ったものでしたが、データの可視化の経験がなかった身としてはすごくいい経験になったと思っています。

ぜひ皆さんも一度データの可視化に挑戦してみてはいかがでしょうか?

一緒にラクーンのサービスを作りませんか? 採用情報を詳しく見る

関連記事

運営会社:株式会社ラクーンホールディングス(c)2000 RACCOON HOLDINGS, Inc