PHPで人口ピラミッドの経年変化を表示する

(1/1)
PHPとJpGraphで人口ピラミッドを表示する」では、あらかじめ用意した統計表ファイルを読み込んで、人口ピラミッドを画面に描画するプログラムを作った。
今回は、「e-Stat 政府の統計窓口」から、毎年の男女別・年齢別人口データを取りだし、スライダーを使ったインタラクティブに人口ピラミッドの経年変化を表示するプログラムを作ってみることにする。

目次

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

PHPで人口ピラミッドの経年変化を表示する
画面上部にある青色のスライダーを左右に動かすと、わが国の100年以上にわたる人口ピラミッドの経年変化を見ることができる。
いまから100年前は釣り鐘型をしていたものが、太平洋戦争終戦の年には男性の壮年人口が極端に減り、戦後には団塊の世代の突出部分と丙午の凹みがあらわれ、21世紀には壺型へと変化していく。80歳以上人口の重みの違いも、あらためて認識することができるだろう。

サンプル・プログラムのダウンロード

圧縮ファイルの内容
shiftPopulationPyramid.phpサンプル・プログラム
.pahooEnvクラウドサービスを利用するためのアカウント情報などを記入する .env ファイル。
使い方は「各種クラウド連携サービス(WebAPI)の登録方法」を参照。include_path が通ったディレクトリに配置すること。
pahooInputData.phpデータ入力に関わる関数群。
使い方は「数値入力とバリデーション」「文字入力とバリデーション」などを参照。include_path が通ったディレクトリに配置すること。
pahooCache.phpキャッシュ処理に関わるクラス pahooCache。
キャッシュ処理に関わるクラスの使い方は「PHPで天気予報を求める」を参照。include_path が通ったディレクトリに配置すること。
p1920_1994.csv1920~1994年の年別・男女別・年齢別人口データを収録したCSVファイル。
shiftPopulationPyramid.php 更新履歴
バージョン 更新日 内容
1.0.0 2026/01/24 初版
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() 追加
pahooCache.php 更新履歴
バージョン 更新日 内容
1.3.0 2025/12/06 PHP8.5対応:curl_closeを使わない
1.2.0 2025/09/06 cLoad() HTTPヘッダを送信できるようにした
1.1.3 2025/08/10 var→public
1.1.2 2023/07/22 bug-fix
1.1.1 2023/02/11 コメント追記

準備:PHP の https対応

クラウド連携や相手先サイトのデータを読み込むのに https通信を使うため、PHPに OpenSSLモジュールが組み込まれている必要がある。関数  phpinfo  を使って、下図のように表示されればOKだ。
OpenSSL - PHP
そうでない場合は、次の手順に従ってOpenSSLを有効化し、PHPを再起動させる必要がある。

Windowsでは、"php.ini" の下記の行を有効化する。
extension=php_openssl.dll
Linuxでは --with-openssl=/usr オプションを付けて再ビルドする。→OpenSSLインストール手順

これで準備は完了だ。

準備:pahooInputData 関数群

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

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

準備:pahooCache クラス

