PHPでCOVID-19情報をグラフ表示

(1/1)
2019 年(平成 31 年)末頃より新型コロナウイルス感染症(COVID-19)が、中国の武漢市を中心に出現した。WHO の発表によると、2020 年(令和 2 年)4 月 16 日現在、世界の患者数は約 199 万人、死亡者数は約 13 万人に達した。
今回は、国内の COVID-19 感染の推移を、最新のデータを読み込んでグラフ表示する PHP プログラムを作ってみることにする。

本プログラムで参照するデータは、東洋経済オンライン編集部の荻原和樹さんが「新型コロナウイルス国内感染の状況」(MIT ライセンス)として公開しているもの、および、jig.jp 創業者&会長の福野泰介さんが「新型コロナウイルス対策ダッシュボード」として公開しているものをマッシュアップした。

(2021 年 6 月 20 日)グラフ画像とメッセージを自動投稿するツイート機能を追加した.
(2021 年 4 月 10 日)キャッシュ・システム導入:pahooCache クラス.表示期間 PERIOD 追加.「実行」ボタン廃止.
(2021 年 2 月 23 日)データファイルの全面変更,対応関数変更

サンプル・プログラム:実行例

PHPでCOVID-19情報をグラフ表示

目次

サンプル・プログラム

圧縮ファイルの内容
viewCOVID-19.phpサンプル・プログラム本体
pahooStat.php統計に関わるクラス pahooStat。
使い方は「PHPで太陽黒点相対数の周期変化を描く」を参照。
pahooCache.phpキャッシュ処理に関わるクラス pahooCache。
キャッシュ処理に関わるクラスの使い方は「PHPで天気予報を求める」を参照。include_path が通ったディレクトリに配置すること。
pahooTwitterAPI.phpTwitter APIを利用するクラス pahooTwitterAPI。
使い方は「PHPでTwitterに投稿(ツイート)する」などを参照。include_path が通ったディレクトリに配置すること。

準備:初期値

0033: //プログラム・タイトル
0034: define('TITLE', 'COVID-19情報をグラフ表示');
0035: 
0036: //ツイート・ボタン  TRUE:有効,FALSE:無効
0037: define('TWITTER', TRUE);
0038: 
0039: //画像化したいオブジェクト
0040: define('TARGET', 'target');
0041: 
0042: //移動平均区間;日
0043: define('INTERVAL', 7);
0044: 
0045: //表示機関:日
0046: define('PERIOD', 365);
0047: 
0048: //傾きを求める区間;日
0049: define('SLOPE', 30);
0050: 
0051: //グラフの表示幅・高さ(単位:ピクセル)
0052: define('WIDTH',  600);
0053: define('HEIGHT', 400);
0054: 
0055: //グラフの色
0056: define('COLOR_COVID', '#88CCFF');
0057: define('COLOR_MEAN',  '#CC0000');
0058: 
0059: //jqPlotのあるフォルダ
0060: define('JQPLOT', '../../../../common/jqplot/');
0061: 
0062: //キャッシュ保持時間(分) 0:キャッシュしない
0063: //アクセス負荷軽減のため,60分以上のキャッシュ保持をお勧めします.
0064: define('LIFE_CACHE', 360);
0065: 
0066: //キャッシュ・ディレクトリ
0067: //書き込み可能で,外部からアクセスされないディレクトリを指定してください.
0068: define('DIR_CACHE', './covid19_pcache/');
0069: 
0070: //統計に関わるクラス:include_pathが通ったディレクトリに配置
0071: require_once('pahooStat.php');
0072: 
0073: //キャッシュ処理に関わるクラス:include_pathが通ったディレクトリに配置
0074: require_once('pahooCache.php');
0075: 
0076: if (TWITTER) {
0077:     //Twitterクラス:include_pathが通ったディレクトリに配置
0078:     require_once('pahooTwitterAPI.php');
0079: }

上記の初期値は任意に変更が可能である。

グラフ描画のために、「PHP で NHK 政治意識月例調査をグラフ表示」で紹介したjQuery のプラグイン jqPlot を用いた。プラグインをダウンロードしたら解凍して、そのパスを定数 JQPLOT に定義する。

データ提供サイトへ負荷をかけないよう、キャッシュ・クラス pahooCache を導入した。使用方法については、「PHP で天気予報を求める - キャッシュ・システム」を参照いただきたい。
キャッシュ保持時間、キャッシュ・ディレクトリともに、自サイトの環境に応じて変更してほしい。

準備:データ・ファイル

参照データは、東洋経済オンライン編集部の荻原和樹さんが「新型コロナウイルス国内感染の状況」として公開しているもの、および、jig.jp 創業者&会長の福野泰介さんが「新型コロナウイルス対策ダッシュボード」として公開しているもの。

