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

サンプル・プログラム
viewChronologic.php | サンプル・プログラム本体 |
chronologic.xml | 年表ファイル |
SVGとは何か

公開当初はサポートしているブラウザが少なく、描画にはアドインが必要だったりしたことから普及が進まなかったが、HTML5ブラウザがサポートしたことからブレイク。アニメーション機能も備えていることから、Flashコンテンツの代替もできる。

ベクター描画であることから、画像を拡大縮小してもジャギーがあらわれないことが大きなメリットである。また、テキストの配置座標も細かく指定できるので、ブラウザによって表やグラフのレイアウトが崩れるという心配もない。
今回は利用していないが、レイヤー機能も備えている。

ぱふぅ家のホームページでは、年表のほか、当コーナーで表示しているXMLデータ構造やフローチャートをSVGを使って描画している。
準備
34: //年表ファイル
35: define('FILE_CHRONOLOGIC', 'chronologic.xml');
36:
37: //年表(幅)
38: define('CHRONOLOGIC_WIDTH', 600);
39:
40: //インデント:左側
41: define('CHRONOLOGIC_LEFT', 30);
42:
43: //イベントの高さ
44: define('CHRONOLOGIC_EVENT_HEIGHT', 34);
45:
46: //年表地図(幅)
47: define('CHRONOLOGICMAP_WIDTH', 600);
48:
49: //年表地図(高さ)
50: define('CHRONOLOGICMAP_HEIGHT', 350);
51:
52: //ぱふぅ家のホームページ家のリンク・パス
53: define('URL_PAHOO', 'https://www.pahoo.org/culture/numbers/year/');
54:
55: //Googleマップ用マーカー・ファイルのパス;png限定
56: define('PATH_MARKER', 'https://www.pahoo.org/common/marker/');
57:
58: //Goole APIキー;各自の環境に合わせてください
59: define('GOOGLE_API_KEY', '*******************************************');
60:
61: //GoogleMapsの数
62: $CountGoogleMaps = 1;
63:
64: /**
65: * 表示フォントファミリー
66: * @global double $ChronoFont
67: */
68: $ChronoFont = 'sans-serif';
69:
70: /**
71: * ピクセル/年
72: * @global double $ChronoPixelYear
73: */
74: $ChronoPixelYear = 0.0;
75:
76: /**
77: * 開始年
78: * @global int $ChronoStart
79: */
80: $ChronoStart = 0;
この他、年表の幅や、表示フォント・ファミリーなど、各種の初期設定を定数に記述している。

XMLファイルに記述したリンク先ファイルは、定数 URL_PAHOO にあることを想定している。適宜書き換えていただきたい。
また、Googleマップに立てるマーカーは、定数 PATH_MARKER にあることを想定している。こちらも必要に応じて書き換えていただきたい。

