DEVELOPERS BLOGデベロッパーズブログ

  1. HOME > 
  2. 加藤 正人 > 
  3. タイトル

Google Chart へのアクセスをヘルパーで実装してみる【その1】

2014/09/16

Google Chart へのアクセスをヘルパーで実装してみる【その1】

 

先日仕事で Google Charts を利用する機会があった。

この機能を使うと、グラフやチャートを簡単に表示できとても便利だ。今後も使う機会は多そうなので、CakePHP のプラグイン化しておくと便利そうなので、作ってみた。

001<?php
002// Google Charts ヘルパー
003App::uses('AppHelper', 'View/Helper');
004 
005class GoogleChartsHelper extends AppHelper
006{
007    public $helpers = array('Html');
008 
009    public function __construct($view, $settings = array())
010    {
011        parent::__construct($view, $settings);
012    }
013 
014    // 与えられたオブジェクトがハッシュ (連想配列) かどうかを判定するメソッド
015    public function is_hash($array)
016    {
017        if ( is_array($array) )
018        {
019            $i = 0;
020            foreach ( $array as $k => $dummy )
021            {
022                if ( $k !== $i++ ) return TRUE;
023            }
024        }
025        return FALSE;
026    }
027 
028    ////////////////////////////////////////////
029    // Google Charts サービス処理メソッド群
030    ////////////////////////////////////////////
031 
032    // チャート名とコンストラクタ、パッケージの一覧
033    private $_chart_info = array(
034        'Annotation'    => array('constructor' => 'AnnotationChart',    'package' => 'annotationchart'),
035        'Area'      => array('constructor' => 'AreaChart',      'package' => 'corechart'),
036        'Bar'       => array('constructor' => 'BarChart',       'package' => 'corechart'),
037        'Bubble'    => array('constructor' => 'BubbleChart',    'package' => 'corechart'),
038        'Calendar'  => array('constructor' => 'Calendar',       'package' => 'calendar'),
039        'Candlestick'   => array('constructor' => 'CandlestickChart',   'package' => 'corechart'),
040        'Column'    => array('constructor' => 'ColumnChart',    'package' => 'corechart'),
041        'Combo'     => array('constructor' => 'ComboChart',     'package' => 'corechart'),
042        'Diff'      => array('constructor' => '',           'package' => 'corechart'),  /* Diff チャートは実際には別のチャート */
043        'Gauge'     => array('constructor' => 'Gauge',      'package' => 'gauge'),
044        'Geo'       => array('constructor' => 'GeoChart',       'package' => 'geochart'),
045        'Histogram' => array('constructor' => 'Histogram',      'package' => 'corechart'),
046        'Interval'  => array('constructor' => 'LineChart',      'package' => 'corechart'),
047        'Line'      => array('constructor' => 'LineChart',      'package' => 'corechart'),
048        'Map'       => array('constructor' => 'Map',        'package' => 'map'),
049        'Org'       => array('constructor' => 'OrgChart',       'package' => 'orgchart'),
050        'Pie'       => array('constructor' => 'PieChart',       'package' => 'corechart'),
051        'Sankey'    => array('constructor' => 'Sankey',         'package' => 'sankey'),
052        'Scatter'   => array('constructor' => 'ScatterChart',   'package' => 'corechart'),
053        'SteppedArea'   => array('constructor' => 'SteppedAreaChart',   'package' => 'corechart'),
054        'Table'     => array('constructor' => 'Table',      'package' => 'table'),
055        'Timeline'  => array('constructor' => 'Timeline',       'package' => 'timeline'),
056        'TreeMap'   => array('constructor' => 'TreeMap',        'package' => 'treemap'),
057    );
058 
059    private $_chart_codes = NULL;   // チャートを生成する JavaScript コードをスタックする変数
060 
061    // Chart 開始処理
062    //
063    //  使用するパッケージ名は不要になった。実際に使用したチャートを自動蓄積し、初期化コードを発生するよう修正済み。 (2014/09/01)
064    public function chartStart()
065    {
066        $this->_chart_codes = NULL; // JavaScript コードをスタックする変数を初期化する
067 
068        $this->Html->script(array('https://www.google.com/jsapi'), array('inline' => FALSE));
069 
070        // chart スクリプトのロード方法を指定する
071        switch ( strtolower(@$this->settings['chartOptions']['loader']) )
072        {
073        default:
074        case 'jquery':
075            $this->_chart_codes[] = "$(function () {\n";
076            break;
077        case 'jquery-ready':
078            $this->_chart_codes[] = "$(document).ready(function () {\n";
079            break;
080        case 'google':
081            $this->_chart_codes[] = "google.setOnLoadCallback(function () {\n";
082        }
083 
084    }
085 
086    // Chart 終了処理
087    public function chartEnd()
088    {
089        $this->_chart_codes[] = "});";
090        array_unshift($this->_chart_codes, "google.load('visualization', '1.1', {'packages':" . json_encode($this->_package_list) . "});\n");
091        $this->Html->scriptBlock(implode('', $this->_chart_codes), array('inline' => FALSE));
092//debug($this->_package_list);
093    }
094 
095    private function _generateDataTableSimple($array, $var_name='data')
096    {
097        $ret = NULL;
098        if ( $array[0] === TRUE || $array[0] === FALSE )
099        {
100            $treat_as_data = array_shift($array);   // 先頭要素を取り出しデータ部だけにする
101            $ret[] = "var {$var_name} = new google.visualization.arrayToDataTable(" . json_encode($array) . ", " . ($treat_as_data?'true':'false') . ");";
102        }
103        else
104        {
105            $ret[] = "var {$var_name} = new google.visualization.arrayToDataTable(" . json_encode($array) . ");";
106        }
107        return $ret;
108    }
109 
110    private function _generateDataTableDetailed($cols, $rows, $var_name='data')
111    {
112        $col_types = NULL;
113        $ret = NULL;
114        // カラム処理
115        $ret[] = "var {$var_name} = new google.visualization.DataTable();";
116        foreach ( $cols as $c )
117        {
118            if ( $this->is_hash($c) )
119            {
120                // カラム定義がハッシュだった場合
121                $ret[] = "{$var_name}.addColumn(" . json_encode($c) . ');';
122                $col_types[] = $c['type'];
123            }
124            else
125            {
126                // カラム定義が配列だった場合
127                $ret[] = "{$var_name}.addColumn(" . join(', ', array_map(function ($d) { return "'" . str_replace("'", "\\'", $d) . "'"; }, $c)) . ');';
128                $col_types[] = $c[0];
129            }
130        }
131        // データ処理
132        $ret[] = "{$var_name}.addRows([";
133 
134        $ret = array_merge($ret,
135        array_map(function ($r) use($col_types) { return "\t[" . join(', ', array_map(function ($d, $t) {
136            if ( is_null($d) )
137                return 'undefined';
138            if ( is_array($d) )
139                return json_encode($d);
140            switch ( strtolower($t) )
141            {
142            case 'string':
143                return "'" . $d . "'";
144            case 'date':
145                if ( ($utime=strtotime($d)) )
146                {
147                    // strtotime で変換できた
148                    $year = (int)date('Y', $utime);
149                    $month = (int)date('n', $utime)-1;  // month は 0 ~ 11
150                    $day = (int)date('j', $utime);
151                    return "new Date({$year}, {$month}, {$day})";
152                }
153                else
154                {
155                    // strtotime では処理できなかった (日付が2038年1月19日を超えている、など)
156                    if ( preg_match('/^([0-9]+)[.\/\-]([1-9]|0[1-9]|10|11|12)[.\/\-]([1-9]|[012][0-9]|3[01])$/', $d, $matches) )
157                    {
158                        $year = (int)$matches[1];
159                        $month = (int)$matches[2]-1;    // month は 0 ~ 11
160                        $day = (int)$matches[3];
161                        return "new Date({$year}, {$month}, {$day})";
162                    }
163                }
164                // いずれにもマッチしなかった場合は fall throuth
165            case 'number':
166                return $d;
167            case 'boolean':
168                return ($d)?'true':'false';
169            }
170        }, $r, $col_types)) . '],'; }, $rows));
171 
172        $ret[] = "]);";
173        return $ret;
174    }
175 
176    // チャートごとの情報テーブル
177    // ※コンストラクタ名がチャート名と同名のものと、チャート名+"Chart" のものが混在するため
178     
179    private function _getConstructorName($type)
180    {
181        return $this->_chart_info[$type]['constructor'];
182    }
183 
184    private $_package_list = array('corechart');
185 
186    private function _pushPackage($type)
187    {
188        $package_name = $this->_chart_info[$type]['package'];
189        if ( !in_array($package_name, $this->_package_list) )
190        {
191            array_push($this->_package_list, $package_name);
192        }
193        return;
194    }
195 
196    private function _chartBase($type, $jq_selector, $data, $chart_options=array(), $handlers=array(), $helper_options=array())
197    {
198        $this->_chart_codes[] = "\t(function() {\n";
199 
200        if ( @$data['cols'] && @$data['rows'] )
201        {
202            $this->_chart_codes[] =  "\t\t" . join("\n\t\t", $this->_generateDataTableDetailed($data['cols'], $data['rows'])) . "\n";
203        }
204        else
205        {
206            $this->_chart_codes[] = "\t\t" . join("\n\t\t", $this->_generateDataTableSimple($data)) . "\n";
207        }
208             
209        $constructor = $this->_getConstructorName($type);
210        $this->_pushPackage($type);
211        $this->_chart_codes[] = "\t\tvar options = " . json_encode($chart_options) . ";\n";
212        $this->_chart_codes[] = "\t\tvar chart = new google.visualization.{$constructor}($('{$jq_selector}')[0]);\n";
213        $this->_chart_codes[] = "\t\tchart.draw(data, options);\n";
214        if ( is_array($handlers) && count($handlers) > 0 )
215        {
216            // イベントハンドラが与えられた場合は設定する
217            foreach ( $handlers as $trigger => $handler )
218            {
219                $this->_chart_codes[] = "\\ttgoogle.visualization.events.addListener(chart, '{$trigger}', {$handler});\n";
220            }
221        }
222        if ( ($callback=@$helper_options['callback']) )
223        {
224            $this->_chart_codes[] = "\t\t({$callback})({data: data, chart: chart, options: options});\n";
225        }
226        $this->_chart_codes[] = "\t})();\n";
227    }
228 
229    // Diff チャート だけは他のチャートと構造が異なるので単独のメソッドとして用意する
230    public function chartDiff($jq_selector, $type, $data, $chart_options=array(), $handlers=array(), $helper_options=array())
231    {
232        $this->_chart_codes[] = "\t(function() {\n";
233        // Before データ生成
234        $before_data = 'beforeData';
235        if ( @$data['before']['cols'] && @$data['before']['rows'] )
236        {
237            $this->_chart_codes[] = "\t\t" . join("\n\t\t", $this->_generateDataTableDetailed($data['before']['cols'], $data['before']['rows'], $before_data)) . "\n";
238        }
239        else
240        {
241            $this->_chart_codes[] = "\t\t" . join("\n\t\t", $this->_generateDataTableSimple($data['before'], $before_data)) . "\n";
242        }
243        // After データ生成
244        $after_data = 'afterData';
245        if ( @$data['after']['cols'] && @$data['after']['rows'] )
246        {
247            $this->_chart_codes[] = "\t\t" . join("\n\t\t", $this->_generateDataTableDetailed($data['after']['cols'], $data['after']['rows'], $after_data)) . "\n";
248        }
249        else
250        {
251            $this->_chart_codes[] = "\t\t" . join("\n\t\t", $this->_generateDataTableSimple($data['after'], $after_data)) . "\n";
252        }
253        $constructor = $this->_getConstructorName($type);
254        $this->_chart_codes[] = "\t\tvar options = " . json_encode($chart_options) . ";\n";
255        $this->_chart_codes[] = "\t\tvar chart = new google.visualization.{$constructor}($('{$jq_selector}')[0]);\n";
256 
257        $this->_chart_codes[] = "\t\tvar diffData = chart.computeDiff({$before_data}, {$after_data});\n";
258 
259        $this->_chart_codes[] = "\t\tchart.draw(diffData, options);\n";
260        if ( ($callback=@$helper_options['callback']) )
261        {
262            $this->_chart_codes[] = "\t\t({$callback})({before_data: {$before_data}, after_data: {$after_data}, diff_data: diffData, chart: chart, options: options);\n";
263        }
264        $this->_chart_codes[] = "\t})();\n";
265    }
266 
267 
268    // ほとんどのメソッドはマジックメソッドで対応
269    public function __call($method, $params)
270    {
271        if ( preg_match('/^chart(.*)$/', $method, $matches) && in_array(@$matches[1], array_keys($this->_chart_info)) )
272        {
273            // マジックメソッド検出されたメソッド名がチャート情報テーブルに登録されていた
274            $this->_chartBase($matches[1], @$params[0], @$params[1], @$params[2], @$params[3], @$params[4]);
275        }
276        else
277        {
278 
279//          throw new Exception("Method '{$method}' is not defined in class '" . __CLASS__ . "'.");
280            throw new Exception("Method '". __CLASS__ . "::{$method}' is not defined.");
281        }
282    }
283}
284?>