0081: //データファイル名(変更不可)
0082: define('DATA_FILE1', 'https://raw.githubusercontent.com/kaz-ogiwara/covid19/master/data/data.json');                       //グラフ描画情報【廃止】
0083: define('DATA_FILE_BEDS1', 'https://raw.githubusercontent.com/code4sabae/covid19/master/data/bedforinfection_current.json');    //感染症ベッド数
0084: define('DATA_FILE_BEDS2', 'https://raw.githubusercontent.com/code4sabae/covid19/master/data/bedforinfection_summary.json');    //感染症ベッド数
0085: define('DATA_FILE_NP1', 'https://raw.githubusercontent.com/code4sabae/covid19/master/data/covid19japan-fast.json');            //現在の入院患者数
0086: define('DATA_FILE_NP2', 'https://raw.githubusercontent.com/code4sabae/covid19/master/data/covid19japan-all.json');         //現在の入院患者数
0087: 
0088: //v.3.0にて追加(変更不可)
0089: //厚生労働省オープンデータ「陽性者数」
0090: define('FILE_PCR_POSITIVE_DAILY', 'https://toyokeizai.net/sp/visual/tko/covid19/csv/pcr_positive_daily.csv');
0091: //厚生労働省オープンデータ「PCR検査実施人数」
0092: define('FILE_PCR_TESTED_DAILY', 'https://toyokeizai.net/sp/visual/tko/covid19/csv/pcr_tested_daily.csv');
0093: //厚生労働省オープンデータ「入院治療等を要する者の数」
0094: define('FILE_CASES_TOTAL', 'https://toyokeizai.net/sp/visual/tko/covid19/csv/cases_total.csv');
0095: //厚生労働省オープンデータ「退院又は療養解除となった者の数」
0096: define('FILE_RECOVERY_TOTAL', 'https://toyokeizai.net/sp/visual/tko/covid19/csv/recovery_total.csv');
0097: //厚生労働省オープンデータ「死亡者数」
0098: define('FILE_DEATH_TOTAL', 'https://toyokeizai.net/sp/visual/tko/covid19/csv/death_total.csv');
0099: //厚生労働省オープンデータ「PCR検査の実施件数」
0100: define('FILE_PCR_CASE_DAILY', 'https://toyokeizai.net/sp/visual/tko/covid19/csv/pcr_case_daily.csv');
0101: //公表日ごとの全国の重症者数
0102: define('FILE_SEVERE_DAILY', 'https://toyokeizai.net/sp/visual/tko/covid19/csv/severe_daily.csv');
0103: //日別全国の実効再生産数
0104: define('FILE_EFFECTIVE_REPRODUCTION_NUMBER', 'https://toyokeizai.net/sp/visual/tko/covid19/csv/effective_reproduction_number.csv');
0105: //年代別の国内発生動向
0106: define('FILE_DEMOGRAPHY', 'https://toyokeizai.net/sp/visual/tko/covid19/csv/demography.csv');
0107: //都道府県別の発生動向
0108: define('FILE_PREFECTURE', 'https://toyokeizai.net/sp/visual/tko/covid19/csv/prefectures.csv');

解説:グラフにプロットするためのデータを配列に格納する

0473: /**
0474:  * グラフにプロットするためのデータを配列に格納する
0475:  * @param   string $fname   CSVファイル名
0476:  * @param   array  $cols    取得結果のvalに合算するカラム番号
0477:  * @param   string $errmsg  エラーメッセージ格納用
0478:  * @param   int    $method  データの積算方法(省略時:0)
0479:  *                          0:そのまま
0480:  *                          1:前日の値を加算
0481:  *                          2:前日の値を減算
0482:  *                          3:値から前日の値を減算
0483:  *                          4:前日までの積算値を加算
0484:  *                          5:前日までの積算値を減算
0485:  *                          6:値から前日までの積算値を減算
0486:  * @param   string $pref    都道府県名(省略時:全国)
0487:  * @return  array  取得結果/FALSE:取得失敗
0488:  *                  [連番][要素名]
0489:  *                  要素名と値
0490:  *                      year    年
0491:  *                      month   月
0492:  *                      day     日
0493:  *                      val     プロット値
0494: */
0495: function readData($fname$cols, &$errmsg$method=0, $pref='全国') {
0496:     $data = array();
0497:     $cnt = 0;
0498:     $errmsg = '';
0499: 
0500:     //ファイル・オープン
0501:     $infp = @fopen($fname, 'r');
0502:     if ($infp == FALSE) {
0503:         $errmsg = $fname . ' にアクセスできません.';
0504:         return FALSE;
0505:     }
0506: 
0507:     //データ読み込み
0508:     $ss = fgets($infp, 1000);        //1行目スキップ
0509:     while (! feof($infp)) {
0510:         $ss   = fgets($infp, 1000);
0511:         $arr  = preg_split('/\,/ui', $ss);
0512:         $flag = FALSE;
0513:         if (isset($arr[0])) {
0514:             if (preg_match('/([0-9]+)\/([0-9]+)\/([0-9]+)/ui', $arr[0]$arr2) > 0) {
0515:                 $data[$cnt]['year']  = (int)$arr2[1];
0516:                 $data[$cnt]['month'] = (int)$arr2[2];
0517:                 $data[$cnt]['day']   = (int)$arr2[3];
0518:                 $flag = TRUE;
0519:             } else if (isset($arr[3]) && ($pref == $arr[3])) {
0520:                 $data[$cnt]['year']  = (int)$arr[0];
0521:                 $data[$cnt]['month'] = (int)$arr[1];
0522:                 $data[$cnt]['day']   = (int)$arr[2];
0523:                 $flag = TRUE;
0524:             }
0525:         }
0526:         if ($flag) {
0527:             //データの積算
0528:             $val = 0.0;
0529:             foreach ($cols as $key) {
0530:                 $val += (float)$arr[$key];
0531:             }
0532:             if ($cnt == 0) {
0533:                 $data[$cnt]['val'] = $val;
0534:                 $old1 = $data[$cnt]['val'];
0535:                 $old2 = $data[$cnt]['val'];
0536:             } else {
0537:                 switch ($method) {
0538:                     case 0:
0539:                         $data[$cnt]['val'] = $val;
0540:                         break;
0541:                     case 1:
0542:                         $data[$cnt]['val'] = $val + $old1;
0543:                         break;
0544:                     case 2:
0545:                         $data[$cnt]['val'] = $val - $old1;
0546:                         break;
0547:                     case 3:
0548:                         $data[$cnt]['val'] = $old1 - $val;
0549:                         break;
0550:                     case 4:
0551:                         $data[$cnt]['val'] = $val + $old2;
0552:                         break;
0553:                     case 5:
0554:                         $data[$cnt]['val'] = $val - $old2;
0555:                         break;
0556:                     case 6:
0557:                         $data[$cnt]['val'] = $old2 - $val;
0558:                         break;
0559:                 }
0560:                 $old1 = $val;
0561:                 $old2 = $data[$cnt]['val'];
0562:             }
0563:             $cnt++;
0564:         }
0565:     }
0566:     fclose($infp);
0567: 
0568:     return $data;
0569: }

