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

サンプル・プログラム
viewPolls.php | サンプル・プログラム本体。 |
polls.xml | 内閣支持率・政党支持率のデータ・ファイル。 |
viewPolls.php | サンプル・プログラム本体。 |
バージョン | 更新日 | 内容 |
---|---|---|
3.5.0 | 2025/05/24 | 2024~2025年のHTML表記ゆれに対応 |
3.4.0 | 2024/05/12 | HTMLヘッダ見直し |
3.3.0 | 2024/03/24 | 2023年8月以降のデータに対応 |
3.2.0 | 2023/05/17 | plotParty()に3政党を追加 |
3.1 | 2021/05/08 | POLLS_FILE へのセーブ/ロード |
解説:準備
viewPolls.php
21: // jqPlotのあるフォルダ
22: define('JQPLOT', '../../../../common/jqplot/');
23:
24: // 描画期間(年)
25: define('PERIOD', 7);
26:
27: // 最古データ(年)
28: define('OLDYEAR', 1998);
29:
30: // グラフを描くDOMオブジェジェクト名
31: define('GRAPH_ID', 'jqPlot_polls');
32:
33: // 表示幅(ピクセル)
34: define('WIDTH', 600);
35:
36: // 表示高さ(ピクセル)
37: define('HEIGHT', 500);
38:
39: // 保存用データファイル名
40: define('POLLS_FILE', './polls.xml');
公式サイト「jqPlot Charts and Graphs for jQuery」から圧縮ファイルをダウンロードし、適当な場所に解凍しておく。その場所を定数 JQPLOT に定義する。

