サンプル・プログラム:実行例
目次
サンプル・プログラム
viewCOVID-19.php | サンプル・プログラム本体 |
pahooStat.php | 統計に関わるクラス pahooStat。 使い方は「PHPで太陽黒点相対数の周期変化を描く」を参照。 |
pahooCache.php | キャッシュ処理に関わるクラス pahooCache。 キャッシュ処理に関わるクラスの使い方は「PHPで天気予報を求める」を参照。include_path が通ったディレクトリに配置すること。 |
pahooTwitterAPI.php | Twitter APIを利用するクラス pahooTwitterAPI。 使い方は「PHPでTwitterに投稿(ツイート)する」などを参照。include_path が通ったディレクトリに配置すること。 |
バージョン | 更新日 | 内容 |
---|---|---|
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で天気予報を求める - キャッシュ・システム」を参照いただきたい。
キャッシュ保持時間、キャッシュ・ディレクトリともに、自サイトの環境に応じて変更してほしい。
準備:データ・ファイル
  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: }
上述のように複数のCSVファイルを読み込み対象としているが、データ構造は似ている――1列目、または1~3列目に年月日、2列目以降、または4列目以降に毎日のデータ――ので、この関数に適切なパラメータを与えることで処理を行う。
まず、 fopen でCVSファイルをオープンし、失敗したらエラーを返す。
冒頭行はラベルなので読み飛ばす。
2行目からがデータ行である。年月日が1列目だけか、1~3列名にまたがっているかは正規表現パターンで判定する。
日々のデータ積算処理だが、データ構造及び描画したいグラフの種類によって、次の7つのケースが考えられる。
- そのまま
- 前日の値を加算
- 前日の値を減算
- 値から前日の値を減算
- 前日までの積算値を加算
- 前日までの積算値を減算
- 値から前日までの積算値を減算
 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: }
 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: }
解説:検査陽性者数を取得
 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: }
対象が全国化都道府県かによって、読み込む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_daily との違いは、データ積算方式 $method である。
解説: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: }
感染者数(累計)を表示するときなどには、「指数関数的に増えている」という話を見やすくするために、縦軸を対数軸にできるようにしている。
実数を棒グラフで、移動平均を折れ線グラフで表示する。
移動平均については、「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: $TablePref= array(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: }
解説:ツイート機能
グラフを描画しているのはクライアントにあるブラウザ(レンダリングエンジン)であることから、クライアント側で画像を作成し、サーバ側でTwitterAPIをコールするという方針とした。
- ブラウザはサーバにグラフ描画をリクエストする。
- サーバはサイトからデータ取得する。(サーバキャッシュにデータがあればそれを利用する)
- サーバはグラフ描画スクリプトを生成する。
- サーバはブラウザへレスポンス(HTML文)を返す。
- ブラウザはグラフをレンダリングする。
- ブラウザはレンダリングしたグラフを画像データとしてサーバへアップロードする。
- サーバはTwitterAPIを使ってツイートする。
- サーバはTwitterへメッセージと画像を送る。
- サーバはブラウザへレスポンス(HTML文)を返す。
FALSE なら、pahooTwitterAPI クラスを読み込まず、ツイート・ボタンも表示しない。ツイート・ボタンの作成については、「HTMLとCSSでさまざまなアイコンを表示する」を参照してほしい。
画像化したいオブジェクト(ID名)は定数 TARGET で指定する。
解説:html2canvasライブラリ
 181: <script src="https://html2canvas.hertzen.com/dist/html2canvas.js"></script>
画像化を実行する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>
レンダリングエンジンによって違うのかもしれないが、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)
解説:メディア付き投稿(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: }
ブラウザから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);
活用例
参考サイト
- PHPで天気予報を求める - キャッシュ・システム:ぱふぅ家のホームページ
- 新型コロナウイルス国内感染の状況:荻原和樹
- 新型コロナウイルス対策ダッシュボード #StopCOVID19JP
- PHPでNHK政治意識月例調査をグラフ表示:ぱふぅ家のホームページ
- JavaScriptでHTML表示を画像保存する:ぱふぅ家のホームページ
- HTMLとCSSでさまざまなアイコンを表示する:ぱふぅ家のホームページ
- Twitter API - WebAPIの登録方法:ぱふぅ家のホームページ
- PHPでTwitterに画像付きメッセージ投稿:ぱふぅ家のホームページ
- グラフで見る COVID-19:みんなの知識 ちょっと便利帳
今回は、国内の COVID-19 感染の推移を、最新のデータを読み込んでグラフ表示するPHPプログラムを作ってみることにする。
本プログラムで参照するデータは、東洋経済オンライン編集部の荻原和樹さんが「新型コロナウイルス国内感染の状況」(MITライセンス)として公開しているもの、および、jig.jp創業者&会長の福野泰介さんが「新型コロナウイルス対策ダッシュボード」として公開しているものをクラウド連携した。
(2023年1月1日)厚労省のサービス終了を受け,PCR検査等実施人数,入院治療等を要する者等を終了
(2022年7月24日)検査の陽性率を追加
(2022年4月1日)ワクチン接種回数に対応