COVID-19 グラフ描画のためのデータを CSV ファイルから読み込み配列に格納するのが、ユーザー関数 readData である。
上述のように複数の CSV ファイルを読み込み対象としているが、データ構造は似ている――1 列目、または 1~3 列目に年月日、2 列目以降、または 4 列目以降に毎日のデータ――ので、この関数に適切なパラメータを与えることで処理を行う。

まず、 fopen  で CVS ファイルをオープンし、失敗したらエラーを返す。
冒頭行はラベルなので読み飛ばす。
2行目からがデータ行である。年月日が 1 列目だけか、1~3 列名にまたがっているかは正規表現パターンで判定する。

日々のデータ積算処理だが、データ構造及び描画したいグラフの種類によって、次の 7 つのケースが考えられる。
  1. そのまま
  2. 前日の値を加算
  3. 前日の値を減算
  4. 値から前日の値を減算
  5. 前日までの積算値を加算
  6. 前日までの積算値を減算
  7. 値から前日までの積算値を減算
積算方法は引数 $method で渡すことによって、配列 $data に格納していく。

0349: /**
0350:  * データ・ファイルを読み込み、現在の都道府県別ベッド数を取得
0351:  * @param   array  $items 都道府県別ベッド数格納用
0352:  * @return  int 全国ベッド数/FALSE
0353: */
0354: function readBeds(&$items) {
0355:     global $TablePref;
0356: 
0357:     //オブジェクト生成
0358:     $pcc = new pahooCache(LIFE_CACHEDIR_CACHE);
0359: 
0360:     //データファイル(その1)
0361: //  $contents = file_get_contents(DATA_FILE_BEDS1);
0362:     $contents = $pcc->load(DATA_FILE_BEDS1);
0363:     if ($contents == FALSE)     return FALSE;
0364:     $data = json_decode($contentsTRUE);
0365:     $cnt = 0;
0366:     foreach ($data as $key=>$arr) {
0367:         if (isset($arr['自治体名']) && isset($arr['新型コロナウイルス対策感染症病床数']) && isset($arr['発表日'])) {
0368:             $pref = $arr['自治体名'];
0369:             $yyyymmdd = $arr['発表日'];
0370:             //新規登録
0371:             if (! isset($items[$pref])) {
0372:                 $items[$pref]['date'] = $yyyymmdd;
0373:                 $items[$pref]['val']  = (int)$arr['新型コロナウイルス対策感染症病床数'];
0374:             } else if ($yyyymmdd > $items[$pref]['date']) {
0375:                 $items[$pref]['date'] = $yyyymmdd;
0376:                 $items[$pref]['val']  = (int)$arr['新型コロナウイルス対策感染症病床数'];
0377:             }
0378:         }
0379:     }
0380: 
0381:     //データファイル(その2)
0382:     $contents = file_get_contents(DATA_FILE_BEDS2);
0383:     if ($contents == FALSE)     return FALSE;
0384:     $data = json_decode($contentsTRUE);
0385:     $cnt = 0;
0386:     foreach ($TablePref as $code=>$arr1) {
0387:         if (! isset($items[$arr1])) {
0388:             foreach ($data['area'] as $key=>$arr2) {
0389:                 if ($arr2['name_ja'] == $arr1) {
0390:                     $items[$arr1]['val'] = $arr2['sum'];
0391:                     break;
0392:                 }
0393:             }
0394:         }
0395:     }
0396: 
0397:     //合計
0398:     $cnt = 0;
0399:     foreach ($items as $arr)    $cnt += $arr['val'];
0400: 
0401:     //オブジェクト解放
0402:     $pcc = NULL;
0403: 
0404:     return $cnt;
0405: }

現在の都道府県別ベッド数を GitHub の 2 つのファイルから読み込み、組み込み関数  json_decode  によって配列に展開する。

0407: /**
0408:  * データ・ファイルを読み込み、現在の都道府県別入院患者数を取得
0409:  * @param   array  $items 都道府県別入院患者数格納用
0410:  * @return  int 全国入院患者数/FALSE
0411: */
0412: function readNP(&$items) {
0413:     global $TablePref;
0414: 
0415:     //オブジェクト生成
0416:     $pcc = new pahooCache(LIFE_CACHEDIR_CACHE);
0417: 
0418:     //データファイル(その1)
0419: //  $contents = file_get_contents(DATA_FILE_NP1);
0420:     $contents = $pcc->load(DATA_FILE_NP1);
0421:     if ($contents == FALSE)     return FALSE;
0422:     $data = json_decode($contentsTRUE);
0423:     $cnt = 0;
0424:     foreach ($data as $key=>$arr) {
0425:         if (isset($arr['name']) && isset($arr['ncurrentpatients']) && isset($arr['lastUpdate'])) {
0426:             $pref = $arr['name'];
0427:             $yyyymmdd = $arr['lastUpdate'];
0428:             //新規登録
0429:             if (! isset($items[$pref])) {
0430:                 $items[$pref]['date'] = $yyyymmdd;
0431:                 $items[$pref]['val']  = (int)$arr['ncurrentpatients'];
0432:             } else if ($yyyymmdd > $items[$pref]['date']) {
0433:                 $items[$pref]['date'] = $yyyymmdd;
0434:                 $items[$pref]['val']  = (int)$arr['ncurrentpatients'];
0435:             }
0436:         }
0437:     }
0438: 
0439:     //データファイル(その2)
0440: //  $contents = file_get_contents(DATA_FILE_NP2);
0441:     $contents = $pcc->load(DATA_FILE_NP2);
0442:     if ($contents == FALSE)     return FALSE;
0443:     $data = json_decode($contentsTRUE);
0444:     $cnt = 0;
0445:     foreach ($TablePref as $code=>$arr1) {
0446:         if (! isset($items[$arr1])) {
0447:             foreach ($data as $key=>$arr2) {
0448:                 if (preg_match('/現在は入院/ui', $arr2['description']) > 0) {
0449:                     foreach ($arr2['area'] as $key=>$arr3) {
0450:                         if ($arr3['name_jp'] == $arr1) {
0451:                             if (! isset($items[$arr1]['val'])) {
0452:                                 $items[$arr1]['val'] = $arr3['ncurrentpatients'];
0453:                             }
0454:                             break;
0455:                         }
0456:                     }
0457:                     break;
0458:                 }
0459:             }
0460:         }
0461:     }
0462: 
0463:     //合計
0464:     $cnt = 0;
0465:     foreach ($items as $arr)    $cnt += $arr['val'];
0466: 
0467:     //オブジェクト解放
0468:     $pcc = NULL;
0469: 
0470:     return $cnt;
0471: }

