目次
サンプル・プログラムの実行例
サンプル・プログラム
| viewChronologic.php | サンプル・プログラム本体 |
| chronologic.xml | 年表ファイル |
| .pahooEnv | クラウドサービスを利用するためのアカウント情報などを記入する .env ファイル。 使い方は「各種クラウド連携サービス(WebAPI)の登録方法」を参照。include_path が通ったディレクトリに配置すること。 |
| pahooInputData.php | データ入力に関わる関数群。 使い方は「数値入力とバリデーション」「文字入力とバリデーション」などを参照。include_path が通ったディレクトリに配置すること。 |
| バージョン | 更新日 | 内容 |
|---|---|---|
| 2.3.1 | 2025/10/02 | 不具合修正 |
| 2.3.0 | 2025/09/06 | .pahooEnv導入 |
| 2.2 | 2022/04/09 | PHP8対応,リファラ・チェック改良 |
| 2.1 | 2017/10/08 | 年表地図では重複イベントをスキップ |
| 2.0 | 2017/10/07 | 年表地図を追加 |
| バージョン | 更新日 | 内容 |
|---|---|---|
| 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() 追加 |
準備:PHP の https対応
Windowsでは、"php.ini" の下記の行を有効化する。
extension=php_openssl.dllLinuxでは --with-openssl=/usr オプションを付けて再ビルドする。→OpenSSLインストール手順
これで準備は完了だ。
準備:pahooInputData 関数群
また、各種クラウドサービスに登録したときに取得するアカウント情報、アプリケーションパスワードなどを登録した .pahooEnv ファイルから読み込む関数 pahooLoadEnv を備えている。こちらについては、「各種クラウド連携サービス(WebAPI)の登録方法」をご覧いただきたい。
SVGとは何か
公開当初はサポートしているブラウザが少なく、描画にはアドインが必要だったりしたことから普及が進まなかったが、HTML5ブラウザがサポートしたことからブレイク。アニメーション機能も備えていることから、Flashコンテンツの代替もできる。
ベクター描画であることから、画像を拡大縮小してもジャギーがあらわれないことが大きなメリットである。また、テキストの配置座標も細かく指定できるので、ブラウザによって表やグラフのレイアウトが崩れるという心配もない。
今回は利用していないが、レイヤー機能も備えている。
ぱふぅ家のホームページでは、年表のほか、当コーナーで表示しているXMLデータ構造やフローチャートをSVGを使って描画している。
準備:各種定数など
YahooParse.php
54: // 各種定数(START) ===========================================================
55:
56: // 表示幅(ピクセル)
57: define('WIDTH', 600);
58:
59: // Yahoo! JAPAN Webサービス アプリケーションID【各自で設定】
60: // 取得方法:https://www.pahoo.org/e-soul/webtech/php06/php06-01-02.shtm#Yahoo
61: if (isset($_ENV['PAHOO_YAHOO_APPLICATION_ID'])) {
62: define('YAHOO_APPLICATION_ID', $_ENV['PAHOO_YAHOO_APPLICATION_ID']);
63: } else {
64: define('YAHOO_APPLICATION_ID', '');
65: }
66:
67: // リクエストURL【変更不可】
68: define('YAHOO_MAService_URL', 'https://jlp.yahooapis.jp/MAService/V2/parse');
69:
70: // 解析テキスト(初期値)
71: define('DEF_SOUR', "Yahoo!JAPANの「日本語形態素解析」は、日本語文を形態素に分割し、品詞、読みがななどの情報を取得できるWebAPIである。\nサーバサイドで利用できる形態素解析は、「PHPとKAKASIを使って単語に分解する」で紹介した「KAKASI」や、「ChaSen」、「MeCab」が有名であるが、サーバに負荷がかかる処理である。この「日本語形態素解析」は処理速度も速く、サーバの負荷分散という意味では有用なWebAPIだ。");
72:
73: // 各種定数(END) ===============================================================
この他、年表の幅や、表示フォント・ファミリーなど、各種の初期設定を定数に記述している。
XMLファイルに記述したリンク先ファイルは、定数 URL_PAHOO にあることを想定している。適宜書き換えていただきたい。
また、Googleマップに立てるマーカーは、定数 PATH_MARKER にあることを想定している。こちらも必要に応じて書き換えていただきたい。
Googleマップを利用するために Google Cloud Platform APIキー が必要で、その入手方法は「Google Cloud Platform - WebAPIの登録方法」を参照されたい。
解説:XMLファイル
解説:配列へ格納
viewChronologic.php
182: /**
183: * 年表:XMLデータを配列へ格納
184: * @param object $xml 年表:XMLデータ
185: * @param array $items 格納する配列
186: * @return int 格納件数
187: */
188: function xml2array($xml, &$items) {
189: $cnt = 0;
190: foreach ($xml->record as $elm) {
191: $items[$cnt]['caption'] = '';
192: $items[$cnt]['label'] = '';
193: $items[$cnt]['event'] = '';
194: $items[$cnt]['caption'] =
195: isset($elm->caption) ? (string)$elm->caption : '';
196: $items[$cnt]['description'] =
197: isset($elm->description) ? (string)$elm->description : '';
198: if (isset($elm->label)) $ss = (string)$elm->label;
199: else if (isset($elm->event)) $ss = (string)$elm->event;
200: else if (isset($elm->name)) $ss = (string)$elm->name;
201: $items[$cnt]['label'] = isset($ss) ? $ss : '';
202: $items[$cnt]['start'] = isset($elm->start) ? (string)$elm->start : '';
203: $items[$cnt]['finish'] = isset($elm->finish) ? (string)$elm->finish : '';
204: $items[$cnt]['filename'] = isset($elm->filename) ? (string)$elm->filename : '';
205: $items[$cnt]['latitude'] = isset($elm->latitude) ? (double)$elm->latitude : '';
206: $items[$cnt]['longitude'] = isset($elm->longitude) ? (double)$elm->longitude : '';
207: $items[$cnt]['icon'] = isset($elm->icon) ? (string)$elm->icon : '';
208: $cnt++;
209: }
210:
211: return $cnt;
212: }
解説:1つのイベントを作成
viewChronologic.php
380: /**
381: * 1つのイベントを作成する
382: * @param array $item 年代記の要素
383: * @param int $y Y座標
384: * @return string SVG文字列
385: */
386: function mkchronologic_sub($item, $y, $id) {
387: global $ChronoPixelYear, $ChronoStart, $ChronoFont;
388:
389: $font_size = 12; // 描画テキストのフォント・サイズ
390: $start = __chronoyear($item['start']);
391: $finish = __chronoyear($item['finish']);
392:
393: // $x1:バーの左端X座表 $width:バーの幅
394: // $x2:開始年のX座標 $x3:終了年のX座標
395: $x1 = (int)(CHRONOLOGIC_LEFT + ($start - $ChronoStart) * $ChronoPixelYear);
396: if ($start == $finish) {
397: $width = 5;
398: $item['start'] = '';
399: $x1 -= 5;
400: $x2 = $x1 + 5;
401: } else {
402: $width = (int)(($finish - $start) * $ChronoPixelYear);
403: $x2 = $x1 - 5;
404: }
405: $x3 = $x1 + $width + 5;
406:
407: // イベントのラベル
408: if ($item['label'] != '') $label = $item['label'];
409: else if ($item['caption'] != '') $label = $item['caption'];
410: else $label = '';
411:
412: // ラベルの配置 $x4:ラベルのX座標
413: $len = mb_strlen($label);
414: // バーの中
415: if ($len * $font_size < $width) {
416: $x4 = (int)($x1 + ($width / 2));
417: $anchor = 'middle';
418: // バーの左
419: } else if ($item['start'] == '') {
420: $x4 = $x2 - $font_size * mb_strlen($item['start']) - 10;
421: $anchor = 'end';
422: } else {
423: $x4 = $x2 - $font_size * mb_strlen($item['start']);
424: $anchor = 'end';
425: }
426: // バーの右
427: if ($x4 - $len * $font_size <= 0) {
428: $x4 = $x3 + $font_size * mb_strlen($item['finish']);
429: $anchor = 'start';
430: }
431: $y1 = $y + 5;
432: $height = CHRONOLOGIC_EVENT_HEIGHT - 10;
433: $y2 = (int)($y1 + $height / 1.3);
434:
435: // ラベルとリンク
436: if (preg_match('/\.xml/', $item['filename']) > 0) {
437: $link = URL_PAHOO . preg_replace('/\.xml/', '.shtm', $item['filename']);
438: $color = '#0000FF';
439: $tooltip = 'サイト内リンク';
440: } else if ($label != '') {
441: $link = 'https://www.google.co.jp/search?q=' . urlencode($label);
442: $color = '#333333';
443: $tooltip = '外部リンク';
444: } else {
445: $link = '';
446: $color = '#000000';
447: }
448: if ($label != '') {
449: if ($link == '') {
450: $link =<<< EOT
451: <text x="{$x4}" y="{$y2}" font-family="{$ChronoFont}" font-size="{$font_size}" fill="{$color}" text-anchor="{$anchor}" >{$label}</text>
452:
453: EOT;
454: } else {
455: $link =<<< EOT
456: <a xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="{$link}">
457: <text x="{$x4}" y="{$y2}" font-family="{$ChronoFont}" font-size="{$font_size}" fill="{$color}" text-anchor="{$anchor}" onmousemove="ShowTooltip(evt, '{$tooltip}', {$x1}, {$y1}, '')" onmouseout="HideTooltip(evt)" >{$label}</text>
458: </a>
459:
460: EOT;
461: }
462: }
463:
464: // バーとツールチップ
465: if ($item['description'] != '') $tooltip = $item['description'];
466: else $tooltip = '';
467: $str = sprintf('rect%04d', $id);
468: if ($tooltip == '') {
469: $bar =<<< EOT
470: <rect id="{$str}" x="{$x1}" y="{$y1}" width="{$width}" height="{$height}" fill="#FFBB00" stroke="none" />
471:
472: EOT;
473: } else {
474: $bar =<<< EOT
475: <rect id="{$str}" x="{$x1}" y="{$y1}" width="{$width}" height="{$height}" fill="#FFBB00" stroke="none" onmousemove="ShowTooltip(evt, '{$tooltip}', {$x1}, {$y1}, '{$str}')" onmouseout="HideTooltip(evt)" />
476:
477: EOT;
478: }
479:
480: $svg =<<< EOT
481: {$bar}
482: <text x="{$x2}" y="{$y2}" font-family="{$ChronoFont}" font-size="{$font_size}" fill="black" text-anchor="end">{$item['start']}</text>
483: <text x="{$x3}" y="{$y2}" font-family="{$ChronoFont}" font-size="{$font_size}" fill="black" text-anchor="start">{$item['finish']}</text>
484: {$link}
485:
486: EOT;
487:
488: return $svg;
489: }
まず、読み込んだデータには、たとえば生年が不明だったり、まだ存命の人物の場合には開始年/終了年が入っていない。そこで、ユーザー関数 __chronoyear を呼び出して、描画用のための仮年号を取得する。
次に、各要素の描画X座標 $x1~$x3 やバーの幅 $width や高さ $height を計算する。
バーが短いときは、ラベルはバーの左側に配置。左側に配置して表枠外にはみ出すようだったら、バーの右側に配置するように座標計算する。
次にラベルを、SVGの text要素を使って描く。text-anchor属性を併用することで、バーに対してテキストを揃えている。
また、リンク先がある場合は、a要素を使ってハイパーリンクを張っている。
バーは、SVGの rect要素を使って描く。
description情報がある場合は、ツールチップを表示するようにした。ツールチップはJavaScriptで実装しており、「How to create an SVG “tooltip”-like box?」(Stack Overflow)の回答を参考にした。
最後に開始年と終了年を、SVGの text要素を使って描く。
解説:横軸を作成
viewChronologic.php
314: /**
315: * 横軸を作成する
316: * @param array $items 年代記の要素
317: * @param int $width 年代記の横幅(単位:ピクセル,200以上)
318: * @return string SVG文字列
319: */
320: function mkscale($items, $width, $height) {
321: global $ChronoPixelYear, $ChronoStart, $ChronoFont;
322:
323: // 年の範囲
324: $year_min = +99999;
325: $year_max = -99999;
326: foreach ($items as $item) {
327: $year = __chronoyear($item['start']);
328: if ($year < $year_min) $year_min = $year;
329: $year = __chronoyear($item['finish']);
330: if ($year > $year_max) $year_max = $year;
331: }
332:
333: // 年の範囲:丸め
334: $table = array(
335: array(1, 5),
336: array(2, 10),
337: array(5, 25),
338: array(10, 50),
339: array(15, 75),
340: array(25, 125),
341: array(50, 250),
342: array(100, 500),
343: array(150, 750),
344: array(200, 1000),
345: array(300, 1500),
346: array(400, 2000)
347: );
348: $period = $year_max - $year_min + 1;
349: foreach ($table as $arr) {
350: $delta = ($arr[0] <= 25) ? $arr[0] : 25;
351: if ($period <= $arr[1]) {
352: $interval = $arr[0];
353: $start = round($year_min / $interval) * $interval - $delta;
354: $finish = round($year_max / $interval) * $interval + $delta;
355: break;
356: }
357: }
358: $ChronoPixelYear = ($width - 80) / ($finish - $start);
359: $ChronoStart = $start;
360:
361: $svg =<<< EOT
362: <rect x="0" y="0" width="{$width}" height="{$height}" fill="none" stroke="#FFBB00" stroke-width="3" />
363: <rect x="0" y="0" width="{$width}" height="30" fill="#FFBB00" stroke="none" />
364:
365: EOT;
366: for ($year = $start; $year <= $finish; $year += $interval) {
367: $x1 = (int)(CHRONOLOGIC_LEFT + ($year - $start) * $ChronoPixelYear);
368: $x2 = $x1 - 15;
369: $y1 = 30;
370: $y2 = $height;
371: $svg .=<<< EOT
372: <text x="{$x2}" y="25" font-family="{$ChronoFont}" font-size="14" fill="black" >{$year}</text>
373: <line x1="{$x1}" y1="{$y1}" x2="{$x1}" y2="{$y2}" stroke="#FFDD88" stroke-width="1" />
374:
375: EOT;
376: }
377: return $svg;
378: }
解説:年表を作成
viewChronologic.php
214: /**
215: * 年表を作成する
216: * @param array $info 年代記の要素
217: * string $items[]['caption'] = 棒グラフ上のキャプション
218: * string $items[]['label'] = 棒グラフ上のラベル
219: * int $items[]['start'] = 開始年(生年)(必須)
220: * int $items[]['finish'] = 終了年(没年)(必須)
221: * string $items[]['filename'] = リンク先ファイル名(XML)
222: * @return string SVG文字列
223: */
224: function get_chronologic($items) {
225: $width = CHRONOLOGIC_WIDTH;
226: $height = count($items) * CHRONOLOGIC_EVENT_HEIGHT + 80;
227: $scale = mkscale($items, $width, $height); // 横軸作成
228: $table = mkchronologic($items, $width); // 年表作成
229:
230: $svg =<<< EOT
231: <!-- 年表 -->
232: <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" onload="init(evt)" width="{$width}" height="{$height}">
233: <style>
234: .caption {
235: font-size: 14px;
236: font-family: Georgia, serif;
237: }
238: .tooltip {
239: font-size: 12px;
240: }
241: .tooltip_bg {
242: fill: white;
243: stroke: black;
244: stroke-width: 1;
245: opacity: 0.85;
246: }
247: </style>
248:
249: <script type="text/ecmascript">
250: <![CDATA[
251: function init(evt) {
252: if (window.svgDocument == null) {
253: svgDocument = evt.target.ownerDocument;
254: }
255: tooltip = svgDocument.getElementById('tooltip');
256: tooltip_bg = svgDocument.getElementById('tooltip_bg');
257: }
258:
259: function ShowTooltip(evt, mouseovertext, x, y, id) {
260: tooltip.setAttributeNS(null,"x",evt.clientX + 11);
261: tooltip.setAttributeNS(null,"y",y + 27);
262: tooltip.firstChild.data = mouseovertext;
263: tooltip.setAttributeNS(null,"visibility","visible");
264:
265: length = tooltip.getComputedTextLength();
266: tooltip_bg.setAttributeNS(null,"width",length + 8);
267: tooltip_bg.setAttributeNS(null,"x",evt.clientX + 8);
268: tooltip_bg.setAttributeNS(null,"y",y + 14);
269: tooltip_bg.setAttributeNS(null,"visibility","visibile");
270: }
271:
272: function HideTooltip(evt) {
273: tooltip.setAttributeNS(null,"visibility","hidden");
274: tooltip_bg.setAttributeNS(null,"visibility","hidden");
275: }
276: ]]>
277: </script>
278:
279: {$scale}
280: {$table}
281:
282: <rect class="tooltip_bg" id="tooltip_bg"
283: x="0" y="0" rx="4" ry="4"
284: width="55" height="17" visibility="hidden"/>
285: <text class="tooltip" id="tooltip"
286: x="0" y="0" visibility="hidden">Tooltip</text>
287: </svg>
288:
289: EOT;
290:
291: return $svg;
292: }
ユーザー関数 mkscale と mkchronologic を呼び出し年表を描画する。
また、ツールチップを表示するためのJavaScriptも記述してある。
解説:年表地図を作成
viewChronologic.php
517: /**
518: * 年表地図を作成する
519: * @param array $info 年代記の要素
520: * string $items[]['caption'] = 棒グラフ上のキャプション
521: * string $items[]['label'] = 棒グラフ上のラベル
522: * int $items[]['start'] = 開始年(生年)(必須)
523: * int $items[]['finish'] = 終了年(没年)(必須)
524: * double $items[]['latitude'] = 緯度
525: * double $items[]['longitude'] = 経度
526: * string $items[]['icon'] = マーカー・ファイル名(拡張子は除く)
527: * double $items[]['description'] = 記事
528: * string $items[]['filename'] = リンク先ファイル名(XML)
529: * @return string HTMLテキスト
530: */
531: function get_chronologicalmap($items) {
532: global $CountGoogleMaps;
533:
534: $apikey = GOOGLE_API_KEY;
535: $width = CHRONOLOGICMAP_WIDTH;
536: $height = CHRONOLOGICMAP_HEIGHT;
537: $mode = 'ROADMAP';
538: $zoom = 1;
539: $lat = '';
540: $lng = '';
541: $js = ($CountGoogleMaps > 1) ? '' :
542: "<script type=\"text/javascript\" src=\"https://maps.google.com/maps/api/js?key={$apikey}&region=JP\"></script>";
543:
544: $flag = FALSE;
545: $str = '';
546: $arrs = array();
547: foreach ($items as $item) {
548: if (($item['latitude'] != '') && ($item['longitude'] != '')) {
549: $key = $item['latitude'] .',' . $item['longitude'];
550: if (! $flag) {
551: // 中心座標
552: $lat = (double)$item['latitude'];
553: $lng = (double)$item['longitude'];
554: $flag = TRUE;
555: }
556: // ラベルとリンク
557: if (preg_match('/\.xml/', $item['filename']) > 0) {
558: $link = URL_PAHOO . preg_replace('/\.xml/', '.shtm', $item['filename']);
559: $color = '#0000FF';
560: $tooltip = 'サイト内リンク';
561: } else if ($label != '') {
562: $link = 'https://www.google.co.jp/search?q=' . urlencode($label);
563: $color = '#333333';
564: $tooltip = '外部リンク';
565: } else {
566: $link = '';
567: $color = '#000000';
568: }
569: // 年号
570: $year = ($item['start'] == $item['finish']) ? $item['start']:
571: $item['start'] . ' - ' . $item['finish'];
572: // マーカー
573: $marker = (isset($item['icon']) && ($item['icon'] != '')) ? PATH_MARKER . $item['icon'] .'.png' : PATH_MARKER . 'history.png';
574: // 配列へ代入
575: $arrs[$key]['latitude'] = $item['latitude'];
576: $arrs[$key]['longitude'] = $item['longitude'];
577: $arrs[$key]['title'] = $item['label'];
578: $label = ($link == '') ? "<span style=\"color:blue;\"{$item['label']}" : "<a href=\"{$link}\" target=\"_blank\">{$item['label']}</a>";
579: $description = ($item['description'] == '') ? '' : "‥‥{$item['description']}";
580: $content = "<p style=\"text-align:left;\">{$label} ({$year}年)<span style=\"font-size:80%;\">{$description}</span></p>";
581: // 重複イベントはスキップ
582: if (isset($arrs[$key]['content'])) {
583: if (mb_strstr($arrs[$key]['content'], $content) == FALSE) {
584: $arrs[$key]['content'] .= $content;
585: }
586: } else {
587: $arrs[$key]['content'] = $content;
588: }
589: $arrs[$key]['marker'] = $marker;
590: }
591: }
592:
593: // JavaScript生成
594: $str = '';
595: $n = 1;
596: foreach ($arrs as $arr) {
597: $str .= <<< EOT
598: var icon_{$n} = new google.maps.MarkerImage('{$arr['marker']}');
599: var marker_{$n} = new google.maps.Marker({
600: position: new google.maps.LatLng({$arr['latitude']}, {$arr['longitude']}),
601: map: map,
602: icon: icon_{$n},
603: title: '{$arr['title']}',
604: zIndex: 250
605: });
606: var infowindow_{$n} = new google.maps.InfoWindow({
607: content: '{$arr['content']}',
608: size: new google.maps.Size(200, 100)
609: });
610: google.maps.event.addListener(marker_{$n}, 'click', function() {
611: infowindow_{$n}.open(map, marker_{$n});
612: });
613:
614: EOT;
615: $n++;
616: }
617: // 地図情報がない
618: if (($lat == '') || ($lng == '')) return '';
619:
620: $dest =<<< EOT
621: {$js}
622: <script type="text/javascript">
623: <!--
624: google.maps.event.addDomListener(window, 'load', function() {
625: var mapdiv = document.getElementById('gmap{$CountGoogleMaps}');
626: var myOptions = {
627: zoom: {$zoom},
628: center: new google.maps.LatLng({$lat}, {$lng}),
629: mapTypeId: google.maps.MapTypeId.{$mode},
630: mapTypeControl: false,
631: scaleControl: true
632: };
633: var map = new google.maps.Map(mapdiv, myOptions);
634: {$str}
635:
636: });
637: -->
638: </script>
639: <div style="margin-bottom:20px;">
640: <div id="gmap{$CountGoogleMaps}" style="width:{$width}px; height:{$height}px;">
641: </div>
642: </div>
643:
644: EOT;
645: $CountGoogleMaps++;
646:
647: return $dest;
648: }
GooleMaps API の使い方は「PHPで地図で指定した場所の天気予報を求める」などで解説しているので、あわせてご覧いただきたい。
冒頭で、Googleマップの縦・横のサイズと、モード $mode、ズーム $zoom を指定している。
次に、引数として与えられた年表情報配列 $items を1つずつ解析していく。
地図の中心は、最初の要素の位置情報を採用する。
参考サイト
- SVG 1.1 仕様 (第2版) 日本語訳
- svg要素でベクターグラフィクスを埋め込もう:ほんっとにはじめてのHTML5とCSS3
- SVGで線、円、矩形を描画する:Symfoware
- SVGテスト用ひな形とプレビュー(基本図形):スタンダード・デザインラボ

今回は、この部分のPHPプログラムを紹介する。
2017年(平成29年)10月、世界のどこでイベントが発生したかを俯瞰できるよう、Googleマップ上にマーカーを立てる機能を追加した。
(2025年10月3日)不具合修正
(2025年9月15日).pahooEnv導入