2018年7月26日木曜日

これからの作業分担について

 前回までに健康管理アプリの土台が出来たため、今回から各自分担して作業を進めていく。
 作業内容は

  1. 患者基本情報項目の洗い出し・デザイン
  2. 患者基本情報データの取得
  3. 患者基本情報表示画面の作成
  4. 検査結果表示の修正・作成

である。
 4人で話し合い、作業分担を決めた後、各自スケジュール表(WBS)を作成した。
 そして、完成目標が10月20日(土)のオープンキャンパス展示となったため、それまでの完成を目指す。

2018年7月19日木曜日

MONACAサンプルアプリの編集7

今回は前回の続きとして、まず、グラフで基準値が一目でわかるように色を付ける改良を行っていく。
.controller('PopulationChartController', ['$scope', function($scope) {
  $scope.formatData = function() {
    if (!$scope.data) {
      return [];
    }
    //alert(JSON.stringify($scope.data,null,2));
    var total = [],
      males = [],
      females =[];

$scope.data.forEach(function(item){
  var strDate = item.sampleDate.replace(/ /g, ' 02:');
  var sampleDate = Date.parse(strDate.replace(/-/g, '/'));
  var value = parseFloat(item.value);
  var normalValue = item.normalValue.split(/-/);
  var lower = parseFloat(normalValue[0]);
  var upper = parseFloat(normalValue[1]);
  
   

      total.push({x: sampleDate, y: upper});
      males.push({x: sampleDate, y: value});
      females.push({x: sampleDate, y: lower});
    });

    return [
      {
        key: '上限',
        values: total
      },
      {
        key: '下限',
        values: females
      },
      {
        key: '検査結果',
        values: males
      }
    ];
  };

上記のグラフデータを作成しているプログラムの中の26行目にある「return」で配列を戻している箇所を以下のように、グラフで基準値が一目でわかるように、色を付けるプログラムを書き加えていく。
.controller('PopulationChartController', ['$scope', function($scope) {
  $scope.formatData = function() {
    if (!$scope.data) {
      return [];
    }
    //alert(JSON.stringify($scope.data,null,2));
    var total = [],
      males = [],
      females =[];

$scope.data.forEach(function(item){
  var strDate = item.sampleDate.replace(/ /g, ' 02:');
  var sampleDate = Date.parse(strDate.replace(/-/g, '/'));
  var value = parseFloat(item.value);
  var normalValue = item.normalValue.split(/-/);
  var lower = parseFloat(normalValue[0]);
  var upper = parseFloat(normalValue[1]);
  
   

      total.push({x: sampleDate, y: upper});
      males.push({x: sampleDate, y: value});
      females.push({x: sampleDate, y: lower});
    });

    return [
      {
        key: '上限',
        values: total,
        color: '#0000CC',
        area:true
      },
      {
        key: '下限',
        values: females,
        color: '#FF0000',
        area:true
      },
      {
        key: '検査結果',
        values: males,
        color: '#00FF00',
        strokeWidth:4
      }
    ];
  };

30行目と36行目と42行目にある、「color」は書き加えることで、グラフに色をつけることができる。それぞれ、上限、下限、検査結果にあった色を指定している。
図1 グラフに色を付けて表示した結果
図1のようにグラフに色付けされて、一目でわかりやすいように表示された。

また、43行目にある「strokeWidth」を書き加えることで、検査結果のグラフの線の太さを太くすることができ、31行目と37行目にある「area:true」を書き加えることで、エリアを0まで色付けすることができる。例えば、上限に書き加えれば、上限より下から0までのエリアが色付けされ、下限にも書き加えれば、下限より下から0までのエリアを色付けすることでできる。

図2 線の太さとエリアの色付けをして表示した結果

次に、アプリケーションタブバーの追加を行う。
onsen-uiではこのようにタブバーの定義を行う事ができる。
<ons-tabbar>
  <ons-tab label="Tab 1" icon="ion-***"active></ons-tab>
  <ons-tab label="Tab 2" icon="ion-***"></ons-tab>
</ons-tabbar>
上記のコーディングはタブバーの定義である。
「ons-tabbar」でタブバーの定義を行い、
「ons-tab~」からタブの追加を行う。 labelはタブアイコン直下に表示する名称であり、icon="ion-***"で表示したいアイコンの定義を行う。
尚、どの様なアイコンが使用できるかは、こちらを参考にするといいだろう。
また、「ons-tab」の属性に「active」を追加することで、そのページをデフォルトで表示することができる。
onsen-uiでのタブバーの定義方法が理解できたところで、実際プログラムに実装を行う。
<ons-navigator var="navi" page="tab.html"></ons-navigator>
まず、上記htmlの「ons-navigator」の直下に追加していく。また、ナビゲータがデフォルト表示する「page」をこれから定義する「tab.html」へ変更しておく
<ons-template id="tab.html">
        <ons-page>
          <ons-tabbar>
            <ons-tab page="main.html" label="メイン" icon="ion-home"active>
            </ons-tab>
            <ons-tab page="demographics.html" label="基本情報" icon="ion-person">
            </ons-tab>
            <ons-tab page="settings.html" label="設定" icon="ion-settings">
            </ons-tab>
          </ons-tabbar>
        </ons-page>
     </ons-template>
まず、1行目の「ons-template」でこのタブバーを「tab.html」として定義し、,2行目で「ons-page」の定義を行う。
また、タグの定義を行う際に開始タグと同時に終了タグの記載もしておくと効率が良い。
次に3行目でタブバーの定義を行う。4行目ではメイン画面(検査項目一覧)の表示、6行目では基本情報(利用者情報)、8行目では設定の定義を行い、タブとして追加した。
また、初めに表示される画面はメイン画面にしたい為、4行目には「active」を記載している。
図3 タブバーの追加を行った結果

これらの修正を行った後、アプリケーションの確認を行うと、上記図3のようにタブバーの表示を行うことができた。
しかし、メイン以外の基本情報、設定タブをタップしても、画面の変更が行われなかった。
これは何故というと、先ほどコーディングした際に定義した「demographics.html」と「settings.html」のファイルが存在しないためである。
そのため、次はこの二つのファイルの作成を行う。
まずMONACA左に表示されているツリーの「www」というディレクトリを右クリックする。その後、新規ファイル作成をクリックする。
図4MONACAアプリの新規ファイル作成画面
すると上記図4のような画面が表示されるので、画面に従って各htmlの追加を行う。また、追加したhtmlは「www」ディレクトリに格納されることを覚えておきたい。
作成したhtmlを表示してみると、予めHTMLのテンプレート(骨組み)が作成されているが、今回は使用しないため、すべて削除し、下記のプログラムを入力する。
<ons-page>
  <ons-toolbar>
    <div class="center">基本情報</div>
  </ons-toolbar>
</ons-page>
上記のプログラムを記入した後、スマホで確認をすると、
図5 基本情報タブをタップした結果

上記図5の様にタブ画面の切り替えを行う事が出来た。

次回は基本情報ページの充実を行いたいが、以前の患者データではアレルギー情報等が記入されていないため、アレルギーデータの格納場所が分からない。
その為、ORCA,またはOpenDolphin上で患者にアレルギー情報の記入を行い、再びWirashark等でパケットキャプチャを行い、アレルギーデータの格納場所の特定を行う。

2018年7月18日水曜日

MONACAサンプルアプリの編集6

今回は、前回編集を行っていく中で見つけた「検査一覧画面に表示される検査値を小数点第3位で四捨五入をする」、「検査値が基準値外の場合、背景色を変更する」といった二点の課題に対して改良を行っていく。

検査値を小数点第3位で四捨五入を行う。

検査値の表示を行った際、データの桁数があまりにも長く、見づらかったため小数点第3位で四捨五入を行い、小数点以下2桁まで表示する。
Countries.get()
    .then(
      function(countries) {
        var list = countries.list[0];
        that.list = [];
        list.items.forEach(function(element){
          that.list.push(
              {
                itemName:element.itemName,
                value:element.value,
                unit:element.unit
              }
            );
        });
      }
    );
これは、前回コーディングしたプログラムである。10行目が、検査結果を連想記憶に設定しているところであるが、これを以下のように書き換える。
value:Math.round(element.value *100) /100
Math.roundは、javascriptで四捨五入を行うときに使う関数であるが,この関数は小数点以下1桁目を四捨五入して整数を返す関数である。小数点以下3桁目を四捨五入するために,まず検査値を100倍にし、それをMath.round関数を使って四捨五入する。その結果を100で割ると、小数点以下3桁目を四捨五入した結果が得られる。
例として、9.785という値があるとする。このままMath.round関数を用いると小数点を四捨五入し、10になってしまう。これでは整数でしか表示を行う事が出来ないため、まず数字を100倍し、978.5にする。その後、Math.round関数を用いて四捨五入を行うと、979となる。ここでこの値を100で割ると9.79となり、実質的に小数点第3位で四捨五入を行った形になる。
図1 検査結果を小数点以下第2位まで表示した結果
この修正を行った後,MONACAアプリを起動すると、上記図1の様に検査値を小数点第2位まで表示することができた。

検査値が基準値外の場合、背景色を変更する。

前回、検査結果が基準値外の値であれば、スタイルシートを使って背景色を変更するために「normal」や「abnormal」というクラスの定義を行った。今回は,検査結果の値に応じてons-list-itemタグにクラス属性の値を設定する機能の実装を試みる。
Countries.get()
    .then(
      function(countries) {
        var list = countries.list[0];
        that.list = [];
        list.items.forEach(function(element){
          that.list.push(
              {
                itemName:element.itemName,
                value:Math.round(element.value *100) /100
                unit:element.unit
              }
            );
        });
      }
    );
まず先ほども修正した上記javascriptのプログラムを以下のように書き換える。
Countries.get()
    .then(
      function(countries) {
        var list = countries.list[0];
          that.list = [];
          list.items.forEach(function(element){
          var normalValue =  element.normalValue.split(/-/);
          var lower = parseFloat(normalValue[0]);
          var upper = parseFloat(normalValue[1]);
          var bgColor;
          if(element.value >= lower && element.value <= upper){
            bgColor ="normal";
          }else{
            bgColor ="abnormal";
          }
          that.list.push(
              {
                itemName:element.itemName,
                value:Math.round(element.value *100) /100 ,
                unit:element.unit,
                bgColor:bgColor
              }
            );
        });
      }
    );
まず、条件判定を行うにあたって、基準値から上限・下限の取りだしを行う必要があるのだが、これは以前のブログに書いたプログラムを流用し、7~9行目のように記入する。尚、7行目に関しては、今回検査データはelementに格納されているため、element.normalValueへ変更した。次に、10行目に背景を表すクラス属性を格納する変数を「bgColor」として定義する。そして、11行目~15行目にif文を使って条件判断を記述する。内容は「検査値(element.value)が下限値(lower)以上かつ、上限値(upper)以下であれば正常(normal),そうでなければ異常(abnormal)」といったものである。最後に、that.list.pushで追加する連想配列の21行目にbgColor:bgColorと記載することで、クラス属性を表すbgColorの設定を行った。
<ons-list>
              <ons-list-item class="{{country.bgColor}}"  
                ng-click="countries.showCountry(country.itemName)" modifier="chevron" 
                ng-repeat="country in countries.list | filter:query">
                <ons-col width="50%">{{ country.itemName }}</ons-col>
                <ons-col>{{country.value}}</ons-col>
                <ons-col>{{country.unit}}</ons-col>
              </ons-list-item>
            </ons-list>
そしてhtmlの2行目にある「ons-list-item」タグに「class="{{country.bgColor}}" 」を記述することで、javascriptで設定したクラス属性値("normal"または"abnormal")を設定することができる。
図2検査結果一覧画面で異常値に色を付けた結果
これらの編集を行うことで、上記図2のように基準値外の値が含まれている場合、背景を赤くすることができた。

次回は,グラフで基準値が一目でわかるように色を付ける改良を行う。また,ユーザの基本情報を表示する機能,検査項目をグルーピングする機能,タブバーで画面を切り替える機能についてどのようにしたら実現できるか考える。

2018年7月12日木曜日

MONACAサンプルアプリの編集5

今回は、検査項目一覧を表示した際に、検査項目名だけでなく、検査値、単位の追加表示を試みる。まず、javascript上での編集を行う。
Countries.get()
    .then(
      function(countries) {
        var list = countries.list[0];
        that.list = [];
        list.items.forEach(function(element){
          that.list.push(element.itemName);
        });
      }
    );
上記のコードは以前のブログで記述したcountries.list配列に検査名を格納する処理であるが、このコード6行目以降を下記の様に変更する。
list.items.forEach(function(element){
          that.list.push(
              {
                itemName:element.itemName,
                value:element.value,
                unit:element.unit
              }
            );
以前のブログで調べた検査―データ中にある検査名、検査値、単位それぞれに対応する値をpushメソッドを利用してthat.list配列の中に格納する。javascriptでは、この様なディクショナリ形式(連想配列)で関連する一塊のデータをまとめて記述できる。
次に、このままでは追加した検査値や単位がスクリーンには表示されないので、htmlの修正も行っていく。
<ons-list>
<ons-list-item ng-click="countries.showCountry(country)" modifier="chevron" 
ng-repeat="country in countries.list | filter:query">
{{ country }}
</ons-list-item>
4行目の{{country}}はcounties.listから取り出した要素データを表示するものだが、これを下記の様に書き換える。
{{ country.itemName }}
{{ country.value}}
{{ country.unit}}
この様にcountry.検査名(itemName),検査値(value),単位(unit)と記述することで、対応したデータを表示することが出来る。
図1 検査項目一覧画面
これらの変更を行う事で、上記図1の様に検査項目のほかに、検査値、単位を表示させることが出来る。しかし、このままの状態では少々見栄えがよくないので、先ほどのhtmlを下記のように書き換える。
<ons-col>{{ country.itemName }}</ons-col>
<ons-col>{{country.value}}</ons-col>
<ons-col>{{country.unit}}</ons-col>
<ons-col>はonsen-UI上で列を表示するタグである。
図2 ons-colタグで整形した検査項目一覧画面
上記はコードを書き換えた後の検査項目一覧画面である。先ほどの比較すると多少は見栄えがよくなってはいるが、まだ少し見栄えがよくない。そのため、再びhtml文を下記のように修正する。
<ons-col width="60%">{{ country.itemName }}</ons-col>
<ons-col>{{country.value}}</ons-col>
<ons-col>{{country.unit}}</ons-col>
1行目にwidthを記述することで幅の調節を行う事が出来る。

図3 width属性で幅の調整を行った検査項目一覧画面
これにより、上記の図3の様に見栄えを整えることが出来た。
次回は検査値が基準値より外れている場合、色を変えて表示するようにプログラムを修正する。その準備として今回は簡単にcssの記述を行っておく。

正常値の場合

<ons-list-item class="normal" ng-click="countries.showCountry(country.itemName)...

異常値の場合

<ons-list-item class="abnormal" ng-click="countries.showCountry(country.itemName)...
まず、htmlの「ons-list-item」タグへ「normal」「abnormal」というclassの値を設定する。
検査値が基準値の範囲内であれば「normal」、範囲外であれば「abnormal」として色の変更を行う。そのために,style.cssでnormal,abnormalの定義を行う。
.abnormal {
    background-color: #FF0000;
}

.normal{
   background-color: #FFFFFF;
}
上記の記述はそれぞれ「abnormal」であれば背景を赤、「normal」であれば背景を白にするといった定義である。
次回は,基準値から検査値が異常値かどうかを判定して,その結果から<ons-list-item>のclass属性に設定する値(normalまたはabnormal)を決定するプログラムを作成する。また、表示されている検査値の桁数があまりに長く見づらいため、表示は小数点以下2桁までにしたい。

中間報告会で受けた指摘と今後の方針

先日行われた中間報告会で「すでに類似したシステムは世にたくさん出ているが,それとこのシステムはいったいどこが違うのか」という質問を受けた。電子カルテシステムと直接つながっているPHR(個人健康記録)などカルテコ以外にそんなに例はないとは思うが,検査結果をグラフ表示するだけのアプリならどこにでも転がっているので,そのように思われてもしかたがないだろう。
そこで,今後,このシステムをどのように発展させていくかを考えてみた。

「医療情報学第5版医療情報システム編」のP74に次のような記載がある。
さらに健康志向の個人の増加に伴い,個人が常に携帯しているスマートフォンなどを利用して日々の運動量,心拍数,食事量,睡眠状態などの多種多様な項目を測定して管理できるアプリケーションが提供され始めている。
さらに
現段階では,これらのアプリケーションはスタンドアローンで動作しており,アプリケーション同士や特定健診システムや保健指導システムとの連携は取れていない。 しかし将来的には,アプリケーションが連動して日々健康指導を密に行うようなことが望まれる。このようなデータも含めた個人の生涯にわたる健康情報を管理し解析することで,個人に最適な快適で効果が高く効率的な健康指導が可能になると期待される。
と続く。
これをヒントに次のような機能を加えてはどうだろうか。
  1. 医師や保健師が電子カルテに入力した保健指導の内容をスマホアプリに表示する。
  2. 利用者の食事内容を写メで撮影して電子カルテ(または画像サーバ)へアップロードし,管理栄養士がそれを見て栄養指導する。
  3. 食事内容を撮影した画像から食材や栄養素,カロリーなどを計算してくれるWebAPIを探して利用する。
  4. 家庭用の体重計,血圧計,体温計などで計測したデータをスマホに自動取り込みするインターフェースがないか調べて,あれば利用する
  5. 天気APIを利用して天気や気温・気圧を自動でスマホに蓄積する

プログラムの改良についてのTODOリスト

  1. 検査項目一覧画面におけるグルーピング(例:生化学的検査)
  2. 検査項目のヘルプ(Google検索へリンクするなど)
  3. グラフの基準値の表示方法の工夫(色を付けるなど)
  4. グラフ画面で他の検査項目を選択する機能
  5. 患者基本属性(名前,生年月日など)の表示 
  6. 「設定」「保健指導」などの機能を追加するためのユーザインターフェース(タブバー,スライディングメニュー)
  7. Googleマップ(フィットネスクラブ)


2018年7月5日木曜日

MONACAサンプルアプリの編集4

今回は、グラフの描画部分のプログラミングを行う。具体的には、指定した国、年の人口を取得するPopulationサービスを書き換えて、OpenDolphinから指定した検査項目の検査結果を検査履歴から取得する処理を行う。
nv.addGraph(function() {
    chart = nv.models.stackedAreaChart()
    .useInteractiveGuideline(true)
    .showControls(false)
    .margin({left: 50, right: 30});
上記のコードはNVD3を用いたグラフ描画の方法なのだが、これを以下のコードに書き換える
nv.addGraph(function() {
    chart = nv.models.lineChart()
    .useInteractiveGuideline(true)
    .showControls(false)
    .margin({left: 50, right: 30});
2行目の「chart = nv.models.lineChart()」という記述を用いることで折れ線グラフの描画を行う事が出来る。しかし、この状態でアプリケーションを実行すると、エラーが表示され、グラフの描画を行う事が出来ない。3~5行目の.から始まる記述はメソッドを示しているのだが、いずれかのメソッドがlineChartのメソッドではないものが記述されている為、エラーが表示されてしまう。ここで、一つ一つのメソッドに対してコメントアウトを行い、どのメソッドがlineChartのものではないのか検証したところ、4行目の「.showControls(false)」といったメソッド(グループモードと積み上げモードを切り替える)がlineChartのメソッドではなかったので、このメソッドをコメントアウトしたところ、折れ線グラフでの描画を行う事が出来た。

次に、グラフのx軸に示されている日時が適切な形式になっていないため、変換を行う必要がある。
chart.xAxis
  .tickFormat(d3.format(',r'));
この「chart.xAxis」というものは、グラフのx軸を示しているものである。2行目の「.tickFormat」というものは目盛の形式を示しており、(,r)のrは数字という意味であり、カンマは数字に3桁ごとにカンマを付与するといったものである。
これを、以下の書式に書き換える。
chart.xAxis
  .tickFormat(
    function(d) {
      return d3.time.format('%Y-%m-%d')(new Date(d));
    }
  );
上記のxAxisのtickFormatメソッドは、変数dを通じて渡されてきた座標目盛りの値を、無名関数を使って任意の形式に編集している。ここでは基準日時からの経過秒が格納されている数値変数dをnew Date(d)によってJavascriptの日付オブジェクトに変換し、それをd3.time.format関数を使って書式'%Y-%m-%d'により「YYYY-MM-DD」の形式に編集している。
図1 x軸の目盛りを日付にしたグラフ画面
これよにり、上記図1のようにx軸に日付を表示することができた。
次に、y軸の値に単位を追加する処理を行う。
chart.yAxis
  .tickFormat(function(d) {
    if ((d / 1000000) >= 1) {
      d = Math.round(d / 100000) / 10 + 'M';
    }
    else if ((d / 1000) >= 1) {
      d = Math.round(d / 100) / 10 + 'K';
    }
    return d;
  });
これは1,000,000や、1,000等の値の大きい数をそれぞれ、MやKの単位をつけて短くするものであるが、今回扱うデータはあまり大きい値ではないので、この部分をコメントアウトする。
chart.yAxis
  .tickFormat(function(d) {
   /* if ((d / 1000000) >= 1) {
      d = Math.round(d / 100000) / 10 + 'M';
    }
    else if ((d / 1000) >= 1) {
      d = Math.round(d / 100) / 10 + 'K';
    }*/
    return d + scope.data[0].unit;
  });
9行目の「d + scope.data[0].unit;」という記述でグラフのy軸の数値に検査単位を付与することが出来る。ここで、scope.dataは、PopulationChartディレクティブのビューであるHTML中のpopulation-chartタグに付与されたdata属性を表している(下記)。
<population-chart ng-if="country.showChart" data="country.population"></population-chart>
このcountry.populationにはPopulationサービスで作成した検査項目の時系列データが配列として格納されており、その直近の検査日の結果scope.data[0]の検査単位であるscope.data[0].unitを使用してy軸の目盛りに単位をつけている。

現状のグラフでは、正常値の上限値と下限値がy軸の最大値と最小値になってしまい、基準値の範囲がわかりくい状況である。そこで、y軸の範囲(最大値と最小値)を広げることにした。尚、検査項目ごとに基準値の値は異なるので、基準値の上限値や下限値に定数を加えたり引いたりしてy軸の最大値や最小値を設定するのは適切ではない。したがって、y軸の最大値を「基準値の上限値 + 上限値と下限値の差」、最大値を「下限値 - 上限値と下限値の差」とすることにした。そのために、lineChartのyDomainメソッドに以下のように目盛りの範囲を設定した。
.yDomain([2 * lower-upper, 2 * upper-lower])
ここで、「lower」は基準値の下限、「upper」は基準値の上限である。
このように座標軸の最小・最大を設定するにはyDomainメソッドを使用する。
次に、基準値の下限「lower」と上限「upper」をscope.dataから求める。
var normalValue =  scope.data[0].normalValue.split(/-/);
var lower = parseFloat(normalValue[0]);
var upper = parseFloat(normalValue[1]);
直近の検査項目の情報はscope.data[0]に入っているので、そこから基準値scope.data[0].normalValueを取り出し、それを区切り文字「-」で分割し、先頭要素を下限値、2番目の要素を上限値として実数に変換する。これにより、下記の図2の様にグラフのy軸の最大値と最小値の設定を行う事ができ、正常値範囲がわかりやすくなった。

図2 基準値範囲を見やすくしたグラフ画面
ただし,図2をよく見ると,縦軸の最大値と最小値の値が計算誤差のせいで「・・・9999」と歯切れの悪い数値になっているので,編集する必要がある。

2018年7月4日水曜日

MONACAのサンプルアプリ編集3

前回は、Populationサービスを使ってOpenDolphinの検査データから、前の画面で選択した検査項目の時系列データを取得する処理を作成した。今回は、この取得したデータを使ってグラフを描画するためのデータ加工処理、およびグラフ描画を行う。
.controller('PopulationChartController', ['$scope', function($scope) {
  $scope.formatData = function() {
    if (!$scope.data) {
      return [];
    }
      alert(JSON.stringify($scope.data,null,2));

    var total = [],
      males = [],
      females =[];

    for (var i = 0; i < $scope.data.length; i++) {
      var data = $scope.data[i];

      total.push({x: data.age, y: data.total});
      males.push({x: data.age, y: data.males});
      females.push({x: data.age, y: data.females});
    }
まず、Javascriptの上記「poplationchartController」の12行目からのプログラムを以下のものに書き換える。
$scope.data.forEach(function(item){
      var strDate = item.sampleDate.replace(/ /g, ' 02:');
      var sampleDate =Date.parse(strDate.replace(/-/g, '/'));
      var value =parseFloat(item.value);
      var normalValue = item.normalValue.split(/-/);
      var lower = parseFloat(normalValue[0]);
      var upper = parseFloat(normalValue[1]);

      total.push({x: sampleDate, y: upper});
      males.push({x: sampleDate, y: value});
      females.push({x: sampleDate, y: lower});
    });

    return [
      {
        key: '上限',
        values: total
      },
      {
        key: '下限',
        values: females
      },
      {
        key: '検査結果',
        values: males
      }
    ];
  };
OpenDolphinから取得した検査結果は以前のブログに書いたように次のようなフォーマットをしている。
{
  "list": [
    {
      "progressState": null,
      "items": [
        {
          "laboCode": null,
          "lipemia": null,
          "hemolysis": null,
          "dialysis": null,
          "reportStatus": "E",
          "groupCode": "01",
          "groupName": "生化学的検査",
          "parentCode": "0001",
          "medisCode": "3J010000002327101",
          "itemName": "総ビリルビン",
          "abnormalFlg": null,
          "normalValue": "0.2-1.2",
          "specimenCode": "023",
          "specimenName": "血清",
          "commentCode1": null,
          "comment1": null,
          "commentCode2": null,
          "comment2": null,
          "sortKey": null,
          "itemCode": "0001",
          "sampleDate": "2018-05-06 00:00",
          "patientId": "1.3.6.1.4.1.9414.70.1:00001",
          "unit": "mg/dL",
          "value": "0.4",
          "id": 1120
        },
       ・
       ・
       ・     
      (中略)
       ・
       ・
       ・
        {
          "laboCode": null,
          "lipemia": null,
          "hemolysis": null,
          "dialysis": null,
          "reportStatus": "E",
          "groupCode": "07",
          "groupName": "一般検査",
          "parentCode": "0493",
          "medisCode": "1B040130201506211",
          "itemName": "便中ヒトヘモ定性2日目",
          "abnormalFlg": null,
          "normalValue": "陰性",
          "specimenCode": "015",
          "specimenName": "便",
          "commentCode1": null,
          "comment1": null,
          "commentCode2": null,
          "comment2": null,
          "sortKey": null,
          "itemCode": "0493",
          "sampleDate": "2018-05-04 00:00",
          "patientId": "1.3.6.1.4.1.9414.70.1:00001",
          "unit": null,
          "value": "インセイ",
          "id": 1084
        }
      ],
      "sampleDate": "2018-05-04 00:00",
      "laboCenterCode": "LSCC",
      "moduleKey": "LSCC2018050442011",
      "reportFormat": null,
      "patientName": "山下 浩介",
      "patientSex": "M",
      "numOfItems": null,
      "patientId": "1.3.6.1.4.1.9414.70.1:00001",
      "id": 1051
    }
  ]
}
この中で、検査日時はsampleDateで、「2018-06-06 00:00」という形式になっている。Javascriptで日時として認識されるデータは「2018/06/06 00:00:00」という形式なので、これを正規表現を使って適切な書式に書き換える。
var strDate = item.sampleDate.replace(/ /g, ' 02:');
まず、「strDate」という変数を定義し、検査日時item.sampleDate中のスペースを「 02:」に置き換える。これにより、元の時刻「00:00」を「02:00:00」という時分秒の形式に置き換えることができる。なお、「02:00:00」は午前2時0分0秒を意味するが、この時間には意味がない。「01:00:00」でも何でもよかった。
var sampleDate =Date.parse(strDate.replace(/-/g, '/'));
次に、strDate.replace(/-/g, '/')で「strDate」の中の「-」を「/」へ置き換えて、日付「2018-06-06」を「2018/06/06」という形式に変換し、「Date.parse」という命令により文字列の日時をJavascriptの日時オブジェクトに変換する(Javascriptの日付オブジェクトは基準日時からの経過秒で表現されている)。これらの置き換え作業によって、検査日時を変数「sampleDate」に設定できた。
 var value =parseFloat(item.value);
これは、検査値「item.value」を「paresFloat」命令で文字列から実数値に変換し、変数「value」に代入しているところである。
var normalValue = item.normalValue.split(/-/);
var lower = parseFloat(normalValue[0]);
var upper = parseFloat(normalValue[1]);
これは「item.normalValue」に入っている検査の基準値を上限と下限に分離して、それぞれ変数「upper」と「lower」に格納する処理である。item.normalValueの中に格納されている値は「0-10」という書式になっているので、「-」を区切り文字として、「-」の前後で数字を分ける。例えば、「0.2-1.2」という値であれば、「0.2」と「1.2」の二つの値に分け、それぞれを下限値、上限値とする。
そのために、splitメソッドを使う。item.normalValue.split(/-/)は、item.normalValueの中身を「-」を区切り文字として分割し、その結果を配列に格納する。したがって変数「normalvalue」の先頭要素「normalValue[0]」には下限値が、2番目の要素「normalValue[1]」には上限値が格納される。
こうして得られた下限値・上限値をparseFloat関数を使って実数値に変換し、それぞれ変数「lower」,「upper」へ代入する。
total.push({x: sampleDate, y: upper});
males.push({x: sampleDate, y: value});
females.push({x: sampleDate, y: lower});

こうして 検査日時、検査結果、基準値の下限値・上限値が得られたので配列「total」へは上限値を、配列「males」へは検査値を、そして配列「females」へは下限値を、それぞれ追加している。
これらの処理により、グラフの描画が行えるようになった。
実際にスマートフォンの方で確認したところ、図1の様な画面が表示され、グラフの描画を行うことが出来た。
図1 検査結果グラフ
ただし、このグラフは折れ線グラフではなく、積み上げグラフになっている。これは、元のアプリがそのような仕様になっているからである。また、X軸の目盛りが日付ではなく数値になっている。これはJavascriptの日付オブジェクトの内部形式(基準日時からの経過秒)がそのまま表示されているからである。さらに、Y軸に単位が表示されていない。
次回は折れ線グラフになるように、そして、X軸の目盛りが日付に、Y軸に単位を表示するように修正を行っていきたい。
 それからもう一つ。全画面の検査一覧画面に検査結果が表示されていない。基準値外であれば色をつけて強調するなどしたい。
 また、単に折れ線グラフだけでなく検査項目に応じたグラフ表示も考えたい。
 さらに、検査項目によっては、複数の項目を比較しながら見たいケースがあるかもしれない。そのような検査項目を調べて、同時に複数の検査値を表示する機能(例えばバランスを見ることができるようなレーダーチャートなど)も持たせたい。
 患者名など患者情報が表示されていないので、それも表示したい。基本情報、バイタル、アレルギーなど。

2018年6月28日木曜日

MONACAサンプルアプリの編集2

今回は、グラフの描画部分のプログラミングを行う。具体的には、指定した国、年の人口を取得するPopulationサービスを書き換えて、OpenDolphinから指定した検査項目の検査結果を検査履歴から取得する処理を行う。
.service('Population', ['$http', function($http) {
  this.get = function(country, year) {
    return $http.jsonp('http://api.population.io:80/1.0/population/' + year + '/' + country + '/?format=jsonp&callback=JSON_CALLBACK')
      .then(
        function(response) {
          return response.data;
        }
      );
  };
}])
まず上記のコードを下記のコードに書き換える。
.service('Population', ['$http', function($http) {
  this.get = function(country, year) {
    return $http.get('http://172.16.108.251:8080/dolphin/openSource/lab/module/00001,0,6')
      .then(
        function(response) {
          alert(JSON.stringify(response.data,null,2));
          return response.data;
        }
      );
  };
}])
これは「$http」サービスを使ってOpenDolphinから検査データを取得するサービスPopulationである。また、確認の為6行目に、タップした際に検査データをアラートで表示する処理を加えている。編集を行った後、検査項目をタップすると、前回と同様の画面が表示された。

.service('Population', ['$http', function($http) {
  this.get = function(country, year) {
    return $http.get('http://172.16.108.251:8080/dolphin/openSource/lab/module/00001,0,6')
      .then(
        function(response) {
          //alert(JSON.stringify(response.data,null,2));
          var  laboResults = [];
          response.data.list.forEach(function(list){
            list.items.forEach(function(item){
              if(item.itemName == country){
                laboResults.push(item);
              }
            });
          });
          alert(JSON.stringify(laboResults,null,2));
          return laboResults;
        }
      );
  };
}])
次に、先ほどのコードを上記のコードに書き換える。
まず、7行目で配列変数laboResultsを定義し、その中身を空にする。そして、8行目はlist配列の各々に格納されている1日分の検査データ1つずつに対してループ処理を行っている。9行目は1日分の検査データ items の個々の検査項目1つずつに対してループ処理を行っている。10行目は選択したデータ(名称がcountryに格納されている)と同じ検査名称(itemName)の検査項目を抽出してlaboResults配列に追加するといった処理である。確認の為、スマートフォンアプリの方で検査項目「総ビリルビン」をタップしたところ、総ビリルビンのデータのみを表示させることが出来た。
図1 MONACAアプリ画面

2018年6月27日水曜日

MONACAサンプルアプリの編集1

前回前々回とMONACAのサンプルアプリのソースコードの解読を行い、おおまかではあるがある程度の構造を把握することが出来たので、今回は実際にサンプルアプリの編集を行う。
まず編集を行う際に、Wiresharkでのパケットキャプチャを利用し、どの様な通信方法が行われているのかを確認する。今回はあらかじめキャプチャされていたデータを用いる。尚、パケットキャプチャの閲覧を行う際にNotepad++というアプリケーションを利用する。Notepad++を実行し、パケットキャプチャを張り付けた後に、言語>J>JSONをクリックする事で、以下の図1の様に表示される。
図1 notepad++図1
1行目の
GET /dolphin/openSource/lab/module/00001,0,6
は検査データを最新から過去6日分までの取得を行う指示である。OpenDolphinで過去6日分ごとしかデータを表示することが出来なかったのはこのためであろう。
図の左に表示されている+ボタンをクリックする事で対応するタグに格納されているデータの開閉を行うことが出来る為、どのタグにどういったデータが存在しているのかを容易に認識することが出来る。下記図2は21行目から始まる「items」のデータを閉じた図である。
図2 notepad++図2

そして、もう一度+ボタンを押す事で、下記図3の様に再び「items」に格納されているデータの表示を行うことが出来る。
図3 notepad++図3
ここで、見覚えのあるデータ群の閲覧を行うことが出来、検査データは「items」の中に格納されていることを理解することが出来た。
データの格納場所が分かったところで、サンプルアプリの編集を行いたいのだが、OpenDolphinに接続するにはユーザIDとパスワードで認証を受ける必要がある。そのためには、図1から、HTTPヘッダにユーザIDとパスワードを設定すればよいことがわかる(4~5行目)。
ここでパスワードに設定した値であるが、MD5ハッシュ関数を用いてハッシュ値に変換している。これは、仮にハッシュ値に変換を行わずパスワードを送信した場合、パケットキャプチャを行うと筒抜けになってしまうためである。
実際に同じ数値に変換されるかどうかをMD5ハッシュ計算ツールを用いて確認を行った。すると、下記図4の様に表示され、同じ値であることを確認することが出来た。
図4 ハッシュ計算ツール図
ユーザIDとパスワードの記述方法について概ね理解することが出来たので、いよいよMONACAのサンプルアプリの編集を行う。尚、名前を「Onsen World Poplation」から「Open Dolphin Client」へ変更した。
まず、index.jsの「angular.module('app', ['onsen'])」の直下に
.config(['$httpProvider',function($httpProvider){  
  $httpProvider.defaults.headers.common.password='6f8e646f95af8e79096477e877245664';
  $httpProvider.defaults.headers.common.userName='1.3.6.1.4.1.9414.70.1:W3415020';
}])
を追加する。これは「$httpProvider」というAngularJS組み込みサービスを用いたものである。このサービスは同じくAngularJSの組み込みサービスである「$http」のデフォルトの動作を変更するために用いるサービスである。ここでは、「headers」属性を使って、HTTPリクエストのヘッダーにパスワードとユーザネームを書き込む処理を行っている。
これで、ヘッダへ設定すべき認証情報の書き込み処理は完了した。
次に、
.service('Countries', ['$http', function($http) {
  this.get = function() {
    return $http.get('countries.json')
      .then(
        function(response) {
          return response.data.countries;
        }
      );
  };
}])
これは国一覧のデータを取得し、リストへ送るサービスであるが、これを
.service('Countries', ['$http', function($http) {
  this.get = function() {
    return $http.get('http://172.16.108.251:8080/dolphin/openSource/lab/module/00001,0,6')
      .then(
        function(response) {
          alert(JSON.stringify(response.data.list[0],null,2));
          return response.data;
        }
      );
  };
}])
の様に書き換える。
1行目はサービスの宣言で、「Countries」はサービス名、次の配列内の最初の要素「$http」はこのサービスで使用する組み込みサービスで、次のfunctionがこのサービスのコンストラクタである。3行目の$http.getは引数に指定したOpenDolphinのURLから検査データの取得を行う。ここで指定するURLは図1の1行目に示されているものと、6行目のHost~に示されているIPアドレスを結合したものである。6行目では、OpenDolphinから取得したデータの内容を確認するためポップアップしている。また、確認のためだけに膨大なデータを出力するのは無駄であるため、list[0]として、最新の検査データのみを出力する。なお、JSON.stringify関数はJavascriptオブジェクトをJSON形式の文字列に変換する関数でである。
実際にスマートフォンアプリの方で確認したところ、図5の様に表示され、無事にデータの取得が行えていることを確認することが出来た。
図5 スマートフォンアプリ図1

次に、国名一覧を各検査項目名一覧に変更する。
Countries.get()
    .then(
      function(countries) {
        that.list = countries;
      }
    );
上記は、取得した国名をリストに送るといったコードであるが、これを下記のコードに書き換える。
Countries.get()
    .then(
      function(countries) {
        var list = countries.list[0];
        that.list = [];
        list.items.forEach(function(element){
          that.list.push(element.itemName);
        });
      }
    );
まず、4行目の記述で、ローカル変数「list」に「countries.list」に格納されている検査データのうち最新のものを代入し、5行目で、検査一覧を格納する属性「that.list」を空配列に初期化する。
そして6~7行目は、検査目「itemName」をそのリストの要素に追加するコードである。
このコードを保存し、スマートフォンアプリで確認すると下記の図6の様になり、無事に検査一覧を表示させることが出来た。
図6 スマートフォンアプリ図2

2018年6月20日水曜日

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

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

#index.html

<ons-template id="country.html">
        <ons-page ng-controller="CountryController as country">
          <ons-toolbar>
            <div class="left"><ons-back-button>Back</ons-back-button></div>
            <div class="center">{{ country.name }}</div>
          </ons-toolbar>

          <ons-list class="year-select-container">
            <ons-list-item>
              <select
                ng-model="country.year"
                ng-options="year for year in country.years"
                class="text-input text-input--transparent"
                style="width: 100%; margin-top: 4px">
              </select>
            </ons-list-item>
          </ons-list>

          <population-chart ng-if="country.showChart" data="country.population"></population-chart>
        </ons-page>
      </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

angular.module('app', ['onsen'])

.controller('CountriesController', ['$scope', 'Countries', function($scope, Countries) {
  var that = this;

  Countries.get()
    .then(
      function(countries) {
        that.list = countries;
      }
    );

  this.showCountry = function(country) {
    //$scope.navi.pushPage('country.html', {data: {name: country}});
    navi.pushPage('country.html', {data: {name: country}});
  };
}])

.controller('CountryController', ['$scope', 'Population', function($scope, Population) {
  var that = this;
  this.name = $scope.navi.topPage.data.name;

  this.showChart = false;
  $scope.navi.on('postpush', function() {
    $scope.$evalAsync(function() {
      that.showChart = true;
    });

    $scope.navi.off('postpush');
  });

  var currentYear = (new Date()).getUTCFullYear();
  this.year = currentYear + '';

  this.years = [];
  for (var i = 1950; i <= 2100; i++) {
    this.years.push('' + i);
  }

  this.getPopulation = function() {
    Population.get(this.name, this.year)
      .then(
        function(population) {
          that.population = population;
        }
      );
  };

  $scope.$watch('country.year', function() {
    that.getPopulation();
  });
}])

.service('Countries', ['$http', function($http) {
  this.get = function() {
    return $http.get('countries.json')
      .then(
        function(response) {
          return response.data.countries;
        }
      );
  };
}])

.service('Population', ['$http', function($http) {
  this.get = function(country, year) {
    return $http.jsonp('http://api.population.io:80/1.0/population/' + year + '/' + country + '/?format=jsonp&callback=JSON_CALLBACK')
      .then(
        function(response) {
          return response.data;
        }
      );
  };
}])

.factory('nv', function() {
  return nv;
})

.factory('d3', function() {
  return d3;
})

.controller('PopulationChartController', ['$scope', function($scope) {
  $scope.formatData = function() {
    if (!$scope.data) {
      return [];
    }

    var total = [],
      males = [],
      females =[];

    for (var i = 0; i < $scope.data.length; i++) {
      var data = $scope.data[i];

      total.push({x: data.age, y: data.total});
      males.push({x: data.age, y: data.males});
      females.push({x: data.age, y: data.females});
    }

    return [
      {
        key: 'Total',
        values: total
      },
      {
        key: 'Women',
        values: females
      },
      {
        key: 'Men',
        values: males
      }
    ];
  };


  $scope.$on('chartloaded', function(event) {
    event.stopPropagation();

    $scope.updateChart($scope.formatData());

    $scope.$watch('data', function() {
      if ($scope.data) {
        $scope.updateChart($scope.formatData());
      }
    });
  });
}])

.directive('populationChart', ['nv', 'd3', function(nv, d3) {
  return {
    restrict: 'E',
    scope: {
      data: '='
    },
    controller: 'PopulationChartController',
    link: function(scope, element, attrs) {
      var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
      element.append(svg);

      var chart;

      scope.updateChart = function(data) {
        d3.select(svg)
          .datum(data)
          .call(chart);
      };

      nv.addGraph(function() {
        chart = nv.models.stackedAreaChart()
        .useInteractiveGuideline(true)
        .showControls(false)
        .margin({left: 50, right: 30});

        chart.xAxis
          .tickFormat(d3.format(',r'));

        chart.yAxis
          .tickFormat(function(d) {
            if ((d / 1000000) >= 1) {
              d = Math.round(d / 100000) / 10 + 'M';
            }
            else if ((d / 1000) >= 1) {
              d = Math.round(d / 100) / 10 + 'K';
            }
            return d;
          });

        nv.utils.windowResize(function() {
          chart.update();
        });

        scope.$emit('chartloaded');

        return chart;
      });
    }
  }
}]);
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を使ってみるのサンプルデータから

<link href="https://cdn.rawgit.com/novus/nvd3/v1.8.1/build/nv.d3.css" rel="stylesheet" type="text/css">
<body>
    <div id="chart">
        <svg style="height:500px;" />
    </div>

    <script src="https://d3js.org/d3.v3.min.js"></script>
    <script src="https://cdn.rawgit.com/novus/nvd3/v1.8.1/build/nv.d3.js"></script>
    <script type="text/javascript">

    nv.addGraph(function() {
        //var chart = nv.models.historicalBarChart();
        //var chart = nv.models.lineWithFocusChart();
        var chart = nv.models.lineChart();

        chart.xAxis.tickFormat(d3.format(',f'));
        chart.yAxis.tickFormat(d3.format(',.2f'));

        d3.select('#chart svg')
            .datum(testData())
            .transition().duration(500)
            .call(chart);

        nv.utils.windowResize(chart.update);

        return chart;
    });

    function testData() {
        return [{color:'red',
                 key:'hoge',
                 values: [{x:1,y:100},
                          {x:2,y:130},
                          {x:3,y:90},]},
                {color:'blue',
                 key:'hage',
                 values: [{x:1,y:70},
                          {x:2,y:60},
                          {x:3,y:80},
                          {x:4,y:90}]}];
    }
    </script>
</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

<div ng-app="app" ng-controller="MyController as main">
  <line-chart height="250px" data="main.data"></line-chart>

  <h3>Change the values: </h3>

  <p ng-repeat="datum in main.data[0].values">
    <input type="number" ng-model="datum.y">
  </p>
</div>

JavaScript

angular.module('app', [])
  .controller('MyController', ['$scope', function($scope) {
    this.data = [{
      key: 'Data',
      values: [{
        x: 0,
        y: 0
      }, {
        x: 1,
        y: 1
      }, {
        x: 2,
        y: 4
      }, {
        x: 3,
        y: 9
      }, {
        x: 4,
        y: 16
      }, {
        x: 5,
        y: 25
      }]
    }];
  }]);

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

2018年6月14日木曜日

MONACAサンプルアプリの導入と、ソースコードの解読1

今回は、MONACAを用いてアプリケーションの開発を試みるのだが、一から作成するといった作業は時間的に余裕がありそうにないので、あらかじめ用意されているテンプレートの編集を行い、アプリケーションの開発を試みる。
まず、MONACA Docsに接続し、サンプルアプリをクリックする。すると、サンプルアプリケーション一覧が表示される。今回作成したいアプリケーションはグラフの描画機能を持っているものなので、「チャートアプリ」の導入を行った。

図1 サンプルアプリ一覧
導入を行う際は、上記図1のアプリ図に示されている「Direct Import」をクリックする事で、MONACAにインポートすることができる。また、インポートを行う際は、下記の図2のような確認ダイアログが表示される。
図2 サンプルアプリインポートダイアログ
インポートを行い、MONACAのダッシュボードに移動すると、下記図3の様に無事にインポートされていることが分かる。尚、MONACAは無料試遊版だとプロジェクトを3つまでしか保有することが出来ないため、注意が必要である。
図3 MONACAダッシュボード
仕様の確認を行うため、スマートフォン版でアプリケーションの確認を行ったのだが、
下記図4のようなポップアップが表示された。
図4 MONACAアプリケーション図1
これは,アンプルアプリのCordovaのバージョンが古いため、表示された警告メッセージである。そこで、一旦ブラウザ版の方に戻り、「onsen World Poplation」を「クラウドIDEで開く」で編集画面に遷移する。そして、設定タブから「Cordova プラグインの管理」をクリックする。
図5 クラウドIDE図1
すると、上記図5のような画面が表示される。どうやら最新バージョン「7.1.0」に対してこちらのバージョンは「6.5.0」であったため、エラーが表示されたようである。「7.1.0にアップグレード」をクリックすると、下記図6のような画面が表示され、
図6 クラウドIDE図2
OKをクリックする事で、図7のような画面が表示され、無事にアップデートを行う事ができた。
図7 クラウドIDE図3
無事にアップデートを行う事が出来たので、次はスマートフォンでアプリケーションの大まかな仕様について確認を行った。
図8 MONACAアプリケーション図2
まずアプリケーションを実行すると、上記図8の様な画面が表示された。
これは各国毎にデータを分けて格納されていることが分かる。
今回は「Japan」のサンプルデータの確認を行ってみた。
図9 MONACAアプリケーション図3
「Japan」をタップすると、上記図9のような図が表示された。とあるデータを参照し、この様なグラフの描画を行っているのだろう。また青色のタブをタップすることで、図10の様な年別でのデータ表示の切り替えも行えることが出来る。
図10 MONACAアプリケーション図4
これでおおよその仕様の確認を行う事が出来た。次にこのアプリケーションのソースコードの解読を行い、どのデータを書き換えれば、我々が作成したいアプリケーションの開発が行えるかどうかを試みる。
このアプリケーションを形成しているものは主に「index.html」,「index.js」,「countries.json」の3つである。また、今回は実際に解読を行った部分のみを抜粋して記載している。

#index.html

<body>
 <ons-navigator page="main.html" var="navi"></ons-navigator>

 <ons-template id="main.html">
  <ons-page ng-controller="CountriesController as countries">
   <ons-toolbar>
    <div class="center">World Population</div>
   </ons-toolbar>

   <div class="filter-input-container">
    <input class="search-input" ng-model="query" placeholder="Filter countries" type="search" value="" />
   </div>
   <ons-list>
    <ons-list-item modifier="chevron" ng-click="countries.showCountry(country)" ng-repeat="country in countries.list | filter:query">
     {{ country }}
    </ons-list-item>
   </ons-list>
  </ons-page>
 </ons-template>

 <ons-template id="country.html">
  <ons-page ng-controller="CountryController as country">
   <ons-toolbar>
    <div class="left">
     <ons-back-button>Back</ons-back-button>
    </div>
    <div class="center">
     {{ country.name }}
    </div>
   </ons-toolbar>

   <ons-list class="year-select-container">
    <ons-list-item>
     <select class="text-input text-input--transparent" ng-model="country.year" ng-options="year for year in country.years" style="margin-top: 4px; width: 100%;">
     </select>
    </ons-list-item>
   </ons-list>

   <population-chart data="country.population" ng-if="country.showChart"></population-chart>
  </ons-page>
 </ons-template>
</body>
1行目の<body></body>の間にあるHTMLがアプリが表示する画面(View)を定義している。2行目~40行目にかけて多く見られる<ons-…>の「ons」は「Onsen-UI」というモバイルアプリの開発に特化したUIコンポーネントである。<ons-page>で囲まれたページの中に、他のHTML要素やOnsen UIコンポーネントを記述してユーザインターフェースを開発する。
<ons-navigator…>は、Onsen UIのサイトの記載によると、ページの切り替え機能を提供するコンポーネントだとわかる。
<ons-template…>は、これもOnsen UIのサイトの記載によると、id属性に指定した名前をpageのURLとしてons-navigatorなどのコンポーネントから参照できるHTMLを定義するものである。これを使うと個々の画面のHTMLファイルを別々に分けなくとも1つのファイル内に記述できるようになる。
この機能を使って、4行目~20行目、22行目~40行目の<ons-template…></ons-template>はそれぞれで独立したページの指定を行っている。
4行目~20行目は「main.html」のファイル、22行目~40行目は「country.html」のファイルを指定している。
15行目にある「…ng-repeat="country in countries.list…」は、AngularJSのCountriesController(国名を管理するコントローラ)からcountry(国名)のデータを繰り返し取得し、16行目の{{ country }}によって、アプリ画面に国名が表示されるようにしている。

#index.js

angular.module('app', ['onsen'])
.controller('CountriesController', ['$scope', 'Countries', function($scope, Countries) {
  var that = this;
  Countries.get()
    .then(
      function(countries) {
        that.list = countries;
      }
    );
  this.showCountry = function(country) {
    $scope.navi.pushPage('country.html', {data: {name: country}});
  };
}])
.service('Countries', ['$http', function($http) {
  this.get = function() {
    return $http.get('countries.json')
      .then(
        function(response) {
          return response.data.countries;
        }
      );
  };
}])
.service('Population', ['$http', function($http) {
  this.get = function(country, year) {
    return $http.jsonp('http://api.population.io:80/1.0/population/' + year + '/' + country + '/?format=jsonp&callback=JSON_CALLBACK')
      .then(
        function(response) {
          return response.data;
        }
      );
  };
}])
MonacaはAngulaJSというJavaScriptのフレームワークを用いている。1行目はAngulaJSのモジュールを定義しているところである。モジュールはAngularJSの最も基本的な単位で、アプリを構成する様々な部品(コントローラ、サービス、ディレクティブなど)をモジュールの下に作成していく。
2行目は「CountriesController」という名前のコントローラメソッドの記述である。$scopeというのは、 ビュー(アプリの画面のこと:すなわちHTML5で作成する画面)にデータなどを渡したり、ビューから発生したイベントを監視するなど、ビューとのやり取りを行うことができる特別なオブジェクトである。具体的には、このコントローラが参照されるHTMLノード(この場合はmain.htmlのons-pageノード)自身を表している。
3行目では、$scopeを意味するthisを一時変数thatに代入している(いろいろな問題を回避するため、$scopeを使わずthisを利用するようになっているらしい)。
4行目~9行目はサービスCountries(14~23行目)からデータ(国名一覧:countries.json)の取得を行い、うまくいった場合は自身のリストにデータを流し込むといった処理を行っている。
10行目~12行目は、引数に指定されたcountryの詳細ページを表示すべく、country.htmlへの画面遷移を行うメソッドを定義している。this.showCountryのようにthisで修飾することで、このコントローラのスコープ内で利用できるメソッドを定義できる。
14行目~22行目は、JSON形式の国名一覧「countires.jon」からデータを取得するサービスメソッドCountriesの定義である。このサービスはCountriesControllerのサービスとしてコントローラに注入されて利用されている(2行目)。
24行~33行は、「Population」というサービスメソッドの定義である。引数に指定した国及び年(それぞれcountryおよびyear)の人口データを取得すべく、26行目で
http://api.population.io:80/1.0/population/' + year + '/' + country + '/?format=jsonp&callback=JSON_CALLBACK
というURLを作成し、Webサービスを提供するサーバに人口データの要求を行っている。
ここで、試しに2018年の日本の人口データをブラウザから要求してみた。手作業で作成したURLは次のとおりである。
http://api.population.io/1.0/population/2018/Japan?format=jsonp
すると、以下の様なJSON形式の人口データが返ってくることを確認できた。
[
  {
    "females": 493727,
    "country": "Japan",
    "age": 0,
    "males": 520667,
    "year": 2018,
    "total": 1014404
  }
今回解読したソースコードをおおまかに整理すると、以下の図11のようになる。
図11 index.jsフロー図
まず、controller「CountryController」に注入したservice「Countries」のgetメソッドを使ってcountries.jsonから国名データを取得する。その後、取得したデータをcontrollerのlist属性に流しこむといった具合だ。
今回はサンプルアプリの導入と、簡単なプログラムの分析を行った。次回は解析しきれていないソースコードを分析し、目的のスマホアプリを開発するにはどの部分の書き換えを行うべきかを見定めていきたい。


2018年6月7日木曜日

スマホアプリを作成する為のMONACAのアカウント登録と試食

今回はスマートフォンアプリを作成する為にMONACAというアプリ開発クラウドを利用した。まずログイン時に以下の図1のようなダッシュボードが表示される。
図1 MONACAダッシュボード画面
また、編集内容が反映されているかどうかを確認するため、スマートフォンアプリの方も導入した。
図2 スマホ版MONACA図1
ログインをすると、図2のような画面が表示された。
次に右下のリングメニューをタップすると、図3のようなメニューが表示される。
図3 リングメニュー一覧
リングメニューとしてはそれぞれ
1,ダッシュボード一覧に戻る
2,リロード
3,スクリーンショットの保存
4,検索?
5,ログの表示
6,リングメニューを閉じる
尚、4に関してはグレーアウトしており、タップすることができなかったのであくまで憶測である。
次にアプリケーションの編集を行うため、WEB版に戻り、はじめてのMONACAアプリ「はじめてのMONACAアプリ」というアイコンをクリックすると右に編集一覧が表示される。今回は図4の様に「クラウドIDEで開く」というアイコンをクリックする。
図4 はじめてのMONACAアプリを開いた状態
クラウドIDEを開くをクリックすると、以下の図5の様な画面が表示された。
図5 クラウドIDEで開いた図
クラウドIDEを開いた後左タブのwww>phongape-demo>index.html画面をクリックする事で、indexの編集を行う事ができる。
今回は試しに、図5の20行目に赤線で示されている「Hello Word!」といった文面を変更する。変更を行った後はメニュータブから保存をクリックする事で、編集を保存することができる。
図6 編集後のindex図
保存を行った後、スマートフォンアプリの方で確認をしたところ、図6の様の変更されていることを確認することができた。
次にボタンを押した際に「終了します」といったポップアップが表示されるようなボタンの追加を行う。
図7 ボタン追加図
再びWEBMONACAの方に戻り、23行目に下記のプログラム行を追加した。
また、一つ目のボタンと間をあけるために22行目に「br」を追加する。
<a class="button--large" href="https://www.blogger.com/null" onclick="alert('終了します')">Stop Demo</a>
図8 Stop Demoをタップした状態
編集を行い、保存をした後またスマートフォンアプリの方で確認をしたところ、Start Demoの下にStop Demoといったボタンが表示され、タップすることで、図8のようなポップアップメニューが表示された。
今回はMONACAの表面上の部分だけを体験する事ができたので、次回はMONACAのチュートリアルを確認しながら、具体的な動作の確認、習得を行いたい。

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

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