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

目次
サンプル・プログラム
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 | 「実行」ボタン廃止 |
準備:初期値
viewCOVID-19.php
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で天気予報を求める - キャッシュ・システム」を参照いただきたい。
キャッシュ保持時間、キャッシュ・ディレクトリともに、自サイトの環境に応じて変更してほしい。
準備:データ・ファイル
viewCOVID-19.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');
解説:グラフにプロットするためのデータを配列に格納する
viewCOVID-19.php
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つのケースが考えられる。
- そのまま
- 前日の値を加算
- 前日の値を減算
- 値から前日の値を減算
- 前日までの積算値を加算
- 前日までの積算値を減算
- 値から前日までの積算値を減算
viewCOVID-19.php
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: }
viewCOVID-19.php
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: }
解説:検査陽性者数を取得
viewCOVID-19.php
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 も変わる。
viewCOVID-19.php
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用のスクリプト
viewCOVID-19.php
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 日間のグラフの傾きを、最小二乗法によって求めるようにした。
解説:データ読み込み~グラフ描画
viewCOVID-19.php
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, '北海道', '青森県', '岩手県', '宮城県', '秋田県', '山形県', '福島県', '茨城県', '栃木県', '群馬県', '埼玉県', '千葉県', '東京都', '神奈川県', '新潟県', '富山県', '石川県', '福井県', '山梨県', '長野県', '岐阜県', '静岡県', '愛知県', '三重県', '滋賀県', '京都府', '大阪府', '兵庫県', '奈良県', '和歌山県', '鳥取県', '島根県', '岡山県', '広島県', '山口県', '徳島県', '香川県', '愛媛県', '高知県', '福岡県', '佐賀県', '長崎県', '熊本県', '大分県', '宮崎県', '鹿児島県', '沖縄県');
viewCOVID-19.php
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ライブラリ
viewCOVID-19.php
181: <script src="https://html2canvas.hertzen.com/dist/html2canvas.js"></script>
画像化を実行するJavaScript関数は html2canvas である。
viewCOVID-19.php
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 クラス
pahooTwitterAPI.php
19: class pahooTwitterAPI {
20: var $responses; //応答データ
21: var $webapi; //直前に呼び出したWebAPI URL
22: var $error; //エラーフラグ
23: var $errmsg; //エラーメッセージ
24: var $errcode; //エラーコード
25: var $connection;
26:
27: //OAuth用パラメータ
28: // https://apps.twitter.com/
29: var $TWTR_CONSUMER_KEY = '***************'; //Cunsumer key
30: var $TWTR_CONSUMER_SECRET = '***************'; //Consumer secret
31: var $TWTR_ACCESS_KEY = '***************'; //Access Token (oauth_token)
32: var $TWTR_ACCESS_SECRET = '***************'; //Access Token Secret (oauth_token_secret)
解説:メディア付き投稿(RAWデータ)
pahooTwitterAPI.php
解説:ツイート処理
viewCOVID-19.php
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 メソッドを使って、メッセージと画像を一気にツイートする。
viewCOVID-19.php
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(現・X)に画像付きメッセージ投稿:ぱふぅ家のホームページ
- グラフで見る COVID-19:みんなの知識 ちょっと便利帳
今回は、国内の COVID-19 感染の推移を、最新のデータを読み込んでグラフ表示するPHPプログラムを作ってみることにする。
本プログラムで参照するデータは、東洋経済オンライン編集部の荻原和樹さんが「新型コロナウイルス国内感染の状況」(MITライセンス)として公開しているもの、および、jig.jp創業者&会長の福野泰介さんが「新型コロナウイルス対策ダッシュボード」として公開しているものをクラウド連携した。
(2023年1月1日)厚労省のサービス終了を受け,PCR検査等実施人数,入院治療等を要する者等を終了
(2022年7月24日)検査の陽性率を追加
(2022年4月1日)ワクチン接種回数に対応