現在の都道府県別入院患者数を GitHub の 2 つのファイルから読み込み、組み込み関数  json_decode  によって配列に展開する。

解説:検査陽性者数を取得

0571: /**
0572:  * 新規検査陽性者数を取得
0573:  * @param   string $errmsg  エラーメッセージ格納用
0574:  * @param   string $pref    都道府県名(省略時:全国)
0575:  * @return  array データ配列
0576: */
0577: function pcr_tested_positive_daily(&$errmsg$pref=PREF_ALL) {
0578:     if ($pref == PREF_ALL) {
0579:         $fname = FILE_PCR_POSITIVE_DAILY;
0580:         $cols = array(1);
0581:         $method = 0;
0582:     } else {
0583:         $fname = FILE_PREFECTURE;
0584:         $cols = array(5);
0585:         $method = 2;
0586:     }
0587: 
0588:     return readData($fname$cols$errmsg$method$pref);
0589: }

日々の新規検査陽性者数を取得するユーザー関数は pcr_tested_positive_daily である。
対象が全国化都道府県かによって、読み込む CSV ファイル名が異なる。また、データが格納されている列 $col と、データ積算方式 $method も変わる。

0591: /**
0592:  * 累計検査陽性者数を取得
0593:  * @param   string $errmsg  エラーメッセージ格納用
0594:  * @param   string $pref    都道府県名(省略時:全国)
0595:  * @return  array データ配列
0596: */
0597: function pcr_tested_positive_total(&$errmsg$pref=PREF_ALL) {
0598:     if ($pref == PREF_ALL) {
0599:         $fname = FILE_PCR_POSITIVE_DAILY;
0600:         $cols = array(1);
0601:         $method = 4;
0602:     } else {
0603:         $fname = FILE_PREFECTURE;
0604:         $cols = array(5);
0605:         $method = 1;
0606:     }
0607: 
0608:     return readData($fname$cols$errmsg$method$pref);
0609: }

累計の検査陽性者数を取得するユーザー関数は pcr_tested_positive_total である。
上述の pcr_tested_positive_daily との違いは、データ積算方式 $method である。
この他、検査人数、検査件数、入院治療等を要する者などを取得するユーザー関数は、すべて、pcr_tested_positive_dailypcr_tested_positive_total のバリエーションになる。

解説:jqPlot用のスクリプト

