PluginProbe
SQL Chart Builder / 3.0.6
SQL Chart Builder v3.0.6
3.0.6 3.0.5 3.0.4 3.0.3 3.0.2 3.0.1 trunk 1.0.2 1.0.3 2.2.2 2.3.0 2.3.1 2.3.2 2.3.3 2.3.4 2.3.5 2.3.6 2.3.7 2.3.7.1 2.3.7.2 2.3.8 3.0.0
sql-chart-builder / functions.php

functions.php in SQL Chart Builder 3.0.6, at functions.php

1,427 lines 62.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 if (!defined('ABSPATH')) {
3 die;
4 }
5 //postinstall function
6 function guaven_sqlcharts_load_defaults()
7 {
8 if (get_option("guaven_sqlcharts_already_installed_2") === false) {
9 update_option("guaven_sqlcharts_already_installed_2", "1");
10 guaven_sqlcharts_install_first_data();
11
12 }
13 }
14
15 function guaven_sqlcharts_install_first_data()
16 {
17 require_once(dirname(__FILE__) . "/initial_data.php");
18 gvn_chart_sample_nonxml_data();
19 }
20
21
22 function guaven_sqlcharts_recommended(){
23 if(!class_exists('WooCommerce'))return;
24 $uid=(int)get_current_user_id();
25 if(isset($_GET["gvnsql_dismiss_recommendation"])){
26 update_option('gvnsql_dismiss_recommendation_'.$uid,1);
27 return;
28 }
29 if(get_option('gvnsql_dismiss_recommendation_'.$uid)!='')return;
30 return '<table class="gf-alert gf-alert-info" style="margin-top:20px">
31 <tbody><tr><td style="width: auto;vertical-align: top;padding: 20px;">
32 <h2>WooCommerce Search Engine – INSTANT, RELEVANT AND SMART Search Box</h2>
33 <h3>Turn your website search into Smart Search which find products by price, SKU, attributes, meta data, categorys, tags etc. </h3>
34 <p>“WooCommerce Search Engine” is a very powerful and easy to use WooCommerce Search Plugin which turns a simple search box of your WooCommerce Store to the powerful multifunctional magic box which helps you to sell more products. The plugin UI is compatible with ALL THEMES.</p>
35 <a target="_blank" style="border:0px solid #6200ee;border-radius:0px;color:white;font-weight:bold;background: #6200ee;" class="button button-secondary"
36 href="https://codecanyon.net/item/woocommerce-search-box/15685698">Get the Search Box </a>
37 </td><td style="position:relative">
38 <a href="'.admin_url().'/edit.php?post_type=gvn_schart&gvnsql_dismiss_recommendation=1'.'" style="position: absolute;right: 0;top: -15px;right: -10px;">{svg}
39 </a>
40 <img src="'.plugin_dir_url( __FILE__ ) . 'asset/img/recommended1.jpg" style="max-width: 430px;"></td></tr>
41 </tbody></table>';
42 }
43
44
45
46 function guaven_sqlcharts_my_admin_notice()
47 {
48 global $post;
49
50
51 if(
52 (!empty($_SERVER["REQUEST_URI"]) and strpos(sanitize_text_field(wp_unslash($_SERVER["REQUEST_URI"])),'post_type=gvn_schart')!==false)
53 or
54 (!empty($post) and $post->post_type == 'gvn_schart')
55 ){
56 echo str_replace('{svg}','<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="11" height="14" viewBox="0 0 11 14">
57 <path d="M10.141 10.328q0 0.312-0.219 0.531l-1.062 1.062q-0.219 0.219-0.531 0.219t-0.531-0.219l-2.297-2.297-2.297 2.297q-0.219 0.219-0.531 0.219t-0.531-0.219l-1.062-1.062q-0.219-0.219-0.219-0.531t0.219-0.531l2.297-2.297-2.297-2.297q-0.219-0.219-0.219-0.531t0.219-0.531l1.062-1.062q0.219-0.219 0.531-0.219t0.531 0.219l2.297 2.297 2.297-2.297q0.219-0.219 0.531-0.219t0.531 0.219l1.062 1.062q0.219 0.219 0.219 0.531t-0.219 0.531l-2.297 2.297 2.297 2.297q0.219 0.219 0.219 0.531z"></path>
58 </svg>',wp_kses_post(guaven_sqlcharts_recommended(),[]));
59 }
60
61
62
63 if (!empty($post) and $post->post_type == 'gvn_schart'):
64 if (!current_user_can('manage_options')) {
65 echo '<br><br>
66 <div class="updated gf-alert gf-alert-danger">Only administrators can manage this page</div>';
67 die();
68 }
69 echo '<div class="updated gf-alert gf-alert-info">';
70 if (empty($_GET["post"]) && strpos(sanitize_text_field(wp_unslash($_SERVER["REQUEST_URI"])), "post-new") === false) {
71 $gf_message = __(
72 'Use <b>Add new</b> button above to create a new SQL report. And click on any existing rule names below to manage them.',
73 'guaven_sqlcharts'
74 );
75 } else {
76 $gf_message = __(
77 '1. Give any name to your report.<br>
78 2. Pick a chart type, build your SQL query with the visual builder (or type it — autocomplete will help), map the X/Y columns and press Publish/Update.<br>
79 3. After update, you will see the shortcode of this chart at the bottom of the page. You can use that shortcode anywhere in your website: in pages, posts, widgets, etc.',
80 'guaven_sqlcharts'
81 );
82 }
83
84 echo '<div style="float:left;max-width:calc(100% - 345px)">';
85 echo wp_kses_post( $gf_message );
86 echo '</div>';
87
88 echo '<div style="float: right;
89 margin-top: 0px;
90 padding-top: 0px;"><a target="_blank" style="text-align:center;border:0px solid #6200ee;border-radius:0px;color:white;font-weight:bold;background: #6200ee;"
91 class="button button-secondary" href="https://guaven.com/contact/solution-request/">Get Premium Support </a>
92 <span style="line-height: 30px;padding: 0 5px;">OR</span>
93 <a target="_blank" style="text-align:center;border:0px solid #26b286;border-radius:0px;color:white;font-weight:bold;background: #26b286;"
94 class="button button-secondary" href="https://guaven.com/service/small-thankyou-premium-support-service/">Make a Small Donation</a>
95 </div> </div>';
96 endif;
97 }
98 add_action('admin_notices', 'guaven_sqlcharts_my_admin_notice');
99
100 function guaven_sqlcharts_onboarding_notice(){
101 if((function_exists('wp_doing_ajax') and wp_doing_ajax()) or get_option('guaven_sqlcharts_onboarding_notice_dismissed') == 1)
102 return;
103
104 printf('
105 <div class="guaven-sqlcharts-notice notice notice-success is-dismissible" data-notice="onboarding_notice">
106 <p>Welcome aboard on MySQL Charts!</p>
107 </div>'
108 );
109 }
110 add_action('admin_notices', 'guaven_sqlcharts_onboarding_notice');
111
112 function guaven_sqlcharts_onboarding_notice_dismissed(){
113 check_ajax_referer('notice_dismissed', 'nonce');
114 if (!current_user_can('manage_options')) return;
115
116 if(empty($_POST['type']))return;
117 switch ($_POST['type']){
118 case 'onboarding_notice':
119 update_option('guaven_sqlcharts_onboarding_notice_dismissed', 1);
120 break;
121 }
122 }
123 add_action('wp_ajax_guaven_sqlcharts_onboarding_notice_dismissed', 'guaven_sqlcharts_onboarding_notice_dismissed');
124
125 function guaven_sqlcharts_enqueue_chart()
126 {
127 wp_enqueue_script('guaven_sqlcharts_chartjs', plugins_url('asset/chart.umd.min.js', __FILE__),array('jquery'),GVNSQLCHARTS_VERSION,false);
128 wp_enqueue_script('guaven_sqlcharts_datepicker', plugins_url('asset/datepicker.min.js', __FILE__),array('jquery'),GVNSQLCHARTS_VERSION,false);
129 wp_enqueue_script('guaven_sqlcharts_front', plugins_url('asset/front.js', __FILE__),array('jquery','guaven_sqlcharts_chartjs','guaven_sqlcharts_datepicker'),GVNSQLCHARTS_VERSION,false);
130 wp_localize_script('guaven_sqlcharts_front', 'guaven_sqlcharts_notice_dismissed', array(
131 'action' => 'guaven_sqlcharts_onboarding_notice_dismissed',
132 'nonce' => wp_create_nonce('notice_dismissed')
133 ));
134
135 }
136 add_action('wp_enqueue_scripts', 'guaven_sqlcharts_enqueue_chart');
137 add_action('admin_enqueue_scripts', 'guaven_sqlcharts_enqueue_chart');
138
139 function guaven_sqlcharts_enqueue_main_style()
140 {
141 wp_enqueue_style('guaven_sqlcharts_main_style', plugins_url('asset/guaven_sqlcharts.css', __FILE__),array(),GVNSQLCHARTS_VERSION);
142 }
143 add_action('wp_enqueue_scripts', 'guaven_sqlcharts_enqueue_main_style');
144 add_action('admin_enqueue_scripts', 'guaven_sqlcharts_enqueue_main_style');
145
146 // admin-only assets: SQL builder, autocomplete, new metabox UI
147 function guaven_sqlcharts_admin_assets($hook)
148 {
149 if (!in_array($hook, array('post.php', 'post-new.php'))) return;
150 $screen = function_exists('get_current_screen') ? get_current_screen() : null;
151 if (empty($screen->post_type) or $screen->post_type != 'gvn_schart') return;
152 if (!current_user_can('manage_options')) return;
153
154 wp_enqueue_style('guaven_sqlcharts_admin_style', plugins_url('asset/admin.css', __FILE__), array(), GVNSQLCHARTS_VERSION);
155 wp_enqueue_script('guaven_sqlcharts_admin', plugins_url('asset/admin.js', __FILE__), array('jquery'), GVNSQLCHARTS_VERSION, true);
156
157 global $wpdb;
158 $tables = $wpdb->get_col('SHOW TABLES');
159 if (!is_array($tables)) $tables = array();
160 wp_localize_script('guaven_sqlcharts_admin', 'gvnSqlBuilder', array(
161 'ajaxurl' => admin_url('admin-ajax.php'),
162 'nonce' => wp_create_nonce('gvnsql_schema'),
163 'tables' => array_values($tables),
164 'prefix' => $wpdb->prefix,
165 ));
166 }
167 add_action('admin_enqueue_scripts', 'guaven_sqlcharts_admin_assets');
168
169 // returns column names of one table for the live SQL builder/autocomplete
170 function guaven_sqlcharts_ajax_columns()
171 {
172 check_ajax_referer('gvnsql_schema', 'nonce');
173 if (!current_user_can('manage_options')) wp_send_json_error('forbidden', 403);
174 global $wpdb;
175 $table = isset($_POST['table']) ? sanitize_text_field(wp_unslash($_POST['table'])) : '';
176 $tables = $wpdb->get_col('SHOW TABLES');
177 if (!is_array($tables) or !in_array($table, $tables, true)) wp_send_json_error('unknown table', 400);
178 $cols = $wpdb->get_results('SHOW COLUMNS FROM `' . str_replace('`', '', $table) . '`');
179 $out = array();
180 if (is_array($cols)) {
181 foreach ($cols as $col) {
182 $out[] = array('name' => $col->Field, 'type' => $col->Type);
183 }
184 }
185 wp_send_json_success($out);
186 }
187 add_action('wp_ajax_gvnsql_get_columns', 'guaven_sqlcharts_ajax_columns');
188
189
190 function guaven_sqlcharts_isJson($string)
191 {
192 json_decode($string);
193 return (json_last_error() == JSON_ERROR_NONE);
194 }
195
196 add_action('init', 'guaven_sqlcharts_register_post');
197 function guaven_sqlcharts_register_post()
198 {
199 register_post_type('gvn_schart', array(
200 'labels' => array(
201 'name' => __('My SQL Charts','guaven_sqlcharts'),
202 'singular_name' => __('SQL Chart','guaven_sqlcharts'),
203 'menu_name' => __('My SQL Charts','guaven_sqlcharts'),
204 'add_new' => __('Add Chart','guaven_sqlcharts'),
205 'add_new_item' => __('Add New Chart','guaven_sqlcharts'),
206 'edit_item' => __('Edit Chart','guaven_sqlcharts'),
207 'new_item' => __('New Chart','guaven_sqlcharts'),
208 'view_item' => __('View Chart','guaven_sqlcharts'),
209 'view_items' => __('View Charts','guaven_sqlcharts'),
210 'search_items' => __('Search Charts','guaven_sqlcharts'),
211 'not_found' => __('No charts found','guaven_sqlcharts'),
212 'not_found_in_trash' => __('No charts found in Trash','guaven_sqlcharts'),
213 'all_items' => __('All Charts','guaven_sqlcharts'),
214 'archives' => __('Chart Archives','guaven_sqlcharts'),
215 'attributes' => __('Chart Attributes','guaven_sqlcharts'),
216 'insert_into_item' => __('Insert into chart','guaven_sqlcharts'),
217 'uploaded_to_this_item' => __('Uploaded to this chart','guaven_sqlcharts'),
218 'filter_items_list' => __('Filter charts list','guaven_sqlcharts'),
219 'items_list_navigation' => __('Charts list navigation','guaven_sqlcharts'),
220 'items_list' => __('Charts list','guaven_sqlcharts'),
221 'item_published' => __('Chart published.','guaven_sqlcharts'),
222 'item_published_privately' => __('Chart published privately.','guaven_sqlcharts'),
223 'item_reverted_to_draft' => __('Chart reverted to draft.','guaven_sqlcharts'),
224 'item_scheduled' => __('Chart scheduled.','guaven_sqlcharts'),
225 'item_updated' => __('Chart updated.','guaven_sqlcharts'),
226 ),
227
228 'public' => true,
229 'show_in_rest' => false,
230 'menu_icon' => 'dashicons-chart-pie',
231 // Charts execute SQL, so every primitive capability of this post type maps to manage_options.
232 // Contributors/Authors cannot create, edit, publish or delete charts through any WordPress
233 // entry point (admin UI, XML-RPC, REST). Published charts stay viewable on the front end.
234 // Only primitive capabilities are remapped: mapping the meta capabilities edit_post/read_post/
235 // delete_post to manage_options would make WordPress treat manage_options itself as a meta
236 // capability and break that check site-wide.
237 'capability_type' => 'post',
238 'map_meta_cap' => true,
239 'capabilities' => array(
240 'edit_posts' => 'manage_options',
241 'edit_others_posts' => 'manage_options',
242 'edit_published_posts' => 'manage_options',
243 'edit_private_posts' => 'manage_options',
244 'publish_posts' => 'manage_options',
245 'read_private_posts' => 'manage_options',
246 'delete_posts' => 'manage_options',
247 'delete_private_posts' => 'manage_options',
248 'delete_published_posts' => 'manage_options',
249 'delete_others_posts' => 'manage_options',
250 'create_posts' => 'manage_options',
251 ),
252 'supports' => array(
253 'title',
254 'postmeta'
255 ),
256 'register_meta_box_cb' => 'guaven_sqlcharts_metabox_area'
257 ));
258
259 guaven_sqlcharts_load_defaults();
260 }
261
262 // All guaven_sqlcharts_* meta keys are protected: they cannot be written through the Custom Fields box,
263 // XML-RPC or the REST API. The plugin's own save handler (update_post_meta) is not affected.
264 add_filter('is_protected_meta', function ($protected, $meta_key) {
265 return strpos((string) $meta_key, 'guaven_sqlcharts_') === 0 ? true : $protected;
266 }, 10, 2);
267
268 // "Add title" placeholder on the chart edit screen
269 add_filter('enter_title_here', function ($title, $post) {
270 if (!empty($post) and $post->post_type == 'gvn_schart') return __('Chart name', 'guaven_sqlcharts');
271 return $title;
272 }, 10, 2);
273
274 add_action('admin_footer', 'guaven_sqlcharts_admin_front');
275
276
277 function guaven_sqlcharts_admin_front()
278 {
279 global $post;
280 if (!empty($post) and $post->post_type == 'gvn_schart') {
281 ?>
282 <style type="text/css">#normal-sortables{display: none}</style>
283 <?php
284 }
285 }
286
287 // metabox for editor
288 function guaven_sqlcharts_metabox_area()
289 {
290 add_meta_box('guaven_sqlcharts_metabox', 'Chart Builder', 'guaven_sqlcharts_metabox', 'gvn_schart', 'advanced', 'default');
291 }
292
293 function guaven_sqlcharts_metabox()
294 {
295 require_once(dirname(__FILE__) . "/admin_metabox.php");
296 }
297
298 /**
299 * Catalog of all supported chart types: label, group, per-type usage guide and example query.
300 * Used by the admin UI to render the type cards and the contextual guides.
301 */
302 function guaven_sqlcharts_type_catalog()
303 {
304 global $wpdb;
305 $p = $wpdb->posts;
306 $u = $wpdb->users;
307 return array(
308 'pie_l' => array(
309 'label' => 'Pie',
310 'group' => 'Circular',
311 'guide' => 'Best for showing how a total splits into a few parts (shares/percentages). Use one query that returns a label column (X) and a numeric value column (Y). Keep it under ~8 slices for readability.',
312 'example_sql' => "select count(*) postcount, SUBSTR(post_date,1,4) yearnum from $p group by yearnum order by yearnum asc limit 10",
313 'example_x' => 'yearnum', 'example_y' => 'postcount',
314 ),
315 'donut_l' => array(
316 'label' => 'Doughnut',
317 'group' => 'Circular',
318 'guide' => 'Same as Pie but with a hole in the middle — slightly easier to compare slice sizes. One query: label column (X) + numeric column (Y).',
319 'example_sql' => "select count(*) postcount, post_type from $p group by post_type order by postcount desc limit 8",
320 'example_x' => 'post_type', 'example_y' => 'postcount',
321 ),
322 'polar_l' => array(
323 'label' => 'Polar Area',
324 'group' => 'Circular',
325 'guide' => 'Like a pie, but every slice has the same angle and the value controls the radius. Good for cyclic data (months, weekdays). One query: label (X) + numeric value (Y).',
326 'example_sql' => "select count(*) postcount, SUBSTRING(post_date,6,2) monthnum from $p group by monthnum order by monthnum",
327 'example_x' => 'monthnum', 'example_y' => 'postcount',
328 ),
329 'radar_l' => array(
330 'label' => 'Radar',
331 'group' => 'Circular',
332 'guide' => 'Compares one or more series across several categories arranged in a circle. Great for profiles/ratings. Use one query per series, separated with ";".',
333 'example_sql' => "select count(*) postcount, SUBSTRING(post_date,6,2) monthnum from $p where post_type=\"post\" group by monthnum order by monthnum",
334 'example_x' => 'monthnum', 'example_y' => 'postcount',
335 ),
336 'line_l' => array(
337 'label' => 'Line',
338 'group' => 'Line',
339 'guide' => 'The classic choice for trends over time (per day/month/year). X should be an ordered value like a date. Add more queries separated with ";" for comparison lines.',
340 'example_sql' => "select count(*) postcount, SUBSTRING(post_date,1,7) monthandyear from $p group by monthandyear order by monthandyear",
341 'example_x' => 'monthandyear', 'example_y' => 'postcount',
342 ),
343 'area_l' => array(
344 'label' => 'Area',
345 'group' => 'Line',
346 'guide' => 'A line chart with the region under the line filled — emphasizes volume/magnitude of a trend. Works well with 2 queries (";" separated) to compare filled regions.',
347 'example_sql' => "select count(*) postcount, SUBSTRING(post_date,1,7) monthandyear from $p where post_type=\"post\" group by monthandyear order by monthandyear;\nselect count(*) postcount, SUBSTRING(post_date,1,7) monthandyear from $p where post_type!=\"post\" group by monthandyear order by monthandyear",
348 'example_x' => 'monthandyear', 'example_y' => 'Posts;Other content',
349 ),
350 'steppedline_l' => array(
351 'label' => 'Stepped Line',
352 'group' => 'Line',
353 'guide' => 'Line chart that moves in steps instead of slopes — perfect for values that change at discrete moments (prices, stock level, settings history).',
354 'example_sql' => "select count(*) postcount, SUBSTRING(post_date,1,4) yearnum from $p group by yearnum order by yearnum",
355 'example_x' => 'yearnum', 'example_y' => 'postcount',
356 ),
357 'bar_l' => array(
358 'label' => 'Bar',
359 'group' => 'Bar',
360 'guide' => 'Compares values across categories with vertical bars. One query: category (X) + numeric value (Y). Multiple ";" separated queries become grouped/stacked bars.',
361 'example_sql' => "select count(*) postcount, SUBSTRING(post_date,1,7) monthandyear from $p group by monthandyear order by monthandyear",
362 'example_x' => 'monthandyear', 'example_y' => 'postcount',
363 ),
364 'horizontalbar_l' => array(
365 'label' => 'Horizontal Bar',
366 'group' => 'Bar',
367 'guide' => 'Bar chart rotated 90° — the best pick when category names are long (user names, product titles).',
368 'example_sql' => "select count(*) as postcount, b.display_name as dname from $p a inner join $u b ON a.post_author=b.ID where a.post_status=\"publish\" group by a.post_author order by postcount desc limit 10",
369 'example_x' => 'dname', 'example_y' => 'postcount',
370 ),
371 'stackedbar_l' => array(
372 'label' => 'Stacked Bar',
373 'group' => 'Bar',
374 'guide' => 'Shows how each category total is composed of parts. Use 2+ queries separated with ";" — each query becomes one segment color of the stack.',
375 'example_sql' => "select count(*) postcount, SUBSTRING(post_date,1,4) yearnum from $p where post_type=\"post\" group by yearnum order by yearnum;\nselect count(*) postcount, SUBSTRING(post_date,1,4) yearnum from $p where post_type!=\"post\" group by yearnum order by yearnum",
376 'example_x' => 'yearnum', 'example_y' => 'Posts;Other content',
377 ),
378 'scatter_l' => array(
379 'label' => 'Scatter',
380 'group' => 'Other',
381 'guide' => 'Plots points on numeric X/Y axes to reveal correlation between two numbers. Both X and Y columns must be numeric (e.g. comment_count vs menu_order).',
382 'example_sql' => "select comment_count ccount, ID from $p where post_status=\"publish\" order by ID limit 100",
383 'example_x' => 'ID', 'example_y' => 'ccount',
384 ),
385 );
386 }
387
388 /**
389 * Maps deprecated Google-Chart era type slugs to their Chart.js equivalents.
390 * Old charts keep working without any manual migration; when the post is re-saved
391 * from the new UI the normalized value is stored automatically.
392 */
393 function guaven_sqlcharts_normalize_type($type)
394 {
395 $map = array(
396 'pie' => 'pie_l',
397 '3dpie' => 'pie_l',
398 'column' => 'bar_l',
399 'bar' => 'horizontalbar_l',
400 'area' => 'area_l',
401 );
402 return isset($map[$type]) ? $map[$type] : $type;
403 }
404
405 function guaven_gutenberg_wrapper($atts){
406 if(isset($atts['sqlcharts_inserted_script'])){
407 global $sqlcharts_inserted_script;
408 $sqlcharts_inserted_script = $atts['sqlcharts_inserted_script'];
409 }
410
411 $post = get_post($atts['chart_id']);
412 if( ! isset($atts['chart_id']) or !isset($post) or $post->post_type != 'gvn_schart'){
413
414 return "Invalid id";
415 }
416
417 return guaven_sqlcharts_local_shortcode(array('id' => $atts['chart_id'])); // temporary explicit value
418 }
419 function guaven_register_gutenberg_blocks()
420 {
421 wp_register_script(
422 'gvn_gutenberg_charts',
423 plugins_url( 'asset/guaven_gutenberg_charts.js', __FILE__ ),
424 array( 'wp-blocks', 'wp-i18n', 'wp-element', 'wp-server-side-render' ),
425 GVNSQLCHARTS_VERSION.'_'.filemtime( plugin_dir_path( __FILE__ ) . 'asset/guaven_gutenberg_charts.js'),
426 false
427 );
428 wp_localize_script('gvn_gutenberg_charts', 'guaven', array(
429 'description' => 'Add My SQL Chart to your post',
430 ));
431
432 register_block_type( 'guaven-sqlcharts/gvn-chart-gutenberg', array(
433 'editor_script' => 'gvn_gutenberg_charts',
434 'render_callback' => 'guaven_gutenberg_wrapper',
435 'attributes' => array(
436 'chart_id' => array(
437 'type' => 'string',
438 'default' => null
439 ),
440 'sqlcharts_inserted_script' => array(
441 'type' => 'number',
442 'default' => null
443 )
444 )
445 ));
446 }
447 add_action('init', 'guaven_register_gutenberg_blocks');
448
449
450
451 function guaven_sqlcharts_save_metabox_area($post_id, $post)
452 {
453 if (!isset($_POST['meta_box_nonce_field']) or !wp_verify_nonce($_POST['meta_box_nonce_field'], 'meta_box_nonce_action')) {
454 return $post->ID;
455 }
456 if ($post->post_type != 'gvn_schart' or !current_user_can('manage_options') or (defined('DOING_AUTOSAVE') and DOING_AUTOSAVE)) {
457 return $post->ID;
458 }
459 $fields = array(
460 "guaven_sqlcharts_chartheight",
461 "guaven_sqlcharts_chartwidth",
462 "guaven_sqlcharts_graphtype",
463 "guaven_sqlcharts_xarg_s",
464 "guaven_sqlcharts_xarg_l",
465 "guaven_sqlcharts_yarg_s",
466 "guaven_sqlcharts_yarg_l",
467 "guaven_sqlcharts_tablepart",
468 "guaven_sqlcharts_variables",
469 "guaven_sqlcharts_formpartrole",
470 "guaven_sqlcharts_formpartbutton",
471 "guaven_sqlcharts_dbhost",
472 "guaven_sqlcharts_dblogin",
473 "guaven_sqlcharts_dbname",
474 "guaven_sqlcharts_colors",
475 "guaven_sqlcharts_begin_with_0_x",
476 "guaven_sqlcharts_begin_with_0_y",
477 "guaven_sqlcharts_round_y_values",
478 "guaven_sqlcharts_legend_position",
479 "guaven_sqlcharts_nostacked",
480 "guaven_sqlcharts_forcetooltips",
481 "guaven_sqlcharts_timeaxis"
482 );
483 foreach ($fields as $key => $value) {
484 if(isset($_POST[$value]))$newval=esc_attr($_POST[$value]);
485 else $newval='';
486
487 update_post_meta($post->ID, $value, $newval);
488 }
489 if(!empty($_POST["guaven_sqlcharts_dbpass"])){
490 $encpass=guaven_sqlcharts_encrypt_decrypt('encrypt',$_POST["guaven_sqlcharts_dbpass"]);
491 update_post_meta($post->ID, 'guaven_sqlcharts_dbpass', ['encrypted',$encpass]);
492 }
493 // Store the SQL as typed. Do not HTML-encode it and do not rewrite quotes:
494 // the editor escapes it on output and the front end decodes entities before running it.
495 $sql_code = isset($_POST['guaven_sqlcharts_code']) ? wp_check_invalid_utf8(wp_unslash($_POST['guaven_sqlcharts_code'])) : '';
496 update_post_meta($post->ID, 'guaven_sqlcharts_code', $sql_code);
497 // Flag that this chart stores raw SQL. Charts without the flag were saved by
498 // versions before 3.0.1, which HTML-encoded the query, and still need decoding.
499 update_post_meta($post->ID, 'guaven_sqlcharts_code_raw', 1);
500 }
501
502 // Returns the stored SQL query exactly as the user typed it.
503 function guaven_sqlcharts_get_code($post_id)
504 {
505 $sql = get_post_meta($post_id, 'guaven_sqlcharts_code', true);
506 if (get_post_meta($post_id, 'guaven_sqlcharts_code_raw', true) != 1) {
507 $sql = html_entity_decode($sql, ENT_QUOTES, 'UTF-8');
508 }
509 return $sql;
510 }
511 add_action('save_post', 'guaven_sqlcharts_save_metabox_area', 1, 2);
512 // save the custom fields
513
514
515
516 // Removes string literals (contents only), backtick identifiers and comments from SQL so keyword checks
517 // see the same code MySQL will execute. "/*!" and "/*+" comments are executable in MySQL and are kept.
518 function guaven_sqlcharts_strip_sql_literals($sql)
519 {
520 $out = ''; $len = strlen($sql); $i = 0;
521 while ($i < $len) {
522 $c = $sql[$i];
523 if ($c === "'" or $c === '"' or $c === '`') {
524 $out .= $c . $c; $i++;
525 while ($i < $len) {
526 if ($sql[$i] === '\\' and $c !== '`') { $i += 2; continue; }
527 if ($sql[$i] === $c) { if ($i + 1 < $len and $sql[$i + 1] === $c) { $i += 2; continue; } $i++; break; }
528 $i++;
529 }
530 continue;
531 }
532 if ($c === '#' or ($c === '-' and substr($sql, $i, 2) === '--' and ($i + 2 >= $len or ctype_space($sql[$i + 2])))) {
533 $nl = strpos($sql, "\n", $i); $i = ($nl === false) ? $len : $nl; continue;
534 }
535 if ($c === '/' and substr($sql, $i, 2) === '/*' and !in_array(substr($sql, $i + 2, 1), array('!', '+'), true)) {
536 $close = strpos($sql, '*/', $i + 2); $i = ($close === false) ? $len : $close + 2; $out .= ' '; continue;
537 }
538 $out .= $c; $i++;
539 }
540 return $out;
541 }
542
543 // Returns 1 when the (fully substituted) SQL must not run, 0 when it is a read-only query.
544 // Called after every {tag}/{argN} replacement so user-supplied values are covered too.
545 function gvn_chart_check_sql_query($sql)
546 {
547 // 1) data-changing statements: checked on the raw text, exactly as in every previous version
548 $write = '/\b(delete|update|insert|replace|drop|truncate|alter|create|rename|grant|revoke|call|handler|load\s+data|load_file|outfile|dumpfile)\b/i';
549 if (preg_match($write, $sql)) return 1;
550
551 // 2) further dangerous statements, matched outside string literals and comments so that ordinary
552 // values such as status = 'reset' keep working
553 $danger = '/\b(prepare|execute|deallocate|lock|unlock|kill|shutdown|flush|reset|purge|install|uninstall|import'
554 . '|set\s+(?:global|session|persist|persist_only|password|@@)|start\s+(?:replica|slave|group_replication)|stop\s+(?:replica|slave)|change\s+(?:master|replication))\b/i';
555 if (preg_match($danger, guaven_sqlcharts_strip_sql_literals($sql))) return 1;
556
557 // 3) every ";"-separated statement must be a read statement. The renderer sends each segment to the
558 // database on its own, so this stops a value from smuggling a second statement behind a ";".
559 foreach (explode(';', $sql) as $segment) {
560 $segment = ltrim(guaven_sqlcharts_strip_sql_literals($segment), " \t\r\n(");
561 if ($segment === '') continue;
562 if (!preg_match('/^(select|with|show|describe|desc|explain)\b/i', $segment)) return 1;
563 }
564 return 0;
565 }
566
567 function guaven_get_labels_and_values($id, $fvs)
568 {
569 $values = array();
570 $labels = array();
571 $xarg_s = get_post_meta($id, 'guaven_sqlcharts_xarg_s', true);
572 $yarg_s = get_post_meta($id, 'guaven_sqlcharts_yarg_s', true);
573 // labels are saved through esc_attr, so "&" is stored as "&amp;"; decode before splitting on ";"
574 // or the entity's own ";" would be taken as a series separator
575 $xarg_l = html_entity_decode((string) get_post_meta($id, 'guaven_sqlcharts_xarg_l', true), ENT_QUOTES, 'UTF-8');
576 $yarg_l = html_entity_decode((string) get_post_meta($id, 'guaven_sqlcharts_yarg_l', true), ENT_QUOTES, 'UTF-8');
577 foreach ($fvs as $key => $value) {
578 $values[$value->$xarg_s] = $value->$yarg_s;
579 $labels[$value->$xarg_s] = '"' . $value->$xarg_s . '"';
580 }
581 return array(
582 $labels,
583 $values,
584 explode(";", $yarg_l),
585 explode(";", $xarg_l)
586 );
587 }
588
589 function guaven_sqlcharts_print_chart_js($print_data)
590 {
591 extract($print_data);
592 $tip_g = guaven_sqlcharts_normalize_type($tip_g);
593
594 switch ($tip_g) {
595 case 'line_l':
596 guaven_sqlcharts_linedata($title, $labels, $values, $ylabel, 'false', $pid);
597 break;
598 case 'area_l':
599 guaven_sqlcharts_linedata($title, $labels, $values, $ylabel, 'true', $pid);
600 break;
601 case 'steppedline_l':
602 guaven_sqlcharts_linedata($title, $labels, $values, $ylabel, 'false', $pid, 'line', true);
603 break;
604 case 'radar_l':
605 guaven_sqlcharts_linedata($title, $labels, $values, $ylabel, 'radarfill', $pid, 'radar');
606 break;
607 case 'pie_l':
608 guaven_sqlcharts_piedata($title, $labels, $values, $ylabel, $pid);
609 break;
610 case 'donut_l':
611 guaven_sqlcharts_piedata($title, $labels, $values, $ylabel, $pid, 'doughnut');
612 break;
613 case 'polar_l':
614 guaven_sqlcharts_piedata($title, $labels, $values, $ylabel, $pid, 'polarArea');
615 break;
616 case 'bar_l':
617 guaven_sqlcharts_bardata($title, $labels, $values, $ylabel, 'bar', $pid);
618 break;
619 case 'horizontalbar_l':
620 guaven_sqlcharts_bardata($title, $labels, $values, $ylabel, 'horizontalBar', $pid);
621 break;
622 case 'stackedbar_l':
623 guaven_sqlcharts_bardata($title, $labels, $values, $ylabel, 'stackedBar', $pid);
624 break;
625 case 'scatter_l':
626 guaven_sqlcharts_scatterdata($title, $labels, $values, $ylabel, $pid);
627 break;
628 case 'custom':
629 guaven_sqlcharts_custom($title, $labels, $values, $ylabel, $pid);
630 break;
631 }
632 }
633
634 function guaven_sqlcharts_custom($title, $labels, $values, $ylabel, $pid){
635 do_action('guaven_sqlcharts_custom',$title, $labels, $values, $ylabel, $pid);
636 }
637
638 function gvn_chart_put_variables($sql,$pid){
639 $sql_initial=$sql;
640
641
642 $default_tag_keys=['{current_user_id}','{current_user_login}','{current_user_email}','{current_user_display_name}'];
643 if(is_user_logged_in( )){
644 $currentuser=wp_get_current_user();
645 $default_tag_values=[$currentuser->ID,$currentuser->user_login,$currentuser->user_email,$currentuser->user_display_name];
646 }
647 else {
648 $default_tag_values='';
649 }
650 $sql_initial=str_replace($default_tag_keys,$default_tag_values,$sql_initial);
651
652 $variables_raw=get_post_meta($pid,'guaven_sqlcharts_variables',true);
653 $variables_arr=explode("|",$variables_raw);
654 foreach($variables_arr as $varfield){
655 $varfield_arr=explode("~",$varfield);
656 if (count($varfield_arr)<3) continue;
657 $varfield_arr=array_map("trim",$varfield_arr);
658 if (!empty($_GET[$varfield_arr[0]])) {
659 // User-supplied input: no () bypass allowed — sanitize strictly
660 $varreplacement = str_replace(';', '', sanitize_text_field(wp_unslash($_GET[$varfield_arr[0]])));
661 if (is_numeric($varreplacement)) {
662 $varreplacement = $varreplacement + 0;
663 } else {
664 $varreplacement = '"' . esc_sql($varreplacement) . '"';
665 }
666 } else {
667 // Admin-configured default value: allow () for SQL functions (e.g. NOW())
668 $varreplacement = $varfield_arr[1];
669 if (!is_numeric($varreplacement) && strpos($varreplacement,'()')===false) {
670 $varreplacement = '"' . esc_sql($varreplacement) . '"';
671 }
672 }
673
674 $sql_initial=str_replace('{'.$varfield_arr[0].'}',$varreplacement,$sql_initial);
675 }
676 return $sql_initial;
677 }
678
679 function gvn_chart_top_form($atts){
680 if (get_post_meta($atts["id"],'guaven_sqlcharts_formpartrole',true)!='' and !is_user_logged_in()) return;
681 // Inside the chart builder's live preview the filter inputs are rendered disabled and without a <form>
682 // or submit button: the preview sits inside WordPress's own post edit form, nested forms are dropped
683 // by the browser, and a filter named e.g. "post_type" would otherwise be submitted with the post and
684 // make WordPress stop with "A post type mismatch has been detected." (chart settings not saved).
685 $preview = !empty($GLOBALS['guaven_sqlcharts_admin_preview']);
686 $dis = $preview ? ' disabled' : '';
687 $topform='';$dateexists=false;
688 $variables_raw=get_post_meta($atts['id'],'guaven_sqlcharts_variables',true);
689 $variables_raw=explode("|",$variables_raw);
690 foreach ($variables_raw as $vrow){
691 $vrow_arr=explode("~",$vrow);
692 $vrow_arr=array_map("trim",$vrow_arr);
693 if (empty($vrow_arr[3])) continue;
694 $gvalue=!empty($_GET[$vrow_arr[0]])?esc_attr(urldecode($_GET[$vrow_arr[0]])):'';
695 $dvalue=(strpos($vrow_arr[1],'()')===false)?esc_attr($vrow_arr[1]):'';
696 if ($vrow_arr[3]=='date') {
697 $dateexists=true;
698 $topform.= '<span class="gvn-filter-field"><label>'.$vrow_arr[2].'</label> <input class="gws_datepicker" autocomplete="off" type="text"
699 value="'.$gvalue.'"
700 data-toggle="datepicker" name="'.$vrow_arr[0].'" placeholder="'.$dvalue.'"'.$dis.'></span>
701 ';}
702 else {
703 $topform.= '<span class="gvn-filter-field"><label>'.$vrow_arr[2].'</label> <input autocomplete="off"
704 type="'.$vrow_arr[3].'"
705 value="'.$gvalue.'" name="'.$vrow_arr[0].'" placeholder="'.$dvalue.'"'.$dis.'></span>
706 ';
707 }
708 }
709 if (!empty($topform)) {
710 $allowed_html = array(
711 'form' => array(
712 'method' => array(),
713 'action' => array(),
714 'class' => array()
715 ),
716 'input' => array(
717 'type' => array(),
718 'value' => array(),
719 'name' => array(),
720 'class' => array(),
721 'data-toggle'=>array(),
722 'placeholder'=>array(),
723 'autocomplete'=>array(),
724 'disabled'=>array(),
725 'style'=>[]
726 ),
727 'span' => array('class' => array()),
728 'label' => array(),
729 'div' => array('class' => array()),
730 );
731
732 $submit_button_value = get_post_meta($atts['id'], 'guaven_sqlcharts_formpartbutton', true) != ''
733 ? esc_attr(get_post_meta($atts['id'], 'guaven_sqlcharts_formpartbutton', true))
734 : 'OK';
735
736 if ($preview) {
737 $topform = '<div class="guaven_sqlcharts_form">' . $topform . '<input type="submit" value="' . $submit_button_value . '" disabled></div>';
738 } else {
739 $topform = '<form method="get" action="" class="guaven_sqlcharts_form">' . $topform . '
740 <input type="submit" value="' . $submit_button_value . '"></form>';
741 }
742
743 echo wp_kses($topform, $allowed_html);
744 }
745 }
746
747
748 function guaven_sqlcharts_encrypt_decrypt($action, $string)
749 {
750 $output = false;
751 $encrypt_method = "AES-256-CBC";
752 $secret_key = 'GWSCHARTPL2022.2016.';
753 $secret_iv = 'GWSCHARTPL2016.2022';
754 $key = hash('sha256', $secret_key);
755 $iv = substr(hash('sha256', $secret_iv), 0, 16);
756 if ( $action == 'encrypt' ) {
757 $output = openssl_encrypt($string, $encrypt_method, $key, 0, $iv);
758 $output = base64_encode($output);
759 } else if( $action == 'decrypt' ) {
760 $output = openssl_decrypt(base64_decode($string), $encrypt_method, $key, 0, $iv);
761 }
762 return $output;
763 }
764
765 function guaven_sqlcharts_local_shortcode($atts) {
766 if(empty($atts['id']))return 'ID is missing.';
767 $atts['id']=intval($atts['id']);
768 $post_g = get_post($atts['id']);
769 if (!$post_g or $post_g->post_type != 'gvn_schart') return 'Chart not found.';
770 $remote_host=get_post_meta($atts['id'], 'guaven_sqlcharts_dbhost', true);
771 if ($remote_host!=''){
772 $remote_db=get_post_meta($atts['id'], 'guaven_sqlcharts_dbname', true);
773 $remote_login=get_post_meta($atts['id'], 'guaven_sqlcharts_dblogin', true);
774 $remote_pass=get_post_meta($atts['id'], 'guaven_sqlcharts_dbpass', true);
775 if(is_array($remote_pass)){
776 $remote_pass=guaven_sqlcharts_encrypt_decrypt('decrypt',$remote_pass[1]);
777 }
778 $wpdb=new wpdb($remote_login,$remote_pass,$remote_db,$remote_host);
779 }
780 else {
781 global $wpdb;
782 }
783
784 $GLOBALS["guaven_sqlcharts_atts"]=$atts;
785
786 $sql = guaven_sqlcharts_get_code($atts['id']);
787 if(empty($sql))return 'SQL query is missing.';
788
789 // {arg1}..{arg19} come from shortcode attributes: [gvn_schart_2 id="1" arg1="41"].
790 // Substituted directly (not via wpdb::prepare) so the same tag may appear any number of times,
791 // e.g. in every query of a ";"-separated comparison chart. Numbers are inserted as-is, anything
792 // else is escaped and quoted; a tag already wrapped in quotes ('{arg1}') is not double-quoted.
793 // ";" is removed from values because the finished SQL is split on ";" below.
794 for($i=1;$i<20;$i++){
795 $tag = '{arg'.$i.'}';
796 if (strpos($sql, $tag) === false) continue;
797 $replacearg = !empty($atts['arg'.$i]) ? $atts['arg'.$i] : 0;
798 if (is_numeric($replacearg)) $replacearg = $replacearg + 0;
799 else $replacearg = "'" . esc_sql(str_replace(';', '', sanitize_text_field((string) $replacearg))) . "'";
800 $sql = str_replace(array("'".$tag."'", '"'.$tag.'"', $tag), $replacearg, $sql);
801 }
802
803 $sql=gvn_chart_put_variables($sql,$atts['id']);
804 $sql=apply_filters('guaven_sqlcharts_rendered_sql',$sql,$atts);
805
806 // command check on the final SQL, after every shortcode argument and filter value is in place
807 $blacklister_f = gvn_chart_check_sql_query($sql);
808 if ($blacklister_f == 1)return 'You given SQL code contains forbidden commands. Remember that you should only use SELECT queries';
809 $tip_g = guaven_sqlcharts_normalize_type(get_post_meta($atts['id'], 'guaven_sqlcharts_graphtype', true));
810
811 $sql_split = explode(';', $sql);
812 $labels_and_values = array();
813 $labels = $values = $ylabel = $xlabel = array();
814
815 global $sqlcharts_inserted_script;
816 ob_start();
817 for ($i = 0; $i < count($sql_split); $i++) {
818 if (!empty($sql_split[$i])) {
819
820 $fvs = $wpdb->get_results($sql_split[$i]);
821 if (strpos($_SERVER["REQUEST_URI"],'wp-admin')!==false){
822 $wpdb->show_errors();
823 ob_start();
824 $wpdb->print_error();
825 $printerror = ob_get_clean();
826 if ($printerror != '' and strpos($printerror, "[]") === false){
827 ob_end_clean();
828 return $printerror;
829 }
830 elseif (empty($fvs)){
831 ob_end_clean();
832 return 'Your SQL returnes empty data, please recheck your SQL query above';
833 }
834 }
835
836 if (empty($sqlcharts_inserted_script))
837 $sqlcharts_inserted_script = 1;
838 $labels_and_values[$i] = guaven_get_labels_and_values($atts['id'], $fvs);
839 $labels[$i] = $labels_and_values[$i][0];
840 $values[$i] = $labels_and_values[$i][1];
841 $ylabel[$i] = !empty($labels_and_values[$i][2][$i]) ? $labels_and_values[$i][2][$i] : '';
842 $xlabel[$i] = !empty($labels_and_values[$i][3][$i]) ? $labels_and_values[$i][3][$i] : '';
843 }
844 }
845
846 gvn_chart_top_form($atts);
847
848 // shortcode width/height attributes override the saved defaults
849 $chart_w = !empty($atts['width']) ? $atts['width'] : get_post_meta($atts['id'], 'guaven_sqlcharts_chartwidth', true);
850 $chart_h = !empty($atts['height']) ? $atts['height'] : get_post_meta($atts['id'], 'guaven_sqlcharts_chartheight', true);
851 $wrap_style = '';
852 if ($chart_w != '') $wrap_style .= 'max-width:' . (int) $chart_w . 'px;';
853 if ($chart_h != '') $wrap_style .= 'height:' . (int) $chart_h . 'px;';
854 ?>
855 <div class="gvn-chartwrap"<?php echo $wrap_style != '' ? ' style="' . esc_attr($wrap_style) . '"' : ''; ?>>
856 <canvas
857 id="ct-chart_<?php echo esc_attr($sqlcharts_inserted_script); ?>"
858 class="guaven_chart_canvas"
859 ></canvas>
860 </div>
861
862 <script type="text/javascript" class="gvn_charts_script" async>
863 var ctx = jQuery("#ct-chart_<?php
864 echo esc_attr($sqlcharts_inserted_script);
865 ?>");
866
867 <?php
868 $print_data=apply_filters('guaven_sqlcharts_pre_print_vars',['tip_g'=>$tip_g, 'title'=>$post_g->post_title,
869 'labels'=>$labels, 'values'=>$values, 'ylabel'=>$ylabel, 'pid'=>$atts['id']]);
870 guaven_sqlcharts_print_chart_js($print_data);
871 ?>
872 </script>
873
874 <?php
875 if (!empty($atts["table"])) guaven_sqlcharts_tablepart($post_g->post_title, $labels, $values, $ylabel,$xlabel);
876 $sqlcharts_inserted_script++;
877 $ret=ob_get_clean();
878 $ret=apply_filters( 'guaven_sqlcharts_final_output', $ret, $atts );
879 return $ret;
880 }
881
882 add_shortcode('gvn_schart_2', 'guaven_sqlcharts_local_shortcode');
883
884 // legacy alias: old Google-Chart era posts produced [gvn_schart id=".."] shortcodes
885 if (!shortcode_exists('gvn_schart')) {
886 add_shortcode('gvn_schart', 'guaven_sqlcharts_local_shortcode');
887 }
888
889 // [gvn_schart_2_cached id="1" expire="3600" arg1=".."] – same as gvn_schart_2 but the output is kept in a
890 // transient. All other attributes (argN, width, height, table, params) are passed through, and each
891 // distinct set of attributes gets its own cache entry. Append ?force_sql_cache_reload to the URL to bypass.
892 add_shortcode("gvn_schart_2_cached",function($atts){
893 if(empty($atts["id"]))return;
894 $atts["id"]=intval($atts["id"]);
895 $expire=!empty($atts["expire"])?intval($atts["expire"]):3600;
896 $inner_atts=$atts;
897 unset($inner_atts['expire']);
898 // One cache entry per user (charts may use {current_user_*} tags), per set of shortcode attributes
899 // and per value of every dynamic filter this chart reads from the URL. A visitor can therefore
900 // never be served, or pre-seed, a result computed for someone else or for other filter values.
901 $key_parts = array('atts' => $inner_atts, 'user' => is_user_logged_in() ? get_current_user_id() : 0, 'get' => array());
902 foreach (explode('|', (string) get_post_meta($atts['id'], 'guaven_sqlcharts_variables', true)) as $vrow) {
903 $vname = trim(current(explode('~', $vrow)));
904 if ($vname !== '' and isset($_GET[$vname])) $key_parts['get'][$vname] = sanitize_text_field(wp_unslash($_GET[$vname]));
905 }
906 $key = 'cached_sql_charts_' . $atts["id"] . '_' . md5(serialize($key_parts));
907 $cached=get_transient($key);
908 if(!empty($cached) and !isset($_GET["force_sql_cache_reload"]) )return $cached;
909 $tobecached=guaven_sqlcharts_local_shortcode($inner_atts);
910 set_transient($key, $tobecached,$expire);
911 return $tobecached;
912 });
913
914 // fixed, colorblind-friendly default palette (Tableau 10) used when no custom colors are set
915 function guaven_sqlcharts_default_palette(){
916 return apply_filters('guaven_sqlcharts_default_palette', array(
917 '#4E79A7', '#F28E2B', '#E15759', '#76B7B2', '#59A14F',
918 '#EDC948', '#B07AA1', '#FF9DA7', '#9C755F', '#BAB0AC'
919 ));
920 }
921
922 function guaven_sqlcharts_colors($index, $pid = null){
923 if(!isset($pid)) {
924 global $post;
925 $pid = $post->ID;
926 }
927 $colors=get_post_meta($pid,'guaven_sqlcharts_colors',true);
928 $colors=explode(",",$colors);
929 if (!empty($colors[$index])) return $colors[$index];
930 $palette = guaven_sqlcharts_default_palette();
931 return $palette[$index % count($palette)];
932 }
933
934 // outputs 'maintainAspectRatio:false,' when an explicit height is set, so the
935 // chart fills its sized wrapper instead of keeping the default aspect ratio
936 function guaven_sqlcharts_mar($pid){
937 $atts = isset($GLOBALS["guaven_sqlcharts_atts"]) ? $GLOBALS["guaven_sqlcharts_atts"] : array();
938 $h = !empty($atts['height']) ? $atts['height'] : get_post_meta($pid, 'guaven_sqlcharts_chartheight', true);
939 return $h != '' ? 'maintainAspectRatio: false,' : '';
940 }
941
942 // outputs 'showAllTooltips: true,' when "Value labels" is checked; the values are drawn by the
943 // gvnShowAllValues plugin in asset/front.js (works for every chart type)
944 function guaven_sqlcharts_value_labels($pid){
945 return get_post_meta($pid, 'guaven_sqlcharts_forcetooltips', true) != '' ? 'showAllTooltips: true,' : '';
946 }
947
948 // Chart.js scale title block built from the "X axis label" / "Y axis label" fields.
949 // $which is 'x' or 'y' (the *field* to use, not the scale). The Y label is only used as an axis
950 // title for single-series charts; with several ";"-separated series the legend names them instead.
951 function guaven_sqlcharts_axis_title($pid, $which){
952 $key = $which == 'x' ? 'guaven_sqlcharts_xarg_l' : 'guaven_sqlcharts_yarg_l';
953 $text = trim(html_entity_decode((string) get_post_meta($pid, $key, true), ENT_QUOTES, 'UTF-8'));
954 if ($text === '' or ($which == 'y' and strpos($text, ';') !== false)) return '';
955 return 'title: {display: true, text: ' . wp_json_encode($text) . '},';
956 }
957
958 // "params" shortcode attribute: extra Chart.js dataset options, e.g. params="borderWidth: 3, borderDash: [5,5],".
959 // The text is placed inside the inline <script>, so only a conservative character set is accepted:
960 // no parentheses, semicolons, "=", "<", ">", "/", "\\", "+" or backticks, which rules out executable JavaScript.
961 function guaven_sqlcharts_dataset_params(){
962 $params = isset($GLOBALS["guaven_sqlcharts_atts"]["params"]) ? (string) $GLOBALS["guaven_sqlcharts_atts"]["params"] : '';
963 if ($params === '' or !preg_match('/^[A-Za-z0-9_\s,:.\'"#%\-\[\]{}]+$/', $params)) return '';
964 return $params;
965 }
966
967 // dataset label as a safe JS string literal (labels saved before 3.0.1 may hold HTML entities)
968 function guaven_sqlcharts_js_label($label){
969 return wp_json_encode(html_entity_decode((string) $label, ENT_QUOTES, 'UTF-8'));
970 }
971
972 // Parses an X value for the "time axis" option. Accepts YYYY, YYYY-MM, YYYY-MM-DD, optionally followed
973 // by HH:MM or HH:MM:SS. Returns a UTC timestamp in milliseconds, or false when the value is not a date.
974 function guaven_sqlcharts_parse_date($str){
975 $str = trim((string) $str);
976 if (!preg_match('/^(\d{4})(?:-(\d{1,2})(?:-(\d{1,2})(?:[ T](\d{1,2}):(\d{2})(?::(\d{2}))?)?)?)?$/', $str, $m)) return false;
977 $y = (int) $m[1]; $mo = isset($m[2]) ? (int) $m[2] : 1; $d = isset($m[3]) ? (int) $m[3] : 1;
978 $h = isset($m[4]) ? (int) $m[4] : 0; $mi = isset($m[5]) ? (int) $m[5] : 0; $sec = isset($m[6]) ? (int) $m[6] : 0;
979 if (!checkdate($mo, $d, $y) or $h > 23 or $mi > 59 or $sec > 59) return false;
980 return gmmktime($h, $mi, $sec, $mo, $d, $y) * 1000;
981 }
982
983 // "Scale X axis by date/time" option. Returns, per dataset, a list of "{x:<ms>,y:<value>}" JS point
984 // literals when the option is on and every X value is a date; false otherwise (normal category axis).
985 function guaven_sqlcharts_time_axis_points($pid, $values){
986 if (get_post_meta($pid, 'guaven_sqlcharts_timeaxis', true) != 1) return false;
987 $out = array();
988 $has_point = false;
989 foreach ($values as $key_ak => $series) {
990 $out[$key_ak] = array();
991 foreach ($series as $x => $y) {
992 $ts = guaven_sqlcharts_parse_date($x);
993 if ($ts === false) return false;
994 $out[$key_ak][] = '{x:' . $ts . ',y:' . (is_numeric($y) ? $y + 0 : 'null') . '}';
995 $has_point = true;
996 }
997 }
998 return $has_point ? $out : false;
999 }
1000
1001 // X scale options for time-axis mode (globals from asset/front.js): gvnSqlChartsTimeTicks replaces the evenly
1002 // spaced ticks Chart.js generates on a linear scale with the actual data dates, gvnSqlChartsTimeTick formats them
1003 function guaven_sqlcharts_time_axis_scale(){
1004 return "type: 'linear', offset: true, afterBuildTicks: gvnSqlChartsTimeTicks, ticks: {callback: gvnSqlChartsTimeTick, maxRotation: 45, autoSkip: true},";
1005 }
1006 // extra entry for the Chart.js "plugins" object in time-axis mode (tooltip title shown as a date)
1007 function guaven_sqlcharts_time_axis_plugins($time_points){
1008 return $time_points !== false ? 'tooltip: {callbacks: {title: gvnSqlChartsTimeTooltipTitle}}' : '';
1009 }
1010
1011 function guaven_sqlcharts_bardata($title, $labels, $values, $ylabel, $type = 'bar', $pid = null)
1012 {
1013 $horizontal = ($type == 'horizontalBar');
1014 $forcestack = ($type == 'stackedBar');
1015 $stacked = ($forcestack or get_post_meta($pid, 'guaven_sqlcharts_nostacked', true) != 1) ? 'true' : 'false';
1016 $time_points = $horizontal ? false : guaven_sqlcharts_time_axis_points($pid, $values);
1017 ?>
1018 var data = {
1019 <?php if ($time_points === false) { ?>labels: [<?php guaven_sqlcharts_merge_labeldata($labels);?>],<?php } ?>
1020 datasets: [
1021 <?php
1022 $values_new=guaven_sqlcharts_key_normalizer($values,$labels,$ylabel)[0];
1023 $i=-1;
1024 foreach ($values_new as $key_ak=>$value_ak) {
1025 $i++;
1026 $points = $time_points !== false ? $time_points[$key_ak] : $values_new[$key_ak];
1027 ?>
1028 {
1029 <?php
1030 echo guaven_sqlcharts_dataset_params(); // "params" shortcode attribute (validated)
1031 ?>
1032 label: <?php echo guaven_sqlcharts_js_label($ylabel[$key_ak]); ?>,
1033 backgroundColor: [
1034 <?php
1035 echo wp_kses(guaven_sqlcharts_colorgenerator(count($points), 0, 0, guaven_sqlcharts_colors($i, $pid)),[]);
1036 ?>
1037 ],
1038 borderColor: [
1039 <?php
1040 echo wp_kses(guaven_sqlcharts_colorgenerator(count($points), 0, 0.2, guaven_sqlcharts_colors($i, $pid)),[]);
1041 ?>
1042 ],
1043 borderWidth: 1,
1044 <?php if ($time_points !== false) echo 'barThickness: 24,'; // fixed width: on a time axis Chart.js would otherwise size bars from the closest pair of dates ?>
1045 data: [<?php
1046 echo wp_kses(implode(",", $points),[]);
1047 ?>],
1048 },
1049 <?php
1050 }
1051 ?>
1052 ]
1053 };
1054 var options={
1055 responsive: true,
1056 <?php echo wp_kses(guaven_sqlcharts_mar($pid),[]); ?>
1057 <?php echo wp_kses(guaven_sqlcharts_value_labels($pid),[]); ?>
1058 <?php if ($horizontal) echo "indexAxis: 'y',"; ?>
1059 scales: {
1060 x: {
1061 <?php if ($time_points !== false) echo guaven_sqlcharts_time_axis_scale(); ?>
1062 <?php echo guaven_sqlcharts_axis_title($pid, $horizontal ? 'y' : 'x'); ?>
1063 stacked: <?php echo esc_js($stacked); ?>,
1064 beginAtZero: <?php echo (get_post_meta($pid, 'guaven_sqlcharts_begin_with_0_x', true) == 1) ? 'true':'false'; ?>
1065 },
1066 y: {
1067 <?php echo guaven_sqlcharts_axis_title($pid, $horizontal ? 'x' : 'y'); ?>
1068 stacked: <?php echo esc_js($stacked); ?>,
1069 beginAtZero: <?php echo (get_post_meta($pid, 'guaven_sqlcharts_begin_with_0_y', true) == 1) ? 'true':'false'; ?>,
1070 ticks: {
1071 <?php if(get_post_meta($pid, 'guaven_sqlcharts_round_y_values', true) == 1) echo 'precision: 0,'; ?>
1072 }
1073 }
1074 }
1075 <?php
1076 guaven_sqlcharts_maybe_additional_parameters($pid, guaven_sqlcharts_time_axis_plugins($time_points));
1077 ?>
1078 };
1079 var myBarChart = new Chart(ctx, {
1080 type: 'bar',
1081 data: data,
1082 options: options
1083 });
1084 <?php
1085 }
1086
1087 function guaven_sqlcharts_merge_labeldata($labels){
1088 if(count($labels)==1){echo wp_kses(implode(",",$labels[0]),[]);return;}
1089 $merged=[];
1090 foreach($labels as $label){
1091 $merged=array_merge($merged,$label);
1092 }
1093 echo wp_kses(implode(",",array_unique($merged)),[]);
1094 }
1095
1096 function guaven_sqlcharts_linedata($title, $labels, $values, $ylabel, $type = 'false', $pid = null, $charttype = 'line', $stepped = false)
1097 {
1098 $time_points = ($charttype == 'radar') ? false : guaven_sqlcharts_time_axis_points($pid, $values);
1099 ?>
1100 var data = {
1101 <?php if ($time_points === false) { ?>labels: [<?php guaven_sqlcharts_merge_labeldata($labels);?>],<?php } ?>
1102 datasets: [
1103 <?php
1104 $values_new=guaven_sqlcharts_key_normalizer($values,$labels,$ylabel)[0];
1105 $dataset_count=count($values_new);
1106 $i=-1;
1107 foreach ($values_new as $key_ak=>$value_ak) {
1108 $i++;
1109 $points = $time_points !== false ? $time_points[$key_ak] : $values_new[$key_ak];
1110 if ($type == 'radarfill') $fill = "'origin'";
1111 elseif ($type == 'false') $fill = 'false';
1112 else $fill = ($i == 0 and $dataset_count > 1) ? '"+1"' : '"origin"';
1113 ?>
1114 {
1115 <?php
1116 echo guaven_sqlcharts_dataset_params(); // "params" shortcode attribute (validated)
1117 ?>
1118 label: <?php echo guaven_sqlcharts_js_label($ylabel[$key_ak]); ?>,
1119 fill: <?php echo wp_kses($fill,[]);
1120 ?>,
1121 tension: 0.1,
1122 <?php if ($stepped) echo 'stepped: true,'; ?>
1123 backgroundColor: <?php
1124 echo wp_kses_post(guaven_sqlcharts_colorgenerator(1, 1, 0.2, guaven_sqlcharts_colors($i, $pid)));
1125 ?>
1126 borderColor: <?php
1127 echo wp_kses_post(guaven_sqlcharts_colorgenerator(1, 1, 0.2, guaven_sqlcharts_colors($i, $pid)));
1128 ?>
1129 pointBorderColor: <?php
1130 echo wp_kses_post(guaven_sqlcharts_colorgenerator(1, 1, 0.2, guaven_sqlcharts_colors($i, $pid)));
1131 ?>
1132 pointHoverBackgroundColor: <?php
1133 echo wp_kses_post(guaven_sqlcharts_colorgenerator(1, 1, 0.2, guaven_sqlcharts_colors($i, $pid)));
1134 ?>
1135 pointHoverBorderColor: <?php
1136 echo wp_kses_post(guaven_sqlcharts_colorgenerator(1, 1, 0.2, guaven_sqlcharts_colors($i, $pid)));
1137 ?>
1138 data: [<?php
1139 echo wp_kses_post(implode(",", $points));
1140 ?>],
1141 spanGaps: false,
1142 },
1143 <?php
1144 }
1145 ?>
1146 ]
1147 };
1148 var myLineChart = new Chart(ctx, {
1149 type: '<?php echo esc_attr($charttype); ?>',
1150 data: data,
1151 options: {
1152 responsive: true,
1153 <?php echo wp_kses(guaven_sqlcharts_mar($pid),[]); ?>
1154 <?php echo wp_kses(guaven_sqlcharts_value_labels($pid),[]); ?>
1155 <?php if ($charttype == 'radar') { ?>
1156 scales: {
1157 r: {
1158 beginAtZero: <?php echo (get_post_meta($pid, 'guaven_sqlcharts_begin_with_0_y', true) == 1) ? 'true':'false'; ?>
1159 }
1160 }
1161 <?php } else { ?>
1162 scales: {
1163 x: {
1164 display: true,
1165 <?php if ($time_points !== false) echo guaven_sqlcharts_time_axis_scale(); ?>
1166 <?php echo guaven_sqlcharts_axis_title($pid, 'x'); ?>
1167 beginAtZero: <?php echo (get_post_meta($pid, 'guaven_sqlcharts_begin_with_0_x', true) == 1) ? 'true':'false'; ?>
1168 },
1169 y: {
1170 <?php echo guaven_sqlcharts_axis_title($pid, 'y'); ?>
1171 beginAtZero: <?php echo (get_post_meta($pid, 'guaven_sqlcharts_begin_with_0_y', true) == 1) ? 'true':'false'; ?>,
1172 ticks: {
1173 <?php if(get_post_meta($pid, 'guaven_sqlcharts_round_y_values', true) == 1) echo 'precision: 0,'; ?>
1174 }
1175 }
1176 }
1177 <?php } ?>
1178 <?php
1179 guaven_sqlcharts_maybe_additional_parameters($pid, guaven_sqlcharts_time_axis_plugins($time_points));
1180 ?>
1181
1182 }
1183 });
1184 <?php
1185 }
1186
1187 function guaven_sqlcharts_scatterdata($title, $labels, $values, $ylabel, $pid = null)
1188 {
1189 ?>
1190 var data = {
1191 datasets: [
1192 <?php
1193 $i=-1;
1194 foreach ($values as $key_ak=>$value_ak) {
1195 $i++;
1196 $points=array();
1197 foreach ($value_ak as $xval=>$yval) {
1198 $x = is_numeric($xval) ? $xval : '"'.esc_js($xval).'"';
1199 $y = is_numeric($yval) ? $yval : '"'.esc_js($yval).'"';
1200 $points[] = '{x:'.$x.',y:'.$y.'}';
1201 }
1202 ?>
1203 {
1204 <?php
1205 echo guaven_sqlcharts_dataset_params(); // "params" shortcode attribute (validated)
1206 ?>
1207 label: <?php echo guaven_sqlcharts_js_label(isset($ylabel[$key_ak])?$ylabel[$key_ak]:''); ?>,
1208 backgroundColor: <?php
1209 echo wp_kses_post(guaven_sqlcharts_colorgenerator(1, 1, 0.2, guaven_sqlcharts_colors($i, $pid)));
1210 ?>
1211 borderColor: <?php
1212 echo wp_kses_post(guaven_sqlcharts_colorgenerator(1, 1, 0.2, guaven_sqlcharts_colors($i, $pid)));
1213 ?>
1214 data: [<?php echo wp_kses(implode(",", $points),[]); ?>],
1215 },
1216 <?php
1217 }
1218 ?>
1219 ]
1220 };
1221 var myScatterChart = new Chart(ctx, {
1222 type: 'scatter',
1223 data: data,
1224 options: {
1225 responsive: true,
1226 <?php echo wp_kses(guaven_sqlcharts_mar($pid),[]); ?>
1227 <?php echo wp_kses(guaven_sqlcharts_value_labels($pid),[]); ?>
1228 scales: {
1229 x: {
1230 <?php echo guaven_sqlcharts_axis_title($pid, 'x'); ?>
1231 beginAtZero: <?php echo (get_post_meta($pid, 'guaven_sqlcharts_begin_with_0_x', true) == 1) ? 'true':'false'; ?>
1232 },
1233 y: {
1234 <?php echo guaven_sqlcharts_axis_title($pid, 'y'); ?>
1235 beginAtZero: <?php echo (get_post_meta($pid, 'guaven_sqlcharts_begin_with_0_y', true) == 1) ? 'true':'false'; ?>,
1236 ticks: {
1237 <?php if(get_post_meta($pid, 'guaven_sqlcharts_round_y_values', true) == 1) echo 'precision: 0,'; ?>
1238 }
1239 }
1240 }
1241 <?php
1242 guaven_sqlcharts_maybe_additional_parameters($pid);
1243 ?>
1244 }
1245 });
1246 <?php
1247 }
1248
1249
1250 function guaven_sqlcharts_maybe_additional_parameters($pid, $extra_plugins = ''){
1251 if(function_exists('guaven_sqlcharts_maybe_additional_parameters_custom')){
1252 wp_kses(guaven_sqlcharts_maybe_additional_parameters_custom($pid),[]);
1253 return;
1254 }
1255 $guaven_sqlcharts_legend_position=get_post_meta($pid, 'guaven_sqlcharts_legend_position', true);
1256 if(in_array($guaven_sqlcharts_legend_position,['top','bottom','left','right'])){
1257 $display='true';$position=$guaven_sqlcharts_legend_position;
1258 }
1259 else {
1260 $display='false';$position='top';
1261 }
1262 echo wp_kses( ",plugins: {legend: {display: ".$display.",position:'".$position."'}".($extra_plugins !== '' ? ','.$extra_plugins : '')."}",[]);
1263 }
1264
1265
1266
1267
1268 function guaven_sqlcharts_piedata($title, $labels, $values, $ylabel, $pid, $type = 'pie')
1269 {
1270 ?>
1271 var options={
1272 <?php echo wp_kses(guaven_sqlcharts_value_labels($pid),[]); ?>
1273 responsive: true
1274 <?php echo get_post_meta($pid,'guaven_sqlcharts_chartheight',true)!=''||!empty($GLOBALS["guaven_sqlcharts_atts"]['height'])?',maintainAspectRatio: false':''; ?>
1275 <?php
1276 guaven_sqlcharts_maybe_additional_parameters($pid);
1277 ?>
1278 };
1279 var data = {
1280 labels: [ <?php guaven_sqlcharts_merge_labeldata($labels);?>],
1281 datasets: [
1282 <?php
1283 for ($i = 0; $i < count($values); $i++) {
1284 ?>
1285 {
1286 <?php
1287 echo guaven_sqlcharts_dataset_params(); // "params" shortcode attribute (validated)
1288 ?>
1289 data: [<?php
1290 echo wp_kses(implode(",", $values[$i]),[]);
1291 ?>],
1292 backgroundColor: [
1293 <?php
1294 $ii=0;
1295 foreach($values[$i] as $vci=>$valuecolor){
1296 echo wp_kses_post(guaven_sqlcharts_colorgenerator(1, 0, -0.1, guaven_sqlcharts_colors($ii, $pid)));
1297 $ii++;
1298 }
1299 ?>
1300 ],
1301 hoverBackgroundColor: [
1302 <?php
1303 $ii=0;
1304 foreach($values[$i] as $vci=>$valuecolor){
1305 echo wp_kses_post(guaven_sqlcharts_colorgenerator(1, 0, 0.2, guaven_sqlcharts_colors($ii, $pid)));
1306 $ii++;
1307 }
1308 ?>
1309 ]
1310 },
1311 <?php
1312 }
1313 ?>
1314 ]
1315 };
1316 var myPieChart = new Chart(ctx,{
1317 type: '<?php
1318 echo esc_attr($type);
1319 ?>',
1320 data: data,
1321 options: options
1322 });
1323 <?php
1324 }
1325
1326
1327
1328
1329 function guaven_sqlcharts_colorgenerator($count, $indic, $darkness = 0, $initcolor = '255,0,0')
1330 {
1331 if (strpos($initcolor,'#')===0) {
1332 $split = str_split(substr($initcolor,1), 2);
1333 $r = hexdec($split[0]);
1334 $g = hexdec($split[1]);
1335 $b = hexdec($split[2]);
1336 $ret='';
1337 for ($i = 0; $i < $count; $i++) {
1338 $ret .= "'rgba(" . $r . ", " . $g . ", " . $b . ",".($darkness + 0.8 - $indic * $i * 0.8 / ($count)).")',
1339 ";
1340 }
1341 return $ret;
1342 }
1343 $initial_colors = array(
1344 'linebg' => 'red',
1345 'linebr' => 'yellow',
1346 'linebc' => 'green',
1347 'linehbg' => 'white',
1348 'linehbc' => 'black'
1349 );
1350 if (!empty($initial_colors[$count]))
1351 return '"' . $initial_colors[$count] . '",
1352 ';
1353 $ret = '';
1354 for ($i = 0; $i < $count; $i++) {
1355 $ret .= "'rgba(" . $initcolor . "," . ($darkness + 0.8 - $indic * $i * 0.8 / ($count)) . ")',
1356 ";
1357 }
1358 return $ret;
1359 }
1360
1361 function guaven_sqlcharts_tablepart($title, $labels, $values, $ylabel,$xlabel){
1362 $tabledata='';
1363 $fcol=[];$scol=[];
1364 $empty_cell=apply_filters( 'guaven_sqlcharts_table_empty_cell','<td></td>');
1365 $tablein='';
1366 foreach($values as $row=>$valuerow){
1367 foreach ($valuerow as $key => $value) {
1368 $putval=$labels[$row][$key]??'';
1369 $fcol[$key]='<td>'.str_replace('"',"",$putval).'</td>';
1370 $scol[$key][$row]='<td>'.$value.'</td>';
1371 }
1372 foreach($scol as $scolkey=>$scolvalue){
1373 for($i=0;$i<count($values);$i++){
1374 //echo $i;
1375 if(!isset($scolvalue[$i]))$scol[$scolkey][$i]=$empty_cell;;
1376 }
1377 ksort($scol[$scolkey]);
1378 }
1379 }
1380
1381 foreach($fcol as $key=>$value){
1382 $tablein.='<tr>'.$value.implode(" ",$scol[$key]).'</tr>'.PHP_EOL;
1383 }
1384 $tabledata.='<div class="gvn-tablewrap"><table class="gvn-table"><tr><th>'.$xlabel[0].'</th><th>'.implode("</th><th>",$ylabel).'</th></tr>
1385 '.$tablein.'</table></div><br>';
1386
1387 echo wp_kses_post($tabledata);
1388
1389 }
1390
1391 function guaven_sqlcharts_graphtype($post){
1392 if (strpos(get_post_meta($post->ID, 'guaven_sqlcharts_graphtype', true), "_l") !== false)
1393 $postfix = '_2';
1394 else $postfix = '';
1395 return $postfix;
1396 }
1397
1398 add_filter('the_content',function($content){
1399 if(!is_singular('gvn_schart'))return $content;
1400 global $post;
1401 $postfix=guaven_sqlcharts_graphtype($post);
1402 return '[gvn_schart'.$postfix.' id="'.$post->ID.'"'.
1403 (get_post_meta($post->ID,'guaven_sqlcharts_tablepart',true)!=''?' table="1"':'')
1404 .']';
1405 });
1406
1407 function guaven_sqlcharts_key_normalizer($values,$labels,$ylabel){
1408 $normalize_keys=[];
1409 $empty_value=apply_filters( 'guaven_sqlcharts_table_empty_value','');
1410 foreach ($values as $key_ak=>$value_ak) {
1411 $normalize_keys=array_merge($normalize_keys,array_keys($values[$key_ak]));
1412 }
1413 $values_normalized=[];$labels_normalized=[];$ylabel_normalized=[];
1414 foreach($normalize_keys as $normalized_key){
1415 foreach ($values as $key_ak=>$value_ak) {
1416 $values_normalized[$key_ak][$normalized_key]=isset( $values[$key_ak][$normalized_key])? $values[$key_ak][$normalized_key]:"'".$empty_value."'";
1417 $labels_normalized[$key_ak][$normalized_key]=isset( $labels[$key_ak][$normalized_key])? $labels[$key_ak][$normalized_key]:"''";
1418 $ylabel_normalized[$key_ak][$normalized_key]=isset( $ylabel[$key_ak][$normalized_key])? $ylabel[$key_ak][$normalized_key]:"";
1419 }
1420 }
1421 return [$values_normalized,$labels_normalized,$ylabel_normalized];
1422 }
1423
1424
1425 add_filter('guaven_sqlcharts_table_empty_cell',function($str){return '<td>#</td>';});
1426 add_filter('guaven_sqlcharts_table_empty_value',function($str){return 'N/A';});
1427