また、描画期間(当年から過去何年分遡るか)を定数 PERIOD に定義しておく。
なお、スクレイピングしたデータはXML形式ファイルとして定数 POLLS_FILE で示すファイルにセーブし、次に描画するときは、このファイルからロードできるようにした。
解説:スクレイピング
viewPolls.php
355: /**
356: * 内閣支持率を解析する
357: * @param int $infp 解析中のURL
358: * @param array $items 解析結果を格納する配列
359: * @return bool TRUE/FALSE
360: */
361: function analyseCabinet($infp, &$items) {
362: $pat1 = "/<h[0-9]+[^>]*>内閣支持率<\/h[0-9]+>/u";
363: $pat2 = "/<tr[^>]*>/u";
364: $pat3 = "/<t[hd][^>]*>(<[^>]+>)?([0-9\-\.]+)/u";
365: $pat31 = "/<td[^>]*>(.+)/u";
366: $pat4 = "/<t[hd][^>]*>支持(<[^>]+>)?する/u";
367: $pat5 = "/<t[hd][^>]*>(<[^>]+>)?支持(<[^>]+>)?しない/u";
368: $pat6 = "/<t[hd][^>]*>内閣/u";
369: $pat61 = "/<\/table>/u";
370: $pat7 = "/<td colspan\=\"([0-9]+)\"[^>]*>([^<]+)/u";
371: $pat8 = "/<td[^>]*>([^<]+)/u";
372: $pat9 = "/<h[0-9]+[^>]*>.*政党支持率<\/h[0-9]+>/u";
373: $flag = 0;
374: while (! feof($infp)) {
375: $instr = trim(fgets($infp));
376: if (($flag == 0) && (preg_match($pat1, $instr) > 0)) {
377: $flag = 1;
378: } else if (($flag == 1) && (preg_match($pat2, $instr) > 0)) {
379: $flag = 2;
380: $i = 0;
381: // 月
382: } else if ($flag == 2) {
383: if (preg_match($pat4, $instr) > 0) {
384: $flag = 3;
385: $i = 0;
386: } else if (preg_match($pat3, $instr, $arr) > 0) {
387: $items[$i]['month'] = $arr[2];
388: $items[$i]['yes'] = '-';
389: $items[$i]['no'] = '-';
390: $items[$i]['cabinet'] = '-';
391: $i++;
392: }
393: // 支持する
394: } else if ($flag == 3) {
395: if (preg_match($pat5, $instr) > 0) {
396: $flag = 4;
397: $i = 0;
398: } else if (preg_match($pat31, $instr, $arr) > 0) {
399: $items[$i]['yes'] = strip_tags($arr[1]);
400: $i++;
401: }
402: // 支持しない
403: } else if ($flag == 4) {
404: if (preg_match($pat31, $instr, $arr) > 0) {
405: $items[$i]['no'] = strip_tags($arr[1]);
406: $i++;
407: } else if (preg_match($pat6, $instr) > 0) {
408: $flag = 5;
409: $i = 0;
410: } else if (preg_match($pat61, $instr) > 0) {
411: $flag = 5;
412: $i = 0;
413: }
414: // 内閣
415: } else if ($flag == 5) {
416: if (preg_match($pat7, $instr, $arr) > 0) {
417: $n = $i + $arr[1];
418: while ($i < $n) {
419: $items[$i]['cabinet'] = $arr[2];
420: $i++;
421: }
422: } else if (preg_match($pat8, $instr, $arr) > 0) {
423: $items[$i]['cabinet'] = $arr[1];
424: $i++;
425: } else if (preg_match($pat9, $instr) > 0) {
426: $flag = 6;
427: }
428: } else if ($flag == 6) {
429: break;
430: }
431: }
432:
433: return TRUE;
434: }
viewPolls.php
542: /**
543: * 政党支持率を解析する
544: * @param int $infp 解析中のURL
545: * @param array $items 解析結果を格納する配列
546: * @return bool TRUE/FALSE
547: */
548: function analyseParty($infp, &$items) {
549: $pat1 = "/<tr[^>]*>/u";
550: $pat2 = "/<t[hd][^>]*>(<[^>]+>)?([0-9\-\.]+)/u";
551: $pat3 = "/<\/tr>/u";
552: $pat4 = "/<t[hd][^>]*>([^<]+)/u";
553: $pat5 = "/<\/table>/u";
554: $flag = 0;
555: while (! feof($infp)) {
556: $instr = trim(fgets($infp));
557: if (($flag == 0) && (preg_match($pat1, $instr) > 0)) {
558: $flag = 1;
559: $i = 0;
560: // 月
561: } else if ($flag == 1) {
562: if (preg_match($pat2, $instr, $arr) > 0) {
563: $items[$i]['month'] = $arr[2];
564: $i++;
565: } else if (preg_match($pat3, $instr) > 0) {
566: $flag = 2;
567: $i = 0;
568: }
569: // 政党支持率
570: } else if ($flag == 2) {
571: if (preg_match($pat2, $instr, $arr) > 0) {
572: $items[$i][$party] = $arr[2];
573: $i++;
574: } else if (preg_match($pat4, $instr, $arr) > 0) {
575: $party = $arr[1];
576: $i = 0;
577: } else if (preg_match($pat5, $instr) > 0) {
578: $flag = 3;
579: }
580: } else if ($flag == 3) {
581: break;
582: }
583: }
584: return TRUE;
585: }
スクレイピングしたデータは、配列に格納する。
viewPolls.php
436: /**
437: * 内閣支持率・政党支持率を解析する【2020年1月以降】
438: * @param int $infp 解析中のURL
439: * @param array $items 内閣支持率の解析結果を格納する配列
440: * @param array $items 政党支持率の解析結果を格納する配列
441: * @return bool TRUE/FALSE
442: */
443: function analyse2020($infp, &$items1, &$items2) {
444: // プロットする政党名
445: static $table = array(
446: '', '自民党', '立憲民主党', '日本維新の会', '公明党', '共産党', '国民民主党', 'れいわ新選組', '社民党', 'みんなでつくる党', '参政党', '特に支持している政党はない', '支持政党なし');
447:
448: $pat01 = "/<i\s+class\=\"far\s+fa\-calendar\-alt\"><\/i>[0-9]+年([0-9]+)月(<span>|\s*)/ui";
449: $pat11 = "/<h3>内閣支持([0-9]+)[%\%]、不支持([0-9]+)[%\%]/ui";
450: // $pat12 = "/(\p{Han}+内閣)を「支持する/ui";
451: $pat12 = "/([一-龠]+内閣)を「支持する/ui";
452: $pat21 = "/<h2><span>政党支持率<\/span>/ui";
453: $pat31 = "/<td>([^<]+)<\/td>/ui";
454: $pat32 = "/<td\s+class\=\"right\">([0-9\.]+)<\/td>/ui";
455: // 2023年8月以降
456: $pat22 = "/各党の支持率は/ui";
457: $pat33 = '/「([^」]+)」が([0-9\.]+)[%\%]/ui';
458:
459: $flag = FALSE;
460: while (! feof($infp)) {
461: $instr1 = trim(fgets($infp));
462: $instr2 = strip_tags($instr1);
463: // 月
464: if (preg_match($pat01, $instr1, $arr) > 0) {
465: $month = (int)$arr[1];
466: // 内閣支持率・不支持率
467: } else if (preg_match($pat11, $instr1, $arr) > 0) {
468: $items1[$month - 1]['month'] = (int)$month;
469: $items1[$month - 1]['yes'] = (int)$arr[1];
470: $items1[$month - 1]['no'] = (int)$arr[2];
471: // 内閣名
472: } else if (preg_match($pat12, $instr2, $arr) > 0) {
473: $items1[$month - 1]['cabinet'] = (string)$arr[1];
474: // 政党名と支持率 2023年8月以降
475: } else if (preg_match($pat22, $instr1) > 0) {
476: $items2[$month - 1]['month'] = (int)$month;
477: if (preg_match_all($pat33, $instr1, $arr, PREG_PATTERN_ORDER) > 0) {
478: foreach ($arr[1] as $key=>$party) {
479: if ($party == '特に支持している政党はない') {
480: $party = '支持政党なし';
481: }
482: if (array_search($party, $table) != FALSE) {
483: $items2[$month - 1][$party] = (string)$arr[2][$key];
484: }
485: }
486: }
487: break;
488: // 政党支持率の解析開始
489: } else if (preg_match($pat21, $instr1, $arr) > 0) {
490: $flag = TRUE;
491: $items2[$month - 1]['month'] = (int)$month;
492: // 政党名
493: } else if ($flag && (preg_match($pat31, $instr1, $arr) > 0)) {
494: $party = (string)$arr[1];
495: if ($party == '特に支持している政党はない') {
496: $party = '支持政党なし';
497: }
498: // 支持率
499: } else if ($flag && (preg_match($pat32, $instr1, $arr) > 0)) {
500: $items2[$month - 1][$party] = (string)$arr[1];
501: }
502: }
503: return TRUE;
504: }
解説:XMLへの格納
viewPolls.php
506: /**
507: * 内閣支持率をXMLオブジェクトに追加/更新する
508: * @param array $items 解析結果を格納した配列
509: * @param int $year 西暦年
510: * @param object $xml XMLオブジェクト
511: * @return bool TRUE/FALSE
512: */
513: function addCabinet($items, $year, &$xml) {
514: // 西暦年の頭出し
515: $flag = FALSE;
516: foreach ($xml->research as $elem1) {
517: if ($elem1->year == $year) {
518: break;
519: }
520: }
521: // 月次処理
522: foreach ($items as $item) {
523: foreach ($elem1->article as $elem2) {
524: if ($elem2->month == $item['month']) {
525: $flag = TRUE;
526: break;
527: }
528: }
529: if (! $flag) {
530: $elem2 = $elem1->addChild('article');
531: $elem2->addChild('month', $item['month']);
532: }
533: if (! isset($elem2->cabinet)) {
534: $elem2->addChild('cabinet');
535: $elem2->cabinet->addChild('name', $item['cabinet']);
536: $elem2->cabinet->addChild('yes', $item['yes']);
537: $elem2->cabinet->addChild('no', $item['no']);
538: }
539: }
540: }
viewPolls.php
587: /**
588: * 政党支持率をXMLオブジェクトに追加/更新する
589: * @param array $items 解析結果を格納した配列
590: * @param int $year 西暦年
591: * @param object $xml XMLオブジェクト
592: * @return bool TRUE/FALSE
593: */
594: function addParty($items, $year, &$xml) {
595: // 西暦年の頭出し
596: $flag = FALSE;
597: foreach ($xml->research as $elem1) {
598: if ($elem1->year == $year) {
599: break;
600: }
601: }
602:
603: // 月次処理
604: foreach ($items as $item) {
605: foreach ($elem1->article as $elem2) {
606: if (isset($item['month']) && ($elem2->month == $item['month'])) {
607: $flag = TRUE;
608: break;
609: }
610: }
611: if (! $flag && isset($item['month'])) {
612: $elem2 = $elem1->addChild('article');
613: $elem2->addChild('month', $item['month']);
614: }
615:
616: foreach ($item as $key=>$val) {
617: if ($key == 'month') continue;
618: $flag = FALSE;
619: foreach ($elem2->party as $elem3) {
620: if (isset($elem3->name) && ($elem3->name == $key)) {
621: $flag = TRUE;
622: break;
623: }
624: }
625: if (! $flag) {
626: $elem4 = $elem2->addChild('party');
627: $elem4->addChild('name', $key);
628: $elem4->addChild('yes', $val);
629: }
630: }
631: }
632: }
解説:XMLファイルへのセーブとロード
viewPolls.php
318: /**
319: * データ・ファイルを書き込む
320: * @param string $fname 出力ファイル名
321: * @param object XMLオブジェクト
322: * @return bool TRUE/FALSE
323: */
324: function writeDataFile($fname, $xml) {
325: $str = $xml->asXML();
326: $str = cleanUpXML($str);
327: $outfp = fopen($fname, 'w');
328: fwrite($outfp, $str);
329:
330: return fclose($outfp);
331: }
viewPolls.php
291: /**
292: * データ・ファイルを読み込む
293: * @param string $fname 入力ファイル名
294: * @return object XMLオブジェクト/FALSE:読み込み失敗
295: */
296: function readDataFile($fname) {
297: $xmlstr =<<< EOT
298: <?xml version="1.0" encoding="utf-8" ?>
299: <polls>
300: </polls>
301:
302: EOT;
303: if (! isphp5over()) return FALSE; // PHP5以上でないと動作しない
304:
305: // ファイル無し
306: if (! file_exists($fname)) {
307: $xml = new SimpleXMLElement($xmlstr);
308:
309: // ファイル有り
310: } else {
311: $xml = simplexml_load_file($fname);
312: if ($xml == FALSE) return FALSE;
313: }
314:
315: return $xml;
316: }
解説:jqPlotの導入