0813: /**
0814:  * jqPlot用のスクリプト
0815:  * @param   array  $items データ配列
0816:  * @param   string $pref  都道府県名
0817:  * @param   string $title グラフのタイトル
0818:  * @param   bool   $log   TRUE:縦軸は対数/FALSE:通常(省略時)
0819:  * @param   bool   $int   TRUE:縦軸は整数(省略時)/FALSE:小数
0820:  * @param   float  $ymax  Y軸の最大値(省略時:空文字=データの最大値)
0821:  * @return  string スクリプト
0822: */
0823: function plot($items$pref='', $title='', $log=FALSE$int=TRUE$ymax='') {
0824:     //Y軸の最大値
0825:     if ($ymax != '') {
0826:         $ymax = sprintf('max: %f,', $ymax);
0827:     }
0828: 
0829:     //グラフの色
0830:     $color_covid = COLOR_COVID;
0831:     $color_mean  = COLOR_MEAN;
0832:     //移動平均を求める区間(日)
0833:     $interval = INTERVAL;
0834: 
0835:     //移動平均
0836:     $x = array();
0837:     $y = array();
0838:     foreach ($items as $key=>$val)  $x[$key] = $val['val'];
0839:     //統計オブジェクト
0840:     $pst = new pahooStat();
0841:     $pst->simple_moving_average(INTERVAL$x$y);        //移動平均
0842:     foreach ($y as $key=>$val)  $items[$key]['mean'] = $val;
0843: 
0844:     //直近の傾き(最小二乗法)
0845:     $x1 = array();
0846:     $y1 = array();
0847:     $n = count($items);
0848:     for ($i = 0; $i < SLOPE$i++) {
0849:         $x1[$i] = $n - SLOPE + $i;
0850:         $y1[$i] = $items[$n - SLOPE + $i]['val'];
0851:     }
0852:     list($a0$a1) = $pst->LSM($x1$y1SLOPE);      //最小二乗法
0853:     $pst = NULL;
0854:     $slope = sprintf('直近%d日間の傾き:%.2f', SLOPE$a1);
0855: 
0856:     $series = '';
0857:     $xmin = date('Y-m-d', (time() - PERIOD * 24 * 60 * 60));
0858:     $xmax = '';
0859:     $rendere_yaxis = $log ? 'renderer: $.jqplot.LogAxisRenderer' : 'min: 0';
0860:     $format_yaxis  = $int ? "%'d" : "%.1f";
0861: 
0862:     //系列の生成
0863:     $cnt = 0;
0864:     $s1 = $s2 = '';
0865:     foreach ($items as $key=>$val) {
0866:         $xmax = sprintf("%04d-%02d-%02d", $val['year'], $val['month'], $val['day']);
0867:         if ($xmax < $xmin)  continue;
0868:         if ($key == 0)  $xmin = $xmax;
0869:         $s1 .= isset($val['val']) ? sprintf("['%s', %.3f],", $xmax$val['val']) : '';
0870:         $s2 .= isset($val['mean']) ? sprintf("['%s', %.3f],", $xmax$val['mean']) : '';
0871:         $cnt++;
0872:     }
0873:     $barwidth = (int)(WIDTH / $cnt * 0.7);
0874: 
0875: $js =<<< EOT
0876: jQuery(function() {
0877:     jQuery.jqplot('jqPlot_polls',
0878:     [
0879:         [ {$s1} ],
0880:         [ {$s2} ]
0881:     ],
0882:     {
0883:         //タイトル
0884:         title: {
0885:             text: '{$pref}の{$title}',
0886:             show: true,
0887:             fontFamily: 'serif',
0888:             fontSize: '20px',
0889:             textAlign: 'center',
0890:             textColor: 'black',
0891:         },
0892:         //系列
0893:         series: [
0894:         {
0895:             label: '{$title}',
0896:             color: '{$color_covid}',
0897:             renderer: jQuery . jqplot . BarRenderer,
0898:             rendererOptions: {
0899:                 barWidth: {$barwidth},
0900:                 shadowOffset: 0
0901:             }
0902:         },
0903:         {
0904:             label: '{$interval}日移動平均',
0905:             color: '{$color_mean}'
0906:         },
0907:         ],
0908:         legend: {
0909:             show: true,
0910:             placement: 'inside',
0911:             location: 'nw',
0912:             renderer: $.jqplot.EnhancedLegendRenderer,
0913:             rendererOptions: { numberRows: 1 }
0914:         },
0915:         seriesDefaults: {
0916:             showLine: true,
0917:             rendererOptions: { smooth: false },
0918:             markerOptions: { size: 0 },
0919:         },
0920:         //軸
0921:         axes: {
0922:             xaxis: {
0923:                 renderer:$.jqplot.DateAxisRenderer,
0924:                 tickOptions: { formatString: '%m/%d' },
0925:                 label: '日付',
0926:                 min: '{$xmin}',
0927:                 max: '{$xmax}',
0928:             },
0929:             yaxis: {
0930:                 {$ymax}
0931:                 {$rendere_yaxis},
0932:                 labelRenderer: $.jqplot.CanvasAxisLabelRenderer,
0933:                 label: '{$title}',
0934:                 tickOptions: {
0935:                     formatString: "{$format_yaxis}",
0936:                     angle: -30,
0937:                 }
0938:             }
0939:         },
0940:         //ハイライター
0941:         highlighter: {
0942:             show: true,
0943:             showMarker: true,
0944:             tooltipLocation: 'sw',
0945:             fadeTooltip: false,
0946:             bringSeriesToFront: true,
0947:             tooltipAxes: 'xy',
0948:             formatString: '%s<br />%s'
0949:         }
0950:     }
0951:     );
0952:     $('#slope').html('{$slope}');
0953: });
0954: 
0955: 
0956: EOT;
0957: 
0958:     return $js;
0959: }

データ配列を与え、jqPlot 用のスクリプトを生成する。
感染者数(累計)を表示するときなどには、「指数関数的に増えている」という話を見やすくするために、縦軸を対数軸にできるようにしている。

実数を棒グラフで、移動平均を折れ線グラフで表示する。
移動平均については、「PHP で太陽黒点相対数の周期変化を描く」で紹介したとおりである。

また、直近 SLOPE 日間のグラフの傾きを、最小二乗法によって求めるようにした。

解説:データ読み込み~グラフ描画