Googleマップを利用するために Google Cloud Platform APIキー が必要で、その入手方法は「Google Cloud Platform - WebAPIの登録方法」を参照されたい。
解説:XMLファイル
解説:配列へ格納
174: /**
175: * 年表:XMLデータを配列へ格納
176: * @param object $xml 年表:XMLデータ
177: * @param array $items 格納する配列
178: * @return int 格納件数
179: */
180: function xml2array($xml, &$items) {
181: $cnt = 0;
182: foreach ($xml->record as $elm) {
183: $items[$cnt]['caption'] = '';
184: $items[$cnt]['label'] = '';
185: $items[$cnt]['event'] = '';
186: $items[$cnt]['caption'] =
187: isset($elm->caption) ? (string)$elm->caption : '';
188: $items[$cnt]['description'] =
189: isset($elm->description) ? (string)$elm->description : '';
190: if (isset($elm->label)) $ss = (string)$elm->label;
191: else if (isset($elm->event)) $ss = (string)$elm->event;
192: else if (isset($elm->name)) $ss = (string)$elm->name;
193: $items[$cnt]['label'] = isset($ss) ? $ss : '';
194: $items[$cnt]['start'] = isset($elm->start) ? (string)$elm->start : '';
195: $items[$cnt]['finish'] = isset($elm->finish) ? (string)$elm->finish : '';
196: $items[$cnt]['filename'] = isset($elm->filename) ? (string)$elm->filename : '';
197: $items[$cnt]['latitude'] = isset($elm->latitude) ? (double)$elm->latitude : '';
198: $items[$cnt]['longitude'] = isset($elm->longitude) ? (double)$elm->longitude : '';
199: $items[$cnt]['icon'] = isset($elm->icon) ? (string)$elm->icon : '';
200: $cnt++;
201: }
202:
203: return $cnt;
204: }
解説:1つのイベントを作成
372: /**
373: * 1つのイベントを作成する
374: * @param array $item 年代記の要素
375: * @param int $y Y座標
376: * @return string SVG文字列
377: */
378: function mkchronologic_sub($item, $y, $id) {
379: global $ChronoPixelYear, $ChronoStart, $ChronoFont;
380:
381: $font_size = 12; //描画テキストのフォント・サイズ
382: $start = __chronoyear($item['start']);
383: $finish = __chronoyear($item['finish']);
384:
385: //$x1:バーの左端X座表 $width:バーの幅
386: //$x2:開始年のX座標 $x3:終了年のX座標
387: $x1 = (int)(CHRONOLOGIC_LEFT + ($start - $ChronoStart) * $ChronoPixelYear);
388: if ($start == $finish) {
389: $width = 5;
390: $item['start'] = '';
391: $x1 -= 5;
392: $x2 = $x1 + 5;
393: } else {
394: $width = (int)(($finish - $start) * $ChronoPixelYear);
395: $x2 = $x1 - 5;
396: }
397: $x3 = $x1 + $width + 5;
398:
399: //イベントのラベル
400: if ($item['label'] != '') $label = $item['label'];
401: else if ($item['caption'] != '') $label = $item['caption'];
402: else $label = '';
403:
404: //ラベルの配置 $x4:ラベルのX座標
405: $len = mb_strlen($label);
406: //バーの中
407: if ($len * $font_size < $width) {
408: $x4 = (int)($x1 + ($width / 2));
409: $anchor = 'middle';
410: //バーの左
411: } else if ($item['start'] == '') {
412: $x4 = $x2 - $font_size * mb_strlen($item['start']) - 10;
413: $anchor = 'end';
414: } else {
415: $x4 = $x2 - $font_size * mb_strlen($item['start']);
416: $anchor = 'end';
417: }
418: //バーの右
419: if ($x4 - $len * $font_size <= 0) {
420: $x4 = $x3 + $font_size * mb_strlen($item['finish']);
421: $anchor = 'start';
422: }
423: $y1 = $y + 5;
424: $height = CHRONOLOGIC_EVENT_HEIGHT - 10;
425: $y2 = (int)($y1 + $height / 1.3);
426:
427: //ラベルとリンク
428: if (preg_match('/\.xml/', $item['filename']) > 0) {
429: $link = URL_PAHOO . preg_replace('/\.xml/', '.shtm', $item['filename']);
430: $color = '#0000FF';
431: $tooltip = 'サイト内リンク';
432: } else if ($label != '') {
433: $link = 'https://www.google.co.jp/search?q=' . urlencode($label);
434: $color = '#333333';
435: $tooltip = '外部リンク';
436: } else {
437: $link = '';
438: $color = '#000000';
439: }
440: if ($label != '') {
441: if ($link == '') {
442: $link =<<< EOT
443: <text x="{$x4}" y="{$y2}" font-family="{$ChronoFont}" font-size="{$font_size}" fill="{$color}" text-anchor="{$anchor}" >{$label}</text>
444:
445: EOT;
446: } else {
447: $link =<<< EOT
448: <a xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="{$link}">
449: <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>
450: </a>
451:
452: EOT;
453: }
454: }
455:
456: //バーとツールチップ
457: if ($item['description'] != '') $tooltip = $item['description'];
458: else $tooltip = '';
459: $str = sprintf('rect%04d', $id);
460: if ($tooltip == '') {
461: $bar =<<< EOT
462: <rect id="{$str}" x="{$x1}" y="{$y1}" width="{$width}" height="{$height}" fill="#FFBB00" stroke="none" />
463:
464: EOT;
465: } else {
466: $bar =<<< EOT
467: <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)" />
468:
469: EOT;
470: }
471:
472: $svg =<<< EOT
473: {$bar}
474: <text x="{$x2}" y="{$y2}" font-family="{$ChronoFont}" font-size="{$font_size}" fill="black" text-anchor="end">{$item['start']}</text>
475: <text x="{$x3}" y="{$y2}" font-family="{$ChronoFont}" font-size="{$font_size}" fill="black" text-anchor="start">{$item['finish']}</text>
476: {$link}
477:
478: EOT;
479:
480: return $svg;
481: }

