PluginProbe
Visualizer – Tables & Charts Manager with Built-in AI Generator / 4.0.2
Visualizer – Tables & Charts Manager with Built-in AI Generator v4.0.2
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 / AIBuilder.php

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

591 lines 20.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * AI Chart Builder module.
4 *
5 * AJAX endpoints for the React AI Chart Builder wizard:
6 * - visualizer-ai-create : create a draft chart post
7 * - visualizer-ai-upload : parse & persist data (CSV/XLSX file, URL, JSON, DB)
8 * - visualizer-ai-save : persist D3.js code and publish
9 *
10 * @category Visualizer
11 * @package Module
12 */
13 class Visualizer_Module_AIBuilder extends Visualizer_Module {
14
15 const NAME = __CLASS__;
16 const CF_D3_CODE = 'visualizer-d3-code';
17
18 /**
19 * Constructor.
20 *
21 * @param Visualizer_Plugin $plugin Plugin instance.
22 */
23 public function __construct( $plugin ) {
24 parent::__construct( $plugin );
25 $this->_addAction( 'wp_ajax_visualizer-ai-create', 'createChart' );
26 $this->_addAction( 'wp_ajax_visualizer-ai-upload', 'uploadData' );
27 $this->_addAction( 'wp_ajax_visualizer-ai-save', 'saveChart' );
28 $this->_addAction( 'wp_ajax_visualizer-ai-generate', 'generateChart' );
29 $this->_addAction( 'wp_ajax_visualizer-ai-status', 'chartStatus' );
30 $this->_addAction( 'wp_ajax_visualizer-ai-chart-nonce', 'getChartNonce' );
31 $this->_addAction( 'wp_ajax_visualizer-ai-fetch', 'fetchChart' );
32 }
33
34 // -------------------------------------------------------------------------
35 // Helpers
36 // -------------------------------------------------------------------------
37
38 /**
39 * Get the Agents workflow slug.
40 *
41 * @return string
42 */
43 private function _get_workflow_slug() {
44 return defined( 'VISUALIZER_AGENTS_WORKFLOW' ) ? VISUALIZER_AGENTS_WORKFLOW : 'visualizer-generate';
45 }
46
47 /**
48 * Resolve the license token used for agent authorization.
49 *
50 * @return string
51 */
52 private function _get_agents_license_token() {
53 $license = get_option( 'visualizer_pro_license_data', 'free' );
54 if ( ! empty( $license ) && is_object( $license ) ) {
55 $license = isset( $license->key ) ? $license->key : 'free';
56 } else {
57 $license = 'free';
58 }
59
60 return (string) $license;
61 }
62
63 /**
64 * Build request headers for the Agents service.
65 *
66 * @param bool $with_content_type Whether to include Content-Type header.
67 * @return array<string, string>
68 */
69 private function _get_agents_headers( $with_content_type = false ) {
70 $headers = array(
71 'X-Site-Url' => home_url(),
72 'Accept' => 'application/json',
73 );
74
75 if ( $with_content_type ) {
76 $headers['Content-Type'] = 'application/json';
77 }
78
79 $license = $this->_get_agents_license_token();
80 if ( ! empty( $license ) ) {
81 $headers['Authorization'] = 'Bearer ' . base64_encode( $license );
82 }
83
84 return $headers;
85 }
86
87 /**
88 * Verify nonce and capability for AI Builder requests.
89 */
90 private function _verify_create_nonce(): void {
91 check_ajax_referer( 'visualizer-ai-builder', 'nonce' );
92 if ( ! current_user_can( 'edit_posts' ) ) {
93 wp_send_json_error( array( 'message' => __( 'Unauthorized.', 'visualizer' ) ), 403 );
94 }
95 }
96
97 /**
98 * Persist chart data + series from a source.
99 *
100 * @param int $chart_id Chart ID.
101 * @param Visualizer_Source $source Data source instance.
102 */
103 private function _persist( $chart_id, $source ): void {
104 update_post_meta( $chart_id, Visualizer_Plugin::CF_SERIES, $source->getSeries() );
105 update_post_meta( $chart_id, Visualizer_Plugin::CF_SOURCE, $source->getSourceName() );
106 update_post_meta( $chart_id, Visualizer_Plugin::CF_DEFAULT_DATA, 0 );
107 wp_update_post(
108 array(
109 'ID' => $chart_id,
110 'post_content' => $source->getData( false ),
111 )
112 );
113 }
114
115 // -------------------------------------------------------------------------
116 // AJAX: create draft
117 // -------------------------------------------------------------------------
118
119 /**
120 * AJAX: create a draft chart post for AI Builder.
121 */
122 public function createChart(): void {
123 $this->_verify_create_nonce();
124
125 $chart_id = wp_insert_post(
126 array(
127 'post_type' => Visualizer_Plugin::CPT_VISUALIZER,
128 'post_status' => 'auto-draft',
129 'post_title' => __( 'AI Chart', 'visualizer' ),
130 ),
131 true
132 );
133
134 if ( is_wp_error( $chart_id ) ) {
135 wp_send_json_error( array( 'message' => $chart_id->get_error_message() ) );
136 }
137
138 update_post_meta( $chart_id, Visualizer_Plugin::CF_CHART_LIBRARY, 'd3' );
139 update_post_meta( $chart_id, Visualizer_Plugin::CF_SOURCE, 'Visualizer_Source_Csv' );
140
141 wp_send_json_success(
142 array(
143 'chart_id' => $chart_id,
144 'upload_nonce' => wp_create_nonce( 'visualizer-ai-upload-' . $chart_id ),
145 )
146 );
147 }
148
149 // -------------------------------------------------------------------------
150 // AJAX: get upload nonce for an existing chart (used in edit mode)
151 // -------------------------------------------------------------------------
152
153 /**
154 * AJAX: get upload nonce for an existing chart (edit mode).
155 */
156 public function getChartNonce(): void {
157 $this->_verify_create_nonce();
158 $chart_id = intval( isset( $_POST['chart_id'] ) ? $_POST['chart_id'] : 0 );
159 if ( ! $chart_id || ! get_post( $chart_id ) ) {
160 wp_send_json_error( array( 'message' => __( 'Chart not found.', 'visualizer' ) ) );
161 }
162 wp_send_json_success(
163 array(
164 'upload_nonce' => wp_create_nonce( 'visualizer-ai-upload-' . $chart_id ),
165 )
166 );
167 }
168
169 // -------------------------------------------------------------------------
170 // AJAX: fetch chart data/spec for edit mode (used on refresh)
171 // -------------------------------------------------------------------------
172
173 /**
174 * AJAX: fetch chart data/spec for edit mode.
175 */
176 public function fetchChart(): void {
177 $this->_verify_create_nonce();
178 $chart_id = intval( isset( $_POST['chart_id'] ) ? $_POST['chart_id'] : 0 );
179 $chart = $chart_id ? get_post( $chart_id ) : null;
180 if ( ! $chart || $chart->post_type !== Visualizer_Plugin::CPT_VISUALIZER ) {
181 wp_send_json_error( array( 'message' => __( 'Chart not found.', 'visualizer' ) ) );
182 }
183
184 $series = get_post_meta( $chart_id, Visualizer_Plugin::CF_SERIES, true );
185 $data = Visualizer_Module::get_chart_data( $chart, '', false );
186 $code = get_post_meta( $chart_id, self::CF_D3_CODE, true );
187 $settings = get_post_meta( $chart_id, Visualizer_Plugin::CF_SETTINGS, true );
188 $title = ( is_array( $settings ) && ! empty( $settings['backend-title'] ) )
189 ? $settings['backend-title']
190 : $chart->post_title;
191
192 wp_send_json_success(
193 array(
194 'title' => $title,
195 'series' => $series,
196 'data' => $data,
197 'code' => $code,
198 )
199 );
200 }
201
202 // -------------------------------------------------------------------------
203 // AJAX: upload / parse data
204 // -------------------------------------------------------------------------
205
206 /**
207 * Determines whether a remote URL serves an XLSX file.
208 *
209 * Uses wp_safe_remote_get() and checks ZIP magic number (PK\x03\x04).
210 *
211 * @access private
212 * @param string $url The remote URL to probe.
213 * @return bool TRUE if the file appears to be XLSX, FALSE otherwise.
214 */
215 private static function _url_is_xlsx( $url ) {
216 $tmpfile = wp_tempnam( 'visualizer_xlsx_probe' );
217 if ( ! $tmpfile ) {
218 return false;
219 }
220
221 $response = wp_safe_remote_get(
222 $url,
223 array(
224 'timeout' => 15,
225 'redirection' => 5,
226 'stream' => true,
227 'filename' => $tmpfile,
228 'headers' => array( 'Range' => 'bytes=0-3' ),
229 )
230 );
231
232 if ( is_wp_error( $response ) ) {
233 @unlink( $tmpfile );
234 return false;
235 }
236
237 $body = file_get_contents( $tmpfile );
238 @unlink( $tmpfile );
239
240 if ( ! empty( $body ) ) {
241 return 0 === strpos( $body, "PK\x03\x04" );
242 }
243
244 $content_type = wp_remote_retrieve_header( $response, 'content-type' );
245 return is_string( $content_type ) && false !== stripos( $content_type, 'sheet' );
246 }
247
248 /**
249 * AJAX: upload/parse data for AI Builder.
250 */
251 public function uploadData(): void {
252 $chart_id = intval( isset( $_POST['chart_id'] ) ? $_POST['chart_id'] : 0 );
253 check_ajax_referer( 'visualizer-ai-upload-' . $chart_id, 'nonce' );
254
255 if ( ! current_user_can( 'edit_posts' ) ) {
256 wp_send_json_error( array( 'message' => __( 'Unauthorized.', 'visualizer' ) ), 403 );
257 }
258 if ( ! get_post( $chart_id ) ) {
259 wp_send_json_error( array( 'message' => __( 'Chart not found.', 'visualizer' ) ) );
260 }
261
262 $source_type = isset( $_POST['source_type'] ) ? sanitize_key( $_POST['source_type'] ) : 'csv_string';
263 $source = null;
264 $tmp_files = array();
265
266 switch ( $source_type ) {
267
268 // ── Manual CSV text ──────────────────────────────────────────────
269 case 'csv_string':
270 if ( empty( $_POST['csv_data'] ) ) {
271 wp_send_json_error( array( 'message' => __( 'No data provided.', 'visualizer' ) ) );
272 }
273 $tmp = tempnam( sys_get_temp_dir(), 'viz_ai_' );
274 file_put_contents( $tmp, wp_unslash( $_POST['csv_data'] ) );
275 $tmp_files[] = $tmp;
276 $source = new Visualizer_Source_Csv( $tmp );
277 break;
278
279 // ── CSV / XLSX file upload ────────────────────────────────────────
280 case 'csv_file':
281 case 'xlsx_file':
282 if ( empty( $_FILES['data_file']['tmp_name'] ) ) {
283 wp_send_json_error( array( 'message' => __( 'No file uploaded.', 'visualizer' ) ) );
284 }
285 $ext = strtolower( pathinfo( $_FILES['data_file']['name'], PATHINFO_EXTENSION ) );
286 if ( $ext === 'xlsx' && class_exists( 'Visualizer_Source_Xlsx' ) ) {
287 $source = new Visualizer_Source_Xlsx( $_FILES['data_file']['tmp_name'] );
288 } else {
289 $source = new Visualizer_Source_Csv( $_FILES['data_file']['tmp_name'] );
290 }
291 break;
292
293 // ── Remote CSV / XLSX URL ─────────────────────────────────────────
294 case 'file_url':
295 if ( empty( $_POST['file_url'] ) ) {
296 wp_send_json_error( array( 'message' => __( 'No URL provided.', 'visualizer' ) ) );
297 }
298 $url = wp_unslash( $_POST['file_url'] );
299
300 // Allow local absolute paths in dev (same CSVs used by Classic).
301 if ( is_string( $url ) && file_exists( $url ) && is_readable( $url ) ) {
302 $ext = strtolower( pathinfo( $url, PATHINFO_EXTENSION ) );
303 if ( 'xlsx' === $ext && class_exists( 'Visualizer_Source_Xlsx' ) ) {
304 $source = new Visualizer_Source_Xlsx( $url );
305 } else {
306 $source = new Visualizer_Source_Csv( $url );
307 }
308 break;
309 }
310
311 if ( function_exists( 'wp_http_validate_url' ) ) {
312 $validated_url = wp_http_validate_url( (string) $url );
313 $url = false === $validated_url ? false : (string) $validated_url;
314 } else {
315 $url = esc_url_raw( (string) $url );
316 }
317 if ( false === $url ) {
318 wp_send_json_error( array( 'message' => __( 'Invalid URL. Please check the URL and try again.', 'visualizer' ) ) );
319 }
320
321 $ext = strtolower( pathinfo( parse_url( $url, PHP_URL_PATH ), PATHINFO_EXTENSION ) );
322 if ( 'xlsx' === $ext || ( 'csv' !== $ext && self::_url_is_xlsx( $url ) ) ) {
323 $source = new Visualizer_Source_Xlsx_Remote( $url );
324 } else {
325 $source = new Visualizer_Source_Csv_Remote( $url );
326 }
327
328 // Optionally store schedule
329 if ( ! empty( $_POST['schedule'] ) ) {
330 update_post_meta( $chart_id, 'visualizer-chart-url', $url );
331 update_post_meta( $chart_id, 'visualizer-chart-schedule', intval( $_POST['schedule'] ) );
332 apply_filters( 'visualizer_pro_chart_schedule', $chart_id, $url, $_POST['schedule'] );
333 }
334 break;
335
336 // ── JSON URL ─────────────────────────────────────────────────────
337 case 'json_url':
338 if ( empty( $_POST['json_url'] ) ) {
339 wp_send_json_error( array( 'message' => __( 'No URL provided.', 'visualizer' ) ) );
340 }
341 $params = array(
342 'url' => esc_url_raw( wp_unslash( $_POST['json_url'] ) ),
343 'root' => isset( $_POST['json_root'] ) ? sanitize_text_field( wp_unslash( $_POST['json_root'] ) ) : '',
344 'paging' => isset( $_POST['json_paging'] ) ? sanitize_text_field( wp_unslash( $_POST['json_paging'] ) ) : '',
345 'method' => ( isset( $_POST['json_method'] ) && $_POST['json_method'] === 'POST' ) ? 'POST' : 'GET',
346 );
347 if ( ! empty( $_POST['json_auth'] ) ) {
348 $params['auth'] = sanitize_text_field( wp_unslash( $_POST['json_auth'] ) );
349 } elseif ( ! empty( $_POST['json_username'] ) ) {
350 $params['username'] = sanitize_text_field( wp_unslash( $_POST['json_username'] ) );
351 $params['password'] = sanitize_text_field( wp_unslash( isset( $_POST['json_password'] ) ? $_POST['json_password'] : '' ) );
352 }
353 if ( ! empty( $_POST['json_headers'] ) ) {
354 $params['headers'] = sanitize_textarea_field( wp_unslash( $_POST['json_headers'] ) );
355 }
356 $source = new Visualizer_Source_Json( $params );
357
358 // Store config for sync
359 update_post_meta( $chart_id, Visualizer_Plugin::CF_JSON_URL, $params['url'] );
360 update_post_meta( $chart_id, Visualizer_Plugin::CF_JSON_ROOT, $params['root'] );
361 if ( ! empty( $_POST['json_schedule'] ) ) {
362 update_post_meta( $chart_id, Visualizer_Plugin::CF_JSON_SCHEDULE, intval( $_POST['json_schedule'] ) );
363 }
364 break;
365
366 // ── Database query ────────────────────────────────────────────────
367 case 'db_query':
368 if ( ! current_user_can( 'manage_options' ) && ! is_super_admin() ) {
369 wp_send_json_error( array( 'message' => __( 'Action not allowed for this user.', 'visualizer' ) ), 403 );
370 }
371 if ( empty( $_POST['db_query'] ) ) {
372 wp_send_json_error( array( 'message' => __( 'No query provided.', 'visualizer' ) ) );
373 }
374 $query = wp_unslash( $_POST['db_query'] );
375 $params = array();
376 if ( ! empty( $_POST['db_host'] ) ) {
377 $params = array(
378 'host' => sanitize_text_field( wp_unslash( $_POST['db_host'] ) ),
379 'port' => intval( isset( $_POST['db_port'] ) ? $_POST['db_port'] : 3306 ),
380 'name' => sanitize_text_field( wp_unslash( isset( $_POST['db_name'] ) ? $_POST['db_name'] : '' ) ),
381 'username' => sanitize_text_field( wp_unslash( isset( $_POST['db_username'] ) ? $_POST['db_username'] : '' ) ),
382 'password' => sanitize_text_field( wp_unslash( isset( $_POST['db_password'] ) ? $_POST['db_password'] : '' ) ),
383 'type' => sanitize_key( isset( $_POST['db_type'] ) ? $_POST['db_type'] : 'mysql' ),
384 );
385 }
386 $source = new Visualizer_Source_Query( $query, $chart_id, $params );
387 update_post_meta( $chart_id, 'visualizer-db-query', $query );
388 break;
389
390 default:
391 wp_send_json_error( array( 'message' => __( 'Unknown source type.', 'visualizer' ) ) );
392 }
393
394 if ( ! $source->fetch() ) {
395 foreach ( $tmp_files as $f ) {
396 @unlink( $f );
397 }
398 wp_send_json_error(
399 array(
400 'message' => __( 'Could not parse data. Check format and try again.', 'visualizer' ),
401 )
402 );
403 }
404
405 foreach ( $tmp_files as $f ) {
406 @unlink( $f );
407 }
408
409 $series = $source->getSeries();
410 if ( empty( $series ) ) {
411 wp_send_json_error( array( 'message' => __( 'No columns found. Check that row 1 has headers and row 2 has types (string/number/date).', 'visualizer' ) ) );
412 }
413
414 $this->_persist( $chart_id, $source );
415
416 wp_send_json_success(
417 array(
418 'series' => $series,
419 'data' => $source->getRawData( false ),
420 )
421 );
422 }
423
424 // -------------------------------------------------------------------------
425 // AJAX: generate chart (async — queues AI job, returns workflow_id)
426 // -------------------------------------------------------------------------
427
428 /**
429 * AJAX: start async chart generation.
430 */
431 public function generateChart(): void {
432 $this->_verify_create_nonce();
433
434 $chart_id = intval( isset( $_POST['chart_id'] ) ? $_POST['chart_id'] : 0 );
435 if ( ! $chart_id || ! get_post( $chart_id ) ) {
436 wp_send_json_error( array( 'message' => __( 'Chart not found.', 'visualizer' ) ) );
437 }
438
439 $prompt = isset( $_POST['prompt'] ) ? sanitize_textarea_field( wp_unslash( $_POST['prompt'] ) ) : '';
440 $series = isset( $_POST['series'] ) ? wp_unslash( $_POST['series'] ) : '';
441 $data = isset( $_POST['data'] ) ? wp_unslash( $_POST['data'] ) : '';
442 $existing_code = isset( $_POST['existing_code'] ) ? wp_unslash( $_POST['existing_code'] ) : '';
443 $ref_image = isset( $_POST['ref_image'] ) ? wp_unslash( $_POST['ref_image'] ) : '';
444 $ref_image_mime = isset( $_POST['ref_image_mime'] ) ? sanitize_text_field( wp_unslash( $_POST['ref_image_mime'] ) ) : '';
445
446 if ( empty( $series ) || empty( $data ) ) {
447 wp_send_json_error( array( 'message' => __( 'No data available. Load data first.', 'visualizer' ) ) );
448 }
449
450 $agents_url = VISUALIZER_AGENTS_URL;
451 $workflow_slug = $this->_get_workflow_slug();
452
453 $request_body = array(
454 'prompt' => $prompt,
455 'series' => $series,
456 'data' => $data,
457 );
458 if ( ! empty( $existing_code ) ) {
459 $request_body['existing_code'] = $existing_code;
460 }
461 if ( ! empty( $ref_image ) ) {
462 $request_body['ref_image'] = $ref_image;
463 $request_body['ref_image_mime'] = ! empty( $ref_image_mime ) ? $ref_image_mime : 'image/jpeg';
464 }
465
466 $headers = $this->_get_agents_headers( true );
467
468 $response = wp_remote_post(
469 trailingslashit( $agents_url ) . 'api/workflows/' . rawurlencode( $workflow_slug ) . '/start',
470 array(
471 'timeout' => 15,
472 'headers' => $headers,
473 'body' => wp_json_encode( $request_body ),
474 )
475 );
476
477 if ( is_wp_error( $response ) ) {
478 wp_send_json_error( array( 'message' => $response->get_error_message() ) );
479 }
480
481 $status = wp_remote_retrieve_response_code( $response );
482 $response_body = json_decode( wp_remote_retrieve_body( $response ), true );
483
484 if ( $status !== 200 && $status !== 201 && $status !== 202 ) {
485 $msg = ( is_array( $response_body ) && isset( $response_body['error'] ) ) ? $response_body['error'] : __( 'Generation request failed.', 'visualizer' );
486 wp_send_json_error( array( 'message' => $msg ) );
487 }
488
489 $workflow_id = '';
490 if ( is_array( $response_body ) ) {
491 if ( isset( $response_body['workflowId'] ) ) {
492 $workflow_id = $response_body['workflowId'];
493 } elseif ( isset( $response_body['workflow_id'] ) ) {
494 $workflow_id = $response_body['workflow_id'];
495 } elseif ( isset( $response_body['data']['workflowId'] ) ) {
496 $workflow_id = $response_body['data']['workflowId'];
497 }
498 }
499
500 wp_send_json_success(
501 array(
502 'workflow_id' => $workflow_id,
503 )
504 );
505 }
506
507 // -------------------------------------------------------------------------
508 // AJAX: poll generation status
509 // -------------------------------------------------------------------------
510
511 /**
512 * AJAX: poll generation status.
513 */
514 public function chartStatus(): void {
515 $this->_verify_create_nonce();
516
517 $workflow_id = isset( $_POST['workflow_id'] ) ? sanitize_text_field( wp_unslash( $_POST['workflow_id'] ) ) : '';
518 if ( empty( $workflow_id ) ) {
519 wp_send_json_error( array( 'message' => __( 'Missing workflow ID.', 'visualizer' ) ) );
520 }
521
522 $agents_url = VISUALIZER_AGENTS_URL;
523 $workflow_slug = $this->_get_workflow_slug();
524 $headers = $this->_get_agents_headers();
525
526 $response = wp_remote_get(
527 trailingslashit( $agents_url ) . 'api/workflows/' . rawurlencode( $workflow_slug ) . '/' . rawurlencode( $workflow_id ),
528 array(
529 'timeout' => 15,
530 'headers' => $headers,
531 )
532 );
533
534 if ( is_wp_error( $response ) ) {
535 wp_send_json_error( array( 'message' => $response->get_error_message() ) );
536 }
537
538 $body = json_decode( wp_remote_retrieve_body( $response ), true );
539 if ( ! is_array( $body ) ) {
540 wp_send_json_error( array( 'message' => __( 'Invalid response from AI service.', 'visualizer' ) ) );
541 }
542
543 wp_send_json_success( $body );
544 }
545
546 // -------------------------------------------------------------------------
547 // AJAX: save chart
548 // -------------------------------------------------------------------------
549
550 /**
551 * AJAX: save chart with D3 code and publish.
552 */
553 public function saveChart(): void {
554 $this->_verify_create_nonce();
555
556 $chart_id = intval( isset( $_POST['chart_id'] ) ? $_POST['chart_id'] : 0 );
557 $code = isset( $_POST['code'] ) ? wp_unslash( $_POST['code'] ) : '';
558 $title = sanitize_text_field( wp_unslash( isset( $_POST['title'] ) ? $_POST['title'] : __( 'AI Chart', 'visualizer' ) ) );
559
560 if ( ! $chart_id || ! get_post( $chart_id ) ) {
561 wp_send_json_error( array( 'message' => __( 'Chart not found.', 'visualizer' ) ) );
562 }
563 if ( empty( $code ) ) {
564 wp_send_json_error( array( 'message' => __( 'No chart code found. Generate a chart first.', 'visualizer' ) ) );
565 }
566
567 update_post_meta( $chart_id, self::CF_D3_CODE, $code );
568 update_post_meta( $chart_id, Visualizer_Plugin::CF_CHART_LIBRARY, 'd3' );
569 $settings = get_post_meta( $chart_id, Visualizer_Plugin::CF_SETTINGS, true );
570 if ( ! is_array( $settings ) ) {
571 $settings = array();
572 }
573 $settings['backend-title'] = $title;
574 update_post_meta( $chart_id, Visualizer_Plugin::CF_SETTINGS, $settings );
575 wp_update_post(
576 array(
577 'ID' => $chart_id,
578 'post_status' => 'publish',
579 'post_title' => $title,
580 )
581 );
582
583 wp_send_json_success(
584 array(
585 'id' => $chart_id,
586 'shortcode' => '[visualizer id="' . $chart_id . '"]',
587 )
588 );
589 }
590 }
591