0110: //処理選択肢
0111: $SelectFuncs = array(
0112:     //関数名,タイトル,ラジオボタンcheked,Y軸を対数軸とするか,
0113:     //目盛りを整数にするかどうか,Y軸の最大値,都道府県を選択可能とするか
0114:     'pcr_tested_positive_daily' => array('title'=>'新規検査陽性者(人)', 'checked'=>'checked', 'log'=>FALSE, 'int'=>TRUE, 'ymax'=>'', 'prefectures'=>TRUE),
0115:     'pcr_tested_positive_total' => array('title'=>'累計検査陽性者(人)', 'checked'=>'', 'log'=>FALSE, 'int'=>TRUE, 'ymax'=>'', 'prefectures'=>TRUE),
0116:     'pcr_tested_daily' => array('title'=>'新規検査人数(人)', 'checked'=>'', 'log'=>FALSE, 'int'=>TRUE, 'ymax'=>'', 'prefectures'=>TRUE),
0117:     'pcr_tested_total' => array('title'=>'累計検査人数(人)', 'checked'=>'', 'log'=>FALSE, 'int'=>TRUE, 'ymax'=>'', 'prefectures'=>TRUE),
0118:     'pcr_case_daily' => array('title'=>'新規検査件数(件)', 'checked'=>'', 'log'=>FALSE, 'int'=>TRUE, 'ymax'=>'', 'prefectures'=>FALSE),
0119:     'pcr_case_total' => array('title'=>'累計検査件数(件)', 'checked'=>'', 'log'=>FALSE, 'int'=>TRUE, 'ymax'=>'', 'prefectures'=>FALSE),
0120:     'case_total'     => array('title'=>'入院治療等を要する者(人)', 'checked'=>'', 'log'=>FALSE, 'int'=>TRUE, 'ymax'=>'', 'prefectures'=>FALSE),
0121:     'recovery_daily' => array('title'=>'新規退院・療養解除(人)', 'checked'=>'', 'log'=>FALSE, 'int'=>TRUE, 'ymax'=>'', 'prefectures'=>TRUE),
0122:     'recovery_total' => array('title'=>'累計退院・療養解除(人)', 'checked'=>'', 'log'=>FALSE, 'int'=>TRUE, 'ymax'=>'', 'prefectures'=>TRUE),
0123:     'severe_daily' => array('title'=>'重症者(人)', 'checked'=>'', 'log'=>FALSE, 'int'=>TRUE, 'ymax'=>'', 'prefectures'=>TRUE),
0124:     'death_daily' => array('title'=>'新規死亡者(人)', 'checked'=>'', 'log'=>FALSE, 'int'=>TRUE, 'ymax'=>'', 'prefectures'=>TRUE),
0125:     'death_total' => array('title'=>'累計死亡者(人)', 'checked'=>'', 'log'=>FALSE, 'int'=>TRUE, 'ymax'=>'', 'prefectures'=>TRUE),
0126:     'reproduction' => array('title'=>'実効再生産数', 'checked'=>'', 'log'=>FALSE, 'int'=>FALSE, 'ymax'=>'', 'prefectures'=>TRUE),
0127: );
0128: 
0129: //全国(変更不可)
0130: define('PREF_ALL', '全国');
0131: 
0132: //都道府県テーブル(変更不可)
0133: $TablePrefarray(PREF_ALL, '北海道', '青森県', '岩手県', '宮城県', '秋田県', '山形県', '福島県', '茨城県', '栃木県', '群馬県', '埼玉県', '千葉県', '東京都', '神奈川県', '新潟県', '富山県', '石川県', '福井県', '山梨県', '長野県', '岐阜県', '静岡県', '愛知県', '三重県', '滋賀県', '京都府', '大阪府', '兵庫県', '奈良県', '和歌山県', '鳥取県', '島根県', '岡山県', '広島県', '山口県', '徳島県', '香川県', '愛媛県', '高知県', '福岡県', '佐賀県', '長崎県', '熊本県', '大分県', '宮崎県', '鹿児島県', '沖縄県');

0989: /**
0990:  * HTML BODYを作成する
0991:  * @param   string $func   jqPlot関数
0992:  * @param   string $pref   都道府県名または'全国'
0993:  * @param   string $res    応答メッセージ
0994:  * @return  string HTML BODY
0995: */
0996: function makeCommonBody($func$pref$res) {
0997:     global $SelectFuncs$TablePref;
0998: 
0999:     $myself   = MYSELF;
1000:     $refere   = REFERENCE;
1001:     $refdata  = REF_DATA;
1002:     $refdata2 = REF_DATA2;
1003:     $title    = TITLE;
1004:     $version  = '<span style="font-size:small;">' . date('Y/m/d版', filemtime(__FILE__)) . '</span>';
1005:     $width    = WIDTH;
1006:     $width2   = WIDTH + 20;
1007:     $height   = HEIGHT;
1008:     $height2  = HEIGHT + 20;
1009:     $target   = TARGET;
1010:     $tweet = $help_tweet = $graph = $errmsg = $debug = '';
1011: 
1012:     //Tweetボタン,ヘルプ
1013:     if (TWITTER) {
1014: $tweet =<<< EOT
1015:  <button id="exec" name="exec" class="tweet_button"><i class="fab fa-twitter" style="padding-right:10px;"></i>ツイート</button>
1016: <input type="hidden" id="base64" name="base64" value="" />
1017: 
1018: EOT;
1019: $help_tweet =<<< EOT
1020: <li>[<span style="font-weight:bold;">ツイート</span>]ボタンを押下すると,メッセージとグラフ画像をツイートします.</li>
1021: 
1022: EOT;
1023:     }
1024: 
1025:     //処理選択ラジオボタン
1026:     $str_radio = '';
1027:     $i = 1;
1028:     foreach ($SelectFuncs as $key=>$val) {
1029:         $str_radio .= "<input type=\"radio\" name=\"func\" value=\"{$key}\" {$val['checked']} onChange=\"submit()\" />{$val['title']} ";
1030:         if ($i % 3 == 0)    $str_radio .= '<br />';
1031:         $i++;
1032:     }
1033: 
1034:     //応答メッセージ
1035:     if ($res != '') {
1036: $res =<<< EOT
1037: <p style="color:blue;">{$res}.</p>
1038: 
1039: EOT;
1040:     }
1041: 
1042:     //デバッグ情報
1043:     if (! FLAG_RELEASE) {
1044:         $phpver = phpversion();
1045: $debug =<<< EOT
1046: <p>
1047: <span style="font-weight:bold;">★デバックモードで動作中...</span><br />
1048: PHPver : {$phpver}
1049: 
1050: EOT;
1051:     }
1052: 
1053:     //データ読み込み
1054:     $data  = array();
1055:     $items = array();
1056:     if ($SelectFuncs[$func]['prefectures'] == FALSE)    $pref = PREF_ALL;
1057: 
1058:     //感染症ベッド数
1059:     $beds = array();
1060:     $beds_total = readBeds($beds);
1061:     $bed = ($pref == PREF_ALL) ? $beds_total : $beds[$pref]['val'];
1062:     $bed = number_format($bed);
1063: 
1064:     //入院患者数
1065:     $nps = array();
1066:     $nps_total = readNP($nps);
1067:     $np = ($pref == PREF_ALL) ? $nps_total : $nps[$pref]['val'];
1068:     $np = number_format($np);
1069: 
1070:     //グラフ描画
1071:     $selector = makeSelector($pref$SelectFuncs[$func]['prefectures']);
1072:     $items = $func($errmsg$pref);
1073:     if ($items != FALSE) {
1074:         //jqPlot
1075:         $js = plot($items$pref$SelectFuncs[$func]['title'], $SelectFuncs[$func]['log'], $SelectFuncs[$func]['int'], $SelectFuncs[$func]['ymax']);
1076: $graph =<<< EOT
1077: <script>
1078: {$js}
1079: </script>
1080: <div id="{$target}" name="{$target}" style="width:{$width2}px; height:{$height2}px;">
1081: <div id="jqPlot_polls" style="margin-top:20px; width:{$width}px; height:{$height}px;"></div>
1082: </div>
1083: <p id="slope"></p>
1084: <p>現在の入院患者数:{$np}人 感染症ベッド数:{$bed}床</p>
1085: 
1086: EOT;
1087:     } else {
1088: $res =<<< EOT
1089: <p style="color:red;">エラー:{$errmsg}.</p>
1090: 
1091: EOT;
1092:     }
1093: 
1094: $html =<<< EOT
1095: <body>
1096: <h2>{$title} {$version}</h2>
1097: <form name="myform" method="post" action="{$myself}">
1098: <p>
1099: 都道府県を選択 {$selector}
1100: {$tweet}
1101: </p>
1102: <p>{$str_radio}</p>
1103: </form>
1104: {$graph}
1105: {$res}
1106: 
1107: <div style="border-style:solid; border-width:1px; margin:20px 0px 0px 0px; padding:5px; width:{$width}px; font-size:small;">
1108: <h3>使い方</h3>
1109: <ol>
1110: <li>都道府県を選択してください.</li>
1111: <li>見たいグラフを選択してください.</li>
1112: <li>グラフを表示します.</li>
1113: {$help_tweet}
1114: </ol>
1115: 参 考:<a href="{$refere}">{$refere}</a><br />
1116: データ:<a href="{$refdata}">東洋経済オンライン「新型コロナウイルス 国内感染の状況」</a>【MITライセンス】<br />
1117:     <a href="{$refdata2}">新型コロナウイルス対策ダッシュボード</a>
1118: {$debug}
1119: </div>
1120: </body>
1121: 
1122: EOT;
1123:     return $html;
1124: }

