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

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

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

(2023年1月1日)厚労省のサービス終了を受け,PCR検査等実施人数,入院治療等を要する者等を終了
(2022年7月24日)検査の陽性率を追加
(2022年4月1日)ワクチン接種回数に対応

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

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

目次

サンプル・プログラム

圧縮ファイルの内容
viewCOVID-19.phpサンプル・プログラム本体
pahooStat.php統計に関わるクラス pahooStat。
使い方は「PHPで太陽黒点相対数の周期変化を描く」を参照。
pahooCache.phpキャッシュ処理に関わるクラス pahooCache。
キャッシュ処理に関わるクラスの使い方は「PHPで天気予報を求める」を参照。include_path が通ったディレクトリに配置すること。
pahooTwitterAPI.phpTwitter APIを利用するクラス pahooTwitterAPI。
使い方は「PHPでTwitterに投稿(ツイート)する」などを参照。include_path が通ったディレクトリに配置すること。
viewCOVID-19.php 更新履歴
バージョン 更新日 内容
4.2 2023/01/01 PCR検査等実施人数、入院治療等を要する者等が終了
4.1 2022/07/24 検査の陽性率を追加
4.0 2022/04/11 ワクチン接種回数に対応
3.4 2021/06/20 ツイート機能追加
3.3 2021/04/11 「実行」ボタン廃止

準備:初期値

  34: //プログラム・タイトル
  35: define('TITLE', 'COVID-19情報をグラフ表示');
  36: 
  37: //ツイート・ボタン  TRUE:有効,FALSE:無効
  38: define('TWITTER', FALSE);
  39: 
  40: //画像化したいオブジェクト
  41: define('TARGET', 'target');
  42: 
  43: //移動平均区間;日
  44: define('INTERVAL', 7);
  45: 
  46: //表示機関:日
  47: define('PERIOD', 365);
  48: 
  49: //傾きを求める区間;日
  50: define('SLOPE', 30);
  51: 
  52: //グラフの表示幅・高さ(単位:ピクセル)
  53: define('WIDTH',  600);
  54: define('HEIGHT', 400);
  55: 
  56: //グラフの色
  57: define('COLOR_COVID', '#88CCFF');
  58: define('COLOR_MEAN',  '#CC0000');
  59: 
  60: define('COLOR_VAC1',  '#330099');       //ワクチン接種1回目
  61: define('COLOR_VAC2',  '#3399FF');       //ワクチン接種2回目
  62: define('COLOR_VAC3',  '#99FFFF');       //ワクチン接種3回目
  63: 
  64: //jqPlotのあるフォルダ
  65: define('JQPLOT', '../../../../common/jqplot/');
  66: 
  67: //キャッシュ保持時間(分) 0:キャッシュしない
  68: //アクセス負荷軽減のため,60分以上のキャッシュ保持をお勧めします.
  69: define('LIFE_CACHE', 360);
  70: 
  71: //キャッシュ・ディレクトリ
  72: //書き込み可能で,外部からアクセスされないディレクトリを指定してください.
  73: define('DIR_CACHE', './covid19_pcache/');
  74: 
  75: //統計に関わるクラス:include_pathが通ったディレクトリに配置
  76: require_once('pahooStat.php');
  77: 
  78: //キャッシュ処理に関わるクラス:include_pathが通ったディレクトリに配置
  79: require_once('pahooCache.php');
  80: 
  81: if (TWITTER) {
  82:     //Twitterクラス:include_pathが通ったディレクトリに配置
  83:     require_once('pahooTwitterAPI.php');
  84: }

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

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

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