公式サイト http://www.jqplot.com/ から最新のライブラリをダウンロードしたら、本プログラム冒頭の定数 JQPLOT で示すディレクトリへ解凍する。jqPlot は jQUery UI を使うので、jQueryも定数 JQPLOT で示すディレクトリへ格納してほしい。jQuery 1.7.1 以降のmin版があればよい。また、jqPlot は canvas文を使うのでHTML5対応ブラウザがあった方がいいが、IE8以前にも対応するよう代替スクリプトを用意している。
viewPolls.php
92: <script type="text/javascript" src="{$jqplot}jquery.min.js"></script>
93: <!--[if lt IE 9]>
94: <script language="javascript" type="text/javascript" src="{$jqplot}excanvas.min.js"></script>
95: <![endif]-->
96: <script type="text/javascript" src="{$jqplot}jquery.jqplot.min.js"></script>
97: <script type="text/javascript" src="{$jqplot}plugins/jqplot.dateAxisRenderer.min.js"></script>
98: <script type="text/javascript" src="{$jqplot}plugins/jqplot.pointLabels.min.js"></script>
99: <script type="text/javascript" src="{$jqplot}plugins/jqplot.highlighter.min.js"></script>
100: <link rel="stylesheet" type="text/css" href="{$jqplot}jquery.jqplot.min.css">
ここでは、jqPlotプラグインの「jqplot.dateAxisRenderer.js」(時間軸を扱う)、「jqplot.highlighter.js」(ツールチップなどハイライト表示を行う)、「jqplot.pointLabels.js」(データポイントラベルを表示する)の3つを使っている。
このように、jqPlotは使いたい機能に応じてJavaScriptのプラグインを読み込む形をとる。
jqPlotの使い方は、「jqPlot - jQuery プラグイン」(アルファシス)が詳しい。
解説:jqPlotスクリプト
viewPolls.php
755: /**
756: * jqPlot用のスクリプト:内閣支持率
757: * @param object $xml XMLオブジェクト
758: * @param int $start 開始年
759: * @param int $finish 終了年
760: * @return string スクリプト
761: */
762: function plotCabinet($xml, $start, $finish) {
763: $graphId = GRAPH_ID;
764:
765: // 系列の生成
766: $series1 = ''; // 内閣支持率
767: $series2 = ''; // 内閣不支持率
768: $old = '';
769: foreach ($xml as $research) {
770: // 描画範囲内かどうか
771: $year = $research->year;
772: if (($year < $start) || ($year > $finish)) continue;
773: // 描画データ作成
774: foreach ($research as $article) {
775: $month = $article->month;
776: if ($month >= 1 && $month <= 12) {
777: $name = '';
778: if ($old != (string)$article->cabinet->name) {
779: $name = (string)$article->cabinet->name;
780: $old = (string)$article->cabinet->name;
781: }
782: $yes = $article->cabinet->yes;
783: $no = $article->cabinet->no;
784: if ($month > 0 && $yes > 0) {
785: $series1 .= sprintf("['%04d-%02d-01',%d,'<span style=\"background-color:#FFFFFF;\">%s</span>'],", $year, $month, $yes, $name);
786: $series2 .= sprintf("['%04d-%02d-01',%d,''],", $year, $month, $no);
787: }
788: }
789: }
790: }
791:
792: // X軸の最小値
793: $xmin = sprintf('%04d-01-01', $start);
794: // X軸の最大値
795: if (date('Y') == $finish) {
796: $xmax = date('Y-m-01', strtotime('next month'));
797: } else {
798: $xmax = sprintf('%04d-12-31', $finish);
799: }
800:
801: // グラフ描画
802: $js =<<< EOT
803: jQuery(function() {
804: jQuery.jqplot('{$graphId}',
805: [
806: [ {$series1} ],
807: [ {$series2} ]
808: ],
809: {
810: // 系列
811: series: [
812: { label: '支持率', color: '#88CC44' },
813: { label: '不支持率', color: '#CC4488' }
814: ],
815: legend: {
816: show: true,
817: placement: 'inside',
818: location: 'sw',
819: renderer: $.jqplot.EnhancedLegendRenderer,
820: rendererOptions: { numberRows: 1 }
821: },
822: seriesDefaults: {
823: showLine: true,
824: rendererOptions: { smooth: false },
825: markerOptions: { size: 0 },
826: pointLabels: {
827: show :true,
828: escapeHTML: false,
829: location: 'ne'
830: }
831: },
832: // 軸
833: axes: {
834: xaxis: {
835: renderer:$.jqplot.DateAxisRenderer,
836: min: '{$xmin}',
837: max: '{$xmax}',
838: tickOptions: { formatString: '%Y/%#m' },
839: label: '年月',
840: },
841: yaxis: {
842: label: '支持率(%)'
843: }
844: },
845: // ハイライター
846: highlighter: {
847: show: true,
848: showMarker: true,
849: tooltipLocation: 'sw',
850: fadeTooltip: false,
851: bringSeriesToFront: true,
852: tooltipAxes: 'xy',
853: formatString: '%s<br>(不)支持率%s%'
854: }
855: }
856: );
857: });
858:
859: EOT;
860:
861: return $js;
862: }

