感染症報告数の推移のグラフを作る

(1/1)
PHPとJpGraphで人口ピラミッドを表示する」では棒グラフを扱ったが、同様にPHPと JpGraph を用い、インフルエンザなどの感染症報告数の毎年の推移を折れ線グラフであらわすプログラムを作ってみる。
感染症報告数は日々変化しているので、IDWR速報(感染症発生動向調査週報)を取り込むようにした。

(2026年2月28日)IDWR速報のページ改編に応じて大幅改訂

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

インフルエンザ患者数のグラフを作る
サンプル・プログラムを実行すると、インフルエンザの毎週の報告数(速報値)を10年分、折れ線グラフで表示する。出力はPNG形式の画像データだ。

グラフ描画する感染症は、インフルエンザを含めて95種類を選択できる(2026年2月現在)。コマンドライン・オプション diseaseCode に0~94の感染症コードを指定する。番号と感染症名の対応は、配列変数 $ListDiseases に定義している。たとえば "influenza.php?d=94" と指定すると、COVID-19 のグラフを表示する。

また、コマンドライン・オプション mode に 1 を指定すると、グラフだけを表示する。2を指定すると、img タグの src 属性に代入する画像データを BASE64エンコード形式で出力する。上図のグラフもこのオプションを使って表示している。
その他のコマンドライン・オプションについては、influenza.php の冒頭のコメントをご覧いただきたい。

目次

サンプル・プログラム

圧縮ファイルの内容
influenza.phpサンプル・プログラム本体
dataIDWR.json感染症報告データのサンプル(JSON形式)
dataIDWR.xml感染症報告データのサンプル(XML形式)
pahooInputData.phpデータ入力に関わる関数群。
使い方は「数値入力とバリデーション」「文字入力とバリデーション」などを参照。include_path が通ったディレクトリに配置すること。
pahooScraping.phpRSS処理に関わるクラス pahooScraping。
スクレイピング処理に関わるクラスの使い方は「PHPで作るRSSビューア」を参照。include_path が通ったディレクトリに配置すること。
influenza.php 更新履歴
バージョン 更新日 内容
3.1.0 2026/03/10 loadXML, saveXML -- メモリ消費抑制
3.0.0 2026/03/06 IDWR速報 のページ改編に応じて大幅改訂
2.4.0 2023/05/05 グラフにフッタを追加
2.3 2022/04/03 PHP8対応,リファラ・チェック改良
2.21 2019/10/06 http→https変更
pahooInputData.php 更新履歴
バージョン 更新日 内容
2.0.1 2025/08/11 getParam() bug-fix
2.0.0 2025/08/11 pahooLoadEnv() 追加
1.9.0 2025/07/26 getParam() 引数に$trim追加
1.8.1 2025/03/15 validRegexPattern() debug
1.8.0 2024/11/12 validRegexPattern() 追加
pahooScraping.php 更新履歴
バージョン 更新日 内容
1.2.1 2024/10/31 __construct() 文字化け対策
1.2.0 2024/09/29 getValueFistrXPath() 属性値でない指定に対応
1.1.0 2023/10/15 getValueFistrXPath() 追加
1.0.1 2023/09/29 __construct() bug-fix
1.0.0 2023/09/18 初版

準備:pahooInputData 関数群

PHPのバージョンや入力データのバリデーションなど、汎用的に使う関数群を収めたファイル "pahooInputData.php" が同梱されているが、include_path が通ったディレクトリに配置してほしい。他のプログラムでも "pahooInputData.php" を利用するが、常に最新のファイルを1つ配置すればよい。

また、各種クラウドサービスに登録したときに取得するアカウント情報、アプリケーションパスワードなどを登録した .pahooEnv ファイルから読み込む関数 pahooLoadEnv を備えている。こちらについては、「各種クラウド連携サービス(WebAPI)の登録方法」をご覧いただきたい。

準備:pahooScraping クラス

