2014/09/16
Google Chart へのアクセスをヘルパーで実装してみる【その1】
先日仕事で Google Charts を利用する機会があった。
この機能を使うと、グラフやチャートを簡単に表示できとても便利だ。今後も使う機会は多そうなので、CakePHP のプラグイン化しておくと便利そうなので、作ってみた。
003 | App::uses( 'AppHelper' , 'View/Helper' ); |
005 | class GoogleChartsHelper extends AppHelper |
007 | public $helpers = array ( 'Html' ); |
009 | public function __construct( $view , $settings = array ()) |
011 | parent::__construct( $view , $settings ); |
015 | public function is_hash( $array ) |
017 | if ( is_array ( $array ) ) |
020 | foreach ( $array as $k => $dummy ) |
022 | if ( $k !== $i ++ ) return TRUE; |
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' ), |
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' ), |
059 | private $_chart_codes = NULL; |
064 | public function chartStart() |
066 | $this ->_chart_codes = NULL; |
071 | switch ( strtolower (@ $this ->settings[ 'chartOptions' ][ 'loader' ]) ) |
075 | $this ->_chart_codes[] = "$(function () {\n" ; |
078 | $this ->_chart_codes[] = "$(document).ready(function () {\n" ; |
081 | $this ->_chart_codes[] = "google.setOnLoadCallback(function () {\n" ; |
087 | public function chartEnd() |
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)); |
095 | private function _generateDataTableSimple( $array , $var_name = 'data' ) |
098 | if ( $array [0] === TRUE || $array [0] === FALSE ) |
100 | $treat_as_data = array_shift ( $array ); |
101 | $ret [] = "var {$var_name} = new google.visualization.arrayToDataTable(" . json_encode( $array ) . ", " . ( $treat_as_data ? 'true' : 'false' ) . ");" ; |
105 | $ret [] = "var {$var_name} = new google.visualization.arrayToDataTable(" . json_encode( $array ) . ");" ; |
110 | private function _generateDataTableDetailed( $cols , $rows , $var_name = 'data' ) |
115 | $ret [] = "var {$var_name} = new google.visualization.DataTable();" ; |
116 | foreach ( $cols as $c ) |
118 | if ( $this ->is_hash( $c ) ) |
121 | $ret [] = "{$var_name}.addColumn(" . json_encode( $c ) . ');' ; |
122 | $col_types [] = $c [ 'type' ]; |
127 | $ret [] = "{$var_name}.addColumn(" . join( ', ' , array_map ( function ( $d ) { return "'" . str_replace ( "'" , "\\'" , $d ) . "'" ; }, $c )) . ');' ; |
128 | $col_types [] = $c [0]; |
132 | $ret [] = "{$var_name}.addRows([" ; |
134 | $ret = array_merge ( $ret , |
135 | array_map ( function ( $r ) use ( $col_types ) { return "\t[" . join( ', ' , array_map ( function ( $d , $t ) { |
139 | return json_encode( $d ); |
140 | switch ( strtolower ( $t ) ) |
143 | return "'" . $d . "'" ; |
145 | if ( ( $utime = strtotime ( $d )) ) |
148 | $year = (int) date ( 'Y' , $utime ); |
149 | $month = (int) date ( 'n' , $utime )-1; |
150 | $day = (int) date ( 'j' , $utime ); |
151 | return "new Date({$year}, {$month}, {$day})" ; |
156 | if ( preg_match( '/^([0-9]+)[.\/\-]([1-9]|0[1-9]|10|11|12)[.\/\-]([1-9]|[012][0-9]|3[01])$/' , $d , $matches ) ) |
158 | $year = (int) $matches [1]; |
159 | $month = (int) $matches [2]-1; |
160 | $day = (int) $matches [3]; |
161 | return "new Date({$year}, {$month}, {$day})" ; |
168 | return ( $d )? 'true' : 'false' ; |
170 | }, $r , $col_types )) . '],' ; }, $rows )); |
179 | private function _getConstructorName( $type ) |
181 | return $this ->_chart_info[ $type ][ 'constructor' ]; |
184 | private $_package_list = array ( 'corechart' ); |
186 | private function _pushPackage( $type ) |
188 | $package_name = $this ->_chart_info[ $type ][ 'package' ]; |
189 | if ( !in_array( $package_name , $this ->_package_list) ) |
191 | array_push ( $this ->_package_list, $package_name ); |
196 | private function _chartBase( $type , $jq_selector , $data , $chart_options = array (), $handlers = array (), $helper_options = array ()) |
198 | $this ->_chart_codes[] = "\t(function() {\n" ; |
200 | if ( @ $data [ 'cols' ] && @ $data [ 'rows' ] ) |
202 | $this ->_chart_codes[] = "\t\t" . join( "\n\t\t" , $this ->_generateDataTableDetailed( $data [ 'cols' ], $data [ 'rows' ])) . "\n" ; |
206 | $this ->_chart_codes[] = "\t\t" . join( "\n\t\t" , $this ->_generateDataTableSimple( $data )) . "\n" ; |
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 ) |
217 | foreach ( $handlers as $trigger => $handler ) |
219 | $this ->_chart_codes[] = "\\ttgoogle.visualization.events.addListener(chart, '{$trigger}', {$handler});\n" ; |
222 | if ( ( $callback =@ $helper_options [ 'callback' ]) ) |
224 | $this ->_chart_codes[] = "\t\t({$callback})({data: data, chart: chart, options: options});\n" ; |
226 | $this ->_chart_codes[] = "\t})();\n" ; |
230 | public function chartDiff( $jq_selector , $type , $data , $chart_options = array (), $handlers = array (), $helper_options = array ()) |
232 | $this ->_chart_codes[] = "\t(function() {\n" ; |
234 | $before_data = 'beforeData' ; |
235 | if ( @ $data [ 'before' ][ 'cols' ] && @ $data [ 'before' ][ 'rows' ] ) |
237 | $this ->_chart_codes[] = "\t\t" . join( "\n\t\t" , $this ->_generateDataTableDetailed( $data [ 'before' ][ 'cols' ], $data [ 'before' ][ 'rows' ], $before_data )) . "\n" ; |
241 | $this ->_chart_codes[] = "\t\t" . join( "\n\t\t" , $this ->_generateDataTableSimple( $data [ 'before' ], $before_data )) . "\n" ; |
244 | $after_data = 'afterData' ; |
245 | if ( @ $data [ 'after' ][ 'cols' ] && @ $data [ 'after' ][ 'rows' ] ) |
247 | $this ->_chart_codes[] = "\t\t" . join( "\n\t\t" , $this ->_generateDataTableDetailed( $data [ 'after' ][ 'cols' ], $data [ 'after' ][ 'rows' ], $after_data )) . "\n" ; |
251 | $this ->_chart_codes[] = "\t\t" . join( "\n\t\t" , $this ->_generateDataTableSimple( $data [ 'after' ], $after_data )) . "\n" ; |
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" ; |
257 | $this ->_chart_codes[] = "\t\tvar diffData = chart.computeDiff({$before_data}, {$after_data});\n" ; |
259 | $this ->_chart_codes[] = "\t\tchart.draw(diffData, options);\n" ; |
260 | if ( ( $callback =@ $helper_options [ 'callback' ]) ) |
262 | $this ->_chart_codes[] = "\t\t({$callback})({before_data: {$before_data}, after_data: {$after_data}, diff_data: diffData, chart: chart, options: options);\n" ; |
264 | $this ->_chart_codes[] = "\t})();\n" ; |
269 | public function __call( $method , $params ) |
271 | if ( preg_match( '/^chart(.*)$/' , $method , $matches ) && in_array(@ $matches [1], array_keys ( $this ->_chart_info)) ) |
274 | $this ->_chartBase( $matches [1], @ $params [0], @ $params [1], @ $params [2], @ $params [3], @ $params [4]); |
280 | throw new Exception( "Method '" . __CLASS__ . "::{$method}' is not defined." ); |
使う場合はコントローラで $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() を使用する。
各チャートメソッドの詳細は次回に。