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

サンプル・プログラム
viewPolls.php | サンプル・プログラム本体。 |
polls.xml | 内閣支持率・政党支持率のデータ・ファイル。 |
viewPolls.php | サンプル・プログラム本体。 |
バージョン | 更新日 | 内容 |
---|---|---|
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 へのセーブ/ロード |
3.0 | 2021/05/08 | 2020年4月以降に対応,PHP8対応 |
解説:準備
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 で示すファイルにセーブし、次に描画するときは、このファイルからロードできるようにした。
解説:スクレイピング
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: }
539: /**
540: * 政党支持率を解析する
541: * @param int $infp 解析中のURL
542: * @param array $items 解析結果を格納する配列
543: * @return bool TRUE/FALSE
544: */
545: function analyseParty($infp, &$items) {
546: $pat1 = "/<tr[^>]*>/u";
547: $pat2 = "/<t[hd][^>]*>(<[^>]+>)?([0-9\-\.]+)/u";
548: $pat3 = "/<\/tr>/u";
549: $pat4 = "/<t[hd][^>]*>([^<]+)/u";
550: $pat5 = "/<\/table>/u";
551: $flag = 0;
552: while (! feof($infp)) {
553: $instr = trim(fgets($infp));
554: if (($flag == 0) && (preg_match($pat1, $instr) > 0)) {
555: $flag = 1;
556: $i = 0;
557: //月
558: } else if ($flag == 1) {
559: if (preg_match($pat2, $instr, $arr) > 0) {
560: $items[$i]['month'] = $arr[2];
561: $i++;
562: } else if (preg_match($pat3, $instr) > 0) {
563: $flag = 2;
564: $i = 0;
565: }
566: //政党支持率
567: } else if ($flag == 2) {
568: if (preg_match($pat2, $instr, $arr) > 0) {
569: $items[$i][$party] = $arr[2];
570: $i++;
571: } else if (preg_match($pat4, $instr, $arr) > 0) {
572: $party = $arr[1];
573: $i = 0;
574: } else if (preg_match($pat5, $instr) > 0) {
575: $flag = 3;
576: }
577: } else if ($flag == 3) {
578: break;
579: }
580: }
581: return TRUE;
582: }
スクレイピングしたデータは、配列に格納する。
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>/ui";
449: $pat11 = "/<h3>内閣支持([0-9]+)[%\%]、不支持([0-9]+)[%\%]/ui";
450: $pat12 = "/(\p{Han}+内閣)を「支持する/ui";
451: $pat21 = "/<h2><span>政党支持率<\/span>/ui";
452: $pat31 = "/<td>([^<]+)<\/td>/ui";
453: $pat32 = "/<td\s+class\=\"right\">([0-9\.]+)<\/td>/ui";
454: //2023年8月以降
455: $pat22 = "/各党の支持率は/ui";
456: $pat33 = '/「([^」]+)」が([0-9\.]+)\%/ui';
457:
458: $flag = FALSE;
459: while (! feof($infp)) {
460: $instr1 = trim(fgets($infp));
461: $instr2 = strip_tags($instr1);
462: //月
463: if (preg_match($pat01, $instr1, $arr) > 0) {
464: $month = (int)$arr[1];
465: //内閣支持率・不支持率
466: } else if (preg_match($pat11, $instr1, $arr) > 0) {
467: $items1[$month - 1]['month'] = (int)$month;
468: $items1[$month - 1]['yes'] = (int)$arr[1];
469: $items1[$month - 1]['no'] = (int)$arr[2];
470: //内閣名
471: } else if (preg_match($pat12, $instr2, $arr) > 0) {
472: $items1[$month - 1]['cabinet'] = (string)$arr[1];
473: //政党支持率の解析開始
474: } else if (preg_match($pat21, $instr1, $arr) > 0) {
475: $flag = TRUE;
476: $items2[$month - 1]['month'] = (int)$month;
477: //政党名
478: } else if ($flag && (preg_match($pat31, $instr1, $arr) > 0)) {
479: $party = (string)$arr[1];
480: if ($party == '特に支持している政党はない') {
481: $party = '支持政党なし';
482: }
483: //支持率
484: } else if ($flag && (preg_match($pat32, $instr1, $arr) > 0)) {
485: $items2[$month - 1][$party] = (string)$arr[1];
486: //政党名と支持率 2023年8月以降
487: } else if (preg_match($pat22, $instr1, $arr) > 0) {
488: if (preg_match_all($pat33, $instr1, $arr, PREG_PATTERN_ORDER) > 0) {
489: foreach ($arr[1] as $key=>$party) {
490: if ($party == '特に支持している政党はない') {
491: $party = '支持政党なし';
492: }
493: if (array_search($party, $table) != FALSE) {
494: $items2[$month - 1][$party] = (string)$arr[2][$key];
495: }
496: }
497: }
498: }
499: }
500: return TRUE;
501: }
解説:XMLへの格納
503: /**
504: * 内閣支持率をXMLオブジェクトに追加/更新する
505: * @param array $items 解析結果を格納した配列
506: * @param int $year 西暦年
507: * @param object $xml XMLオブジェクト
508: * @return bool TRUE/FALSE
509: */
510: function addCabinet($items, $year, &$xml) {
511: //西暦年の頭出し
512: $flag = FALSE;
513: foreach ($xml->research as $elem1) {
514: if ($elem1->year == $year) {
515: break;
516: }
517: }
518: //月次処理
519: foreach ($items as $item) {
520: foreach ($elem1->article as $elem2) {
521: if ($elem2->month == $item['month']) {
522: $flag = TRUE;
523: break;
524: }
525: }
526: if (! $flag) {
527: $elem2 = $elem1->addChild('article');
528: $elem2->addChild('month', $item['month']);
529: }
530: if (! isset($elem2->cabinet)) {
531: $elem2->addChild('cabinet');
532: $elem2->cabinet->addChild('name', $item['cabinet']);
533: $elem2->cabinet->addChild('yes', $item['yes']);
534: $elem2->cabinet->addChild('no', $item['no']);
535: }
536: }
537: }
584: /**
585: * 政党支持率をXMLオブジェクトに追加/更新する
586: * @param array $items 解析結果を格納した配列
587: * @param int $year 西暦年
588: * @param object $xml XMLオブジェクト
589: * @return bool TRUE/FALSE
590: */
591: function addParty($items, $year, &$xml) {
592: //西暦年の頭出し
593: $flag = FALSE;
594: foreach ($xml->research as $elem1) {
595: if ($elem1->year == $year) {
596: break;
597: }
598: }
599:
600: //月次処理
601: foreach ($items as $item) {
602: foreach ($elem1->article as $elem2) {
603: if (isset($item['month']) && ($elem2->month == $item['month'])) {
604: $flag = TRUE;
605: break;
606: }
607: }
608: if (! $flag && isset($item['month'])) {
609: $elem2 = $elem1->addChild('article');
610: $elem2->addChild('month', $item['month']);
611: }
612:
613: foreach ($item as $key=>$val) {
614: if ($key == 'month') continue;
615: $flag = FALSE;
616: foreach ($elem2->party as $elem3) {
617: if (isset($elem3->name) && ($elem3->name == $key)) {
618: $flag = TRUE;
619: break;
620: }
621: }
622: if (! $flag) {
623: $elem4 = $elem2->addChild('party');
624: $elem4->addChild('name', $key);
625: $elem4->addChild('yes', $val);
626: }
627: }
628: }
629: }
解説:XMLファイルへのセーブとロード
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: }
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以前にも対応するよう代替スクリプトを用意している。
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スクリプト
747: /**
748: * jqPlot用のスクリプト:内閣支持率
749: * @param object $xml XMLオブジェクト
750: * @param int $start 開始年
751: * @param int $finish 終了年
752: * @return string スクリプト
753: */
754: function plotCabinet($xml, $start, $finish) {
755: $graphId = GRAPH_ID;
756:
757: //系列の生成
758: $series1 = ''; //内閣支持率
759: $series2 = ''; //内閣不支持率
760: $old = '';
761: foreach ($xml as $research) {
762: //描画範囲内かどうか
763: $year = $research->year;
764: if (($year < $start) || ($year > $finish)) continue;
765: //描画データ作成
766: foreach ($research as $article) {
767: $month = $article->month;
768: if ($month >= 1 && $month <= 12) {
769: $name = '';
770: if ($old != (string)$article->cabinet->name) {
771: $name = (string)$article->cabinet->name;
772: $old = (string)$article->cabinet->name;
773: }
774: $yes = $article->cabinet->yes;
775: $no = $article->cabinet->no;
776: if ($month > 0 && $yes > 0) {
777: $series1 .= sprintf("['%04d-%02d-01',%d,'<span style=\"background-color:#FFFFFF;\">%s</span>'],", $year, $month, $yes, $name);
778: $series2 .= sprintf("['%04d-%02d-01',%d,''],", $year, $month, $no);
779: }
780: }
781: }
782: }
783:
784: //X軸の最小値
785: $xmin = sprintf('%04d-01-01', $start);
786: //X軸の最大値
787: if (date('Y') == $finish) {
788: $xmax = date('Y-m-01', strtotime('next month'));
789: } else {
790: $xmax = sprintf('%04d-12-31', $finish);
791: }
792:
793: //グラフ描画
794: $js =<<< EOT
795: jQuery(function() {
796: jQuery.jqplot('{$graphId}',
797: [
798: [ {$series1} ],
799: [ {$series2} ]
800: ],
801: {
802: //系列
803: series: [
804: { label: '支持率', color: '#88CC44' },
805: { label: '不支持率', color: '#CC4488' }
806: ],
807: legend: {
808: show: true,
809: placement: 'inside',
810: location: 'sw',
811: renderer: $.jqplot.EnhancedLegendRenderer,
812: rendererOptions: { numberRows: 1 }
813: },
814: seriesDefaults: {
815: showLine: true,
816: rendererOptions: { smooth: false },
817: markerOptions: { size: 0 },
818: pointLabels: {
819: show :true,
820: escapeHTML: false,
821: location: 'ne'
822: }
823: },
824: //軸
825: axes: {
826: xaxis: {
827: renderer:$.jqplot.DateAxisRenderer,
828: min: '{$xmin}',
829: max: '{$xmax}',
830: tickOptions: { formatString: '%Y/%#m' },
831: label: '年月',
832: },
833: yaxis: {
834: label: '支持率(%)'
835: }
836: },
837: //ハイライター
838: highlighter: {
839: show: true,
840: showMarker: true,
841: tooltipLocation: 'sw',
842: fadeTooltip: false,
843: bringSeriesToFront: true,
844: tooltipAxes: 'xy',
845: formatString: '%s<br>(不)支持率%s%'
846: }
847: }
848: );
849: });
850:
851: EOT;
852:
853: return $js;
854: }

