はじめに
この記事はteratailのBluemixアドベントカレンダーに参加しています。
日にちは過ぎていますが13日目が空いていたので投稿しました。
よかったらNiceをお願いします。
今回は歩く食べログと言われてみたかった僕が、Bluemixで近くのお店の評判を分析してみたいと思います。
概要
まず今回作ったものはこんな感じです。(デザインは見て見ぬふりを。。)
https://sykmhmh.mybluemix.net/main
※意外と口コミまで取れる店が少ないので、店が見つからない場合は位置情報の取得を拒否すると渋谷で検索します。
大まかな処理の流れは、
① 現在地を取得する
② YahooローカルサーチAPIで近くのお店のIDを取得する
③ Yahooクチコミ検索APIを使ってお店の口コミを取得
④ 口コミをまとめてTranslateAPIで英語に翻訳
⑤ 口コミをTone Analyzerで感情分析
⑥ 結果を表示
みたいな感じです!
事前準備
Node-REDの準備
こちらの記事をもとにNode-REDのフロー編集画面を表示するところまで行います。APIキーの取得
今回はYahooのローカルサーチAPI、クチコミ検索APIとYandexのTranslateAPIを使うのでそれぞれのAPIキーを取得しておきます。Tone Analyzerの準備
BluemixのカタログページからTone Analyzerを選択して作成し、usernameとpasswordをメモしておきます。
・参考記事
開発
ローカルサーチAPIで近くの店を検索してみる
Node-REDは画面左側にある様々なノードを組み合わせることで簡単にアプリケーションを作ることができます。
まずローカルサーチAPIを使って飲食店を検索して見たいと思います。
injectノードでトリガーを設定し、httpノードで指定のURLにリクエストを送ってdebugノードで画面右側にあるdebugタブに結果を表示することができます。
ローカルサーチAPIのリクエストの形式は以下のような感じです。
http://search.olp.yahooapis.jp/OpenLocalPlatform/V1/localSearch?appid={api_key}&output=json&image=true&sort=-review&results=3&lat=35.6581&lon=139.701742&dist=0.5&query=焼肉
ここでは検索ワードが「焼肉」で場所が渋谷駅の近くで写真があるお店を口コミ件数が多い順に3件取ってきています。
このURLをhttpノードのURLの欄に入れてDeployし、injectノードの左側のボタンを押すと処理が始まり以下のような感じでdebugタブに結果が表示されます。リクエストの結果はmsg.payloadという変数に入っています。
この結果のUidの値がお店特有のIDであり、これを使って口コミを検索していきます。
口コミ検索APIでお店の口コミを検索
口コミ検索APIは先程のUidの値を使って以下のようにリクエストします。
http://api.olp.yahooapis.jp/v1/review/{Uid}?output=json&appid={api_key}
上手くいくとこのように結果が返ってきます。
こんな感じで処理を組み合わせていくことで実装することができます。
近隣のお店の評判検索アプリを作っていく
今回はフォームで値を受け取って、ajaxで検索結果と1店舗ずつの口コミの感情分析結果を取得し表示するというような流れを作りました。
先に出来上がったフローはこんな感じです。(きたな...)
順番に見ていきます。
まずここでは、/local_searchにアクセスされたら現在地の緯度経度とフォームの値を受け取って近くのお店を検索してます
Node-REDではPOSTで送られてきた値はmsg.payload、GETで送られてきた値はmsg.req.queryの中に入っています。
また、msg.urlの変数にURLを入れると、httpノードのURLが空の状態で勝手にmsg.urlに入っているURLにアクセスしてくれます。
次にここでは取得したお店を1件ずつ処理していくので、ループの中で使う変数などの定義をしています。
ループの終了判定はflow.typeの値で行うようにし、値が"through"になった時に抜けるようにするようにしました。
次のノードではmsg.headersとmsg.urlの値を変更しようとすると怒られたので一度リセットしてます。
特定のプロパティは上書きできないみたいなことが書いてあります。
https://github.com/node-red/node-red/wiki/Deprecated:-Message-properties-overriding-set-node-properties
次のノードでお店のUidを取得し、口コミ検索の値を設定します。
その後で取得した口コミを1文にまとめて、翻訳します。
Translate APIのリクエストの形式は以下のような感じです。
https://translate.yandex.net/api/v1.5/tr.json/translate?key={api_key}&lang=en&format=json&text={text}
翻訳結果を受け取ったらTone Analyzerで口コミの感情分析をします。
Tone Analyzerの使い方は左側のノードのリストからTone Analyzerを探して配置し、UsernameとPasswordにメモしておいた値を入れ、入力に分析したいテキストを渡すことで分析してくれます。
結果はmsg.responseにこんな感じで返ってきます。
これを配列に追加していきます。
今回は、リクエスト投げまくっててすごい遅いので評価があるお店が5件取得できた時点でループを抜け出すようにしました。
これでとりあえず一連の流れができました。
次にUIをつくります。
メインページ(/main)ページのHTMLはこんな感じです。
<html lang="ja">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
<title>近隣評判検索</title>
<style type="text/css">{{{ payload.style }}}</style>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script>
<script src="https://d3js.org/d3.v3.min.js" charset="utf-8"></script>
</head>
<body>
<div class="container-fluid no-padding">
<h1 class="text-center">近隣スポットの評判を調べよう</h1>
<div id="forms" class="text-center">
<div class="form-inline">
<input type="text" id="query" placeholder="例) 焼肉" class="form-control">
<button id="submit" class="btn btn-primary">Submit</button>
</div>
</div>
<div class="row">
<div id="color-list" class="col-lg-12 col-xs-12" style="display:none;">
<div class="color-box col-lg-2 col-lg-offset-1 col-xs-4">
<div class="color col-xs-5" style="background-color:#e53935;"></div>
<p class="tone col-xs-7">Anger<br>(怒り)</p>
</div>
<div class="color-box col-lg-2 col-xs-4">
<div class="color col-xs-5" style="background-color:#D500F9;"></div>
<p class="tone col-xs-7">Disgust<br>(嫌悪感)</p>
</div>
<div class="color-box col-lg-2 col-xs-4">
<div class="color col-xs-5" style="background-color:#607D8B;"></div>
<p class="tone col-xs-7">Fear<br>(恐れ)</p>
</div>
<div class="color-box col-lg-2 col-lg-offset-0 col-xs-4 col-xs-offset-2">
<div class="color col-xs-5" style="background-color:#FF5722;"></div>
<p class="tone col-xs-7">Joy<br>(喜び)</p>
</div>
<div class="color-box col-lg-2 col-xs-4">
<div class="color col-xs-5" style="background-color:#283593;"></div>
<p class="tone col-xs-7">Sadness<br>(悲しみ)</p>
</div>
</div>
<div id="loading-box" class="col-xs-12" style="display: none;">
<p>読み込み中・・・</p>
<div class="progress">
<div class="progress-bar progress-bar-striped progress-bar-success active" role="progressbar" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100" style="width: 100%">
<span class="sr-only"></span>
</div>
</div>
</div>
<div id="no-data" class="col-xs-12" style="display: none;">
<p>お店がないです。。。</p>
</div>
<div id="result" class="col-xs-12">
<div class="shop-list">
</div>
</div>
</div>
</div>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
<script type="text/javascript">{{{ payload.script }}}</script>
</body>
</html>
{{{ payload.style }}}や{{{ payload.script }}}でその前のノードで定義したJSやCSSを読み込めます。
Submitが押されたときにフォームの値を受け取ってajaxで/local_searchにアクセスして結果を受け取り、結果をチャートで表示するJSはこんな感じです。
今回はD3.jsを使ってみました。
(function(){
var chart_color = ["#e53935", "#D500F9", "#607D8B", "#FF5722", "#283593"],
win = d3.select(window),
pie = d3.layout.pie().sort(null).value(function(d) { return d.value }),
arc = d3.svg.arc().innerRadius(0),
is_animated = false,
size = { width: 200, height: 200 },
lat, lon;
// 現在地の取得
if (!navigator.geolocation){
alert("ブラウザが位置情報に対応していないみたいです( ;´Д`)");
} else {
var option = {
enableHighAccuracy: true,
maximumAge: 1,
timeout: 10000
};
navigator.geolocation.getCurrentPosition(
function(position) { success(position); },
function(error) { err(error); },
option
);
}
function success(position) {
lat = position.coords.latitude;
lon = position.coords.longitude;
}
function err(error){
console.log('位置情報を取れませんでした')
// 位置情報が取得できなかった場合は渋谷駅の緯度経度を使う
lat = 35.6581;
lon = 139.701742;
}
// submit時の処理
d3.select('#submit').on('click', function() {
var query = d3.select('#query').property("value");
if (!query) {
alert('検索する値を入れてください');
return;
}
d3.selectAll(".shop-list div")
.remove();
d3.select("#color-list")
.style({'display':'none'});
sendRequest(query);
});
function sendRequest(query) {
var search_url = '/local_search',
post_data = {
'lat' : lat,
'lon' : lon,
'query' : query
};
d3.select("#loading-box")
.style({'display':'block'});
// ajaxで/local_searchにリクエストを送る
d3.json(search_url)
.header("Content-Type", "application/json")
.post(JSON.stringify(post_data), function(error, data) {
if (error !== null) {
console.log(error);
return;
}
d3.select("#loading-box")
.style({'display':'none'});
console.log(data);
if (data.length === 0 || Object.keys(data).length === 0) {
d3.select("#no-data")
.style({'display':'block'});
} else {
d3.select("#color-list")
.style({'display':'block'});
// 一件ずつお店を表示
data.forEach(function(d, index) {
displayShop(d.shop, d.emotion, index+1);
});
}
});
}
// お店を表示
function displayShop(shop, emotion, shop_number) {
var shop_class = "shop" + shop_number;
d3.select(".shop-list")
.append("div")
.attr({"class": "shop" + shop_number + " panel panel-primary"});
d3.select("." + shop_class)
.html('<div class="shop-title panel-heading">' + shop.Name + '</div><div class="shop-detail panel-body"><img class="col-xs-4 col-md-3" src="' + shop.Property.LeadImage + '"/><table class="shop-info col-xs-8 col-md-6"><tbody><tr><th class="col-xs-3">住所</th><td class="col-xs-9">"' + shop.Property.Address + '"</td></tr><tr><th class="col-xs-3">最寄駅</th><td class="col-xs-9">' + shop.Property.Station[0].Name + '駅 徒歩' + shop.Property.Station[0].Time + '分</td></tr></tbody></table><div class="shop-chart-box col-xs-12 col-md-3"><svg class="chart shop-chart' + shop_number + ' col-xs-12"></svg></div></div>');
drawChart(emotion, shop_number);
}
// 感情分析結果をpie chartで表示
function drawChart(emotion, shop_number) {
var chart_data = [],
svg = d3.select(".shop-chart" + shop_number);
emotion.forEach(function(d, index) {
chart_data.push({ tone_name: d.tone_name, value: d.score, color: chart_color[index] });
});
render(svg, chart_data);
update(svg);
animate(svg, chart_data);
win.on("resize", function() {
svg = d3.selectAll("svg");
update(svg);
});
}
// pie chart描画
function render(svg, chart_data) {
var g = svg.selectAll(".arc")
.data(pie(chart_data))
.enter()
.append("g")
.attr("class", "arc");
g.append("path")
.attr("stroke", "white")
.attr("fill", function(d){ return d.data.color; });
var maxValue = d3.max(chart_data, function(d){ return d.value; });
g.append("text")
.attr("dy", ".35em")
.attr("font-size", function(d){ return d.value / maxValue * 20 })
.style({"text-anchor":"middle", "fill":"white"})
.text(function(d) { return d.data.tone_name });
}
// pie chartをレスポンシブに
function update(svg) {
size.width = parseInt(svg.style("width"));
size.height = parseInt(svg.style("height"));
arc.outerRadius(size.width / 2);
svg
.attr("width", size.width)
.attr("height", size.width);
var g = svg.selectAll(".arc")
.attr("transform", "translate(" + (size.width / 2) + "," + (size.width / 2) + ")");
if (is_animated) {
g.selectAll("path").attr("d", arc);
}
g.selectAll("text").attr("transform", function(d) { return "translate(" + arc.centroid(d) + ")"; });
}
// pie chartのアニメーション
function animate(svg, chart_data) {
var g = svg.selectAll(".arc"),
length = chart_data.length,
i = 0;
g.selectAll("path")
.transition()
.ease("cubic-out")
.delay(400)
.duration(1000)
.attrTween("d", function(d) {
var interpolate = d3.interpolate(
{startAngle: 0, endAngle: 0},
{startAngle: d.startAngle, endAngle: d.endAngle}
);
return function(t) {
return arc(interpolate(t));
};
})
.each("end", function(transition, callback) {
i++;
is_animated = i === length;
});
}
})();
途中の無理矢理感がすごい。
とりあえずこれで一応動くようにはなリました。
結果
結果を見てみます。
結果①
"サークルの新年会を渋谷でやることになり以前から予約していたこのお店に♪決め手は、店内の写真が綺麗でよさそうだったのとコースが豊富で料理もおいしそうだったからです!大人数用の個室があるのもよかったです☆お店は期待通りのお店ですごく良かったです!!歓送迎会もまた利用したいな♪女子会に強いお店っぽかったので女子会新年会で利用ー♪予約していたから個室がとれました☆通してもらうとすごくオシャレな個室♪♪女子のつぼおさえてます笑お鍋もどれもおいしくすごーく満足!いい新年会になりました!!前から気になっていた居酒屋だったんですが先日ついに行きました!鍋メニューもいっぱいありどれを注文するか迷ったのですが、トマトチーズ鍋が入っているコースを注文しました!!めーーーーっちゃおいしかったです♪www鍋だけでなくコース内容も女子向けメニューが豊富ですごくおいしかったーーーお店の雰囲気もよかったですよ♪HPを見ると女子会利用に良さそうだったので、利用してみました♪写真通り店内はきれい!!料理もコースがいっぱいあってどれもおいしい!そしてヘルシー♪女子会にはもってこい☆女子会の時はまた利用させていただきます☆"
この口コミに対して結果が
結果②
"こちらのお店は、以前も訪れた事があり(約5年前)同じ系列のお店にも何度か訪れた事があります。料理がおいしい上に、気持ちが良い接客態度を理由に今回予約をしましたが、かなりがっかりさせられました。ウェイターの方々の接客の基本のようなものが身に付いていなくて、それぞれの接客態度の質にかなりの差が感じられました。まるで居酒屋のように、注文したドリンクをただテーブルに置くだけ。空いたお皿を気付いて下げる方が少ない、あるいは下げる時にも黙って下げていくなど。挙句の果てに、注文した品が来なくて何度が催促をしたら30分経ってようやく席に訪れ、「注文がされてなかったので、すみません」とサラッと言われました。怒りと共にあきれました。お詫びのサービスが最後にはありましたが、接客態度の悪さに二度と訪れまいと思いました。"
この口コミにに対して結果が
途中で翻訳してることも考えると割りと当たってる?
まとめ
ということで今回はBluemixと口コミ検索APIなどを使って口コミを感情分析してみました。
Node-REDは使い方に慣れるまでがちょっとめんどくさいですが簡単なアプリケーションならすぐ試せて便利だなと思いました。
一応、今回作ったフローのjsonファイルを載せておきます。
[{"id":"99a6aa52.4050a","type":"function","z":"a28316.e5abbce8","name":"i++","func":"index = flow.get('i');\nshop_count = flow.get('shop_count');\nevaluations = global.get('evaluations');\n\nif (evaluations.length < 5 && index < shop_count - 1) {\n index++;\n flow.set('i', index)\n} else {\n flow.set('type', 'through');\n}\nreturn msg;","outputs":"1","noerr":0,"x":557.6666870117188,"y":271.77777099609375,"wires":[["b20983e3.e398d8"]]},{"id":"f34a92a.1f3307","type":"http in","z":"a28316.e5abbce8","name":"/local_search","url":"/local_search","method":"post","swaggerDoc":"","x":75,"y":187,"wires":[["c25bef7c.941c18"]]},{"id":"abcb38aa.9ce038","type":"http in","z":"a28316.e5abbce8","name":"メインページ","url":"/main","method":"get","swaggerDoc":"","x":81.58328247070312,"y":78,"wires":[["a9366f14.35101"]]},{"id":"2f68c8c5.ed5ed","type":"http response","z":"a28316.e5abbce8","name":"return response","x":642.833251953125,"y":77.49998474121094,"wires":[]},{"id":"7146fa6f.b3ad8c","type":"comment","z":"a28316.e5abbce8","name":"メインページ","info":"","x":81.58328247070312,"y":38,"wires":[]},{"id":"bd14db16.e2d4a8","type":"template","z":"a28316.e5abbce8","name":"HTML","field":"payload","fieldType":"msg","format":"html","syntax":"mustache","template":"<html lang=\"ja\">\n<head>\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no\" />\n <link rel=\"stylesheet\" href=\"https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css\" integrity=\"sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u\" crossorigin=\"anonymous\">\n <title>近隣評判検索</title>\n <style type=\"text/css\"> {{{ payload.style }}}</style>\n <script src=\"//ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js\"></script>\n <script src=\"//d3js.org/d3.v3.min.js\" charset=\"utf-8\"></script>\n</head>\n<body>\n <div class=\"container-fluid no-padding\">\n\n <h1 class=\"text-center\">近隣スポットの評判を調べよう</h1>\n <div id=\"forms\" class=\"text-center\">\n <div class=\"form-inline\">\n <input type=\"text\" id=\"query\" placeholder=\"例) 焼肉\" class=\"form-control\">\n <button id=\"submit\" class=\"btn btn-primary\">Submit</button>\n </div>\n </div>\n \n <div class=\"row\">\n <div id=\"color-list\" class=\"col-lg-12 col-xs-12\" style=\"display:none;\">\n <div class=\"color-box col-lg-2 col-lg-offset-1 col-xs-4\">\n <div class=\"color col-xs-5\" style=\"background-color:#e53935;\"></div>\n <p class=\"tone col-xs-7\">Anger<br>(怒り)</p>\n </div>\n <div class=\"color-box col-lg-2 col-xs-4\">\n <div class=\"color col-xs-5\" style=\"background-color:#D500F9;\"></div>\n <p class=\"tone col-xs-7\">Disgust<br>(嫌悪感)</p>\n </div>\n <div class=\"color-box col-lg-2 col-xs-4\">\n <div class=\"color col-xs-5\" style=\"background-color:#607D8B;\"></div>\n <p class=\"tone col-xs-7\">Fear<br>(恐れ)</p>\n </div>\n <div class=\"color-box col-lg-2 col-lg-offset-0 col-xs-4 col-xs-offset-2\">\n <div class=\"color col-xs-5\" style=\"background-color:#FF5722;\"></div>\n <p class=\"tone col-xs-7\">Joy<br>(喜び)</p>\n </div>\n <div class=\"color-box col-lg-2 col-xs-4\">\n <div class=\"color col-xs-5\" style=\"background-color:#283593;\"></div>\n <p class=\"tone col-xs-7\">Sadness<br>(悲しみ)</p>\n </div>\n </div>\n\n <div id=\"loading-box\" class=\"col-xs-12\" style=\"display: none;\">\n <p>読み込み中・・・</p>\n <div class=\"progress\">\n <div class=\"progress-bar progress-bar-striped progress-bar-success active\" role=\"progressbar\" aria-valuenow=\"100\" aria-valuemin=\"0\" aria-valuemax=\"100\" style=\"width: 100%\">\n <span class=\"sr-only\"></span>\n </div>\n </div>\n </div>\n\n <div id=\"no-data\" class=\"col-xs-12\" style=\"display: none;\">\n <p>お店がないです。。。</p>\n </div>\n\n <div id=\"result\" class=\"col-xs-12\">\n <div class=\"shop-list\">\n </div>\n </div>\n\n </div>\n </div>\n <script src=\"https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js\" integrity=\"sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa\" crossorigin=\"anonymous\"></script>\n <script type=\"text/javascript\">{{{ payload.script }}}</script>\n</body>\n</html>\n","x":497.08331298828125,"y":77.49998474121094,"wires":[["2f68c8c5.ed5ed"]]},{"id":"1b82409.ecb41bf","type":"http request","z":"a28316.e5abbce8","name":"shop検索","method":"GET","ret":"obj","url":"","tls":"","x":420.5,"y":187,"wires":[["cf3d679e.39083"]]},{"id":"c25bef7c.941c18","type":"function","z":"a28316.e5abbce8","name":"プロパティ設定","func":"var lat = msg.payload.lat;\nvar lon = msg.payload.lon;\nvar query = msg.payload.query;\nmsg.url = 'http://search.olp.yahooapis.jp/OpenLocalPlatform/V1/localSearch?appid={api_key}&output=json&image=true&sort=-review&results=10&lat=' + lat + '&lon=' + lon + '&dist=0.5&query=' + encodeURI(query);\nreturn msg;","outputs":1,"noerr":0,"x":257.25,"y":187,"wires":[["1b82409.ecb41bf"]]},{"id":"3709d32c.8d51ec","type":"function","z":"a28316.e5abbce8","name":"shop_idの取得","func":"index = flow.get('i');\nshop_list = flow.get('shop_list');\nmsg.shop = shop_list[index];\nshop_id = shop_list[index].Property.Uid;\nmsg.url = 'http://api.olp.yahooapis.jp/v1/review/' + shop_id + '?output=json&appid={api_key}';\nreturn msg;","outputs":1,"noerr":0,"x":289.75,"y":387.75,"wires":[["d6caaef.28b825"]]},{"id":"b20983e3.e398d8","type":"switch","z":"a28316.e5abbce8","name":"switch","property":"type","propertyType":"flow","rules":[{"t":"eq","v":"continue","vt":"str"},{"t":"eq","v":"through","vt":"str"}],"checkall":"true","outputs":2,"x":90.00003051757812,"y":334.75,"wires":[["31d7deb9.b66722"],["fc529fef.cd9c28"]]},{"id":"d6caaef.28b825","type":"http request","z":"a28316.e5abbce8","name":"口コミ検索","method":"GET","ret":"obj","url":"","tls":"","x":286.25,"y":445.24993896484375,"wires":[["6291de3a.38228"]]},{"id":"89cdd295.d7cab8","type":"function","z":"a28316.e5abbce8","name":"プロパティ設定","func":"var i = 0;\nvar shop_list = msg.payload.Feature;\nvar shop_count = msg.payload.ResultInfo.Count;\nvar evaluations = [];\nflow.set('i', i);\nflow.set('type', 'continue');\nflow.set('shop_list', shop_list);\nflow.set('shop_count', shop_count);\ncontext.global.set('evaluations', evaluations);\nreturn msg;","outputs":1,"noerr":0,"x":101.08332824707031,"y":286.2778015136719,"wires":[["b20983e3.e398d8"]]},{"id":"31d7deb9.b66722","type":"change","z":"a28316.e5abbce8","name":"headers削除","rules":[{"t":"delete","p":"headers","pt":"msg"},{"t":"delete","p":"url","pt":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":290.5000305175781,"y":328.5,"wires":[["3709d32c.8d51ec"]]},{"id":"6291de3a.38228","type":"switch","z":"a28316.e5abbce8","name":"","property":"payload.ResultInfo.Count","propertyType":"msg","rules":[{"t":"gt","v":"0","vt":"str"},{"t":"else"}],"checkall":"true","outputs":2,"x":433.3055419921875,"y":445.611083984375,"wires":[["74bf31ef.ec8038"],["99a6aa52.4050a"]]},{"id":"9de87deb.fe72b","type":"function","z":"a28316.e5abbce8","name":"口コミ整形","func":"var base_url = 'https://translate.yandex.net/api/v1.5/tr.json/translate';\nvar api_key = 'api_key';\nvar review = '';\nmsg.payload.Feature.forEach(function(value) {\n review += value.Property.Comment.Body;\n});\nreview = review.replace(/\\r?\\n/g,\"\");\nmsg.review = review;\nmsg.url = base_url + '?key=' + api_key + '&text=' + encodeURI(review) + '&lang=en&format=json';\nreturn msg;","outputs":1,"noerr":0,"x":611,"y":501,"wires":[["cbb4b37c.4bb82"]]},{"id":"74bf31ef.ec8038","type":"change","z":"a28316.e5abbce8","name":"headers削除","rules":[{"t":"delete","p":"headers","pt":"msg"},{"t":"delete","p":"url","pt":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":603.5,"y":439.5,"wires":[["9de87deb.fe72b"]]},{"id":"cbb4b37c.4bb82","type":"http request","z":"a28316.e5abbce8","name":"翻訳","method":"GET","ret":"obj","url":"","tls":"","x":608.5,"y":563.5,"wires":[["991ad11e.36ea6"]]},{"id":"6d23d815.2726e8","type":"function","z":"a28316.e5abbce8","name":"翻訳結果を返す","func":"text = msg.payload.text[0];\nmsg.payload = text;\nreturn msg;","outputs":1,"noerr":0,"x":934.6666259765625,"y":556.083251953125,"wires":[["d343f875.dce958"]]},{"id":"fc529fef.cd9c28","type":"function","z":"a28316.e5abbce8","name":"json","func":"msg.payload = JSON.stringify(global.get('evaluations'));\nreturn msg;","outputs":1,"noerr":0,"x":90.00003051757812,"y":388.5,"wires":[["d630de21.2f9fc8"]]},{"id":"9a5dcaa9.bfec2","type":"function","z":"a28316.e5abbce8","name":"配列に追加","func":"var emotion = msg.response.document_tone.tone_categories[0].tones;\nindex = flow.get('i');\nevaluations = global.get('evaluations');\nevaluation = {\n \"shop\": msg.shop,\n \"review\": msg.review,\n \"emotion\": emotion\n};\nevaluations.push(evaluation);\nreturn msg;","outputs":1,"noerr":0,"x":917.583251953125,"y":444.5833435058594,"wires":[["99a6aa52.4050a"]]},{"id":"d630de21.2f9fc8","type":"http response","z":"a28316.e5abbce8","name":"return response","x":137.33316040039062,"y":605.5830688476562,"wires":[]},{"id":"991ad11e.36ea6","type":"switch","z":"a28316.e5abbce8","name":"","property":"payload.code","propertyType":"msg","rules":[{"t":"eq","v":"200","vt":"str"},{"t":"else"}],"checkall":"true","outputs":2,"x":749.3333740234375,"y":563.5,"wires":[["6d23d815.2726e8"],["99a6aa52.4050a"]]},{"id":"d343f875.dce958","type":"watson-tone-analyzer-v3","z":"a28316.e5abbce8","name":"感情分析","tones":"emotion","sentences":"false","contentType":"false","x":925.583251953125,"y":500.08331298828125,"wires":[["9a5dcaa9.bfec2"]]},{"id":"cf3d679e.39083","type":"switch","z":"a28316.e5abbce8","name":"switch","property":"payload.ResultInfo.Count","propertyType":"msg","rules":[{"t":"gt","v":"0","vt":"str"},{"t":"else"}],"checkall":"true","outputs":2,"x":550.388916015625,"y":187.25,"wires":[["89cdd295.d7cab8"],["d3715da1.deaed8"]]},{"id":"47480baa.54dd5c","type":"http response","z":"a28316.e5abbce8","name":"return response","x":888.3333740234375,"y":192.50003051757812,"wires":[]},{"id":"26f6ed37.5cb59a","type":"template","z":"a28316.e5abbce8","name":"CSS","field":"payload.style","fieldType":"msg","format":"css","syntax":"plain","template":"#result {\n margin-top: 30px;\n}\n\n.shop-info th {\n font-size: 1.8rem;\n}\n\n.shop-info td {\n font-size: 1.8rem;\n padding: 0;\n}\n\n.row {\n padding: 3%;\n}\n\n.chart {\n padding: 0;\n}\n\n#color-list {\n padding: 0;\n}\n\n.color {\n width: 30px;\n height: 30px;\n}\n\n.color-box {\n padding: 0 0 0 5%;\n margin-top: 10px;\n}\n\n@media screen and (max-width: 780px) {\n .shop-info {\n margin: 0;\n }\n .shop-info th, .shop-info td {\n font-size: 1rem;\n }\n .shop-info th {\n padding-left: 0;\n }\n .shop-chart-box {\n margin: 15px 0;\n }\n .btn {\n margin: 10px;\n }\n .tone {\n padding: 0 0 0 5px;\n }\n}","x":378.08331298828125,"y":77.49998474121094,"wires":[["bd14db16.e2d4a8"]]},{"id":"d3715da1.deaed8","type":"function","z":"a28316.e5abbce8","name":"0件","func":"msg.payload = {};\nreturn msg;","outputs":1,"noerr":0,"x":717.7500610351562,"y":192.25,"wires":[["47480baa.54dd5c"]]},{"id":"a52efea.5c1508","type":"comment","z":"a28316.e5abbce8","name":"input","info":"","x":56.083251953125,"y":138.1944580078125,"wires":[]},{"id":"8a02a267.61d21","type":"comment","z":"a28316.e5abbce8","name":"response","info":"","x":146.083251953125,"y":561.9444580078125,"wires":[]},{"id":"a9366f14.35101","type":"template","z":"a28316.e5abbce8","name":"Javascript","field":"payload.script","fieldType":"msg","format":"javascript","syntax":"plain","template":"(function(){\n\n var chart_color = [\"#e53935\", \"#D500F9\", \"#607D8B\", \"#FF5722\", \"#283593\"],\n win = d3.select(window),\n pie = d3.layout.pie().sort(null).value(function(d) { return d.value }),\n arc = d3.svg.arc().innerRadius(0),\n is_animated = false,\n size = { width: 200, height: 200 },\n lat, lon;\n\n // 現在地の取得\n if (!navigator.geolocation){\n alert(\"ブラウザが位置情報に対応していないみたいです( ;´Д`)\");\n } else {\n var option = {\n enableHighAccuracy: true,\n maximumAge: 1,\n timeout: 10000\n };\n\n navigator.geolocation.getCurrentPosition(\n function(position) { success(position); },\n function(error) { err(error); },\n option\n );\n }\n\n function success(position) {\n lat = position.coords.latitude;\n lon = position.coords.longitude;\n }\n\n function err(error){\n console.log('位置情報を取れませんでした')\n\n // 位置情報が取得できなかった場合は渋谷駅の緯度経度を使う\n lat = 35.6581;\n lon = 139.701742;\n }\n\n // submit時の処理\n d3.select('#submit').on('click', function() {\n var query = d3.select('#query').property(\"value\");\n\n if (!query) {\n alert('検索する値を入れてください');\n return;\n }\n d3.selectAll(\".shop-list div\")\n .remove();\n\n d3.select(\"#color-list\")\n .style({'display':'none'});\n\n sendRequest(query);\n });\n\n function sendRequest(query) {\n var search_url = '/local_search',\n post_data = {\n 'lat' : lat,\n 'lon' : lon,\n 'query' : query\n };\n\n d3.select(\"#loading-box\")\n .style({'display':'block'});\n\n // ajaxで/local_searchにリクエストを送る\n d3.json(search_url)\n .header(\"Content-Type\", \"application/json\")\n .post(JSON.stringify(post_data), function(error, data) {\n if (error !== null) {\n console.log(error);\n return;\n }\n\n d3.select(\"#loading-box\")\n .style({'display':'none'});\n\n console.log(data);\n if (data.length === 0 || Object.keys(data).length === 0) {\n d3.select(\"#no-data\")\n .style({'display':'block'});\n } else {\n d3.select(\"#color-list\")\n .style({'display':'block'});\n\n // 一件ずつお店を表示\n data.forEach(function(d, index) {\n displayShop(d.shop, d.emotion, index+1);\n });\n }\n });\n }\n\n // お店を表示\n function displayShop(shop, emotion, shop_number) {\n var shop_class = \"shop\" + shop_number;\n d3.select(\".shop-list\")\n .append(\"div\")\n .attr({\"class\": \"shop\" + shop_number + \" panel panel-primary\"});\n\n d3.select(\".\" + shop_class)\n .html('<div class=\"shop-title panel-heading\">' + shop.Name + '</div><div class=\"shop-detail panel-body\"><img class=\"col-xs-4 col-md-3\" src=\"' + shop.Property.LeadImage + '\"/><table class=\"shop-info col-xs-8 col-md-6\"><tbody><tr><th class=\"col-xs-3\">住所</th><td class=\"col-xs-9\">\"' + shop.Property.Address + '\"</td></tr><tr><th class=\"col-xs-3\">最寄駅</th><td class=\"col-xs-9\">' + shop.Property.Station[0].Name + '駅 徒歩' + shop.Property.Station[0].Time + '分</td></tr></tbody></table><div class=\"shop-chart-box col-xs-12 col-md-3\"><svg class=\"chart shop-chart' + shop_number + ' col-xs-12\"></svg></div></div>');\n\n drawChart(emotion, shop_number);\n }\n\n // 感情分析結果をpie chartで表示\n function drawChart(emotion, shop_number) {\n var chart_data = [],\n svg = d3.select(\".shop-chart\" + shop_number);\n\n emotion.forEach(function(d, index) {\n chart_data.push({ tone_name: d.tone_name, value: d.score, color: chart_color[index] });\n });\n\n render(svg, chart_data);\n update(svg);\n animate(svg, chart_data);\n\n win.on(\"resize\", function() {\n svg = d3.selectAll(\"svg\");\n update(svg);\n });\n }\n\n // pie chart描画\n function render(svg, chart_data) {\n var g = svg.selectAll(\".arc\")\n .data(pie(chart_data))\n .enter()\n .append(\"g\")\n .attr(\"class\", \"arc\");\n\n g.append(\"path\")\n .attr(\"stroke\", \"white\")\n .attr(\"fill\", function(d){ return d.data.color; });\n\n var maxValue = d3.max(chart_data, function(d){ return d.value; });\n\n g.append(\"text\")\n .attr(\"dy\", \".35em\")\n .attr(\"font-size\", function(d){ return d.value / maxValue * 20 })\n .style({\"text-anchor\":\"middle\", \"fill\":\"white\"})\n .text(function(d) { return d.data.tone_name });\n }\n\n // pie chartをレスポンシブに\n function update(svg) {\n size.width = parseInt(svg.style(\"width\"));\n size.height = parseInt(svg.style(\"height\"));\n\n arc.outerRadius(size.width / 2);\n\n svg\n .attr(\"width\", size.width)\n .attr(\"height\", size.width);\n\n var g = svg.selectAll(\".arc\")\n .attr(\"transform\", \"translate(\" + (size.width / 2) + \",\" + (size.width / 2) + \")\");\n\n if (is_animated) {\n g.selectAll(\"path\").attr(\"d\", arc);\n }\n\n g.selectAll(\"text\").attr(\"transform\", function(d) { return \"translate(\" + arc.centroid(d) + \")\"; });\n\n }\n\n // pie chartをアニメーションさせながら描画\n function animate(svg, chart_data) {\n var g = svg.selectAll(\".arc\"),\n length = chart_data.length,\n i = 0;\n\n g.selectAll(\"path\")\n .transition()\n .ease(\"cubic-out\")\n .delay(400)\n .duration(1000)\n .attrTween(\"d\", function(d) {\n var interpolate = d3.interpolate(\n {startAngle: 0, endAngle: 0},\n {startAngle: d.startAngle, endAngle: d.endAngle}\n );\n return function(t) {\n return arc(interpolate(t));\n };\n })\n .each(\"end\", function(transition, callback) {\n i++;\n is_animated = i === length;\n });\n }\n\n})();\n","x":239.833251953125,"y":78.1944580078125,"wires":[["26f6ed37.5cb59a"]]}]