PluginProbe
M Chart / 1.3.1
M Chart v1.3.1
2.3.2 2.3.1 2.3 2.2.2 2.2.1 2.2 trunk 1.0 1.1 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 1.10 1.10.1 1.11 1.11.1 1.11.2 1.12 1.2 1.2.1 1.3 1.3.1 1.3.2 All 53 releases
m-chart / components / class-m-chart-highcharts.php

class-m-chart-highcharts.php in M Chart 1.3.1, at components/class-m-chart-highcharts.php

453 lines 12.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 class M_Chart_Highcharts {
4 public $library = 'highcharts';
5 public $value_labels_limit = 15;
6 public $value_labels_div = 10;
7 public $original_labels;
8 public $post;
9 public $post_meta;
10 public $args;
11 public $type_options = array(
12 'line',
13 'spline',
14 'area',
15 'column',
16 'bar',
17 'pie',
18 );
19 public $theme_directories;
20
21 /**
22 * Constructor
23 */
24 public function __construct() {
25 add_action( 'm_chart_update_post_meta', array( $this, 'm_chart_update_post_meta' ), 10, 2 );
26
27 $this->theme_directories = array(
28 get_stylesheet_directory() . '/m-chart-highcharts-themes/', // Child theme
29 get_template_directory() . '/m-chart-highcharts-themes/', // Parent theme
30 __DIR__ . '/highcharts-themes/',
31 );
32 }
33
34 /**
35 * Get the necessary chart data for a given chart and assign it the right class vars
36 *
37 * @param int $post_id the WP ID for the chart post
38 * @param array $args any args we want to override the defaults for
39 */
40 public function get_chart_data( $post_id, $args ) {
41 $this->args = wp_parse_args( $args, m_chart()->get_chart_default_args );
42 $this->post = get_post( $post_id );
43 $this->post_meta = m_chart()->get_post_meta( $this->post->ID );
44 }
45
46 /**
47 * Get the chart options
48 *
49 * @return array an array of Highcharts wide options we want to set
50 */
51 public function get_chart_options() {
52 $chart_options = array(
53 'lang' => array(
54 'numericSymbols' => array(
55 /* Thousands */
56 'K',
57 /* Millions */
58 'M',
59 /* Billions */
60 'B',
61 /* Trillions */
62 'T',
63 ),
64 'thousandsSep' => array(
65 ',',
66 ),
67 ),
68 );
69
70 return apply_filters( 'm_chart_chart_options', $chart_options, $this->library );
71 }
72
73 /**
74 * Returns an arary of all settings and data needed to build a chart
75 *
76 * @param int $post_id WP post ID of the post you want chart args for
77 * @param array $args any args we want to override the defaults for
78 * @param bool $force optional param to force a rebuild of the data even if a cache was found
79 * @param bool $cache optional param to override the default behavior to cache the results
80 *
81 * @return string URL to the plugin directory with path if parameter was passed
82 */
83 public function get_chart_args( $post_id, $args, $force = false, $cache = true ) {
84 // There's a ton of work that goes into generating the chart args so we cache them
85 $cache_key = $post_id . '-chart-args';
86
87 if ( ! $force && $chart_args = wp_cache_get( $cache_key, m_chart()->slug ) ) {
88 // The width can be set via the args so we'll override whatever the cache has with the arg value
89 $chart_args['graph']['width'] = isset( $this->args['width'] ) && is_numeric( $this->args['width'] ) ? $this->args['width'] : '';
90 return $chart_args;
91 }
92
93 if ( ! $this->args || ! $this->post || ! $this->post_meta ) {
94 $this->get_chart_data( $post_id, $args );
95 }
96
97 // Run the parse class on the data
98 m_chart()->parse()->parse_data( $this->post_meta['data'], $this->post_meta['parse_in'] );
99
100 $chart_args = array(
101 'chart' => array(
102 'type' => $this->post_meta['type'],
103 'show_labels' => $this->post_meta['labels'] ? true : false,
104 'renderTo' => 'm-chart-' . $this->post->ID,
105 'height' => $this->post_meta['height'],
106 ),
107 'title' => array(
108 'text' => $this->esc_title( apply_filters( 'the_title', $this->post->post_title ) ),
109 ),
110 'subtitle' => array(
111 'text' => $this->esc_title( $this->post_meta['subtitle'] ),
112 ),
113 'legend' => array(
114 'enabled' => $this->post_meta['legend'] ? true : false,
115 ),
116 'credits' => array(
117 'href' => $this->post_meta['source_url'],
118 'text' => $this->post_meta['source'],
119 ),
120 'exporting' => array(
121 'enabled' => false,
122 ),
123 'plotOptions' => array(
124 'series' => array(
125 'dataLabels' => array(
126 'enabled' => $this->post_meta['labels'] ? true : false,
127 ),
128 'connectNulls' => true,
129 ),
130 $this->post_meta['type'] => array(
131 'dataLabels' => array(
132 'enabled' => $this->post_meta['labels'] ? true : false,
133 ),
134 ),
135 ),
136 );
137
138 // We don't want to set a width unless an explicit width was given
139 if ( is_numeric( $this->args['width'] ) ) {
140 $chart_args['chart']['width'] = $this->args['width'];
141 }
142
143 // Forcing a minimum value of 0 prevents the built in fudging which sometimes looks weird
144 if ( $this->post_meta['y_min'] ) {
145 $chart_args['yAxis']['min'] = 0;
146 }
147
148 // The x axis values need to be set or else you end up with a single notch :P
149 $chart_args['xAxis']['categories'] = $this->get_value_labels_array();
150
151 $chart_args = $this->add_axis_labels( $chart_args );
152 $chart_args = $this->add_data_sets( $chart_args );
153
154 // Add prefix/suffix if appropriate
155 if ( '' != m_chart()->parse()->data_prefix ) {
156 $chart_args['tooltip']['valuePrefix'] = m_chart()->parse()->data_prefix;
157 }
158
159 if ( '' != m_chart()->parse()->data_suffix ) {
160 $chart_args['tooltip']['valueSuffix'] = m_chart()->parse()->data_suffix;
161 }
162
163 $chart_args['plotOptions']['series']['dataLabels']['format'] = m_chart()->parse()->data_prefix . '{y:,f}' . m_chart()->parse()->data_suffix;
164
165 // Apply the theme
166 if ( $theme = $this->get_theme( $this->post_meta['theme'] ) ) {
167 $chart_args = array_merge( $chart_args, $theme );
168 }
169
170 $chart_args = apply_filters( 'm_chart_chart_args', $chart_args, $this->post, $this->post_meta, $this->args );
171
172 // Set the cache, we'll regenerate this when someone updates the post
173 if ( $cache ) {
174 wp_cache_set( $cache_key, $chart_args, m_chart()->slug );
175 }
176
177 // Clear out all of the class vars so the next chart instance starts fresh
178 $this->args = null;
179 $this->post = null;
180 $this->post_meta = null;
181
182 return $chart_args;
183 }
184
185 /**
186 * Hook to the m_chart_update_post_meta action and refresh the chart args cache
187 *
188 * @param int $post_id WP post ID of the post you want chart args for
189 * @param array $parsed_meta the parsed chart meta passed by the action hook
190 */
191 public function m_chart_update_post_meta( $post_id, $parsed_meta ) {
192 // Refresh arg cache
193 $this->args = $this->get_chart_default_args;
194 $this->post = (object) array(
195 'ID' => $post_id,
196 );
197 $this->post_meta = $parsed_meta;
198
199 $this->get_chart_args( $post_id, array(), true );
200 }
201
202 /**
203 * Returns the value labels array
204 *
205 * @return array an array of the value labels need for the active chart
206 */
207
208 public function get_value_labels_array() {
209 $value_labels = m_chart()->parse()->value_labels;
210
211 if ( isset( $value_labels['first_column'] ) ) {
212 $label_key = 'rows' == $this->post_meta['parse_in'] ? 'first_row' : 'first_column';
213
214 return $value_labels[ $label_key ];
215 }
216
217 return $value_labels;
218 }
219
220 /**
221 * Handle adding units to axis labels and/or flipping axis labels on bar chart
222 *
223 * @param array the current array of chart args
224 *
225 * @return array the chart args array with axis labels (and units) added to it
226 */
227 public function add_axis_labels( $chart_args ) {
228 $chart_args['xAxis']['title']['text'] = $this->esc_title( $this->post_meta['x_title'] );
229
230 // We've got x axis units so we'll add them to the axis label
231 if ( '' != $this->post_meta['x_units'] ) {
232 $units = get_term_by( 'slug', $this->post_meta['x_units'], m_chart()->slug . '-units' );
233 $x_units = '' != $this->post_meta['x_title'] ? ' (' . $units->name . ')' : $units->name;
234
235 $chart_args['xAxis']['title']['text'] .= $x_units;
236 }
237
238 $chart_args['yAxis']['title']['text'] = $this->esc_title( $this->post_meta['y_title'] );
239
240 // We've got y axis units so we'll add them to the axis label
241 if ( $this->post_meta['y_units'] != '' ) {
242 $units = get_term_by( 'slug', $this->post_meta['y_units'], m_chart()->slug . '-units' );
243 $y_units = '' != $this->post_meta['y_title'] ? ' (' . $units->name . ')' : $units->name;
244
245 $chart_args['yAxis']['title']['text'] .= $y_units;
246 }
247
248 // Bar charts are just rotated column charts in Highcharts so we flip things around to keep the UI consistent
249 if ( 'bar' == $this->post_meta['type'] ) {
250 $x_title = $chart_args['xAxis']['title']['text'];
251
252 $chart_args['xAxis']['title']['text'] = $chart_args['yAxis']['title']['text'];
253 $chart_args['yAxis']['title']['text'] = $x_title;
254 }
255
256 return $chart_args;
257 }
258
259 /**
260 * Handle adding data sets (series in Highcharts terminology) to the chart args
261 *
262 * @param array the current array of chart args
263 *
264 * @return array the chart args array with data sets added to it
265 */
266 public function add_data_sets( $chart_args ) {
267 // When Highcharts encounters an empty data value it stops so we set them to NULL
268 $data_array = array_map( array( $this, 'fix_null_values' ), m_chart()->parse()->set_data );
269
270 if ( 'pie' != $this->post_meta['type'] && 'both' == m_chart()->parse()->value_labels_position ) {
271 $set_data = array();
272
273 $label_key = ( $this->post_meta['parse_in'] == 'rows' ) ? 'first_column' : 'first_row';
274
275 foreach ( $data_array as $key => $data_chunk ) {
276 $set_data[ $key ] = array(
277 'name' => m_chart()->parse()->value_labels[ $label_key ][ $key ],
278 'data' => array(),
279 );
280
281 foreach ( $data_chunk as $data ) {
282 $set_data[ $key ]['data'][] = $data;
283 }
284 }
285
286 $chart_args['series'] = $set_data;
287 }
288 else
289 {
290 $new_data_array = array();
291
292 foreach ( $chart_args['xAxis']['categories'] as $key => $label ) {
293 $new_data_array[ $key ] = array(
294 $label,
295 $data_array[ $key ],
296 );
297 }
298
299 if ( 'pie' == $this->post_meta['type'] ) {
300 // Don't need these anymore for pie charts
301 unset( $chart_args['xAxis']['categories'] );
302 }
303
304 $chart_args['series'] = array(
305 array(
306 'type' => $this->post_meta['type'],
307 'showInLegend' => true,
308 'data' => $new_data_array,
309 ),
310 );
311
312 $chart_args['tooltip'] = array(
313 'pointFormat' => '<b>{point.y}</b>',
314 );
315 }
316
317 return $chart_args;
318 }
319
320 /**
321 * Helper function escapes and modifies text/title values so they work in Highcharts
322 *
323 * @param string an string you want to use in Highcharts
324 *
325 * @return string an escaped and modified string
326 */
327 public function esc_title( $string ) {
328 $string = html_entity_decode( $string, ENT_QUOTES );
329
330 $find = array(
331 "\n",
332 "\r",
333 '<br><br>',
334 '—',
335 '–',
336 );
337
338 $replace = array(
339 '<br />',
340 '<br />',
341 '<br />',
342 '-',
343 '-',
344 );
345
346 $string = str_replace( $find, $replace, $string );
347
348 // @TODO: See if this addslashes/stripslashes is still necessary (need to remember why I did it first...)
349 return addslashes( stripslashes( $string ) );
350 }
351
352 /**
353 * Helper function sets empty values to NULL so that Highcharts handles them correctly.
354 *
355 * @param string/int a data value
356 *
357 * @return int/null the integer value or NULL if the value was not numeric
358 */
359 public function fix_null_values( $value ) {
360 if ( is_array( $value ) ) {
361 return array_map( array( $this, 'fix_null_values' ), $value );
362 }
363
364 if ( ! is_numeric( $value ) ) {
365 return null;
366 }
367
368 return $value;
369 }
370
371 /**
372 * Get all themes available from the various theme directories
373 *
374 * @return array an array of themes
375 */
376 public function get_themes() {
377 $themes = array();
378
379 foreach ( $this->theme_directories as $directory ) {
380 $themes = array_merge( $themes, $this->_get_themes_readdir( $directory ) );
381 }
382
383 return $themes;
384 }
385
386 /**
387 * Returns the theme options for a given theme
388 *
389 * @param string a theme slug
390 *
391 * @return string/boolean requested theme options or false if they could not be found
392 */
393 private function get_theme( $slug ) {
394 foreach ( $this->theme_directories as $directory ) {
395 if ( ! $themes = $this->_get_themes_readdir( $directory ) ) {
396 continue;
397 } // END if
398
399 foreach ( $themes as $theme )
400 {
401 if ( $theme->slug == $slug )
402 {
403 return $theme->options;
404 } // END if
405 } // END foreach
406 } // END foreach
407
408 return false;
409 }
410
411 /**
412 * Get all themes from a given directory
413 *
414 * @param string a path to a server directory
415 *
416 * @return array an array of all the themes available in a given directory
417 */
418 private function _get_themes_readdir( $theme_base ) {
419 // Sanity check to make sure we have a real directory
420 if ( ! is_dir( $theme_base ) ) {
421 return array();
422 }
423
424 $theme_dir = new DirectoryIterator( $theme_base );
425 $themes = array();
426
427 foreach ( $theme_dir as $file ) {
428 if ( ! $file->isFile() || 'php' != $file->getExtension() ) {
429 continue;
430 }
431
432 $theme_data = implode( '', file( $theme_base . $file ) );
433
434 if ( preg_match( '|Theme Name:(.*)$|mi', $theme_data, $name ) ) {
435 $name = trim( _cleanup_header_comment( $name[1] ) );
436 } // END if
437
438 if ( isset( $name ) && '' != $name )
439 {
440 $file = basename( $file );
441
442 $themes[ $file ] = (object) array(
443 'name' => $name,
444 'slug' => substr( $file, 0, -4 ),
445 'file' => $file,
446 'options' => require $theme_base . $file,
447 );
448 } // END if
449 } // END foreach
450
451 return $themes;
452 }
453 }