内閣支持率と不支持率の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%' に指定する。
856: /**
857: * jqPlot用のスクリプト:政党支持率
858: * @param object $xml XMLオブジェクト
859: * @param int $start 開始年
860: * @param int $finish 終了年
861: * @return string スクリプト
862: */
863: function plotParty($xml, $start, $finish) {
864: $graphId = GRAPH_ID;
865:
866: //プロットする政党名
867: static $table = array(
868: '自民党', '公明党', '共産党', '民進党', '立憲民主党', '民主党', '日本維新の会', '国民民主党', 'れいわ新選組', '支持政党なし');
869:
870: //系列の生成
871: $str2 = '';
872: foreach ($table as $key=>$val) {
873: $str2 .= "\t\t\t{ label: '{$val}' },\n";
874: $series[] = '';
875: }
876:
877: foreach ($xml as $research) {
878: $year = (int)$research->year;
879: //描画範囲内かどうか
880: if (($year < $start) || ($year > $finish)) continue;
881: //描画データ作成
882: foreach ($research as $article) {
883: $month = (int)$article->month;
884: foreach ($article as $party) {
885: foreach ($table as $key=>$val) {
886: if ($val == (string)$party->name) {
887: if (preg_match('/[0-9\.]+/', $party->yes) > 0 && $year > 0 && $month >= 1 && $month <= 12) {
888: $series[$key] .= sprintf("['%04d-%02d-01',%4.1f],", $year, $month, (double)$party->yes);
889: }
890: }
891: }
892: }
893: }
894: }
895: $str1 = '';
896: foreach ($series as $val) {
897: $str1 .= "\t\t[ " . $val . "],\n";
898: }
899:
900: //X軸の最小値
901: $xmin = sprintf('%04d-01-01', $start);
902: if (date('Y') == $finish) {
903: $xmax = date('Y-m-01', strtotime('next month'));
904: } else {
905: $xmax = sprintf('%04d-12-31', $finish);
906: }
907:
908: //グラフ描画
909: $js =<<< EOT
910: jQuery(function() {
911: jQuery.jqplot('{$graphId}',
912: [
913: {$str1}
914: ],
915: {
916: //系列
917: series: [
918: {$str2}
919: ],
920: legend: {
921: show: true,
922: placement: 'inside',
923: location: 'sw',
924: renderer: $.jqplot.EnhancedLegendRenderer,
925: rendererOptions: { numberRows: 1 }
926: },
927: seriesDefaults: {
928: showLine: true,
929: rendererOptions: { smooth: false },
930: markerOptions: { size: 0 },
931: },
932: //軸
933: axes: {
934: xaxis: {
935: renderer: $.jqplot.DateAxisRenderer,
936: min: '{$xmin}',
937: max: '{$xmax}',
938: tickOptions: { formatString: '%Y/%#m' },
939: label: '年月',
940: },
941: yaxis: {
942: label: '支持率(%)'
943: }
944: },
945: //ハイライター
946: highlighter: {
947: show: true,
948: showMarker: true,
949: tooltipLocation: 'sw',
950: fadeTooltip: false,
951: bringSeriesToFront: true,
952: tooltipAxes: 'xy',
953: formatString: '%s<br>支持率%s%'
954: }
955: }
956: );
957: });
958:
959: EOT;
960:
961: return $js;
962: }
(2024年5月12日)HTMLヘッダ見直し
(2024年3月24日)2023年(令和5年)8月以降のデータに対応
(2023年5月17日)政党支持率に「日本維新の会」「国民民主党」「れいわ新選組」を追加