前述のユーザー関数は、ラジオボタンに応じて変数 $func によって呼び出すようにしている。

解説:ツイート機能

PHPでCOVID-19情報をグラフ表示
表示したグラフを画像として、メッセージと一緒にボタン 1 つでツイートする機能を追加した。流れは次の通りである。
グラフを描画しているのはクライアントにあるブラウザ(レンダリングエンジン)であることから、クライアント側で画像を作成し、サーバ側で TwitterAPI をコールするという方針とした。
  1. ブラウザはサーバにグラフ描画をリクエストする。
  2. サーバはサイトからデータ取得する。(サーバキャッシュにデータがあればそれを利用する)
  3. サーバはグラフ描画スクリプトを生成する。
  4. サーバはブラウザへレスポンス(HTML 文)を返す。
  5. ブラウザはグラフをレンダリングする。
  6. ブラウザはレンダリングしたグラフを画像データとしてサーバへアップロードする。
  7. サーバは TwitterAPI を使ってツイートする。
  8. サーバは Twitter へメッセージと画像を送る。
  9. サーバはブラウザへレスポンス(HTML 文)を返す。
ブラウザでレンダリングしたグラフを画像に変換する処理は、JavaScript のhtml2canvas ライブラリを利用した。このライブラリの使い方については「JavaScript で HTML表示を画像保存する」をご覧いただきたい。TwitterAPI コールについては、「PHP で Twitter に投稿(ツイート)する」で作成した pahooTwitterAPI クラスを利用する。

0036: //ツイート・ボタン  TRUE:有効,FALSE:無効
0037: define('TWITTER', TRUE);
0038: 
0039: //画像化したいオブジェクト
0040: define('TARGET', 'target');

なお、ツイート機能を使うかどうかは、定数 TWITTER で指定する。
FALSE なら、pahooTwitterAPI クラスを読み込まず、ツイート・ボタンも表示しない。ツイート・ボタンの作成については、「HTML と CSS でさまざまなアイコンを表示する」を参照してほしい。
画像化したいオブジェクト(ID 名)は定数 TARGET で指定する。

解説:html2canvasライブラリ

0171: <script src="https://html2canvas.hertzen.com/dist/html2canvas.js"></script>
0172: 
0173: <script>
0174: //初期設定
0175: $(function() {
0176:     //HTML画像化イベント登録
0177:     $('#exec').on('click', function() {
0178:         html2image('#{$target}');
0179:     });
0180: });
0181: 
0182: /**
0183:  * グラフを画像化してアップロード
0184:  * @param   string html  画像化するオブジェクト
0185:  * @return  なし
0186: */
0187: function html2image(html) {
0188:     var capture = document.querySelector(html);
0189:     html2canvas(capture, {useCORS: true}).then(canvas => {
0190:         var base64 = canvas.toDataURL('image/png');       //画像化
0191:         $('#base64').val(base64);
0192:         document.myform.submit();                       //アップロード
0193:     });
0194: }
0195: </script>

HTML表示を画像化するために、html2canvas ライブラリを利用する。このライブラリは Niklas von Hertzen 氏によって開発されたもので、ライセンスは MIT となっている。
画像化を実行する JavaScript関数は html2canvas である。