準備:データ・ファイル

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

  86: //データファイル名(変更不可)
  87: define('DATA_FILE1', 'https://raw.githubusercontent.com/kaz-ogiwara/covid19/master/data/data.json');                        //グラフ描画情報【廃止】
  88: define('DATA_FILE_BEDS1', 'https://raw.githubusercontent.com/code4sabae/covid19/master/data/bedforinfection_current.json'); //感染症ベッド数
  89: define('DATA_FILE_BEDS2', 'https://raw.githubusercontent.com/code4sabae/covid19/master/data/bedforinfection_summary.json'); //感染症ベッド数
  90: define('DATA_FILE_NP1', 'https://raw.githubusercontent.com/code4sabae/covid19/master/data/covid19japan-fast.json');         //現在の入院患者数
  91: define('DATA_FILE_NP2', 'https://raw.githubusercontent.com/code4sabae/covid19/master/data/covid19japan-all.json');          //現在の入院患者数
  92: 
  93: //v.3.0にて追加(変更不可)
  94: //厚生労働省オープンデータ「陽性者数」
  95: define('FILE_PCR_POSITIVE_DAILY', 'https://toyokeizai.net/sp/visual/tko/covid19/csv/pcr_positive_daily.csv');
  96: //厚生労働省オープンデータ「PCR検査実施人数」
  97: define('FILE_PCR_TESTED_DAILY', 'https://toyokeizai.net/sp/visual/tko/covid19/csv/pcr_tested_daily.csv');
  98: //厚生労働省オープンデータ「入院治療等を要する者の数」
  99: define('FILE_CASES_TOTAL', 'https://toyokeizai.net/sp/visual/tko/covid19/csv/cases_total.csv');
 100: //厚生労働省オープンデータ「退院又は療養解除となった者の数」
 101: define('FILE_RECOVERY_TOTAL', 'https://toyokeizai.net/sp/visual/tko/covid19/csv/recovery_total.csv');
 102: //厚生労働省オープンデータ「死亡者数」
 103: define('FILE_DEATH_TOTAL', 'https://toyokeizai.net/sp/visual/tko/covid19/csv/death_total.csv');
 104: //厚生労働省オープンデータ「PCR検査の実施件数」
 105: define('FILE_PCR_CASE_DAILY', 'https://toyokeizai.net/sp/visual/tko/covid19/csv/pcr_case_daily.csv');
 106: //公表日ごとの全国の重症者数
 107: define('FILE_SEVERE_DAILY', 'https://toyokeizai.net/sp/visual/tko/covid19/csv/severe_daily.csv');
 108: //日別全国の実効再生産数
 109: define('FILE_EFFECTIVE_REPRODUCTION_NUMBER', 'https://toyokeizai.net/sp/visual/tko/covid19/csv/effective_reproduction_number.csv');
 110: //年代別の国内発生動向
 111: define('FILE_DEMOGRAPHY', 'https://toyokeizai.net/sp/visual/tko/covid19/csv/demography.csv');
 112: //都道府県別の発生動向
 113: define('FILE_PREFECTURE', 'https://toyokeizai.net/sp/visual/tko/covid19/csv/prefectures.csv');

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

 483: /**
 484:  * グラフにプロットするためのデータを配列に格納する
 485:  * @param   string $fname   CSVファイル名
 486:  * @param   array  $cols    取得結果のvalに合算するカラム番号
 487:  * @param   string $errmsg  エラーメッセージ格納用
 488:  * @param   int    $method  データの積算方法(省略時:0)
 489:  *                          0:そのまま
 490:  *                          1:前日の値を加算
 491:  *                          2:前日の値を減算
 492:  *                          3:値から前日の値を減算
 493:  *                          4:前日までの積算値を加算
 494:  *                          5:前日までの積算値を減算
 495:  *                          6:値から前日までの積算値を減算
 496:  * @param   string $pref    都道府県名(省略時:全国)
 497:  * @return  array  取得結果/FALSE:取得失敗
 498:  *                  [連番][要素名]
 499:  *                  要素名と値
 500:  *                      year    年
 501:  *                      month   月
 502:  *                      day     日
 503:  *                      val     プロット値
 504: */
 505: function readData($fname, $cols, &$errmsg, $method=0, $pref='全国') {
 506:     $data = array();
 507:     $cnt = 0;
 508:     $errmsg = '';
 509: 
 510:     //ファイル・オープン
 511:     $infp = @fopen($fname, 'r');
 512:     if ($infp == FALSE) {
 513:         $errmsg = $fname . ' にアクセスできません.';
 514:         return FALSE;
 515:     }
 516: 
 517:     //データ読み込み
 518:     $ss = fgets($infp, 1000);       //1行目スキップ
 519:     while (! feof($infp)) {
 520:         $ss   = fgets($infp, 1000);
 521:         $arr  = preg_split('/\,/ui', $ss);
 522:         $flag = FALSE;
 523:         if (isset($arr[0])) {
 524:             if (preg_match('/([0-9]+)\/([0-9]+)\/([0-9]+)/ui', $arr[0], $arr2> 0) {
 525:                 $data[$cnt]['year']  = (int)$arr2[1];
 526:                 $data[$cnt]['month'] = (int)$arr2[2];
 527:                 $data[$cnt]['day']   = (int)$arr2[3];
 528:                 $flag = TRUE;
 529:             } else if (isset($arr[3]) && ($pref == $arr[3])) {
 530:                 $data[$cnt]['year']  = (int)$arr[0];
 531:                 $data[$cnt]['month'] = (int)$arr[1];
 532:                 $data[$cnt]['day']   = (int)$arr[2];
 533:                 $flag = TRUE;
 534:             }
 535:         }
 536:         if ($flag) {
 537:             //データの積算
 538:             $val = 0.0;
 539:             foreach ($cols as $key) {
 540:                 $val += (float)$arr[$key];
 541:             }
 542:             if ($cnt == 0) {
 543:                 $data[$cnt]['val'] = $val;
 544:                 $old1 = $data[$cnt]['val'];
 545:                 $old2 = $data[$cnt]['val'];
 546:             } else {
 547:                 switch ($method) {
 548:                     case 0:
 549:                         $data[$cnt]['val'] = $val;
 550:                         break;
 551:                     case 1:
 552:                         $data[$cnt]['val'] = $val + $old1;
 553:                         break;
 554:                     case 2:
 555:                         $data[$cnt]['val'] = $val - $old1;
 556:                         break;
 557:                     case 3:
 558:                         $data[$cnt]['val'] = $old1 - $val;
 559:                         break;
 560:                     case 4:
 561:                         $data[$cnt]['val'] = $val + $old2;
 562:                         break;
 563:                     case 5:
 564:                         $data[$cnt]['val'] = $val - $old2;
 565:                         break;
 566:                     case 6:
 567:                         $data[$cnt]['val'] = $old2 - $val;
 568:                         break;
 569:                 }
 570:                 $old1 = $val;
 571:                 $old2 = $data[$cnt]['val'];
 572:             }
 573:             $cnt++;
 574:         }
 575:     }
 576:     fclose($infp);
 577: 
 578:     return $data;
 579: }

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 に格納していく。

 359: /**
 360:  * データ・ファイルを読み込み、現在の都道府県別ベッド数を取得
 361:  * @param   array  $items 都道府県別ベッド数格納用
 362:  * @return  int 全国ベッド数/FALSE
 363: */
 364: function readBeds(&$items) {
 365:     global $TablePref;
 366: 
 367:     //オブジェクト生成
 368:     $pcc = new pahooCache(LIFE_CACHE, DIR_CACHE);
 369: 
 370:     //データファイル(その1)
 371: //  $contents = file_get_contents(DATA_FILE_BEDS1);
 372:     $contents = $pcc->load(DATA_FILE_BEDS1);
 373:     if ($contents == FALSE)     return FALSE;
 374:     $data = json_decode($contents, TRUE);
 375:     $cnt = 0;
 376:     foreach ($data as $key=>$arr) {
 377:         if (isset($arr['自治体名']) && isset($arr['新型コロナウイルス対策感染症病床数']) && isset($arr['発表日'])) {
 378:             $pref = $arr['自治体名'];
 379:             $yyyymmdd = $arr['発表日'];
 380:             //新規登録
 381:             if (! isset($items[$pref])) {
 382:                 $items[$pref]['date'] = $yyyymmdd;
 383:                 $items[$pref]['val']  = (int)$arr['新型コロナウイルス対策感染症病床数'];
 384:             } else if ($yyyymmdd > $items[$pref]['date']) {
 385:                 $items[$pref]['date'] = $yyyymmdd;
 386:                 $items[$pref]['val']  = (int)$arr['新型コロナウイルス対策感染症病床数'];
 387:             }
 388:         }
 389:     }
 390: 
 391:     //データファイル(その2)
 392:     $contents = file_get_contents(DATA_FILE_BEDS2);
 393:     if ($contents == FALSE)     return FALSE;
 394:     $data = json_decode($contents, TRUE);
 395:     $cnt = 0;
 396:     foreach ($TablePref as $code=>$arr1) {
 397:         if (! isset($items[$arr1])) {
 398:             foreach ($data['area'as $key=>$arr2) {
 399:                 if ($arr2['name_ja'] == $arr1) {
 400:                     $items[$arr1]['val'] = $arr2['sum'];
 401:                     break;
 402:                 }
 403:             }
 404:         }
 405:     }
 406: 
 407:     //合計
 408:     $cnt = 0;
 409:     foreach ($items as $arr)    $cnt +$arr['val'];
 410: 
 411:     //オブジェクト解放
 412:     $pcc = NULL;
 413: 
 414:     return $cnt;
 415: }

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

 417: /**
 418:  * データ・ファイルを読み込み、現在の都道府県別入院患者数を取得
 419:  * @param   array  $items 都道府県別入院患者数格納用
 420:  * @return  int 全国入院患者数/FALSE
 421: */
 422: function readNP(&$items) {
 423:     global $TablePref;
 424: 
 425:     //オブジェクト生成
 426:     $pcc = new pahooCache(LIFE_CACHE, DIR_CACHE);
 427: 
 428:     //データファイル(その1)
 429: //  $contents = file_get_contents(DATA_FILE_NP1);
 430:     $contents = $pcc->load(DATA_FILE_NP1);
 431:     if ($contents == FALSE)     return FALSE;
 432:     $data = json_decode($contents, TRUE);
 433:     $cnt = 0;
 434:     foreach ($data as $key=>$arr) {
 435:         if (isset($arr['name']) && isset($arr['ncurrentpatients']) && isset($arr['lastUpdate'])) {
 436:             $pref = $arr['name'];
 437:             $yyyymmdd = $arr['lastUpdate'];
 438:             //新規登録
 439:             if (! isset($items[$pref])) {
 440:                 $items[$pref]['date'] = $yyyymmdd;
 441:                 $items[$pref]['val']  = (int)$arr['ncurrentpatients'];
 442:             } else if ($yyyymmdd > $items[$pref]['date']) {
 443:                 $items[$pref]['date'] = $yyyymmdd;
 444:                 $items[$pref]['val']  = (int)$arr['ncurrentpatients'];
 445:             }
 446:         }
 447:     }
 448: 
 449:     //データファイル(その2)
 450: //  $contents = file_get_contents(DATA_FILE_NP2);
 451:     $contents = $pcc->load(DATA_FILE_NP2);
 452:     if ($contents == FALSE)     return FALSE;
 453:     $data = json_decode($contents, TRUE);
 454:     $cnt = 0;
 455:     foreach ($TablePref as $code=>$arr1) {
 456:         if (! isset($items[$arr1])) {
 457:             foreach ($data as $key=>$arr2) {
 458:                 if (preg_match('/現在は入院/ui', $arr2['description']) > 0) {
 459:                     foreach ($arr2['area'as $key=>$arr3) {
 460:                         if ($arr3['name_jp'] == $arr1) {
 461:                             if (! isset($items[$arr1]['val'])) {
 462:                                 $items[$arr1]['val'] = $arr3['ncurrentpatients'];
 463:                             }
 464:                             break;
 465:                         }
 466:                     }
 467:                     break;
 468:                 }
 469:             }
 470:         }
 471:     }
 472: 
 473:     //合計
 474:     $cnt = 0;
 475:     foreach ($items as $arr)    $cnt +$arr['val'];
 476: 
 477:     //オブジェクト解放
 478:     $pcc = NULL;
 479: 
 480:     return $cnt;
 481: }

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

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

 581: /**
 582:  * 新規検査陽性者数を取得
 583:  * @param   string $errmsg  エラーメッセージ格納用
 584:  * @param   string $pref    都道府県名(省略時:全国)
 585:  * @return  array データ配列
 586: */
 587: function pcr_tested_positive_daily(&$errmsg, $pref=PREF_ALL) {
 588:     if ($pref == PREF_ALL) {
 589:         $fname = FILE_PCR_POSITIVE_DAILY;
 590:         $cols = array(1);
 591:         $method = 0;
 592:     } else {
 593:         $fname = FILE_PREFECTURE;
 594:         $cols = array(5);
 595:         $method = 2;
 596:     }
 597: 
 598:     return readData($fname, $cols, $errmsg, $method, $pref);
 599: }

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

 601: /**
 602:  * 累計検査陽性者数を取得
 603:  * @param   string $errmsg  エラーメッセージ格納用
 604:  * @param   string $pref    都道府県名(省略時:全国)
 605:  * @return  array データ配列
 606: */
 607: function pcr_tested_positive_total(&$errmsg, $pref=PREF_ALL) {
 608:     if ($pref == PREF_ALL) {
 609:         $fname = FILE_PCR_POSITIVE_DAILY;
 610:         $cols = array(1);
 611:         $method = 4;
 612:     } else {
 613:         $fname = FILE_PREFECTURE;
 614:         $cols = array(5);
 615:         $method = 1;
 616:     }
 617: 
 618:     return readData($fname, $cols, $errmsg, $method, $pref);
 619: }

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

解説:jqPlot用のスクリプト

 860: /**
 861:  * jqPlot用のスクリプト
 862:  * @param   array  $items データ配列
 863:  * @param   string $pref  都道府県名
 864:  * @param   string $title グラフのタイトル
 865:  * @param   bool   $log   TRUE:縦軸は対数/FALSE:通常(省略時)
 866:  * @param   bool   $int   TRUE:縦軸は整数(省略時)/FALSE:小数
 867:  * @param   float  $ymax  Y軸の最大値(省略時:空文字=データの最大値)
 868:  * @return  string スクリプト
 869: */
 870: function plot($items, $pref='', $title='', $log=FALSE, $int=TRUE, $ymax='') {
 871:     //Y軸の最大値
 872:     if ($ymax !'') {
 873:         $ymax = sprintf('max: %f,', $ymax);
 874:     }
 875: 
 876:     //グラフの色
 877:     $color_covid = COLOR_COVID;
 878:     $color_mean  = COLOR_MEAN;
 879:     //移動平均を求める区間(日)
 880:     $interval = INTERVAL;
 881: 
 882:     //移動平均
 883:     $x = array();
 884:     $y = array();
 885:     foreach ($items as $key=>$val)  $x[$key] = $val['val'];
 886:     //統計オブジェクト
 887:     $pst = new pahooStat();
 888:     $pst->simple_moving_average(INTERVAL, $x, $y);      //移動平均
 889:     foreach ($y as $key=>$val)  $items[$key]['mean'] = $val;
 890: 
 891:     //直近の傾き(最小二乗法)
 892:     $x1 = array();
 893:     $y1 = array();
 894:     $n = count($items);
 895:     for ($i = 0$i < SLOPE$i++) {
 896:         $x1[$i] = $n - SLOPE + $i;
 897:         $y1[$i] = $items[$n - SLOPE + $i]['val'];
 898:     }
 899:     list($a0, $a1) = $pst->LSM($x1, $y1, SLOPE);        //最小二乗法
 900:     $pst = NULL;
 901:     $slope = sprintf('直近%d日間の傾き:%.2f', SLOPE, $a1);
 902: 
 903:     $series = '';
 904:     $xmin = date('Y-m-d', (time() - PERIOD * 24 * 60 * 60));
 905:     $xmax = '';
 906:     $rendere_yaxis = $log ? 'renderer: $.jqplot.LogAxisRenderer' : 'min: 0';
 907:     $format_yaxis  = $int ? "%'d" : "%.1f";
 908: 
 909:     //系列の生成
 910:     $cnt = 0;
 911:     $s1 = $s2 = '';
 912:     foreach ($items as $key=>$val) {
 913:         $xmax = sprintf("%04d-%02d-%02d", $val['year'], $val['month'], $val['day']);
 914:         if ($xmax < $xmin)   continue;
 915:         if ($key == 0)  $xmin = $xmax;
 916:         $s1 .isset($val['val']) ? sprintf("['%s', %.3f],", $xmax, $val['val']) : '';
 917:         $s2 .isset($val['mean']) ? sprintf("['%s', %.3f],", $xmax, $val['mean']) : '';
 918:         $cnt++;
 919:     }
 920:     $barwidth = (int)(WIDTH / $cnt * 0.7);
 921: 
 922:     $js =<<< EOT
 923: jQuery(function() {
 924:     jQuery.jqplot('jqPlot_polls',
 925:     [
 926:         [ {$s1} ],
 927:         [ {$s2} ]
 928:     ],
 929:     {
 930:         //タイトル
 931:         title: {
 932:             text: '{$pref}の{$title}',
 933:             show: true,
 934:             fontFamily: 'serif',
 935:             fontSize: '20px',
 936:             textAlign: 'center',
 937:             textColor: 'black',
 938:         },
 939:         //系列
 940:         series: [
 941:         {
 942:             label: '{$title}',
 943:             color: '{$color_covid}',
 944:             renderer: jQuery . jqplot . BarRenderer,
 945:             rendererOptions: {
 946:                 barWidth: {$barwidth},
 947:                 shadowOffset: 0
 948:             }
 949:         },
 950:         {
 951:             label: '{$interval}日移動平均',
 952:             color: '{$color_mean}'
 953:         },
 954:         ],
 955:         legend: {
 956:             show: true,
 957:             placement: 'inside',
 958:             location: 'nw',
 959:             renderer: $.jqplot.EnhancedLegendRenderer,
 960:             rendererOptions: { numberRows: 1 }
 961:         },
 962:         seriesDefaults: {
 963:             showLine: true,
 964:             rendererOptions: { smooth: false },
 965:             markerOptions: { size: 0 },
 966:         },
 967:         //軸
 968:         axes: {
 969:             xaxis: {
 970:                 renderer:$.jqplot.DateAxisRenderer,
 971:                 tickOptions: { formatString: '%m/%d' },
 972:                 label: '日付',
 973:                 min: '{$xmin}',
 974:                 max: '{$xmax}',
 975:             },
 976:             yaxis: {
 977:                 {$ymax}
 978:                 {$rendere_yaxis},
 979:                 labelRenderer: $.jqplot.CanvasAxisLabelRenderer,
 980:                 label: '{$title}',
 981:                 tickOptions: {
 982:                     formatString: "{$format_yaxis}",
 983:                     angle: -30,
 984:                 }
 985:             }
 986:         },
 987:         //ハイライター
 988:         highlighter: {
 989:             show: true,
 990:             showMarker: true,
 991:             tooltipLocation: 'sw',
 992:             fadeTooltip: false,
 993:             bringSeriesToFront: true,
 994:             tooltipAxes: 'xy',
 995:             formatString: '%s<br />%s'
 996:         }
 997:     }
 998:     );
 999:     $('#slope').html('{$slope}');
1000: });
1001: 
1002: EOT;
1003: 
1004:     return $js;
1005: }

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

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

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

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

 118: //処理選択肢
 119: $SelectFuncs = array(
 120:     //関数名,タイトル,ラジオボタンcheked,Y軸を対数軸とするか,
 121:     //目盛りを整数にするかどうか,Y軸の最大値,都道府県を選択可能とするか
 122:     'pcr_tested_positive_daily' => array('title'=>'新規検査陽性者()', 'checked'=>'checked', 'log'=>FALSE, 'int'=>TRUE, 'ymax'=>'', 'prefectures'=>TRUE),
 123:     'pcr_tested_positive_total' => array('title'=>'累計検査陽性者()', 'checked'=>'', 'log'=>FALSE, 'int'=>TRUE, 'ymax'=>'', 'prefectures'=>TRUE),
 124: //  'pcr_tested_daily' => array('title'=>'新規検査人数(人)', 'checked'=>'', 'log'=>FALSE, 'int'=>TRUE, 'ymax'=>'', 'prefectures'=>TRUE),
 125: //  'pcr_tested_total' => array('title'=>'累計検査人数(人)', 'checked'=>'', 'log'=>FALSE, 'int'=>TRUE, 'ymax'=>'', 'prefectures'=>TRUE),
 126: //  'pcr_positive_rate' => array('title'=>'検査陽性率(%)', 'checked'=>'', 'log'=>FALSE, 'int'=>FALSE, 'ymax'=>'', 'prefectures'=>FALSE),
 127: //  'pcr_case_daily' => array('title'=>'新規検査件数(件)', 'checked'=>'', 'log'=>FALSE, 'int'=>TRUE, 'ymax'=>'', 'prefectures'=>FALSE),
 128: //  'pcr_case_total' => array('title'=>'累計検査件数(件)', 'checked'=>'', 'log'=>FALSE, 'int'=>TRUE, 'ymax'=>'', 'prefectures'=>FALSE),
 129: //  'case_total'     => array('title'=>'入院治療等を要する者(人)', 'checked'=>'', 'log'=>FALSE, 'int'=>TRUE, 'ymax'=>'', 'prefectures'=>FALSE),
 130:     'recovery_daily' => array('title'=>'新規退院・療養解除(人)', 'checked'=>'', 'log'=>FALSE, 'int'=>TRUE, 'ymax'=>'', 'prefectures'=>TRUE),
 131:     'recovery_total' => array('title'=>'累計退院・療養解除(人)', 'checked'=>'', 'log'=>FALSE, 'int'=>TRUE, 'ymax'=>'', 'prefectures'=>TRUE),
 132:     'severe_daily' => array('title'=>'重症者(人)', 'checked'=>'', 'log'=>FALSE, 'int'=>TRUE, 'ymax'=>'', 'prefectures'=>TRUE),
 133:     'death_daily' => array('title'=>'新規死亡者(人)', 'checked'=>'', 'log'=>FALSE, 'int'=>TRUE, 'ymax'=>'', 'prefectures'=>TRUE),
 134:     'death_total' => array('title'=>'累計死亡者(人)', 'checked'=>'', 'log'=>FALSE, 'int'=>TRUE, 'ymax'=>'', 'prefectures'=>TRUE),
 135:     'reproduction' => array('title'=>'実効再生産数', 'checked'=>'', 'log'=>FALSE, 'int'=>FALSE, 'ymax'=>'', 'prefectures'=>TRUE),
 136:     'vaccination' => array('title'=>'ワクチン接種回数', 'checked'=>'', 'log'=>FALSE, 'int'=>TRUE, 'ymax'=>'', 'prefectures'=>FALSE),
 137: );
 138: 
 139: //全国(変更不可)
 140: define('PREF_ALL', '全国');
 141: 
 142: //都道府県テーブル(変更不可)
 143: $TablePrefarray(PREF_ALL, '北海道', '青森県', '岩手県', '宮城県', '秋田県', '山形県', '福島県', '茨城県', '栃木県', '群馬県', '埼玉県', '千葉県', '東京都', '神奈川県', '新潟県', '富山県', '石川県', '福井県', '山梨県', '長野県', '岐阜県', '静岡県', '愛知県', '三重県', '滋賀県', '京都府', '大阪府', '兵庫県', '奈良県', '和歌山県', '鳥取県', '島根県', '岡山県', '広島県', '山口県', '徳島県', '香川県', '愛媛県', '高知県', '福岡県', '佐賀県', '長崎県', '熊本県', '大分県', '宮崎県', '鹿児島県', '沖縄県');

1209: /**
1210:  * HTML BODYを作成する
1211:  * @param   string $func   jqPlot関数
1212:  * @param   string $pref   都道府県名または'全国'
1213:  * @param   string $res    応答メッセージ
1214:  * @return  string HTML BODY
1215: */
1216: function makeCommonBody($func, $pref, $res) {
1217:     global $SelectFuncs, $TablePref;
1218: 
1219:     $myself   = MYSELF;
1220:     $refere   = REFERENCE;
1221:     $refdata  = REF_DATA;
1222:     $refdata2 = REF_DATA2;
1223:     $refdata3 = REF_DATA3;
1224:     $title    = TITLE;
1225:     $version  = '<span style="font-size:small;">' . date('Y/m/d版', filemtime(__FILE__)) . '</span>';
1226:     $width    = WIDTH;
1227:     $width2   = WIDTH + 20;
1228:     $height   = HEIGHT;
1229:     $height2  = HEIGHT + 20;
1230:     $target   = TARGET;
1231:     $tweet = $help_tweet = $graph = $errmsg = $debug = '';
1232: 
1233:     //Tweetボタン,ヘルプ
1234:     if (TWITTER) {
1235:         $tweet =<<< EOT
1236:  <button id="exec" name="exec" class="tweet_button"><i class="fab fa-twitter" style="padding-right:10px;"></i>ツイート</button>
1237: <input type="hidden" id="base64" name="base64" value="" />
1238: 
1239: EOT;
1240:         $help_tweet =<<< EOT
1241: <li>[<span style="font-weight:bold;">ツイート</span>]ボタンを押下すると,メッセージとグラフ画像をツイートします.</li>
1242: 
1243: EOT;
1244:     }
1245: 
1246:     //処理選択ラジオボタン
1247:     $str_radio = '';
1248:     $i = 1;
1249:     foreach ($SelectFuncs as $key=>$val) {
1250:         $str_radio ."<input type=\"radio\" name=\"func\" value=\"{$key}\" {$val['checked']} onChange=\"submit()\" />{$val['title']} ";
1251:         if ($i % 3 == 0)    $str_radio .'<br />';
1252:         $i++;
1253:     }
1254: 
1255:     //応答メッセージ
1256:     if ($res !'') {
1257:         $res =<<< EOT
1258: <p style="color:blue;">{$res}.</p>
1259: 
1260: EOT;
1261:     }
1262: 
1263:     //デバッグ情報
1264:     if (! FLAG_RELEASE) {
1265:         $phpver = phpversion();
1266:         $debug =<<< EOT
1267: <p>
1268: <span style="font-weight:bold;">★デバックモードで動作中...</span><br />
1269: PHPver : {$phpver}
1270: 
1271: EOT;
1272:     }
1273: 
1274:     //データ読み込み
1275:     $data  = array();
1276:     $items = array();
1277: 
1278:     //ワクチン接種回数
1279:     if ($func == 'vaccination') {
1280:         $n = readVaccination($data, $errmsg, 4);
1281:         //var_dump($n);
1282:         //var_dump($data);
1283:         $selector = makeSelector($pref, $SelectFuncs[$func]['prefectures']);
1284:         if ($n !FALSE) {
1285:             //jqPlot
1286:             $js = plotVaccination($data, '全国', $title='ワクチン接種回数');
1287:             $graph =<<< EOT
1288: <script>
1289: {$js}
1290: </script>
1291: <div id="{$target}" name="{$target}" style="width:{$width2}px; height:{$height2}px;">
1292: <div id="jqPlot_polls" style="margin-top:20px; width:{$width}px; height:{$height}px;"></div>
1293: </div>
1294: <p id="comment"></p>
1295: 
1296: EOT;
1297:         } else {
1298:             $res =<<< EOT
1299: <p style="color:red;">エラー:{$errmsg}.</p>
1300: 
1301: EOT;
1302:         }
1303: 
1304:     //それ以外
1305:     } else {
1306:         if ($SelectFuncs[$func]['prefectures'] == FALSE)    $pref = PREF_ALL;
1307: 
1308:         //感染症ベッド数
1309:         $beds = array();
1310:         $beds_total = readBeds($beds);
1311:         $bed = ($pref == PREF_ALL? $beds_total : $beds[$pref]['val'];
1312:         $bed = number_format($bed);
1313: 
1314:         //入院患者数
1315:         $nps = array();
1316:         $nps_total = readNP($nps);
1317:         $np = ($pref == PREF_ALL? $nps_total : $nps[$pref]['val'];
1318:         $np = number_format($np);
1319: 
1320:         //グラフ描画
1321:         $selector = makeSelector($pref, $SelectFuncs[$func]['prefectures']);
1322:         $items = $func($errmsg, $pref);
1323:         if ($items !FALSE) {
1324:             //jqPlot
1325:             $js = plot($items, $pref, $SelectFuncs[$func]['title'], $SelectFuncs[$func]['log'], $SelectFuncs[$func]['int'], $SelectFuncs[$func]['ymax']);
1326:             $graph =<<< EOT
1327: <script>
1328: {$js}
1329: </script>
1330: <div id="{$target}" name="{$target}" style="width:{$width2}px; height:{$height2}px;">
1331: <div id="jqPlot_polls" style="margin-top:20px; width:{$width}px; height:{$height}px;"></div>
1332: </div>
1333: <p id="slope"></p>
1334: <p>現在の入院患者数:{$np}人 感染症ベッド数:{$bed}床</p>
1335: 
1336: EOT;
1337:         } else {
1338:             $res =<<< EOT
1339: <p style="color:red;">エラー:{$errmsg}.</p>
1340: 
1341: EOT;
1342:         }
1343:     }
1344: 
1345:     $html =<<< EOT
1346: <body>
1347: <h2>{$title} {$version}</h2>
1348: <form name="myform" method="post" action="{$myself}">
1349: <p>
1350: 都道府県を選択 {$selector}
1351: {$tweet}
1352: </p>
1353: <p>{$str_radio}</p>
1354: </form>
1355: {$graph}
1356: {$res}
1357: 
1358: <div style="border-style:solid; border-width:1px; margin:20px 0px 0px 0px; padding:5px; width:{$width}px; font-size:small;">
1359: <h3>使い方</h3>
1360: <ol>
1361: <li>都道府県を選択してください.</li>
1362: <li>見たいグラフを選択してください.</li>
1363: <li>グラフを表示します.</li>
1364: {$help_tweet}
1365: </ol>
1366: 参 考:<a href="{$refere}">{$refere}</a><br />
1367: データ:<a href="{$refdata}">東洋経済オンライン「新型コロナウイルス 国内感染の状況」</a>【MITライセンス】<br />
1368:     <a href="{$refdata2}">新型コロナウイルス対策ダッシュボード</a><br />
1369:     <a href="{$refdata3}">ワクチン接種記録システム(VRS)</a>
1370: {$debug}
1371: </div>
1372: </body>
1373: 
1374: EOT;
1375:     return $html;
1376: }

前述のユーザー関数は、ラジオボタンに応じて変数 $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 クラスを利用する。

  37: //ツイート・ボタン  TRUE:有効,FALSE:無効
  38: define('TWITTER', FALSE);
  39: 
  40: //画像化したいオブジェクト
  41: define('TARGET', 'target');

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

解説:html2canvasライブラリ

 181: <script src="https://html2canvas.hertzen.com/dist/html2canvas.js"></script>

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

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

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

準備:pahooTwitterAPI クラス

  15: class pahooTwitterAPI {
  16:     var $webapi;        //直前に呼び出したWebAPI URL
  17:     var $error;     //エラーフラグ
  18:     var $errmsg;        //エラーメッセージ
  19:     var $errcode;       //エラーコード
  20:     var $responses//直前の結果(配列)
  21: 
  22:     //OAuth用パラメータ
  23:     // https://apps.twitter.com/
  24:     var $TWTR_CONSUMER_KEY    = '***************';  //Cunsumer key
  25:     var $TWTR_CONSUMER_SECRET = '***************';  //Consumer secret
  26:     var $TWTR_ACCESS_KEY      = '***************';  //Access Token (oauth_token)
  27:     var $TWTR_ACCESS_SECRET   = '***************';  //Access Token Secret (oauth_token_secret)

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

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

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

解説:ツイート処理

 334: /**
 335:  * ツイート処理
 336:  * @param   string $message 投稿文
 337:  * @param   string $res     応答メッセージ格納用
 338:  * @return  bool TRUE:成功/FALSE:失敗または未処理
 339: */
 340: function mediaTweet($message, &$res) {
 341:     if (! TWITTER)  return FALSE;
 342: 
 343:     $ret = TRUE;
 344:     if (isset($_POST['base64']) && ($_POST['base64'!'')) {
 345:         $base64 = preg_replace('/data\:image\/png\;base64\,/ui', '', $_POST['base64']);
 346:         $raws = array(base64_decode($base64));
 347:         $ptw = new pahooTwitterAPI();
 348:         $ptw->tweet_media_raw($message, $raws);
 349:         $errmsg = $ptw->errmsg;
 350:         $ret = ! $ptw->error;
 351:         $ptw = NULL;
 352:         if ($ret) {
 353:             $res = 'ツイートしました';
 354:         }
 355:     }
 356:     return $ret;
 357: }

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

1387: //ツイート機能
1388: $dt = date('Y年n月j日現在');
1389: $message =<<< EOT
1390: 😷新型コロナ・ウイルス感染情報 {$dt}
1391: -{$pref}の{$SelectFuncs[$func]['title']}
1392: 
1393: (ご参考)PHPでCOVID-19情報をグラフ表示 https://www.pahoo.org/e-soul/webtech/phpgd/phpgd-33-01.shtm
1394: 
1395: EOT;
1396: mediaTweet($message, $res);

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

活用例

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

参考サイト

(この項おわり)
header