PluginProbe
Visualizer – Tables & Charts Manager with Built-in AI Generator / 4.0.6
Visualizer – Tables & Charts Manager with Built-in AI Generator v4.0.6
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.6, at classes/Visualizer/Module/AIBuilder.php

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