2018年6月20日水曜日

MONACAサンプルアプリのソースコード解読2

前回はMONACAのサンプルアプリであるチャートアプリの導入を行い、ソースコードの解読を試みた。今回は残りのソースコードの解読を試みる。

#index.html

  1. <ons-template id="country.html">
  2. <ons-page ng-controller="CountryController as country">
  3. <ons-toolbar>
  4. <div class="left"><ons-back-button>Back</ons-back-button></div>
  5. <div class="center">{{ country.name }}</div>
  6. </ons-toolbar>
  7.  
  8. <ons-list class="year-select-container">
  9. <ons-list-item>
  10. <select
  11. ng-model="country.year"
  12. ng-options="year for year in country.years"
  13. class="text-input text-input--transparent"
  14. style="width: 100%; margin-top: 4px">
  15. </select>
  16. </ons-list-item>
  17. </ons-list>
  18.  
  19. <population-chart ng-if="country.showChart" data="country.population"></population-chart>
  20. </ons-page>
  21. </ons-template>
3行目の<ons-toolbar></ons-toolbar>でアプリのツールバーを作成している。4行目でツールバーの左側に「Back」ボタンを表示するようにして、5行目では、国名が中央に表示されるようにしている。
8行目の<ons-list class=”year-select-container”>はOnsen-UIでリスト(箇条書き)を作成するときに用いるコンポーネントである。リストの個々の項目は<ons-list-item>~</ons-list-item>の中に書く。それが10行目~14行目の<select>タグである。これは、人口をグラフ表示する際の西暦年を選択するための選択ボックスを定義している。<select>タグ内の属性ng-modelにはCountryController内で定義しているコントローラのプロパティcountry.yearを設定しているが、これが選択ボックスのデフォルト値になる。ちなみにコントローラ内でこのプロパティ(this.year)には現在の西暦年が設定されている。
ng-optionsは選択ボックスのオプション(選択肢)を生成するAngularJSのディレクティブで、”year for year in country.years”が、コントローラのプロパティcountry.years配列から西暦年を取り出して<option>タグを生成している。ちなみに、”year for year in country.years”は、country.years配列から一つずつ配列要素を取り出してきてそれをyearに設定し、そのyearを使って<option value="year">year</select>タグを生成するといった意味である。
19行目の<population-chart></population-chart>は、本アプリで独自にAngularJSのdirectiveメソッドを用いて作成しているタグ(下記index.jsの132~181行目)で、nvd3.jsライブラリを用いてグラフを作成するように定義されている。AngularJSのdirectiveメソッドはHTMLに独自のタグを追加する機能があり、これによってHTMLの拡張が可能になる。19行目だけでグラフを作っているのは驚くべきことである。