使う場合はコントローラで $helpers に GoogleCharts を指定しておく必要がある。

チャートを表示する際には、まず GoogleChartsHelper::chartStart() を呼び出し、チャート環境を初期化したあと各チャート機能を利用し、最後に GoogleChartsHelper::chartEnd() を呼び出すことで必要なコードを出力する。

Google Charts は表示するチャートやグラフごとにパッケージが分離されているので、表示する対象に応じたパッケージをコーディング者が明示する必要があるが、このヘルパーでは実際に描画する対象に応じて自動的に必要なパッケージを判定し事前に呼び出すようにコードを生成する

1つのページ表示に際し、chartStart() あるいは/および chartEnd() を2回以上呼び出してはならない。2回以上呼び出した場合の動作は規定されていない。

また、各チャートの呼び出しは diff チャート以外はマジックメソッド呼び出しで実装される。マジックメソッド呼び出しで与えられたメソッドが実在するかどうかは、GoogleChartHelper クラスのプライベート変数 $_chart_info に保持されている情報で判定する。この変数には、チャート名とそれが含まれる Google Chart パッケージ名、およびその JavaScript コンストラクタ名が格納されている。呼び出そうとしているメソッドがマジックメソッドとして無効な場合は、Exception クラスの例外が throw され処理は中断される。

GoogleChartsHelper::chartStart() を呼び出した後、各チャートを呼び出すと当該チャートに必要な情報 (Google Chart のパッケージ名や当該チャートのコンストラクタ名) がクラス内部の変数に蓄積され、GoogleChartsHelper::chartEnd() を呼び出した時点でそれらが必要な順序で 呼び出されるよう Script  タグを生成する。Script タグの生成には HtmlHelper::scriptBlock() を使用する。

各チャートメソッドの詳細は次回に。

この記事は加藤 正人さんが書いています!

加藤 正人

氏名
加藤 正人
役職
多分SE
血液型
秘密
出没
美味しいもののあるところ
特色
タヒチ大好き。ちょいメタボ。

関連エントリー