sails.js + d3.jsでデータ可視化
こんにちは、開発チームのさわむらです。
こちら弊社エントランスです。クリスマス仕様。
そのエントランスに置かれているのがこちら。
私の所属するユニットが作成した「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について
sail.jsの導入
今回は以下の環境で進めていきます。
Ubuntu 16.04 LTS
Node.js v4.2.6
まずはsails.jsのインストールから
$
sudo npm install sails -g
$ sails new visualizer_techBlog
liftコマンドでHTTPサーバーを起動
ブラウザで http://localhost:1337/ にアクセスするとデフォルトのプロジェクトページが表示されます。
controller、viewの作成
サーバーの稼働確認をしたところで、controllerの作成からはじめます。
今回はRetailerCountというcontrollerとindexアクションを作成しています。
$ sails generate controller RetailerCount index
ここで、サーバーを再起動してhttp://localhost:1337/retailerCount/にアクセスすると以下のように表示されます。
次に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
表示されました。
世界地図を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/ にアクセスして地図を確認してみましょう。
世界地図が描画されるはずです。
データ描画
■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 にアクセス。
■世界の首都の緯度経度のデータ作成
先ほど取得したデータの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/ にアクセスして地図を確認してみます。
データが描画できました。
おわりに
今回はsails.jsとd3.jsを使ってのデータの可視化のご紹介でした。
もともとSD export visualizerはjsの知識を深めようということで、実務以外でユニットの目標として作ったものでしたが、データの可視化の経験がなかった身としてはすごくいい経験になったと思っています。
ぜひ皆さんも一度データの可視化に挑戦してみてはいかがでしょうか?