#index.js

  1. angular.module('app', ['onsen'])
  2.  
  3. .controller('CountriesController', ['$scope', 'Countries', function($scope, Countries) {
  4. var that = this;
  5.  
  6. Countries.get()
  7. .then(
  8. function(countries) {
  9. that.list = countries;
  10. }
  11. );
  12.  
  13. this.showCountry = function(country) {
  14. //$scope.navi.pushPage('country.html', {data: {name: country}});
  15. navi.pushPage('country.html', {data: {name: country}});
  16. };
  17. }])
  18.  
  19. .controller('CountryController', ['$scope', 'Population', function($scope, Population) {
  20. var that = this;
  21. this.name = $scope.navi.topPage.data.name;
  22.  
  23. this.showChart = false;
  24. $scope.navi.on('postpush', function() {
  25. $scope.$evalAsync(function() {
  26. that.showChart = true;
  27. });
  28.  
  29. $scope.navi.off('postpush');
  30. });
  31.  
  32. var currentYear = (new Date()).getUTCFullYear();
  33. this.year = currentYear + '';
  34.  
  35. this.years = [];
  36. for (var i = 1950; i <= 2100; i++) {
  37. this.years.push('' + i);
  38. }
  39.  
  40. this.getPopulation = function() {
  41. Population.get(this.name, this.year)
  42. .then(
  43. function(population) {
  44. that.population = population;
  45. }
  46. );
  47. };
  48.  
  49. $scope.$watch('country.year', function() {
  50. that.getPopulation();
  51. });
  52. }])
  53.  
  54. .service('Countries', ['$http', function($http) {
  55. this.get = function() {
  56. return $http.get('countries.json')
  57. .then(
  58. function(response) {
  59. return response.data.countries;
  60. }
  61. );
  62. };
  63. }])
  64.  
  65. .service('Population', ['$http', function($http) {
  66. this.get = function(country, year) {
  67. return $http.jsonp('http://api.population.io:80/1.0/population/' + year + '/' + country + '/?format=jsonp&callback=JSON_CALLBACK')
  68. .then(
  69. function(response) {
  70. return response.data;
  71. }
  72. );
  73. };
  74. }])
  75.  
  76. .factory('nv', function() {
  77. return nv;
  78. })
  79.  
  80. .factory('d3', function() {
  81. return d3;
  82. })
  83.  
  84. .controller('PopulationChartController', ['$scope', function($scope) {
  85. $scope.formatData = function() {
  86. if (!$scope.data) {
  87. return [];
  88. }
  89.  
  90. var total = [],
  91. males = [],
  92. females =[];
  93.  
  94. for (var i = 0; i < $scope.data.length; i++) {
  95. var data = $scope.data[i];
  96.  
  97. total.push({x: data.age, y: data.total});
  98. males.push({x: data.age, y: data.males});
  99. females.push({x: data.age, y: data.females});
  100. }
  101.  
  102. return [
  103. {
  104. key: 'Total',
  105. values: total
  106. },
  107. {
  108. key: 'Women',
  109. values: females
  110. },
  111. {
  112. key: 'Men',
  113. values: males
  114. }
  115. ];
  116. };
  117.  
  118.  
  119. $scope.$on('chartloaded', function(event) {
  120. event.stopPropagation();
  121.  
  122. $scope.updateChart($scope.formatData());
  123.  
  124. $scope.$watch('data', function() {
  125. if ($scope.data) {
  126. $scope.updateChart($scope.formatData());
  127. }
  128. });
  129. });
  130. }])
  131.  
  132. .directive('populationChart', ['nv', 'd3', function(nv, d3) {
  133. return {
  134. restrict: 'E',
  135. scope: {
  136. data: '='
  137. },
  138. controller: 'PopulationChartController',
  139. link: function(scope, element, attrs) {
  140. var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
  141. element.append(svg);
  142.  
  143. var chart;
  144.  
  145. scope.updateChart = function(data) {
  146. d3.select(svg)
  147. .datum(data)
  148. .call(chart);
  149. };
  150.  
  151. nv.addGraph(function() {
  152. chart = nv.models.stackedAreaChart()
  153. .useInteractiveGuideline(true)
  154. .showControls(false)
  155. .margin({left: 50, right: 30});
  156.  
  157. chart.xAxis
  158. .tickFormat(d3.format(',r'));
  159.  
  160. chart.yAxis
  161. .tickFormat(function(d) {
  162. if ((d / 1000000) >= 1) {
  163. d = Math.round(d / 100000) / 10 + 'M';
  164. }
  165. else if ((d / 1000) >= 1) {
  166. d = Math.round(d / 100) / 10 + 'K';
  167. }
  168. return d;
  169. });
  170.  
  171. nv.utils.windowResize(function() {
  172. chart.update();
  173. });
  174.  
  175. scope.$emit('chartloaded');
  176.  
  177. return chart;
  178. });
  179. }
  180. }
  181. }]);