内閣支持率と不支持率の2本の折れ線グラフを描くのだが、各々、プロットする1つの点を "[x, y]" で表し、これをカンマで区切った文字列として生成する。ここでは "[日付, パーセント]" の形式で、支持率を変数 $seriese1 へ、不支持率を 変数 $seriese2 へ格納していく。

jqPlotスクリプトは jQuery(function() 以降の部分になる。
まず、グラフを描画する領域のDOMオブジェクト名を設定する。本プログラムでは、HTML側のdivタグで表示をしており、グラフの縦横サイズはdivタグのスタイルとして指定する。
上述のプロットデータ(文字列)はオブジェクト series に格納する。
オブジェクト legend は凡例の表示方法を指定する。表示場所(placement)はグラフの内側(inside)で、表示位置を南西(sw;下右)に、表示は1列(numberRows: 1)にする。表示を1列にするのに rendererOptionsを使うので、あわせて拡張機能 renderer: $.jqplot.EnhancedLegendRenderer を指定する。
オブジェクト seriesDefaults はグラフの表示方法を指定する。折れ線(showLine)を表示し(true)、折れ線のスムーズ処理(smooth)は行わず(false)、点(markerOptions)をプロットしない(size: 0)。
オブジェクト axes はX軸のラベルや目盛りの表示方法を指定する。X軸の最小値(min)は変数 $xmin を指定し、最大値(max)は $xmax を指定する。目盛りの書式(formatString)は年/月(%Y/%#m)にして、ラベル(label)は年月にする。ここで、目盛りの書式を使うために、拡張機能 renderer: $.jqplot.DateAxisRenderer を指定する。Y軸については、ラベルのみを表示する。
オブジェクト highlighter は、折れ線の上にマウスを乗せるデータを表示するハイライト表示する方法を指定する。ハイライターを表示し(show: true)、マーカーも表示し(showMarker: true)、表示位置(tooltipLocation)は南西(sw;下右)に、フェード効果(fadeTooltip)は無効に(false)、ハイライト表示した系列を他の系列よりも前面に移動し(bringSeriesToFront: true)、ツールチップにXとYの値を表示し(tooltipAxes: 'xy')、表示書式(formatString)は '%s
支持率%s%' に指定する。
viewPolls.php
864: /**
865: * jqPlot用のスクリプト:政党支持率
866: * @param object $xml XMLオブジェクト
867: * @param int $start 開始年
868: * @param int $finish 終了年
869: * @return string スクリプト
870: */
871: function plotParty($xml, $start, $finish) {
872: $graphId = GRAPH_ID;
873:
874: // プロットする政党名
875: static $table = array(
876: '自民党', '公明党', '共産党', '民進党', '立憲民主党', '民主党', '日本維新の会', '国民民主党', 'れいわ新選組', '支持政党なし');
877:
878: // 系列の生成
879: $str2 = '';
880: foreach ($table as $key=>$val) {
881: $str2 .= "\t\t\t{ label: '{$val}' },\n";
882: $series[] = '';
883: }
884:
885: foreach ($xml as $research) {
886: $year = (int)$research->year;
887: // 描画範囲内かどうか
888: if (($year < $start) || ($year > $finish)) continue;
889: // 描画データ作成
890: foreach ($research as $article) {
891: $month = (int)$article->month;
892: foreach ($article as $party) {
893: foreach ($table as $key=>$val) {
894: if ($val == (string)$party->name) {
895: if (preg_match('/[0-9\.]+/', $party->yes) > 0 && $year > 0 && $month >= 1 && $month <= 12) {
896: $series[$key] .= sprintf("['%04d-%02d-01',%4.1f],", $year, $month, (double)$party->yes);
897: }
898: }
899: }
900: }
901: }
902: }
903: $str1 = '';
904: foreach ($series as $val) {
905: $str1 .= "\t\t[ " . $val . "],\n";
906: }
907:
908: // X軸の最小値
909: $xmin = sprintf('%04d-01-01', $start);
910: if (date('Y') == $finish) {
911: $xmax = date('Y-m-01', strtotime('next month'));
912: } else {
913: $xmax = sprintf('%04d-12-31', $finish);
914: }
915:
916: // グラフ描画
917: $js =<<< EOT
918: jQuery(function() {
919: jQuery.jqplot('{$graphId}',
920: [
921: {$str1}
922: ],
923: {
924: // 系列
925: series: [
926: {$str2}
927: ],
928: legend: {
929: show: true,
930: placement: 'inside',
931: location: 'sw',
932: renderer: $.jqplot.EnhancedLegendRenderer,
933: rendererOptions: { numberRows: 1 }
934: },
935: seriesDefaults: {
936: showLine: true,
937: rendererOptions: { smooth: false },
938: markerOptions: { size: 0 },
939: },
940: // 軸
941: axes: {
942: xaxis: {
943: renderer: $.jqplot.DateAxisRenderer,
944: min: '{$xmin}',
945: max: '{$xmax}',
946: tickOptions: { formatString: '%Y/%#m' },
947: label: '年月',
948: },
949: yaxis: {
950: label: '支持率(%)'
951: }
952: },
953: // ハイライター
954: highlighter: {
955: show: true,
956: showMarker: true,
957: tooltipLocation: 'sw',
958: fadeTooltip: false,
959: bringSeriesToFront: true,
960: tooltipAxes: 'xy',
961: formatString: '%s<br>支持率%s%'
962: }
963: }
964: );
965: });
966:
967: EOT;
968:
969: return $js;
970: }
(2025年5月24日)2024~2025年(令和7年)のHTML表記ゆれに対応