まず、読み込んだデータには、たとえば生年が不明だったり、まだ存命の人物の場合には開始年/終了年が入っていない。そこで、ユーザー関数 __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要素を使って描く。
解説:横軸を作成
306: /**
307: * 横軸を作成する
308: * @param array $items 年代記の要素
309: * @param int $width 年代記の横幅(単位:ピクセル,200以上)
310: * @return string SVG文字列
311: */
312: function mkscale($items, $width, $height) {
313: global $ChronoPixelYear, $ChronoStart, $ChronoFont;
314:
315: //年の範囲
316: $year_min = +99999;
317: $year_max = -99999;
318: foreach ($items as $item) {
319: $year = __chronoyear($item['start']);
320: if ($year < $year_min) $year_min = $year;
321: $year = __chronoyear($item['finish']);
322: if ($year > $year_max) $year_max = $year;
323: }
324:
325: //年の範囲:丸め
326: $table = array(
327: array(1, 5),
328: array(2, 10),
329: array(5, 25),
330: array(10, 50),
331: array(15, 75),
332: array(25, 125),
333: array(50, 250),
334: array(100, 500),
335: array(150, 750),
336: array(200, 1000),
337: array(300, 1500),
338: array(400, 2000)
339: );
340: $period = $year_max - $year_min + 1;
341: foreach ($table as $arr) {
342: $delta = ($arr[0] <= 25) ? $arr[0] : 25;
343: if ($period <= $arr[1]) {
344: $interval = $arr[0];
345: $start = round($year_min / $interval) * $interval - $delta;
346: $finish = round($year_max / $interval) * $interval + $delta;
347: break;
348: }
349: }
350: $ChronoPixelYear = ($width - 80) / ($finish - $start);
351: $ChronoStart = $start;
352:
353: $svg =<<< EOT
354: <rect x="0" y="0" width="{$width}" height="{$height}" fill="none" stroke="#FFBB00" stroke-width="3" />
355: <rect x="0" y="0" width="{$width}" height="30" fill="#FFBB00" stroke="none" />
356:
357: EOT;
358: for ($year = $start; $year <= $finish; $year += $interval) {
359: $x1 = (int)(CHRONOLOGIC_LEFT + ($year - $start) * $ChronoPixelYear);
360: $x2 = $x1 - 15;
361: $y1 = 30;
362: $y2 = $height;
363: $svg .=<<< EOT
364: <text x="{$x2}" y="25" font-family="{$ChronoFont}" font-size="14" fill="black" >{$year}</text>
365: <line x1="{$x1}" y1="{$y1}" x2="{$x1}" y2="{$y2}" stroke="#FFDD88" stroke-width="1" />
366:
367: EOT;
368: }
369: return $svg;
370: }
解説:年表を作成
206: /**
207: * 年表を作成する
208: * @param array $info 年代記の要素
209: * string $items[]['caption'] = 棒グラフ上のキャプション
210: * string $items[]['label'] = 棒グラフ上のラベル
211: * int $items[]['start'] = 開始年(生年)(必須)
212: * int $items[]['finish'] = 終了年(没年)(必須)
213: * string $items[]['filename'] = リンク先ファイル名(XML)
214: * @return string SVG文字列
215: */
216: function get_chronologic($items) {
217: $width = CHRONOLOGIC_WIDTH;
218: $height = count($items) * CHRONOLOGIC_EVENT_HEIGHT + 80;
219: $scale = mkscale($items, $width, $height); //横軸作成
220: $table = mkchronologic($items, $width); //年表作成
221:
222: $svg =<<< EOT
223: <!-- 年表 -->
224: <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" onload="init(evt)" width="{$width}" height="{$height}">
225: <style>
226: .caption {
227: font-size: 14px;
228: font-family: Georgia, serif;
229: }
230: .tooltip {
231: font-size: 12px;
232: }
233: .tooltip_bg {
234: fill: white;
235: stroke: black;
236: stroke-width: 1;
237: opacity: 0.85;
238: }
239: </style>
240:
241: <script type="text/ecmascript">
242: <![CDATA[
243: function init(evt) {
244: if (window.svgDocument == null) {
245: svgDocument = evt.target.ownerDocument;
246: }
247: tooltip = svgDocument.getElementById('tooltip');
248: tooltip_bg = svgDocument.getElementById('tooltip_bg');
249: }
250:
251: function ShowTooltip(evt, mouseovertext, x, y, id) {
252: tooltip.setAttributeNS(null,"x",evt.clientX + 11);
253: tooltip.setAttributeNS(null,"y",y + 27);
254: tooltip.firstChild.data = mouseovertext;
255: tooltip.setAttributeNS(null,"visibility","visible");
256:
257: length = tooltip.getComputedTextLength();
258: tooltip_bg.setAttributeNS(null,"width",length + 8);
259: tooltip_bg.setAttributeNS(null,"x",evt.clientX + 8);
260: tooltip_bg.setAttributeNS(null,"y",y + 14);
261: tooltip_bg.setAttributeNS(null,"visibility","visibile");
262: }
263:
264: function HideTooltip(evt) {
265: tooltip.setAttributeNS(null,"visibility","hidden");
266: tooltip_bg.setAttributeNS(null,"visibility","hidden");
267: }
268: ]]>
269: </script>
270:
271: {$scale}
272: {$table}
273:
274: <rect class="tooltip_bg" id="tooltip_bg"
275: x="0" y="0" rx="4" ry="4"
276: width="55" height="17" visibility="hidden"/>
277: <text class="tooltip" id="tooltip"
278: x="0" y="0" visibility="hidden">Tooltip</text>
279: </svg>
280:
281: EOT;
282:
283: return $svg;
284: }
ユーザー関数 mkscale と mkchronologic を呼び出し年表を描画する。