19行目~52行目はコントローラCountryControllerの定義である。先に見た西暦年選択ボックス作成のために必要となるデータが定義されている。32行目でcurrentYearに現時点の西暦年を4桁で設定している。
33行目はyearにcurrentYearの値に空文字('')を結合することで、数値型を文字列型に変換している。
35行目でyearsを空の配列に初期化し、36行目~38行目のfor文でiを1950~2100の範囲で1ずつ増やしながらyears配列に追加(push)している。こうして西暦年配列yearsが作られ、先に見た<select>タグのng-options属性で<option>タグを生成するのに用いられている。このようにAngularJSのコントローラとHTMLのng-で始まるディレクティブは相互に関連付けられている。

132行目はdirectiveメソッドを用いて、populatationChartというタグ(カスタムディレクティブ)の定義を行っており、この中でNVD3というグラフィックライブラリを用いてグラフを生成している。

133行目にあるように、directiveメソッドの本体部分は、return命令で133行目~180行目に定義されたオブジェクトを返すだけの処理からなっている。
では、このreturn命令がどのようなオブジェクトを返しているかを見てみよう。
まず、134行目のrestrictは、HTMLの中でこのディレクティブをどのように使うかを指定する。「E」はタグ(コンポーネント)として使うことを指定している。
135行目~137行目のscopeは、ディレクティブに独自のスコープを定義するためのものである。136行目のdata:'='は、HTML内に書いたdata属性を、ディレクティブ内のdata属性と同じにすることを指定している(双方向バインディング)。
138行目は、このディレクティブを「PopulationChartController」というコントローラと連携させることを意味している。このディレクティブで定義したタグ内の属性で「ng-controller="PopulationChartController"」と記述するのと同義である。
そのPopulationChartControllerはHTMLのdata属性に指定されたデータを用いてグラフィックライブラリNVD3が使うデータフォーマットに変換することを主な機能とするコントローラで、84行目~116行目にかけて定義されている。

139行目のlinkはディレクティブにおいて一番重要な部分であり、タグの役割、機能を定義している。functionという記述から、関数として定義されている。HTMLでPopulationChartを呼び出すと、この関数が実行される。この関数を実行すると139行目から179行まで実行され、return chartで結果をグラフで返す処理を行う。
140行目のdocument.createElementNSというのは、HTMLのタグ(DOMという)を動的に作っているというものである。ここではsvgタグというものを作っている。svgタグというものは、グラフを作るためのXMLタグであり、グラフを描画するときには必ず使うものである。
145行目~149行目はUpdateChart関数の定義であり、グラフを更新する際にこの関数が呼び出される。
146行目でsvgタグの選択を行い、147行目でデータを与えて、148行目でチャート(グラフ)を生成している。
151行目以降はグラフの追加を行っている記述であるのだが、少々小難しく、理解しづらかったため、似たようなサンプルプログラムを活用し、理解を深めていく。

#NVD3.jsを使ってみるのサンプルデータから

  1. <link href="https://cdn.rawgit.com/novus/nvd3/v1.8.1/build/nv.d3.css" rel="stylesheet" type="text/css">
  2. <body>
  3. <div id="chart">
  4. <svg style="height:500px;" />
  5. </div>
  6.  
  7. <script src="https://d3js.org/d3.v3.min.js"></script>
  8. <script src="https://cdn.rawgit.com/novus/nvd3/v1.8.1/build/nv.d3.js"></script>
  9. <script type="text/javascript">
  10.  
  11. nv.addGraph(function() {
  12. //var chart = nv.models.historicalBarChart();
  13. //var chart = nv.models.lineWithFocusChart();
  14. var chart = nv.models.lineChart();
  15.  
  16. chart.xAxis.tickFormat(d3.format(',f'));
  17. chart.yAxis.tickFormat(d3.format(',.2f'));
  18.  
  19. d3.select('#chart svg')
  20. .datum(testData())
  21. .transition().duration(500)
  22. .call(chart);
  23.  
  24. nv.utils.windowResize(chart.update);
  25.  
  26. return chart;
  27. });
  28.  
  29. function testData() {
  30. return [{color:'red',
  31. key:'hoge',
  32. values: [{x:1,y:100},
  33. {x:2,y:130},
  34. {x:3,y:90},]},
  35. {color:'blue',
  36. key:'hage',
  37. values: [{x:1,y:70},
  38. {x:2,y:60},
  39. {x:3,y:80},
  40. {x:4,y:90}]}];
  41. }
  42. </script>
  43. </body>