pahooScraping.php

  13: class pahooScraping {
  14:     private $dom;           //DOMDocument
  15:     private $xpath;         //DOMXPath
  16:     private $language;      //使用言語(初期値は日本語)
  17:     private $error;         //エラー・メッセージ
  18:     private $errorTable;    //エラー番号 => エラー・メッセージ 一覧
  19: 
  20: /**
  21:  * コンストラクタ
  22:  * @param   string $contents 解析対象テキスト(UTF-8)
  23:  * @return  object /NULL:PHP8未満のため実行できない.
  24: */
  25: function __construct($contents) {
  26:     //PHPのバージョンをチェックする.
  27:     if (! $this->isphp8over()) {
  28:         $this->error = 111;
  29:         throw new Exception($this->getErrorMessage());
  30:         return NULL;
  31:     }
  32:     //プロパティの初期値を設定する.
  33:     try {
  34:         // Unicodeの範囲を指定
  35:         $map = [ 0x80, 0xFFFF, 0, 0xFFFF ];
  36:         // DOMの文字化け対策:UTF-8文字を数値エンティティに変換
  37:         $contents = mb_encode_numericentity($contents, $map, 'UTF-8');
  38:         // DOMDocument クラス
  39:         $this->dom = new DOMDocument();
  40:         libxml_use_internal_errors(TRUE);
  41:         @$this->dom->loadHTML($contents);
  42:         libxml_clear_errors();
  43:         $this->xpath = new DOMXPath($this->dom);
  44:     } catch (Exception $e) {
  45:         $this->error = 121;
  46:         throw new Exception($e->getMessage());
  47:         return FALSE;
  48:     }
  49: 
  50:     //エラー番号 => エラー・メッセージ 一覧
  51:     $this->errorTable = array(
  52:         111 => array('PHP8以上が必要です', 'PHP8 or higher is required'),
  53:         121 => array('DOMエラーです', 'DOM error'),
  54:         999 => array('不明のエラー', 'unknown error'),
  55:     );
  56: }

Webサイトのスクレイピング処理を行うためのクラスが pahooScraping である。同梱のクラス・ファイル "pahooScraping.php" は include_path が通ったディレクトリに配置してほしい。他のプログラムでも pahooScrapingクラス を利用するが、常に最新のクラス・ファイルを1つ配置すればよい。

PHPのクラスについては「PHPでクラスを使ってテキストの読みやすさを調べる」を参照されたい。

準備:JpGraphライブラリ

influenza.php

  66: // JpGraph:include_pathが通ったディレクトリに配置
  67: require_once('jpgraph/jpgraph.php');
  68: require_once('jpgraph/jpgraph_line.php');
  69: 

国内ではアシアル株式会社がサポートしている PHPで様々なグラフを生成するライブラリ「JpGraph」を準備する。JpGraph コミュニティ版は、QPL (Qt Free License) 1.0 ライセンスの下で、非営利目的で使用する場合に無償利用できる。
JpGraph は、サーバ側(PHP側)でグラフを画像データとして生成し、クライアントへ配信する。このため、GDライブラリが有効化されていることが前提である。
また、使用する機能によってインクルードする PHPファイルの種類が異なることに留意してほしい。

公式サイトからダウンロードしたファイルを解凍したら、include_path が通ったディレクトリに配置してほしい。

凡例やラベルに日本語を使う場合は、日本語フォント(TrueTypeファイル)を用意しておく。これについても、アシアル社のサイトに詳しい説明がある。
具体的には、フォント・ファイルを格納したフォルダを "jpg-config.inc.php" の定数 TTF_DIR および MBTTF_DIR に設定してやる。
実際に使うフォントは "jpgraph_ttf.inc.php" の定数 MINCHO_TTF_FONTPMINCHO_TTF_FONTGOTHIC_TTF_FONTPGOTHIC_TTF_FONT に設定されている。デフォルトで設定されているのは IPA の無償フォントで、文字情報技術促進協議会の IPAフォントから入手できる。

解説:各種定数

influenza.php

  70: // 各種定数(START) ===========================================================
  71: 
  72: // 表示モードのデフォルト値
  73: define('DEF_MODE', 0);  // 0:パラメータ選択入力, 1:画像のみ出力, 2:画像をBASE64出力
  74: 
  75: // グラフ描画領域のサイズ(デフォルト値)
  76: define('DEF_WIDTH',  600);      // 幅(ピクセル)
  77: define('DEF_HEIGHT', 400);      // 高さ(ピクセル)
  78: 
  79: // グラフの背景色(デフォルト値)
  80: define('DEF_BGCOLOR', 'FFFFFF');
  81: 
  82: // 感染症コードのデフォルト値
  83: define('DEF_DISEASE_CODE', 75);
  84: 
  85: // 感染症報告データ保管用XMLファイル名
  86: define('FILENAME_XML', 'dataIDWR.xml');
  87: 
  88: // 感染症報告データ保管用XMLファイル名
  89: define('FILENAME_JSON', 'dataIDWR.json');
  90: 
  91: // IDWR速報データ
  92: define('URL_IDWR', 'https://id-info.jihs.go.jp/surveillance/idwr/provisional/sokuhou.html');
  93: 
  94: // IDWR速報データのエンコード
  95: define('IDWR_ENCODING', 'SJIS');
  96: 
  97: // 1年の最終週
  98: define('MAX_WEEK', 53);
  99: 
 100: // 感染症リスト
 101: $ListDiseases = array(
 102:   0 => 'エボラ出血熱',
 103:   1 => 'クリミア・コンゴ出血熱',
 104:   2 => '痘そう',
 105:   3 => '南米出血熱',
 106:   4 => 'ペスト',
 107:   5 => 'マールブルグ病',
 108:   6 => 'ラッサ熱',
 109:   7 => '急性灰白髄炎',
 110:   8 => '結核',
 111:   9 => 'ジフテリア',
 112:  10 => '重症急性呼吸器症候群',
 113:  11 => '鳥インフルエンザ(H5N1)',
 114:  12 => 'コレラ',
 115:  13 => '細菌性赤痢',
 116:  14 => '腸管出血性大腸菌感染症',
 117:  15 => '腸チフス',
 118:  16 => 'パラチフス',
 119:  17 => 'E型肝炎',
 120:  18 => 'ウエストナイル熱',
 121:  19 => 'A型肝炎',
 122:  20 => 'エキノコックス症',
 123:  21 => '黄熱',
 124:  22 => 'オウム病',
 125:  23 => 'オムスク出血熱',
 126:  24 => '回帰熱',
 127:  25 => 'キャサヌル森林病',
 128:  26 => 'Q熱',
 129:  27 => '狂犬病',
 130:  28 => 'コクシジオイデス症',
 131:  29 => 'サル痘',
 132:  30 => '腎症候性出血熱',
 133:  31 => '西部ウマ脳炎',
 134:  32 => 'ダニ媒介脳炎',
 135:  33 => '炭疽',
 136:  34 => 'チクングニア熱',
 137:  35 => 'つつが虫病',
 138:  36 => 'デング熱',
 139:  37 => '東部ウマ脳炎',
 140:  38 => '鳥インフルエンザ(H5N1を除く)',
 141:  39 => 'ニパウイルス感染症',
 142:  40 => '日本紅斑熱',
 143:  41 => '日本脳炎',
 144:  42 => 'ハンタウイルス肺症候群',
 145:  43 => 'Bウイルス病',
 146:  44 => '鼻疽',
 147:  45 => 'ブルセラ症',
 148:  46 => 'ベネズエラウマ脳炎',
 149:  47 => 'ヘンドラウイルス感染症',
 150:  48 => '発しんチフス',
 151:  49 => 'ボツリヌス症',
 152:  50 => 'マラリア',
 153:  51 => '野兎病',
 154:  52 => 'ライム病',
 155:  53 => 'リッサウイルス感染症',
 156:  54 => 'リフトバレー熱',
 157:  55 => '類鼻疽',
 158:  56 => 'レジオネラ症',
 159:  57 => 'レプトスピラ症',
 160:  58 => 'ロッキー山紅斑熱',
 161:  59 => 'アメーバ赤痢',
 162:  60 => 'ウイルス性肝炎',
 163:  61 => '急性脳炎',
 164:  62 => 'クリプトスポリジウム症',
 165:  63 => 'クロイツフェルト・ヤコブ病',
 166:  64 => '劇症型溶血性レンサ球菌感染症',
 167:  65 => '後天性免疫不全症候群',
 168:  66 => 'ジアルジア症',
 169:  67 => '髄膜炎菌性髄膜炎',
 170:  68 => '先天性風しん症候群',
 171:  69 => '梅毒',
 172:  70 => '破傷風',
 173:  71 => 'バンコマイシン耐性黄色ブドウ球菌感染症',
 174:  72 => 'バンコマイシン耐性腸球菌感染症',
 175:  73 => '風しん',
 176:  74 => '麻しん',
 177:  75 => 'インフルエンザ',
 178:  76 => 'RSウイルス感染症',
 179:  77 => '咽頭結膜熱',
 180:  78 => 'A群溶血性レンサ球菌咽頭炎',
 181:  79 => '感染性胃腸炎',
 182:  80 => '水痘',
 183:  81 => '手足口病',
 184:  82 => '伝染性紅斑',
 185:  83 => '突発性発しん',
 186:  84 => '百日咳',
 187:  85 => 'ヘルパンギーナ',
 188:  86 => '流行性耳下腺炎',
 189:  87 => '急性出血性結膜炎',
 190:  88 => '流行性角結膜炎',
 191:  89 => '細菌性髄膜炎',
 192:  90 => '無菌性髄膜炎',
 193:  91 => 'マイコプラズマ肺炎',
 194:  92 => 'クラミジア肺炎',
 195:  93 => '感染性胃腸炎(ロタウイルス)',
 196:  94 => 'COVID-19',
 197: );
 198: 
 199: // 都道府県JISコード
 200: $PrefList = array(
 201:  1 => '北海道',
 202:  2 => '青森県',
 203:  3 => '岩手県',
 204:  4 => '宮城県',
 205:  5 => '秋田県',
 206:  6 => '山形県',
 207:  7 => '福島県',
 208:  8 => '茨城県',
 209:  9 => '栃木県',
 210: 10 => '群馬県',
 211: 11 => '埼玉県',
 212: 12 => '千葉県',
 213: 13 => '東京都',
 214: 14 => '神奈川県',
 215: 15 => '新潟県',
 216: 16 => '富山県',
 217: 17 => '石川県',
 218: 18 => '福井県',
 219: 19 => '山梨県',
 220: 20 => '長野県',
 221: 21 => '岐阜県',
 222: 22 => '静岡県',
 223: 23 => '愛知県',
 224: 24 => '三重県',
 225: 25 => '滋賀県',
 226: 26 => '京都府',
 227: 27 => '大阪府',
 228: 28 => '兵庫県',
 229: 29 => '奈良県',
 230: 30 => '和歌山県',
 231: 31 => '鳥取県',
 232: 32 => '島根県',
 233: 33 => '岡山県',
 234: 34 => '広島県',
 235: 35 => '山口県',
 236: 36 => '徳島県',
 237: 37 => '香川県',
 238: 38 => '愛媛県',
 239: 39 => '高知県',
 240: 40 => '福岡県',
 241: 41 => '佐賀県',
 242: 42 => '長崎県',
 243: 43 => '熊本県',
 244: 44 => '大分県',
 245: 45 => '宮崎県',
 246: 46 => '鹿児島県',
 247: 47 => '沖縄県'
 248: );
 249: 
 250: // 折れ線グラフの色(年ごとに少しずつ色を変える)
 251: $PlotColor = array(
 252:  9 => '#0072B2',
 253:  8 => '#009E73',
 254:  7 => '#E69F00',
 255:  6 => '#56B4E9',
 256:  5 => '#CC79A7',
 257:  4 => '#D55E00',
 258:  3 => '#8B4513',
 259:  2 => '#6A3D9A',
 260:  1 => '#1B9E77',
 261:  0 => '#FF0000',
 262: );
 263: 
 264: // 折れ線グラフの太さ(直近のグラフだけ太線にする)
 265: $PlotWeight = array(
 266:  9 => 1,
 267:  8 => 1,
 268:  7 => 1,
 269:  6 => 1,
 270:  5 => 1,
 271:  4 => 1,
 272:  3 => 1,
 273:  2 => 1,
 274:  1 => 3,
 275:  0 => 3,
 276: );
 277: 
 278: // グラフの透明処理用の色コード(描画に使っていないものであること)
 279: // define('ALPHA_COLOR_CODE', '#FFFFCC');
 280: define('ALPHA_COLOR_CODE', '#FF00FF');
 281: 
 282: // 各種定数(END) ===============================================================

患者数の増減は折れ線グラフで描くが、読みやすいように、年によってグラフの色や太さを変えている。
色はRGB各8bitで配列変数 $PlotColor に、太さは配列変数 $PlotWeight にあらかじめ代入しておく。

解説:IDWR速報サイトの構造

IDWR速報サイトの構造 - 感染症報告数のグラフを作る
IDWR速報(感染症発生動向調査週報)のサイト構造は上図のようになっている。
集計は週単位で、各週のページの中に報告データが CSV形式ファイルとして保存されている。このうち、"\d{4}-\d{2}-zensu.csv" が全数把握疾患、"\d{4}-\d{2}-teiten.csv" が定点把握疾患の報告数を格納しており、この2つのファイルの内容を取得して利用する。

まず、この2つの CSVファイルのURLを求めるユーザー関数を以下に掲げる。
サイト構造の各層の URL を求めるための関数を用意し、最後に getURLreports を使って CSVファイルのURLを求める。

influenza.php

 392: /**
 393:  * IDWR速報がある URLリスト(年次)を取得する.
 394:  * @param   なし
 395:  * @return  array URLリスト [年]=URL / FALSE:取得エラー
 396: */
 397: function getURLlistYear() {
 398:     // ページ取得
 399:     $html = @file_get_contents(URL_IDWR);
 400:     // エラーチェック
 401:     if ($html === FALSE)    return FALSE;
 402: 
 403:     // インスタンスを生成する.
 404:     $pcr = new pahooScraping($html);
 405: 
 406:     // "IDWR速報" を含むリンクを取得する.
 407:     $nodes = $pcr->queryXPath('//a[contains(normalize-space(.),"IDWR速報")]');
 408: 
 409:     // 配列に URLを格納する.
 410:     $items = [];
 411: 
 412:     foreach ($nodes as $node) {
 413:         $text = trim($node->textContent);
 414:         $url  = $node->getAttribute('href');
 415: 
 416:         // 年(4桁)を抽出
 417:         if (preg_match('/(\d+)年/u', $text, $m)) {
 418:             $year = $m[1];
 419:             $items[$year] = resolveUrl(URL_IDWR, $url);
 420:         }
 421:     }
 422: 
 423:     // インスタンスを解放する.
 424:     $pcr = NULL;
 425: 
 426:     return $items;
 427: }

influenza.php

 429: /**
 430:  * IDWR速報がある年次URLを渡し,週次の URLリストを取得する.
 431:  * @param   string $urlYear 年次URL
 432:  * @return  array URLリスト [週番号]=URL / FALSE:取得エラー
 433: */
 434: function getURLlistWeek($urlYear) {
 435:     // ページ取得
 436:     $html = @file_get_contents($urlYear);
 437:     // エラーチェック
 438:     if ($html === FALSE)    return FALSE;
 439: 
 440:     // インスタンスを生成する.
 441:     $pcr = new pahooScraping($html);
 442: 
 443:     // "IDWR速報" を含むリンクを取得する.
 444:     $nodes = $pcr->queryXPath('//a[contains(normalize-space(.),"IDWR速報")]');
 445: 
 446:     // 配列に URLを格納する.
 447:     $items = [];
 448: 
 449:     foreach ($nodes as $node) {
 450:         $text = trim($node->textContent);
 451:         $url  = $node->getAttribute('href');
 452: 
 453:         // 年(4桁)を抽出
 454:         if (preg_match('/第(\d+)週/ui', $text, $m)) {
 455:             $week = $m[1];
 456:             $items[$week] = resolveUrl($urlYear, $url);
 457:         }
 458:     }
 459: 
 460:     // インスタンスを解放する.
 461:     $pcr = NULL;
 462: 
 463:     return $items;
 464: }

influenza.php

 466: /**
 467:  * IDWR速報がある週次URLを渡し,報告数CSVファイルの URLを取得する.
 468:  * @param   string $urlWeek 週次URL
 469:  * @return  array(全数把握疾患, 定点把握疾患)
 470: */
 471: function getURLreports($urlWeek) {
 472:     // 取得したいCSVファイル名
 473:     $targetReps = [
 474:         '/\d+\-\d+\-zensu.csv/i',       // 全数把握疾患
 475:         '/\d+\-\d+\-teiten.csv/i'       // 定点把握疾患
 476:     ];
 477: 
 478:     // ページ取得
 479:     $html = @file_get_contents($urlWeek);
 480:     // エラーチェック
 481:     if ($html === FALSE)    return FALSE;
 482: 
 483:     // インスタンスを生成する.
 484:     $pcr = new pahooScraping($html);
 485: 
 486:     // CSVリンク取得
 487:     $nodes = $pcr->queryXPath('//a[contains(@href, ".csv")]');
 488: 
 489:     // CSVファイルの URLを取得する.
 490:     $urls = [];
 491:     foreach ($nodes as $a) {
 492:         $href = $a->getAttribute('href');
 493:         $file = basename($href);
 494: 
 495:         foreach ($targetReps as $key=>$rep) {
 496:             if (preg_match($rep, $href> 0) {
 497:                 $urls[$key] = resolveUrl($urlWeek, $href);
 498:             }
 499:         }
 500:     }
 501: 
 502:     return $urls;
 503: }

解説:CSVファイルの構造

CSVファイルの構造 - 感染症報告数のグラフを作る
全数把握疾患、定点把握疾患のCSVファイルの構造は上図のようになっており、構造が似ていることから同じ関数 getData を使って解析し、得られたデータを配列 $items に代入する。配列 $items の構造は以下の通りだ。
$items[年][週][病名][都道府県コード] = 報告数

influenza.php

 505: /**
 506:  * IDWR速報CSVファイルを渡し,報告数を配列に格納する.
 507:  * @param   string $urlCSV  CSVファイル名(URL)
 508:  * @param   array  $items   報告数を格納する配列
 509:  * @param   int    $year    報告年を格納する変数
 510:  * @param   int    $week    報告週を格納する変数
 511:  * @return  bool TRUE:取得成功/FALSE:失敗
 512: */
 513: function getData($urlCSV, &$items, &$year, &$week) {
 514:     global $PrefList;
 515: 
 516:     // ページ取得
 517:     $contents = @file_get_contents($urlCSV);
 518:     // エラーチェック
 519:     if ($contents === FALSE)    return FALSE;
 520:     // エンコード変換
 521:     $ss = mb_convert_encoding($contents, INTERNAL_ENCODING, IDWR_ENCODING);
 522:     // 行に分解
 523:     $lines = explode("\n", str_replace(["\r\n", "\r"], "\n", $ss));
 524: 
 525:     // 1行ずつ処理する
 526:     $year = $week = NULL;
 527:     $disease_row = [];
 528:     $disease_map = [];
 529:     foreach ($lines as $line) {
 530:         // カンマで分割(簡易的なCSV分割)
 531:         $data = str_getcsv($line, ",", "\"", "\\"); 
 532:         $first_cell = trim($data[0?? '');
 533: 
 534:         // 1.報告年・週がまだ決まっていない場合
 535:         if ($year === NULL) {
 536:             if (preg_match('/(\d+)年\s*(\d+)週/iu', $line, $arr> 0) {
 537:                 $year = (int)$arr[1];
 538:                 $week = (int)$arr[2];
 539:             }
 540: 
 541:         // 2. ヘッダーがまだ決まっていない場合
 542:         } else if (empty($disease_map)) {
 543:             if (in_array('エボラ出血熱', $data|| in_array('インフルエンザ', $data)) {
 544:                 $disease_row = $data;
 545:                 continue;
 546:             }
 547:             if (in_array('報告', $data)) {
 548:                 $current_disease = "";
 549:                 foreach ($data as $index => $label) {
 550:                     // 病名行から病名を取得
 551:                     if (isset($disease_row[$index]) && trim($disease_row[$index]) !== "") {
 552:                         $current_disease = trim($disease_row[$index]);
 553:                     }
 554:                     // 「報告」列のみマップに登録
 555:                     if (trim($label) === '報告' && $current_disease !== "") {
 556:                         $disease_map[$index] = $current_disease;
 557:                     }
 558:                 }
 559:                 continue;
 560:             }
 561:             continue;
 562:         }
 563: 
 564:         // 3. 終了判定:空行、または注釈行でストップ
 565:         if ($first_cell === "" || $first_cell === "注") {
 566:             if (! empty($items))    break;      // すでにデータがあれば終了
 567:             continue;
 568:         }
 569: 
 570:         // 4. データ行の処理
 571:         if (($first_cell !== '総数'&& ! str_contains($first_cell, '報告数')) {
 572:             foreach ($disease_map as $col_index => $disease_name) {
 573:                 $val = isset($data[$col_index]) ? (int)str_replace(',', '', $data[$col_index]) : 0;
 574:                 // $items[][][病名][都道府県コード] = 報告数
 575:                 $prefCode = (string)array_search($first_cell, $PrefList);
 576:                 $items[$year][$week][$disease_name][$prefCode] = $val;
 577:             }
 578:         }
 579:     }
 580: 
 581:     return TRUE;
 582: }

解説:報告データをXMLファイルに保存

influenza.php

 724: /**
 725:  * 報告データをXMLファイルに保存する.
 726:  * @param   string $fname XMLファイル名
 727:  * @param   array  $items 報告データ
 728:  * @return  bool TRUE:保存成功/FALSE:失敗
 729: */
 730: function saveXML($fname, $items) {
 731:     // $items を年の古い順、週の古い順にソートする
 732:     ksort($items);
 733:     foreach ($items as $year => &$weeks) {
 734:         ksort($weeks);
 735:     }
 736:     unset($weeks);
 737: 
 738:     // 保存データを作成する.
 739:     $fp = fopen($fname, "w");
 740:     if (! $fp)  return FALSE;
 741: 
 742:     $now = date('Y-m-d H:m:s');
 743: 
 744:     fwrite($fp,"<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n");
 745:     fwrite($fp,"<!-- IDWR速報データ -->\n");
 746:     fwrite($fp,"<atit:database xmlns:atit=\"urn:IDWRdata\">\n");
 747:     fwrite($fp,"<update>{$now}</update>\n");
 748: 
 749:     foreach ($items as $year=>$item) {
 750:         fwrite($fp,"<item year=\"{$year}\">\n");
 751:         foreach ($item as $week=>$dataset) {
 752:             fwrite($fp,"\t<dataset week=\"{$week}\">\n");
 753:             foreach ($dataset as $disease=>$prefs) {
 754:                 fwrite($fp,"\t\t<dataset disease=\"{$disease}\">\n");
 755:                 foreach ($prefs as $prefCode=>$data) {
 756:                     fwrite($fp,"\t\t\t<data prefCode=\"{$prefCode}\">{$data}</data>\n");
 757:                 }
 758:                 fwrite($fp,"\t\t</dataset>\n");
 759:             }
 760:             fwrite($fp,"\t</dataset>\n");
 761:         }
 762:         fwrite($fp,"</item>\n");
 763:     }
 764:     fwrite($fp,"</atit:database>\n");
 765: 
 766:     fclose($fp);
 767:     return TRUE;
 768: }

上述の getData を使って、感染症報告データを読み込んだ配列の内容を XMLファイルに保存するユーザー関数が saveXML である。XMLの構造は下図の通り。
一度読み込んだデータをファイルに保存することで、IDWR速報サイトへ負荷をかけずにグラフを描くようにしている。
XMLファイルの構造 - 感染症報告数のグラフを作る

解説:報告データをJSONファイルに保存

influenza.php

 649: /**
 650:  * 報告データを JSONファイルに保存する.
 651:  * @param   string $fname JSONファイル名
 652:  * @param   array  $items 報告データ
 653:  * @return  bool TRUE:保存成功/FALSE:失敗
 654: */
 655: function saveJSON($fname, $items) {
 656:     global $ListDiseases;
 657: 
 658:     $dataSet = [];
 659:     foreach ($items as $year=>$datasetWeek) {
 660:         foreach ($datasetWeek as $week=>$datasetDisease) {
 661:             foreach ($datasetDisease as $disease=>$datasetPref) {
 662:                 $diseaseCode = (string)array_search($disease, $ListDiseases);
 663:                 if ($diseaseCode === FALSE)     continue;
 664:                 $x = 0;
 665:                 foreach ($datasetPref as $pref=>$value) {
 666:                     $x +$value;
 667:                 }
 668:                 $dataSet[$year][$week][$diseaseCode] = $x;
 669:             }
 670:         }
 671:     }
 672:     $json = json_encode($dataSet);
 673: 
 674:     // ファイルに保存する.
 675:     return @file_put_contents($fname, $json) === FALSE ? FALSE : TRUE;
 676: }

上述の saveXML を使って保存する XMLファイルは100Mバイトを越える巨大なデータファイルだ。これを読み込むには相応の時間がかかるため、グラフ描画に必要なデータに絞って JSONファイルに保存するユーザー関数が saveJSON である。
XMLファイルと異なり、週ごとの都道府県の報告数を合計し、全国の報告数としてファイルに保存する。

解説:報告データの読み込み

上述の saveJSON を使って保存したデータファイルを読み込めばグラフを描くことができる。
一方で、IDWR速報サイトには毎週、新しいデータが掲載される。
そこで、報告データの読み込むタイミングで、もし IDWR速報サイトに新しいデータがあれば、サイトのデータを読み込んで配列に追加し、XMLデータファイルと JSONデータファイルを更新するようにする。だいたいの週で、翌々週の火曜日には新しいデータが追加されるようなので、その差分を新しいかどうかの判定基準にしている。
これを行うユーザー関数 readData の流れと、ソースコードを以下に示す。
報告データの読み込み - 感染症報告数のグラフを作る

influenza.php

 794: /**
 795:  * 報告データを配列に読み込む.
 796:  * 必要に応じて,最新のデータを公式から取得し,XMLとJSONの2つのファイルに保存
 797:  * @param   array  $items  報告データを格納する配列
 798:  * @param   string $errmsg エラー・メッセージを格納する変数
 799:  * @return  string 読み込んだファイルのタイムスタンプ/FALSE:読み込み失敗
 800: */
 801: function readData(&$items, &$errmsg) {
 802:     // 報告データ(JSON形式)を読み込む
 803:     $timestamp = loadJSON(FILENAME_JSON, $items);
 804:     if ($timestamp == FALSE) {
 805:         $errmsg = FILENAME_JSON . ' を読むことができません';
 806:         return FALSE;
 807:     }
 808: 
 809:     // 配列にある最新の年,週
 810:     list($lastYear, $lastWeek) = latestYearWeek($items);
 811: 
 812:     // 次の週を求める
 813:     $lastWeek++;
 814:     $date = new DateTime(sprintf('%04d-12-31', $lastYear));
 815:     // 次の年
 816:     if ($lastWeek > (int)$date->format('W')) {
 817:         $lastYear++;
 818:         $lastWeek = 1;
 819:     }
 820: 
 821:     // 今日の年,週を求める
 822:     $today = new DateTime('today');
 823:     $year = (int)$today->format('Y');
 824:     $week = (int)$today->format('W');
 825: 
 826:     // 今日より2週間前の週を求める(最新の公開週)
 827:     $week -2;
 828:     if ($week <0) {
 829:         $year--;
 830:         $date = new DateTime(sprintf('%04d-12-31', $year));
 831:         $week = (int)$date->format('W'- $week;
 832:     }
 833: 
 834:     if (($lastYear < $year|| (($lastYear == $year&& $lastWeek <$week)) {
 835:         // XMLデータを読み込む
 836:         $items = [];
 837:         $res = loadXML(FILENAME_XML, $items);
 838:         if ($res === FALSE) {
 839:             $errmsg = FILENAME_XML . ' を読み込めない';
 840:             return FALSE;
 841:         }
 842:         // 最大10回分の未登録データを読み込み
 843:         for ($i = 0$i < 10$i++) {
 844:             $res = readNewData($lastYear, $lastWeek, $items);
 845:             if ($res === FALSE)     break;
 846:             // 次の週を求める
 847:             $lastWeek++;
 848:             $date = new DateTime(sprintf('%04d-12-31', $lastYear));
 849:             // 次の年
 850:             if ($lastWeek > (int)$date->format('W')) {
 851:                 $lastYear++;
 852:                 $lastWeek = 1;
 853:             }
 854:         }
 855:         // XMLファイルに保存する
 856:         $res = saveXML(FILENAME_XML, $items);
 857:         if ($res === FALSE) {
 858:             $errmsg = FILENAME_XML . ' に書き込めない';
 859:             return FALSE;
 860:         }
 861:         // JSONファイルに保存する
 862:         $res = saveJSON(FILENAME_JSON, $items);
 863:         if ($res === FALSE) {
 864:             $errmsg = FILENAME_JSON . ' に書き込めない';
 865:             return FALSE;
 866:         }
 867:         // ファイルのタイムスタンプ
 868:         $timestamp = date('c', filemtime(FILENAME_JSON));
 869:     }
 870:     return $timestamp;
 871: }

influenza.php

 770: /**
 771:  * 公式サイトから指定年・週の報告データを読み込んで,配列に追加する.
 772:  * @param   int   $year  報告年
 773:  * @param   int   $week  報告週
 774:  * @param   array $items 報告データを格納する配列
 775:  * @return  bool TRUE:読込成功/FALSE:失敗
 776: */
 777: function readNewData($year, $week, &$items) {
 778:     $urlListYear = getURLlistYear();
 779:     if ($urlListYear === FALSE)         return FALSE;
 780:     if (! isset($urlListYear[$year]))   return FALSE;
 781:     $urlListWeek = getURLlistWeek($urlListYear[$year]);
 782:     if ($urlListWeek === FALSE)         return FALSE;
 783:     if (! isset($urlListWeek[$week]))   return FALSE;
 784:     $urlList     = getURLreports($urlListWeek[$week]);
 785:     if ($urlList === FALSE)             return FALSE;
 786:     $res = getData($urlList[0], $items, $year, $week);
 787:     if ($res === FALSE)                 return FALSE;
 788:     $res = getData($urlList[1], $items, $year, $week);
 789:     if ($res === FALSE)                 return FALSE;
 790: 
 791:     return TRUE;
 792: }

解説:グラフを描く

influenza.php

 880: /**
 881:  * 感染症グラフ描画
 882:  * @param   array  $item      報告データ
 883:  * @param   int    $startYear 描画開始年
 884:  * @param   int    $goalYear  描画終了年
 885:  * @param   int    $width     グラフの高さ(ピクセル)
 886:  * @param   int    $height    グラフの幅(ピクセル)
 887:  * @param   string $bgcolor   グラフの背景色
 888:  * @param   string $diseaseCode 感染症コード
 889:  * @return  object graphオブジェクト
 890: */
 891: function drawGraph($items,  $startYear, $goalYear, $width, $height, $bgcolor, $diseaseCode) {
 892:     global $PlotColor, $PlotWeight, $ListDiseases;
 893: 
 894:     $disease = $ListDiseases[$diseaseCode];
 895:     $plotdata = array();
 896: 
 897:     // 月(横軸)を作成
 898:     for ($i = 1$i <MAX_WEEK$i++) {
 899:         $data_x[$i] = (int)($i / 4.5 + 1);
 900:     }
 901: 
 902:     // グラフ描画処理開始
 903:     $graph = new Graph($width, $height, 'auto');
 904:     $graph->SetScale('textlin');
 905:     $graph->SetMargin(80, 100, 20, 40);
 906: 
 907:     // 背景色の指定
 908:     $graph->SetBackgroundGradient($bgcolor, $bgcolor, GRAD_HOR, BGRAD_MARGIN);
 909: 
 910:     // タイトル
 911:     $graph->img->SetAntiAliasing(FALSE);    // アンチエイリアスOFF(太い折れ線を引くため)
 912:     $graph->title->SetColor('black');
 913:     $graph->title->SetFont(FF_PGOTHIC, FS_NORMAL, 14);
 914:     $graph->title->Set($disease . ':報告数(速報値)');
 915:     // フッタ
 916:     $graph->footer->right->SetFont(FF_PGOTHIC, FS_NORMAL, 10);
 917:     $graph->footer->right->Set('produced by NIID and JpGraph');
 918:     // X軸
 919:     $graph->xaxis->SetColor('black');
 920:     $graph->xaxis->title->SetFont(FF_PGOTHIC);
 921:     $graph->xaxis->SetTitle('月','center');
 922:     $graph->xaxis->SetTickLabels($data_x);
 923:     $graph->xaxis->SetTextTickInterval(5);
 924:     $graph->xaxis->SetTextLabelInterval(1);
 925:     // Y軸
 926:     $graph->yaxis->SetColor('black');
 927:     $graph->yaxis->title->SetFont(FF_PGOTHIC);
 928:     $graph->yaxis->title->Set('報告数');
 929:     $graph->yaxis->SetTitleMargin(60);
 930:     $graph->yaxis->SetLabelFormatCallback('numberFormatCallback');
 931: 
 932:     // 折れ線グラフを描く
 933:     foreach ($items as $year=>$data) {
 934:         // 描画版以外ならcontinue
 935:         if (($year < $startYear|| ($year > $goalYear)) continue;
 936:         // データがない年はスキップする.
 937:         if (! isset($items[$year][1][$diseaseCode]) &&
 938:             ! isset($items[$year][MAX_WEEK - 1][$diseaseCode])) continue;
 939: 
 940:         // プロットデータを初期化する.
 941:         for ($i = 0$i < MAX_WEEK$i++)    $data_y[$i] = NULL;
 942:         // プロットデータを代入する.
 943:         foreach ($data as $week=>$val) {
 944:             if (isset($items[$year][$week][$diseaseCode])) {
 945:                 $data_y[$week - 1] = $items[$year][$week][$diseaseCode];
 946:             } else {
 947:                 $data_y[$week - 1] = NULL;
 948:             }
 949:         }
 950: 
 951:         // 折れ線グラフ作成
 952:         $colorIndex = ($goalYear - $startYear- ($year - $startYear% count($PlotColor);
 953:         $plotdata[$year] = new LinePlot($data_y);
 954:         $plotdata[$year]->SetLegend($year . '年');                  // 凡例
 955:         // グラフを結合する
 956:         $graph->Add($plotdata[$year]);
 957:         $plotdata[$year]->SetColor($PlotColor[$colorIndex]);        // 
 958:         $plotdata[$year]->SetWeight($PlotWeight[$colorIndex]);      // 太さ
 959:     }
 960: 
 961:     // 凡例
 962:     $graph->legend->SetColor('black');
 963:     $graph->legend->SetFont(FF_PGOTHIC);                    // フォント
 964:     $graph->legend->SetColumns(1);                          // 表示列数
 965:     $graph->legend->SetPos(0.01, 0.1, 'right', 'top');      // 位置
 966: 
 967:     return $graph;
 968: }

感染症報告データを格納した配列のデータから、 JpGraph の機能を利用してグラフを描くユーザー関数が drawGraph である。

まず、1年分のデータを配列 $data_y に代入し、折れ線グラフを1本描く。
この作業を年数分繰り返してゆく。

解説:画面出力

influenza.php

1240: // グラフを表示する.
1241: $ImageBase64 = '';
1242: if ($error == '') {
1243:     // グラフを生成する.
1244:     $graph = drawGraph($items, $startYear, $goalYear, $width, $height, $bgcolor, $diseaseCode);
1245:     $im = $graph->Stroke(_IMG_HANDLER);
1246: 
1247:     // 画像データのみを出力する.
1248:     if ($mode == 1) {
1249:         header('Content-type: image/webp');
1250:         imagewebp($im, NULL, 75);               // WebP出力
1251:         // PHP8.5:非推奨関数
1252:         if (PHP_VERSION_ID < 80500) {
1253:             imagedestroy($im);                  // 後処理
1254:         }
1255:         exit(1);
1256:     // 画像データをBASE64エンコーディングする.
1257:     } else {
1258:         ob_start();
1259:         imagewebp($im, NULL, 75);               // WebP出力
1260:         // PHP8.5:非推奨関数
1261:         if (PHP_VERSION_ID < 80500) {
1262:             imagedestroy($im);                  // 後処理
1263:         }
1264:         $ImageBase64 = base64_encode(ob_get_clean());
1265:     }
1266: }
1267: 
1268: // 表示HTMLを作成する.
1269: $HtmlBody = makeCommonBody($mode, $startYear, $goalYear, $height, $width, $diseaseCode, $ImageBase64, $items, $error, $timestamp);
1270: 
1271: // 画面に表示する.
1272: echo $HtmlHeader;
1273: echo $HtmlBody;
1274: echo $HtmlFooter;
1275: 
1276: /*

前述のユーザー関数 drawGraph で生成したグラフデータを、 JpGraphStroke メソッドを使って GdImageオブジェクトとして取得する。あとは、変数 $mode の値に応じて、出力方式を変更してやる。

参考サイト

(この項おわり)
header