PluginProbe
Visualizer – Tables & Charts Manager with Built-in AI Generator / 4.0.7
Visualizer – Tables & Charts Manager with Built-in AI Generator v4.0.7
4.0.7 4.0.6 4.0.5 4.0.4 4.0.3 3.0.5 3.0.6 3.0.7 3.0.8 3.0.9 3.1.0 3.1.1 3.1.2 3.1.3 3.10.0 3.10.1 3.10.10 3.10.11 3.10.12 3.10.13 3.10.14 3.10.15 3.10.2 3.10.3 3.10.4 All 148 releases
visualizer / classes / Visualizer / Module / Utility.php

Utility.php in Visualizer – Tables & Charts Manager with Built-in AI Generator 4.0.7, at classes/Visualizer/Module/Utility.php

473 lines 14.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 // +----------------------------------------------------------------------+
3 // | Copyright 2013 Madpixels (email : visualizer@madpixels.net) |
4 // +----------------------------------------------------------------------+
5 // | This program is free software; you can redistribute it and/or modify |
6 // | it under the terms of the GNU General Public License, version 2, as |
7 // | published by the Free Software Foundation. |
8 // | |
9 // | This program is distributed in the hope that it will be useful, |
10 // | but WITHOUT ANY WARRANTY; without even the implied warranty of |
11 // | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
12 // | GNU General Public License for more details. |
13 // | |
14 // | You should have received a copy of the GNU General Public License |
15 // | along with this program; if not, write to the Free Software |
16 // | Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, |
17 // | MA 02110-1301 USA |
18 // +----------------------------------------------------------------------+
19 // | Author: Eugene Manuilov <eugene@manuilov.org> |
20 // +----------------------------------------------------------------------+
21 /**
22 * Utilities module class.
23 *
24 * @category Visualizer
25 * @package Module
26 *
27 * @since 3.3.0
28 */
29 class Visualizer_Module_Utility extends Visualizer_Module {
30
31 const NAME = __CLASS__;
32
33 /**
34 * Some default distinct colors.
35 *
36 * @since 3.3.0
37 *
38 * @access private
39 * @var string[]
40 */
41 private static $_CHART_COLORS = array(
42 '#e6194b', '#3cb44b', '#ffe119', '#4363d8', '#f58231', '#911eb4', '#46f0f0', '#f032e6', '#bcf60c', '#fabebe', '#008080', '#e6beff', '#9a6324', '#fffac8', '#800000', '#aaffc3', '#808000', '#ffd8b1', '#000075', '#808080',
43 );
44
45
46 /**
47 * Constructor.
48 *
49 * @since 3.3.0
50 *
51 * @access public
52 *
53 * @param Visualizer_Plugin $plugin The instance of the plugin.
54 */
55 public function __construct( Visualizer_Plugin $plugin ) {
56 parent::__construct( $plugin );
57 $this->_addFilter( Visualizer_Plugin::FILTER_GET_CHART_SETTINGS, 'apply_global_style_settings', 999, 3 );
58 }
59
60
61 /**
62 * Convert hexdec color string to rgb(a) string.
63 *
64 * Props to https://mekshq.com/how-to-convert-hexadecimal-color-code-to-rgb-or-rgba-using-php/
65 *
66 * @since 3.3.0
67 *
68 * @access private
69 */
70 private static function hex2rgba( $color, $opacity = false ) {
71 $default = 'rgb(0,0,0)';
72
73 // Return default if no color provided
74 if ( empty( $color ) ) {
75 return $default;
76 }
77
78 // Sanitize $color if "#" is provided
79 if ( strpos( $color, '#' ) === 0 ) {
80 $color = substr( $color, 1 );
81 }
82
83 // Check if color has 6 or 3 characters and get values
84 if ( strlen( $color ) === 6 ) {
85 $hex = array( $color[0] . $color[1], $color[2] . $color[3], $color[4] . $color[5] );
86 } elseif ( strlen( $color ) === 3 ) {
87 $hex = array( $color[0] . $color[0], $color[1] . $color[1], $color[2] . $color[2] );
88 } else {
89 return $default;
90 }
91
92 // Convert hexadec to rgb
93 $rgb = array_map( 'hexdec', $hex );
94
95 // Check if opacity is set(rgba or rgb)
96 if ( $opacity ) {
97 if ( abs( $opacity ) > 1 ) {
98 $opacity = 1.0;
99 }
100 $output = 'rgba(' . implode( ',', $rgb ) . ',' . $opacity . ')';
101 } else {
102 $output = 'rgb(' . implode( ',', $rgb ) . ')';
103 }
104
105 // Return rgb(a) color string
106 return $output;
107 }
108
109 /**
110 * Returns a color at a specific index from the palette, with its transparent equivalent.
111 *
112 * @return array{string, string}
113 * @access private
114 */
115 private static function get_color_at( int $index ): array {
116 $colors = self::get_color_palette();
117 $color = $colors[ $index % count( $colors ) ];
118 return array( self::hex2rgba( $color, 0.5 ), $color );
119 }
120
121 /**
122 * Mixes a hex color toward white by the given factor (0 = original, 1 = white).
123 *
124 * @access private
125 */
126 private static function tint_color( string $hex, float $factor ): string {
127 $hex = ltrim( $hex, '#' );
128 if ( strlen( $hex ) === 3 ) {
129 $hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2];
130 }
131 $r = (int) round( hexdec( substr( $hex, 0, 2 ) ) + ( 255 - hexdec( substr( $hex, 0, 2 ) ) ) * $factor );
132 $g = (int) round( hexdec( substr( $hex, 2, 2 ) ) + ( 255 - hexdec( substr( $hex, 2, 2 ) ) ) * $factor );
133 $b = (int) round( hexdec( substr( $hex, 4, 2 ) ) + ( 255 - hexdec( substr( $hex, 4, 2 ) ) ) * $factor );
134 return sprintf( '#%02x%02x%02x', $r, $g, $b );
135 }
136
137 /**
138 * Returns the color palette to use for charts.
139 *
140 * When global primary/secondary colors are configured, generates a palette of
141 * alternating tints: [primary, secondary, primary@25%, secondary@25%, ...].
142 * Falls back to the built-in colors otherwise.
143 *
144 * @return string[]
145 * @access private
146 */
147 private static function get_color_palette(): array {
148 $global = self::get_global_style_defaults();
149 $primary = $global['color_primary'];
150 $secondary = $global['color_secondary'];
151
152 if ( empty( $primary ) && empty( $secondary ) ) {
153 return self::$_CHART_COLORS;
154 }
155
156 $bases = array_filter( array( $primary, $secondary ) );
157 $factors = array( 0, 0.25, 0.5, 0.75 );
158 $palette = array();
159
160 foreach ( $factors as $factor ) {
161 foreach ( $bases as $base ) {
162 $palette[] = $factor === 0 ? $base : self::tint_color( $base, $factor );
163 }
164 }
165
166 return $palette;
167 }
168
169 /**
170 * Returns the global style defaults stored in the plugin settings.
171 *
172 * @return array<string, string>
173 * @access public
174 * @static
175 */
176 public static function get_global_style_defaults(): array {
177 $option = get_option( Visualizer_Module_Admin::OPTION_GLOBAL_SETTINGS, array() );
178 return wp_parse_args(
179 $option,
180 array(
181 'color_primary' => '',
182 'color_secondary' => '',
183 'apply_existing' => '0',
184 )
185 );
186 }
187
188 /**
189 * Applies global styles to existing charts at render time.
190 *
191 * @param array<string, mixed>|mixed $settings Chart settings.
192 * @param int $chart_id Chart ID.
193 * @param string $type Chart type.
194 * @return array<string, mixed>
195 * @access public
196 */
197 public function apply_global_style_settings( $settings, $chart_id, $type ): array {
198 $settings = is_array( $settings ) ? $settings : array();
199 $global = self::get_global_style_defaults();
200
201 if ( empty( $global['apply_existing'] ) ) {
202 return $settings;
203 }
204
205 if ( empty( $global['color_primary'] ) && empty( $global['color_secondary'] ) ) {
206 return $settings;
207 }
208
209 if ( empty( $chart_id ) ) {
210 return $settings;
211 }
212
213 $library = get_post_meta( $chart_id, Visualizer_Plugin::CF_CHART_LIBRARY, true );
214 $library = is_string( $library ) ? strtolower( $library ) : '';
215 if ( empty( $type ) ) {
216 $type = get_post_meta( $chart_id, Visualizer_Plugin::CF_CHART_TYPE, true );
217 }
218 $type = is_string( $type ) ? strtolower( $type ) : '';
219
220 if ( empty( $library ) ) {
221 $library = in_array( $type, array( 'datatable', 'tabular' ), true ) ? 'datatable' : 'googlecharts';
222 }
223
224 if ( 'googlecharts' === $library || 'google' === $library ) {
225 if ( 'geo' !== $type ) {
226 $has_explicit = false;
227 if ( ! empty( $settings['colors'] ) ) {
228 $has_explicit = true;
229 }
230 if ( ! $has_explicit && isset( $settings['series'] ) && is_array( $settings['series'] ) ) {
231 foreach ( $settings['series'] as $series_settings ) {
232 if ( ! empty( $series_settings['color'] ) ) {
233 $has_explicit = true;
234 break;
235 }
236 }
237 }
238 if ( ! $has_explicit && isset( $settings['slices'] ) && is_array( $settings['slices'] ) ) {
239 foreach ( $settings['slices'] as $slice_settings ) {
240 if ( ! empty( $slice_settings['color'] ) ) {
241 $has_explicit = true;
242 break;
243 }
244 }
245 }
246 if ( ! $has_explicit ) {
247 $settings['colors'] = self::get_color_palette();
248 }
249 }
250 return $settings;
251 }
252
253 if ( 'chartjs' === $library ) {
254 $has_explicit = false;
255 if ( isset( $settings['series'] ) && is_array( $settings['series'] ) ) {
256 foreach ( $settings['series'] as $series_settings ) {
257 if ( ! empty( $series_settings['backgroundColor'] ) || ! empty( $series_settings['borderColor'] ) || ! empty( $series_settings['hoverBackgroundColor'] ) ) {
258 $has_explicit = true;
259 break;
260 }
261 }
262 }
263 if ( ! $has_explicit && isset( $settings['slices'] ) && is_array( $settings['slices'] ) ) {
264 foreach ( $settings['slices'] as $slice_settings ) {
265 if ( ! empty( $slice_settings['backgroundColor'] ) || ! empty( $slice_settings['borderColor'] ) || ! empty( $slice_settings['hoverBackgroundColor'] ) ) {
266 $has_explicit = true;
267 break;
268 }
269 }
270 }
271 if ( $has_explicit ) {
272 return $settings;
273 }
274 $series = get_post_meta( $chart_id, Visualizer_Plugin::CF_SERIES, true );
275 if ( is_array( $series ) ) {
276 $settings = self::apply_chartjs_palette( $settings, $type, $series, $chart_id );
277 }
278 }
279
280 return $settings;
281 }
282
283 /**
284 * Applies the global palette to ChartJS settings for a chart.
285 *
286 * @param array<string, mixed> $settings Current chart settings.
287 * @param string $type Chart type.
288 * @param array<int, mixed> $series Series definitions.
289 * @param int $chart_id Chart ID.
290 * @return array<string, mixed>
291 * @access private
292 * @static
293 */
294 private static function apply_chartjs_palette( array $settings, string $type, array $series, int $chart_id ): array {
295 $attributes = array();
296 $name = 'series';
297 $count = count( $series );
298 $max = $count - 1;
299
300 switch ( $type ) {
301 case 'polarArea':
302 // fall through.
303 case 'pie':
304 $chart = get_post( $chart_id );
305 $raw = $chart instanceof WP_Post ? $chart->post_content : array();
306 $data = self::maybe_decode_content( $raw );
307 $name = 'slices';
308 $max = is_array( $data ) ? count( $data ) : $count;
309 // fall through.
310 case 'column':
311 // fall through.
312 case 'bar':
313 for ( $i = 0; $i < $max; $i++ ) {
314 $colors = self::get_color_at( $i );
315 $attributes[] = array( 'backgroundColor' => $colors[0], 'hoverBackgroundColor' => $colors[1] );
316 }
317 break;
318 case 'radar':
319 // fall through.
320 case 'line':
321 // fall through.
322 case 'area':
323 for ( $i = 0; $i < $max; $i++ ) {
324 $colors = self::get_color_at( $i );
325 $attributes[] = array( 'borderColor' => $colors[0] );
326 }
327 break;
328 }
329
330 if ( $attributes ) {
331 $settings[ $name ] = $attributes;
332 }
333
334 return $settings;
335 }
336
337 /**
338 * Sets some defaults in the chart.
339 *
340 * @since 3.3.0
341 *
342 * @access public
343 */
344 public static function set_defaults( $chart, $post_status = 'auto-draft' ) {
345 $type = get_post_meta( $chart->ID, Visualizer_Plugin::CF_CHART_TYPE, true );
346 $library = get_post_meta( $chart->ID, Visualizer_Plugin::CF_CHART_LIBRARY, true );
347
348 // if post_status is null, operate on the chart irrespective of the post_status
349 if ( ( ! is_null( $post_status ) && $chart->post_status !== $post_status ) ) {
350 return;
351 }
352
353 $series = get_post_meta( $chart->ID, Visualizer_Plugin::CF_SERIES, true );
354 if ( ! $series || ! is_array( $series ) ) {
355 return;
356 }
357
358 switch ( $library ) {
359 case 'ChartJS':
360 self::set_defaults_chartjs( $chart, $post_status );
361 break;
362 case 'GoogleCharts':
363 self::set_defaults_google( $chart, $post_status );
364 break;
365 }
366 }
367
368 /**
369 * Sets some defaults in the chart for Google charts.
370 *
371 * @since 3.3.0
372 *
373 * @access private
374 */
375 private static function set_defaults_google( $chart, $post_status ) {
376 $type = get_post_meta( $chart->ID, Visualizer_Plugin::CF_CHART_TYPE, true );
377 $series = get_post_meta( $chart->ID, Visualizer_Plugin::CF_SERIES, true );
378
379 $attributes = array();
380 if ( $post_status === 'auto-draft' ) {
381 switch ( $type ) {
382 case 'combo':
383 // chart type 'bars' and randomly choose the type of each series.
384 $types = array( 'area', 'line', 'steppedArea', 'bar' );
385 $attributes['seriesType'] = 'bars';
386 for ( $x = 0; $x < count( $series ); $x++ ) {
387 $attributes['series'][ $x ]['type'] = $types[ rand( 0, count( $types ) - 1 ) ];
388 }
389 break;
390 case 'candlestick':
391 // add stroke and fill color so that behavior is consistent.
392 $attributes['candlestick']['fallingColor']['stroke'] = '#3366cc';
393 $attributes['candlestick']['fallingColor']['fill'] = '#fff';
394 $attributes['candlestick']['risingColor']['stroke'] = '#3366cc';
395 $attributes['candlestick']['risingColor']['fill'] = '#3366cc';
396 break;
397 }
398
399 // Apply global color defaults to new Google Charts (not Geo — it uses colorAxis).
400 if ( 'geo' !== $type ) {
401 $global = self::get_global_style_defaults();
402 if ( ! empty( $global['color_primary'] ) || ! empty( $global['color_secondary'] ) ) {
403 $attributes['colors'] = self::get_color_palette();
404 }
405 }
406 }
407
408 if ( $attributes ) {
409 $settings = get_post_meta( $chart->ID, Visualizer_Plugin::CF_SETTINGS, true );
410 update_post_meta( $chart->ID, Visualizer_Plugin::CF_SETTINGS, array_merge( $settings, $attributes ) );
411 }
412 }
413
414 /**
415 * Sets some defaults in the chart for ChartJS charts. Only during first creation.
416 *
417 * @since 3.3.0
418 *
419 * @access private
420 */
421 private static function set_defaults_chartjs( $chart, $post_status ) {
422 if ( $post_status !== 'auto-draft' ) {
423 return;
424 }
425
426 $type = get_post_meta( $chart->ID, Visualizer_Plugin::CF_CHART_TYPE, true );
427 $series = get_post_meta( $chart->ID, Visualizer_Plugin::CF_SERIES, true );
428 $settings = get_post_meta( $chart->ID, Visualizer_Plugin::CF_SETTINGS, true );
429
430 $attributes = array();
431 $name = 'series';
432 $count = count( $series );
433 $max = $count - 1;
434 switch ( $type ) {
435 case 'polarArea':
436 // fall through.
437 case 'pie':
438 $data = self::decode_content( $chart->post_content );
439 $name = 'slices';
440 $max = count( $data );
441 // fall through.
442 case 'column':
443 // fall through.
444 case 'bar':
445 for ( $i = 0; $i < $max; $i++ ) {
446 $colors = self::get_color_at( $i );
447 $attributes[] = array( 'backgroundColor' => $colors[0], 'hoverBackgroundColor' => $colors[1] );
448 }
449 break;
450 case 'radar':
451 // fall through.
452 case 'line':
453 // fall through.
454 case 'area':
455 for ( $i = 0; $i < $max; $i++ ) {
456 $colors = self::get_color_at( $i );
457 $attributes[] = array( 'borderColor' => $colors[0] );
458 }
459 break;
460 }
461
462 if ( $post_status === 'auto-draft' ) {
463 // the charts are huge in size so let's always get them down to 50%.
464 $settings['width'] = $settings['height'] = '50%';
465 }
466
467 if ( $attributes ) {
468 $settings[ $name ] = $attributes;
469 update_post_meta( $chart->ID, Visualizer_Plugin::CF_SETTINGS, $settings );
470 }
471 }
472 }
473