4行目は、グラフィックライブラリNVD3でグラフを描画するのに使うsvgタグである。
11行目~27行目がグラフを書いている本体である。
16行目はグラフのx軸の目盛りのフォーマット指定部分である。ここでは実数形式にすることを指定している
17行目はグラフのy軸の目盛りフォーマット指定である。ここでは、小数点以下2桁まで表示するように指定している。
19行目~23行目は4行目のsvgタグにテストデータを入れ、チャートを作っている部分である。
19行目はスタイルシートで使うセレクタを使って4行目のsvgタグをグラフを埋め込むタグとして選択している。セレクタでは接頭語の#はidを意味するので、セレクタ'#chart svg'は、idがchartのタグ(すなわち3行目のdivタグ)の中にあるsvgタグ(すなわち4行目)を表している。
20行目は29行目~41行目で定義されたテストデータをグラフ描画の対象データに設定している。
22行目はグラフの描画を行っている。
先にも書いたように、29行目~41行目は折れ線グラフのデータを定義している。データは2系列あり、配列要素として格納されている。各系列のデータにはcolor属性、key属性、values属性があり、それぞれグラフの色、系列名、グラフを構成する(x, y)座標の配列を表している。
NVD3でグラフを作成する場合はこのフォーマットに変換すればよいことがわかる。
この観点でアプリのソースコードを眺めると、index.jsのPopulationChartControllerでNVD3のデータを作成しているのがわかる。90行目~92行目にかけて、total, males, femalesの3つの配列を定義し、94行目~100行目のfor文でdata属性のageをx座標に、人数(total, males, females)をy座標にした点オブジェクトを各々の配列に追加(push)している。そして、最後に102行目~116行目でNVD3のフォーマット(key属性とvalues属性)にしてreturnしている。こうして3系列(合計、男性、女性の人口)のデータを作成していることがわかる。

#NVD3を使って、AngularJSで作ったハイブリッドアプリのグラフを統合しようより

HTML

  1. <div ng-app="app" ng-controller="MyController as main">
  2. <line-chart height="250px" data="main.data"></line-chart>
  3.  
  4. <h3>Change the values: </h3>
  5.  
  6. <p ng-repeat="datum in main.data[0].values">
  7. <input type="number" ng-model="datum.y">
  8. </p>
  9. </div>

JavaScript

  1. angular.module('app', [])
  2. .controller('MyController', ['$scope', function($scope) {
  3. this.data = [{
  4. key: 'Data',
  5. values: [{
  6. x: 0,
  7. y: 0
  8. }, {
  9. x: 1,
  10. y: 1
  11. }, {
  12. x: 2,
  13. y: 4
  14. }, {
  15. x: 3,
  16. y: 9
  17. }, {
  18. x: 4,
  19. y: 16
  20. }, {
  21. x: 5,
  22. y: 25
  23. }]
  24. }];
  25. }]);

HTML(上図)の2行目にある<line-char>タグは、AngularJSのディレクティブを使って定義したグラフ作成のためのタグである。その属性にdata=”main.data”とあるが、これはJavascript(下図)の中にあるAngularJSのコントローラ"MyController"の3行目で定義されたdata属性で、この中には3行目~24行目にかけて定義されたデータが格納されている。

0 件のコメント:

コメントを投稿

レーダーチャートの表示2

前回 レーダーチャートの表示を行うことが出来たので、今回は実際の値を代入したグラフの描画を試みる。 .controller('RaderChartController', ['$scope', 'Countries', funct...