pahooCache.php

  13: class pahooCache {
  14:     const LIFE_CACHE = (2 * 60);        // キャッシュ保持時間(デフォルト;分)
  15:     const DEF_DIRCACHE = './pcache/';   // キャッシュ・ディレクトリ(デフォルト)
  16: 
  17:     public $lifeCache;      // キャッシュ保持時間(分)(0:キャッシュしない)
  18:     public $dirCache;       // キャッシュ用ディレクトリ
  19:     public $httpHeader;     // HTTPヘッダ(空文字の時は何も送らない)
  20:     public $error;          // エラーフラグ
  21:     public $errmsg;         // エラーメッセージ
  22:     public $debug;          // デバッグ用ファイル名
  23: 
  24: /**
  25:  * コンストラクタ
  26:  * 参考サイト https://www.pahoo.org/e-soul/webtech/php06/php06-72-01.shtm
  27:  * @param   int    $life キャッシュ保持時間(分)(省略可能)
  28:  * @param   string $dir キャッシュ・ディレクトリ(省略可能)
  29:  * @param   array  $httpHeader httpヘッダに渡す配列(省略可能)
  30:  *          USER AGENT偽装に用いることを想定
  31:  *          (例)
  32:  *           array(
  33:  *              'User-Agent: Mozilla/5.0(Windows NT 10.0; Win64; x64) pahooAppy/pahoo.org AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36',
  34:  *              'Accept-Language: ja-JP'
  35:  *          );
  36:  * @return  なし
  37: */
  38: function __construct($life=self::LIFE_CACHE, $dir=self::DEF_DIRCACHE, $httpHeader=NULL) {
  39:     if ($life < 0) {
  40:         $life = 0;
  41:     }
  42:     if (preg_match('/\/$/ui', $dir) == 0) {
  43:         $dir = $dir . '/';
  44:     }
  45:     $this->error      = FALSE;
  46:     $this->errmsg     = '';
  47:     $this->debug      = '';
  48:     $this->lifeCache  = $life;
  49:     $this->dirCache   = $dir;
  50:     $this->httpHeader = $httpHeader;
  51: 
  52:     // PHP5以上であることを調べる.
  53:     if (! $this->isphp5over()) {
  54:         $this->error   = TRUE;
  55:         $this->errmsg  = '動作にはPHP5以上が必要です';
  56:         return;
  57:     }
  58: 
  59:     // キャッシュ・ディレクトリが無ければ作成する.
  60:     if (! is_dir($this->dirCache)) {
  61:         $res = mkdir($this->dirCache, 0744);
  62:         if ($res == FALSE) {
  63:             $this->error   = TRUE;
  64:             $this->errmsg  = 'キャッシュ・ディレクトリ "' . $this->$dirCache . '" の作成に失敗しました';
  65:             return;
  66:         }
  67:     }
  68: }

クラウド連携や相手先サイトの公開データを利用する際、こちらのアプリを起動する都度、APIを呼び出したりデータをダウンロードするのでは、相手サーバに負荷をかけてしまう。
そこで、頻繁に変更がないデータについては、一度取り込んだら、こちら側のサーバのローカルストレージにキャッシュしておく仕組みを用意した。それが pahooCacheクラス である。同梱のクラス・ファイル "pahooCache.php" は include_path が通ったディレクトリに配置してほしい。他のプログラムでも pahooCacheクラス を利用するが、常に最新のクラス・ファイルを1つ配置すればよい。

pahooCacheクラス の注意ポイントは、キャッシュ時間(単位:分)とキャッシュを保存するディレクトリをコンストラクタで指定している点だ。これらはプログラムによって変わるものである。インスタンス化するときの値は、pahooCacheクラス を利用するメイン・プログラムの方で解説する。

サイトによっては、User-Agent などを必要とすることがあるだろう。そこで、第3引数に HTTPヘッダ として送信するデータを配列で渡すことができるようにした。配列の構造はコメントを参照していただきたい。

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

準備:jqPlotプラグイン

shiftPopulationPyramid.php

 124: <!-- jqPlot 本体 -->
 125: <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jqPlot/1.0.9/jquery.jqplot.min.css">
 126: <script src="https://code.jquery.com/jquery-1.12.4.min.js"></script>
 127: <script src="https://cdnjs.cloudflare.com/ajax/libs/jqPlot/1.0.9/jquery.jqplot.min.js"></script>
 128: 
 129: <!-- プラグイン -->
 130: <script src="https://cdnjs.cloudflare.com/ajax/libs/jqPlot/1.0.9/plugins/jqplot.barRenderer.min.js"></script>
 131: <script src="https://cdnjs.cloudflare.com/ajax/libs/jqPlot/1.0.9/plugins/jqplot.categoryAxisRenderer.min.js"></script>
 132: <script src="https://cdnjs.cloudflare.com/ajax/libs/jqPlot/1.0.9/plugins/jqplot.canvasAxisTickRenderer.min.js"></script>
 133: <script src="https://cdnjs.cloudflare.com/ajax/libs/jqPlot/1.0.9/plugins/jqplot.canvasTextRenderer.min.js"></script>
 134: <style type="text/css">

サーバに負荷をかけないよう、グラフ描画はクライアント側で行う。今回は、JavaScriptの jQueryプラグイン「jqPlot Charts and Graphs for jQuery」を利用する。公式サイトからダウンロードしてサーバに展開してもいいし、今回のように、無料の JavaScript / CSS ライブラリ配信サービス「Cloudflare CDNJS」を利用してもいい。
HTMLの <head> タグに、ライブラリ「jQuery」と、必要な jPlotプラグインを記述する。

プログラムの流れ

大雑把に、サーバとクライアントの役割分担を決めておく。
  • サーバ‥‥データの読み込み,グラフ描画スクリプトの生成
  • クライアント‥‥スライダーを動かしたときにインタラクティブにグラフを描き変える

準備:各種定数など

shiftPopulationPyramid.php

  51: // 各種定数(START) ===========================================================
  52: 
  53: // グラフの幅・高さ
  54: define('GRAPH_WIDTH',  600);
  55: define('GRAPH_HEIGHT', 600);
  56: 
  57: // jqPlotのあるフォルダ
  58: //define('JQPLOT', '../../../../common/jqplot/');
  59: define('JQPLOT', './common/jqplot/');
  60: 
  61: // 1920~1994年の年齢別人口データを収録したCSVファイル
  62: // 元データ https://api.e-stat.go.jp/rest/3.0/app/getStatsData?appId=858e563093591e6063fb302cbc5669fdef933ae7&statsDataId=0003448228
  63: define('POPULATION_CSV', './p1920_1994.csv');
  64: 
  65: // 年齢の最大値・最小値
  66: define('AGE_MAX', 100);
  67: define('AGE_MIN',   0);
  68: 
  69: // 年齢別人口の最大値(万人)
  70: define('HEADCOUNT_MAX', 140);
  71: 
  72: // キャッシュ保持時間(分) 0:キャッシュしない
  73: // 政府統計 e-Stat へのアクセス負荷軽減
  74: define('LIFE_CACHE_DATA', (60 * 24 * 30));
  75: 
  76: // キャッシュ・ディレクトリ
  77: // 書き込み可能で,外部からアクセスされないディレクトリを指定してください.
  78: define('DIR_CACHE_DATA',   './pcache_estat/');
  79: 
  80: // APPID
  81: if (isset($_ENV['PAHOO_ESTAR_APIKEY'])) {
  82:     define('ESTAT_APPID', $_ENV['PAHOO_ESTAR_APIKEY']);
  83: else {
  84:     define('ESTAT_APPID', '');
  85: }
  86: 
  87: // 各種定数(END) ===============================================================

各種パラメータは定数を defineしている。とくに記載のないものは、適宜変更してかまわない。

今回は、e-Stat 政府の統計窓口の API を使ってデータを引き出すため、APPID が必要となる。無料で取得できる。取得方法は、「e-Stat 政府統計の総合窓口 - 各種クラウド連携サービス(WebAPI)の登録方法」をご覧いただきたい。

解説:e-Stat API を使ってデータを取得する

shiftPopulationPyramid.php

 227: /**
 228:  * 政府統計 e-Stat API を使ってデータを取得する
 229:  * @param   string $appid  APPID
 230:  * @param   array  $populations 人口データを格納する配列
 231:  * @return  int 取得データ数/FALSE:取得失敗
 232: */
 233: function getPopulationDataFromEstat($appid, &$populations) {
 234:     $dataIDlist = array(
 235:         '0003448228',       // 2020~2024年
 236:         '0003459018',       // 2015~2019年
 237:         '0004008040',       // 2010~2014年
 238:         '0004010040',       // 2005~2009年
 239:         '0004018845',       // 2000~2004年
 240:         '0004029959',       // 1995~1999年
 241:     );
 242:     $classList = array(
 243:         'cat01', 'cat02', 'cat03', 'time'
 244:     );
 245: 
 246:     $codeList = array();
 247:     $cnt = 0;
 248: 
 249:     // インスタンス生成
 250:     $pcc = new pahooCache(LIFE_CACHE_DATA, DIR_CACHE_DATA);
 251: 
 252:     // データ読み込み
 253:     foreach ($dataIDlist as $dataID) {
 254:         $url = "https://api.e-stat.go.jp/rest/3.0/app/getStatsData?appId={$appid}&statsDataId={$dataID}";
 255: 
 256:         // データ取得
 257:         $xmlString = $pcc->load($url);
 258:         // エラーチェック
 259:         if ($xmlString === FALSE)       return FALSE;
 260: 
 261:         // XMLパーシング
 262:         $xml = @simplexml_load_string($xmlString);
 263:         // エラーチェック
 264:         if ($xml === FALSE)     return FALSE;
 265:         if (! isset($xml->STATISTICAL_DATA->CLASS_INF->CLASS_OBJ))  return FALSE;
 266:         if (! isset($xml->STATISTICAL_DATA->DATA_INF->VALUE))       return FALSE;
 267: 
 268:         // データ要素を取得する
 269:         foreach ($xml->STATISTICAL_DATA->CLASS_INF->CLASS_OBJ as $obj) {
 270:             $attr = $obj->attributes();
 271:             $objID = (string)$attr['id'];
 272:             foreach ($obj->CLASS as $class) {
 273:                 $attr = $class->attributes();
 274:                 $className = (string)$attr['name'];
 275:                 // 年齢のコード
 276:                 if (preg_match('/^([0-9]+)歳/ui', $className, $arr> 0) {
 277:                     $codeList[$objID][(string)$attr['code']] = (int)$arr[1];
 278:                 // 西暦年のコード
 279:                 } else if (preg_match('/^([0-9]+)年/ui', $className, $arr> 0) {
 280:                     $codeList[$objID][(string)$attr['code']] = (int)$arr[1];
 281:                 // その他のコード
 282:                 } else if (preg_match('/^(男|女|総人口|日本人人口)$/ui', $className, $arr> 0) {
 283:                     $codeList[$objID][(string)$attr['code']] = $className;
 284:                 }
 285:             }
 286:         }
 287: 
 288:         // データを連想配列に格納する
 289:         foreach ($xml->STATISTICAL_DATA->DATA_INF->VALUE as $value) {
 290:             $attr = $value->attributes();
 291:             $flag = TRUE;
 292:             // 西暦年
 293:             if (! isset($codeList['time'][(string)$attr['time']])) {
 294:                 $flag = FALSE;
 295:             } else {
 296:                 $time  = $codeList['time'][(string)$attr['time']];
 297:             }
 298:             // 総人口|日本人人口
 299:             if (! isset($codeList['cat02'][(string)$attr['cat02']])) {
 300:                 $flag = FALSE;
 301:             } else {
 302:                 $cat02  = $codeList['cat02'][(string)$attr['cat02']];
 303:             }
 304:             // 男|女
 305:             if (! isset($codeList['cat01'][(string)$attr['cat01']])) {
 306:                 $flag = FALSE;
 307:             } else {
 308:                 $cat01  = $codeList['cat01'][(string)$attr['cat01']];
 309:             }
 310:             // 年齢
 311:             if (! isset($codeList['cat03'][(string)$attr['cat03']])) {
 312:                 $flag = FALSE;
 313:             } else {
 314:                 $cat03  = $codeList['cat03'][(string)$attr['cat03']];
 315:             }
 316:             // データを配列に代入
 317:             if ($flag) {
 318:                 $populations[$time][$cat02][$cat01][$cat03] = (int)$value * 1000;
 319:                 $cnt++;
 320:             }
 321:         }
 322:     }
 323:     // インスタンス解放
 324:     $pcc = NULL;
 325: 
 326:     return $cnt;
 327: }

e-Stat 政府の統計窓口 API を使ってデータを得るのがユーザー関数 getPopulationDataFromEstat である。API に負荷をかけないよう、pahooCacheクラスを使ってデータ取得する。
得られた XML形式データを連想配列 $populations に格納する。ここで、人口ピラミッドを描きやすいように連想配列の構造は
$populations[西暦年(int)][総人口)|日本人人口)][男|女)][年齢(int)]
のようにした。

e-Stat API では、5年ごとに行われる国勢調査を基準にデータが作成され、5年ごとに dataID が変わる。そこで、dataID のリストを、あらかじめ配列 $dataIDlist に用意した。今後、国勢調査が行われる都度、要素である dataID を増やしていく必要がある。
なお、1994年(平成6年)以前は後述するように Excelファイルとして提供されているため、別の関数を使って読み込むことにする。

応答データ(XML形式)は下記のような構造をしている。
<DATA_INF> タグの中にある <VALUE> タグに1つ1つの人口データが格納されている。属性に cat91, cat02, vat03, area, time, unit があるが、それぞれが意味する内容は、<CLASS_INF> タグの中に記載されている。
応答データ構造(xml) GET_STATS_DATA RESULT STATUS ステータス ERROR_MSG 応答メッセージ DATE 応答日時 STATISTICAL_DATA TABLE_INF STAT_NAME 人口推計 GOV_ORG 総務省 STATISTICS_NAME 統計表の名称 TITLE 統計表のタイトル CYCLE 年次 OPEN_DATE 公開年月日 UPDATED_DATE 更新年月日 OVERALL_TOTAL_NUMBER データ件数 STATISTICS_NAME_SPEC TABULATION_CATEGORY 人口推計 TABULATION_SUB_CATEGORY1 各年10月1日現在人口 TABULATION_SUB_CATEGORY2 国勢調査の基準年 TITLE_SPEC TABLE_NAME 統計表の名称 CLASS_INF CLASS_OBJ CLASS code=001:男女計(千人) CLASS code=002:男性(千人) CLASS code=003:女性(千人) CLASS code=004:人口性比(女性=100) CLASS_OBJ CLASS code=001:総人口 CLASS code=002:日本人人口 CLASS_OBJ CLASS code=01000:総数 CLASS code=01001:0歳人口 CLASS code=01002:1歳人口 CLASS code=01003:2歳人口 CLASS code=01004:3歳人口 CLASS code=01005:4歳人口 CLASS code=01101:100歳以上人口 CLASS_OBJ CLASS CLASS_OBJ CLASS code=1601:2020年10月1日現在 CLASS code=1301:2021年10月1日現在 CLASS code=1701:2022年10月1日現在 CLASS code=1801:2023年10月1日現在 CLASS code=1901:2024年10月1日現在 DATA_INF NOTE 該当数値がないもの NOTE 該当数値がないもの  VALUE 126146 VALUE 125502 VALUE 124947 VALUE 124352 VALUE 123802 VALUE 837 VALUE 830 VALUE 798 VALUE 757 VALUE 716 VALUE 872
ユーザー関数 getPopulationDataFromEstat は、この応答データを解釈し、前述した連想配列 $populations へ代入していく。

解説:1920~1994年の人口データを収録したCSVファイルからデータを取得する

shiftPopulationPyramid.php

 182: /**
 183:  * 1920~1994年の年齢別人口データを収録したCSVファイルからデータを取得する
 184:  * @param   string $fname CSVファイル名
 185:  * @param   array  $populations 人口データを格納する配列
 186:  * @return  int 取得データ数/FALSE:取得失敗
 187: */
 188: function getPopulationDataFromCSV($fname, &$populations) {
 189:     $infp = fopen($fname, 'r');
 190:     if ($infp == FALSE)     return FALSE;
 191: 
 192:     // 年号
 193:     $years = array();
 194:     $str = fgets($infp);
 195:     if (($arr = preg_split('/\,/i', $str)) === FALSE)   return FALSE;
 196:     foreach ($arr as $val) {
 197:         if (preg_match('/([0-9]+)/i', $val, $arr2> 0) {
 198:             $years[] = (int)$arr2[1];
 199:         }
 200:     }
 201: 
 202:     // 男女ラベル(読み飛ばす)
 203:     $str = fgets($infp);
 204: 
 205:     // 年齢別人口
 206:     $cnt = $age = 0;
 207:     while (! feof($infp)) {
 208:         $str = fgets($infp);
 209:     if (($arr = preg_split('/\,/i', $str)) === FALSE)   continue;
 210:         for ($i = 1$i < count($arr); $i++) {
 211:             $year = $years[(int)floor(($i - 1) / 2)];
 212:             $sex = (($i - 1% 2 === 0? '男' : '女';
 213:             if (preg_match('/([0-9]+)/i', $arr[$i], $arr2> 0) {
 214:                 $populations[$year]['総人口'][$sex][$age] = (int)$arr2[1* 1000;
 215:                 $populations[$year]['日本人人口'][$sex][$age] = (int)$arr2[1* 1000;
 216:                 $cnt +2;
 217:             } else {
 218:                 $populations[$year]['総人口'][$sex][$age] = 0;
 219:                 $populations[$year]['日本人人口'][$sex][$age] = 0;
 220:             }
 221:         }
 222:         $age++;
 223:     }
 224:     return $cnt;
 225: }

e-Stat 政府の統計窓口 API では、1920~1994年(平成6年)の人口データは https://api.e-stat.go.jp/rest/3.0/app/getStatsData?appId=858e563093591e6063fb302cbc5669fdef933ae7&statsDataId=0003448228 から Excelファイルとしてダウンロードすることができる。
このファイルを直接パースしてもいいのだが、よく見ると、年によって年齢の最大値・最小値が異なっていたり、太平洋戦争中に集計できていない年があったりと、シートによって構造がまちまちである。
そこで、生成AIサービス「Microsoft Copilot」を使って、読み込みやすいCSVファイル形式に変換したものが、同梱のファイル "p1920_1994.csv" である。

ユーザー関数 getPopulationDataFromCSV は、このCSVファイルを読み込んで、前述の連想配列 $populations に格納する。
数字以外の記号が入っていたり、数字が入っていない要素は 0 を代入するようにした。

解説:指定年の人口ピラミッドを描画するスクリプトを生成する

shiftPopulationPyramid.php

 329: /**
 330:  * jqPlotを使って指定年の人口ピラミッドを描画するスクリプトを生成する
 331:  * @param   array  $populations 人口データ
 332:  * @return  string JavaScript
 333: */
 334: function makeJavaScriptDrawPopulationPyramid($populations) {
 335:     // 年齢
 336:     $ageMin = AGE_MIN;
 337:     $ageMax = AGE_MAX;
 338:     // 年齢別人口
 339:     $headcountMin = 0 - HEADCOUNT_MAX;
 340:     $headcountMax = HEADCOUNT_MAX;
 341:     // 棒グラフの幅
 342:     $barWidth = (int)(GRAPH_HEIGHT / ($ageMax - $ageMin));
 343: 
 344:     $js =<<< EOT
 345: function drawPopulationPyramid(year) {
 346:     const populations = {
 347: 
 348: EOT;
 349:     foreach ($populations as $year=>$values) {
 350:         $ages = '';
 351:         for ($age = $ageMin$age <$ageMax$age++) {
 352:             $ages = $ages . $age . ',';
 353:         }
 354:         $js .=<<< EOT
 355: {$year} : {
 356: age: [ {$ages} ],
 357: 
 358: EOT;
 359: 
 360:         // 男性人口
 361:         $male = '';
 362:         for ($age = $ageMin$age <$ageMax$age++) {
 363:             if (isset($values['日本人人口']['男'][$age])) {
 364:                 $male = $male . '-' . (int)($values['日本人人口']['男'][$age] / 10000. ',';
 365:             } else {
 366:                 $male = $male . '-0,';
 367:             }
 368:         }
 369:         $js .=<<< EOT
 370: male: [ {$male} ],
 371: 
 372: EOT;
 373: 
 374:         // 女性人口
 375:         $female = '';
 376:         for ($age = $ageMin$age <$ageMax$age++) {
 377:             if (isset($values['日本人人口']['女'][$age])) {
 378:                 $female = $female . (int)($values['日本人人口']['男'][$age] / 10000. ',';
 379:             } else {
 380:                 $female = $female . '0,';
 381:             }
 382:         }
 383:         $js .=<<< EOT
 384: female: [ {$female} ]
 385: },
 386: 
 387: EOT;
 388:     }
 389:     $js .=<<< EOT
 390: };
 391: 
 392:     const ages = populations[year].age;
 393:     const males = populations[year].male;
 394:     const females = populations[year].female;
 395: 
 396:     let yTicks = ages.map(function (age) {
 397:         return (age % 5 === 0) ? age.toString() : "";
 398:     });
 399: 
 400:     // jqPlot 用データ形式 [値, カテゴリ]
 401:     const maleSeries = [];
 402:     const femaleSeries = [];
 403: 
 404:     for (let i = 0; i < ages.length; i++) {
 405:         maleSeries.push([males[i], ages[i]]);
 406:         femaleSeries.push([females[i], ages[i]]);
 407:     }
 408: 
 409:     // 再描画対策
 410:     $('#chart').empty();
 411: 
 412:     // 描画関数
 413:     $.jqplot('chart', [maleSeries, femaleSeries], {
 414: //      title: year + '年',
 415:         stackSeries: true,
 416:         seriesDefaults: {
 417:             renderer: $.jqplot.BarRenderer,
 418:             rendererOptions: {
 419:                 barDirection: 'horizontal',
 420:                 fillToZero: true,
 421:                 barWidth: {$barWidth},
 422:                 shadow: false
 423:             }
 424:         },
 425:         series: [
 426:             { label: '男性' },
 427:             { label: '女性' }
 428:         ],
 429:         // グラフの色(正数・負数)
 430:         seriesColors:           ['dodgerblue', 'violet'],
 431:         negativeSeriesColors:   ['dodgerblue', 'violet'],
 432:         // 軸の設定
 433:         axes: {
 434:             yaxis: {
 435:                 renderer: $.jqplot.CategoryAxisRenderer,
 436:                 label: '年齢',
 437:                 ticks: yTicks,
 438:             },
 439:             xaxis: {
 440:                 label: '人口(万人)',
 441:                 min: {$headcountMin},
 442:                 max: {$headcountMax},
 443:                 tickInterval: 20,
 444:                 tickOptions: {
 445:                     formatter: function (format, value) {
 446:                         return Math.abs(value);
 447:                     }
 448:                 }
 449:             }
 450:         },
 451:         legend: {
 452:             show: true,
 453:             location: 'e'
 454:         }
 455:     });
 456: }
 457: 
 458: EOT;
 459:     return $js;
 460: }

ユーザー関数 makeJavaScriptDrawPopulationPyramid は、jqPlot プラグインを使って、指定年の人口ピラミッドを描く JavaScript関数 drawPopulationPyramid を生成する。

関数 drawPopulationPyramid の中では、まず、前述の連想配列 [$population] を JavaScript で利用しやすいオブジェクト配列 population に変換する。
次に、グラフ描画関数 $.jqplot を用意する。
スライダーでグラフを再描画していくので、事前に empty() メソッドを使って、1つ前のグラフを消去しておく。

人口ピラミッドは、jqPlot プラグインから見た場合には横棒グラフであり、男性は負数のグラフになっている。横軸のメモリが負数にならないよう、xaxis:tickOptions で絶対値を表示するように指定している。
また、正数(女性)と負数(男性)の棒グラフのカラー指定ができるよう、seriesColorsnegativeSeriesColors の両方に同じカラーコードを指定する点に留意されたい。

解説:西暦年を指定するスライダーを生成する

shiftPopulationPyramid.php

 462: /**
 463:  * 西暦年を指定するスライダーを作成
 464:  * @param   array  $populations 人口データ
 465:  * @return  string JavaScript
 466: */
 467: function makeSlider($populations) {
 468:     // 西暦年を取得する
 469:     $years = array_keys($populations);
 470:     sort($years);
 471: 
 472:     $width = GRAPH_WIDTH;
 473:     $html =<<< EOT
 474: <div style="width:{$width}px; text-align:center; margin-top:16px;" id="valueDisplay"></div>
 475: <input style="width:{$width}px;" type="range" name="slider" id="slider" min="0" max="0" step="1">
 476: <script>
 477: const keys = [
 478: 
 479: EOT;
 480: 
 481:     foreach ($years as $year) {
 482:         $html .$year . ',';
 483:     }
 484: 
 485:     $html .=<<< EOT
 486: ];
 487: 
 488: // スライダーの設定
 489: const slider = document.getElementById('slider');
 490: slider.max = keys.length - 1;
 491: slider.value = 2020;
 492: 
 493: // 値の表示
 494: const display = document.getElementById('valueDisplay');
 495: function updateDisplay() {
 496:     const index = parseInt(slider.value);
 497:     display.innerHTML = keys[index] + '年';
 498: 
 499:     drawPopulationPyramid(keys[index]);
 500: }
 501: slider.addEventListener('input', updateDisplay);
 502: 
 503: // 初期表示;1秒待ってから表示
 504: new Promise(function(resolve) {
 505:     setTimeout(function() {
 506:         resolve();
 507:     }, 1000);
 508: }).then(function() {
 509:     updateDisplay();
 510: });
 511: </script>
 512: 
 513: EOT;
 514: 
 515:     return $html;
 516: }

ユーザー関数 makeSlider は、西暦年を指定するスライダーを生成するユーザー関数である。
スライダーは、スライダー本体を構築する html 文と、スライダーを動かしたときのインタラクティブなイベントを指定する JavaScript 部分に分かれる。

スライダーが動いたときに発火する JavaScript 関数が updateDisplay である。スライダーから西暦年を取りだし、前述のユーザー関数ユーザー関数 makeJavaScriptDrawPopulationPyramid で生成した JavaScript 関数 drawPopulationPyramid を呼び出す。こうすることで、グラフの再描画はクライアントサイド(JavaScript)で完結する。
なお、前述のユーザー関数 makeJavaScriptDrawPopulationPyramid が PHP の連想配列 [$population] を JavaScript のオブジェクト配列 population に変換する時間を担保する意味で、初回表示は Promise を使って1秒間待つようにしておいた。

解説:メイン・プログラム

shiftPopulationPyramid.php

 580: // メイン・プログラム ======================================================
 581: 
 582: $populations = array();
 583: $errmsg = '';
 584: 
 585: // 1920~1994年のデータ取得
 586: getPopulationDataFromCSV(POPULATION_CSV, $populations);
 587: // 1995年以降のデータ取得
 588: getPopulationDataFromEstat(ESTAT_APPID,  $populations);
 589: 
 590: // 描画用JavaScript生成
 591: $js = makeJavaScriptDrawPopulationPyramid($populations);
 592: 
 593: // スライダー作成
 594: $slider = makeSlider($populations);
 595: 
 596: // 表示HTML作成
 597: $HtmlBody = makeCommonBody($js, $slider, $errmsg);
 598: 
 599: // 表示処理
 600: echo $HtmlHeader;
 601: echo $HtmlBody;
 602: echo $HtmlFooter;
 603: 
 604: 
 605: /*

メイン・プログラムは、これまでの関数を順々に実行すればよい。
  1. 1920~1994年(平成6年)のデータ取得
  2. 1995年(平成7年)以降のデータ取得
  3. 描画用JavaScript生成
  4. スライダー作成
  5. 表示HTML作成
  6. 表示処理

参考サイト

(この項おわり)
header