また、ツールチップを表示するためのJavaScriptも記述してある。
解説:年表地図を作成
509: /**
510: * 年表地図を作成する
511: * @param array $info 年代記の要素
512: * string $items[]['caption'] = 棒グラフ上のキャプション
513: * string $items[]['label'] = 棒グラフ上のラベル
514: * int $items[]['start'] = 開始年(生年)(必須)
515: * int $items[]['finish'] = 終了年(没年)(必須)
516: * double $items[]['latitude'] = 緯度
517: * double $items[]['longitude'] = 経度
518: * string $items[]['icon'] = マーカー・ファイル名(拡張子は除く)
519: * double $items[]['description'] = 記事
520: * string $items[]['filename'] = リンク先ファイル名(XML)
521: * @return string HTMLテキスト
522: */
523: function get_chronologicalmap($items) {
524: global $CountGoogleMaps;
525:
526: $apikey = GOOGLE_API_KEY;
527: $width = CHRONOLOGICMAP_WIDTH;
528: $height = CHRONOLOGICMAP_HEIGHT;
529: $mode = 'ROADMAP';
530: $zoom = 1;
531: $lat = '';
532: $lng = '';
533: $js = ($CountGoogleMaps > 1) ? '' :
534: "<script type=\"text/javascript\" src=\"https://maps.google.com/maps/api/js?key={$apikey}&region=JP\"></script>";
535:
536: $flag = FALSE;
537: $str = '';
538: $arrs = array();
539: foreach ($items as $item) {
540: if (($item['latitude'] != '') && ($item['longitude'] != '')) {
541: $key = $item['latitude'] .',' . $item['longitude'];
542: if (! $flag) {
543: //中心座標
544: $lat = (double)$item['latitude'];
545: $lng = (double)$item['longitude'];
546: $flag = TRUE;
547: }
548: //ラベルとリンク
549: if (preg_match('/\.xml/', $item['filename']) > 0) {
550: $link = URL_PAHOO . preg_replace('/\.xml/', '.shtm', $item['filename']);
551: $color = '#0000FF';
552: $tooltip = 'サイト内リンク';
553: } else if ($label != '') {
554: $link = 'https://www.google.co.jp/search?q=' . urlencode($label);
555: $color = '#333333';
556: $tooltip = '外部リンク';
557: } else {
558: $link = '';
559: $color = '#000000';
560: }
561: //年号
562: $year = ($item['start'] == $item['finish']) ? $item['start']:
563: $item['start'] . ' - ' . $item['finish'];
564: //マーカー
565: $marker = (isset($item['icon']) && ($item['icon'] != '')) ? PATH_MARKER . $item['icon'] .'.png' : PATH_MARKER . 'history.png';
566: //配列へ代入
567: $arrs[$key]['latitude'] = $item['latitude'];
568: $arrs[$key]['longitude'] = $item['longitude'];
569: $arrs[$key]['title'] = $item['label'];
570: $label = ($link == '') ? "<span style=\"color:blue;\"{$item['label']}" : "<a href=\"{$link}\" target=\"_blank\">{$item['label']}</a>";
571: $description = ($item['description'] == '') ? '' : "‥‥{$item['description']}";
572: $content = "<p style=\"text-align:left;\">{$label} ({$year}年)<span style=\"font-size:80%;\">{$description}</span></p>";
573: //重複イベントはスキップ
574: if (isset($arrs[$key]['content'])) {
575: if (mb_strstr($arrs[$key]['content'], $content) == FALSE) {
576: $arrs[$key]['content'] .= $content;
577: }
578: } else {
579: $arrs[$key]['content'] = $content;
580: }
581: $arrs[$key]['marker'] = $marker;
582: }
583: }
584:
585: //JavaScript生成
586: $str = '';
587: $n = 1;
588: foreach ($arrs as $arr) {
589: $str .= <<< EOT
590: var icon_{$n} = new google.maps.MarkerImage('{$arr['marker']}');
591: var marker_{$n} = new google.maps.Marker({
592: position: new google.maps.LatLng({$arr['latitude']}, {$arr['longitude']}),
593: map: map,
594: icon: icon_{$n},
595: title: '{$arr['title']}',
596: zIndex: 250
597: });
598: var infowindow_{$n} = new google.maps.InfoWindow({
599: content: '{$arr['content']}',
600: size: new google.maps.Size(200, 100)
601: });
602: google.maps.event.addListener(marker_{$n}, 'click', function() {
603: infowindow_{$n}.open(map, marker_{$n});
604: });
605:
606: EOT;
607: $n++;
608: }
609: //地図情報がない
610: if (($lat == '') || ($lng == '')) return '';
611:
612: $dest =<<< EOT
613: {$js}
614: <script type="text/javascript">
615: <!--
616: google.maps.event.addDomListener(window, 'load', function() {
617: var mapdiv = document.getElementById('gmap{$CountGoogleMaps}');
618: var myOptions = {
619: zoom: {$zoom},
620: center: new google.maps.LatLng({$lat}, {$lng}),
621: mapTypeId: google.maps.MapTypeId.{$mode},
622: mapTypeControl: false,
623: scaleControl: true
624: };
625: var map = new google.maps.Map(mapdiv, myOptions);
626: {$str}
627:
628: });
629: -->
630: </script>
631: <div style="margin-bottom:20px;">
632: <div id="gmap{$CountGoogleMaps}" style="width:{$width}px; height:{$height}px;">
633: </div>
634: </div>
635:
636: EOT;
637: $CountGoogleMaps++;
638:
639: return $dest;
640: }
GooleMaps API の使い方は「PHPで地図で指定した場所の天気予報を求める」などで解説しているので、あわせてご覧いただきたい。

冒頭で、Googleマップの縦・横のサイズと、モード $mode、ズーム $zoom を指定している。

次に、引数として与えられた年表情報配列 $items を1つずつ解析していく。
地図の中心は、最初の要素の位置情報を採用する。
参考サイト
- SVG 1.1 仕様 (第2版) 日本語訳
- svg要素でベクターグラフィクスを埋め込もう:ほんっとにはじめてのHTML5とCSS3
- SVGで線、円、矩形を描画する:Symfoware
- SVGテスト用ひな形とプレビュー(基本図形):スタンダード・デザインラボ
今回は、この部分のPHPプログラムを紹介する。
2017年(平成29年)10月、世界のどこでイベントが発生したかを俯瞰できるよう、Googleマップ上にマーカーを立てる機能を追加した。
(2022年4月9日)PHP8対応,リファラ・チェック改良