1080: <div id="{$target}" name="{$target}" style="width:{$width2}px; height:{$height2}px;">
1081: <div id="jqPlot_polls" style="margin-top:20px; width:{$width}px; height:{$height}px;"></div>
1082: </div>

画像化するオブジェクトは、<div id="{$target}"> で指定する範囲である。
レンダリングエンジンによって違うのかもしれないが、html2canvas ライブラリによって画像化される範囲が実際よりやや小さいため、あえてグラフ領域より、幅、高さともに 20 ピクセル大きくした範囲を画像化範囲としている。

準備:pahooTwitterAPI クラス

0015: class pahooTwitterAPI {
0016:     var $webapi;     //直前に呼び出したWebAPI URL
0017:     var $error;      //エラーフラグ
0018:     var $errmsg;     //エラーメッセージ
0019:     var $errcode;        //エラーコード
0020:     var $responses;  //直前の結果(配列)
0021: 
0022:     //OAuth用パラメータ
0023:     // https://apps.twitter.com/
0024:     var $TWTR_CONSUMER_KEY    = '***************';  //Cunsumer key
0025:     var $TWTR_CONSUMER_SECRET = '***************';  //Consumer secret
0026:     var $TWTR_ACCESS_KEY      = '***************';  //Access Token (oauth_token)
0027:     var $TWTR_ACCESS_SECRET   = '***************';  //Access Token Secret (oauth_token_secret)

Twitter API」を利用するために、API keyAPI secret keyAccess tokenAccess token secret が必要で、入手方法は「Twitter API - WebAPI の登録方法」を参照されたい。

解説:メディア付き投稿(RAWデータ)

0385: /**
0386:  * メディア付き投稿(RAWデータ)
0387:  * @param   string $message 投稿メッセージ(UTF-8限定)
0388:  * @param   array  $raws    メディアデータ(RAWデータ配列)
0389:  * @return  bool TRUE:リクエスト成功/FALSE:失敗
0390: */
0391: function tweet_media_raw($message$raws) {
0392:     static $url_upload    = 'https://upload.twitter.com/1.1/media/upload.json';
0393:     static $url_tweet     = 'https://api.twitter.com/1.1/statuses/update.json';
0394:     static $method        = 'POST' ;
0395: 
0396:     //メディアのアップロード
0397:     $media_ids = '';
0398:     $cnt = 0;
0399:     foreach ($raws as $raw) {
0400:         $media_id = $this->upload($url_upload, 'POST', 'media', $raw);
0401:         if ($media_id == NULL)      break;
0402:         if ($cnt > 0)   $media_ids .= ',';
0403:         $media_ids .= $media_id;
0404:         $cnt++;
0405:         if ($cnt > 3)   break;       //最大4つまで
0406:     }
0407: 
0408:     //ツイート
0409:     if (! $this->error) {
0410:         $option = array('status' => $message, 'media_ids' => $media_ids);
0411:         $res = $this->request_user($url_tweet$method$option);
0412:     } else {
0413:         $res = FALSE;
0414:     }
0415: 
0416:     return $res;
0417: }

メッセージと画像を同時にツイートするのに、「PHP で Twitter に画像付きメッセージ投稿」で作ったメソッド tweet_media を改良し、引数に画像バイナリデータ(RAW データ)を渡して TwitterAPI を呼び出す tweet_media_raw メソッドを用意した。

解説:ツイート処理

0324: /**
0325:  * ツイート処理
0326:  * @param   string $message 投稿文
0327:  * @param   string $res     応答メッセージ格納用
0328:  * @return  bool TRUE:成功/FALSE:失敗または未処理
0329: */
0330: function mediaTweet($message, &$res) {
0331:     if (! TWITTER)   return FALSE;
0332: 
0333:     $ret = TRUE;
0334:     if (isset($_POST['base64']) && ($_POST['base64'] != '')) {
0335:         $base64 = preg_replace('/data\:image\/png\;base64\,/ui', '', $_POST['base64']);
0336:         $raws = array(base64_decode($base64));
0337:         $ptw = new pahooTwitterAPI();
0338:         $ptw->tweet_media_raw($message$raws);
0339:         $errmsg = $ptw->errmsg;
0340:         $ret = ! $ptw->error;
0341:         $ptw = NULL;
0342:         if ($ret) {
0343:             $res = 'ツイートしました';
0344:         }
0345:     }
0346:     return $ret;
0347: }

メッセージと画像をツイート処理するのはサーバ側の PHP ユーザー関数 mediaTweet で処理する。
ブラウザから POST で受け取った画像データ $_POST['base64'&$x5D; は BASE64 でデコードされており、冒頭に余計なヘッダ情報が付いているので、このヘッダ情報を除き( preg_replace )、バイナリデータにデコードする( base64_decode )。
続いて pahooTwitterAPI クラスを呼び出し、tweet_media_raw メソッドを使って、メッセージと画像を一気にツイートする。

1135: //ツイート機能
1136: $dt = date('Y年n月j日現在');
1137: $message =<<< EOT
1138: 😷新型コロナ・ウイルス感染情報 {$dt}
1139: -{$pref}の{$SelectFuncs[$func]['title']}
1140: 
1141: (ご参考)PHPでCOVID-19情報をグラフ表示 https://www.pahoo.org/e-soul/webtech/phpgd/phpgd-33-01.shtm
1142: 
1143: EOT;
1144: mediaTweet($message$res);

ユーザー関数 mediaTweet を呼び出しメインプログラム側のコードは上述の通りである。メッセージの内容は自由に変更していただいて構わない。

活用例

みんなの知識 ちょっと便利帳」では、「グラフで見る COVID-19」で本プログラムを利用し、見やすいページを提供している。ありがとうございます。

参考サイト

(この項おわり)
header