| 1 |
<?php |
| 2 |
/** |
| 3 |
* Ability: wpdatatables/create-chart |
| 4 |
* |
| 5 |
* Creates a new wpDataTables chart linked to an existing wpDataTable. |
| 6 |
* |
| 7 |
* Supports all chart engines available on the installation: |
| 8 |
* - Google Charts (all tiers) |
| 9 |
* - Chart.js (all tiers) |
| 10 |
* - HighCharts (Standard+, requires WDT_HC_INTEGRATION) |
| 11 |
* - ApexCharts (Standard+, requires WDT_AC_INTEGRATION) |
| 12 |
* - HighCharts Stock (Pro+, requires WDT_HS_INTEGRATION) |
| 13 |
* |
| 14 |
* The AI should first call get-system-info to check which engines are available, |
| 15 |
* then list-tables / get-table-info to find the source table and its columns. |
| 16 |
* |
| 17 |
* @package wpDataTables_MCP_Server |
| 18 |
* @since 0.2.0 |
| 19 |
*/ |
| 20 |
|
| 21 |
defined( 'ABSPATH' ) or die('Access denied.'); |
| 22 |
|
| 23 |
add_action( 'wp_abilities_api_init', 'wdtmcp_register_create_chart_ability' ); |
| 24 |
|
| 25 |
function wdtmcp_register_create_chart_ability() { |
| 26 |
wp_register_ability( |
| 27 |
'wpdatatables/create-chart', |
| 28 |
array( |
| 29 |
'label' => __( 'Create Chart', 'wpdatatables' ), |
| 30 |
'description' => __( 'Creates a new chart from an existing wpDataTable. Specify the source table ID, chart engine (google or chartjs — these are the engines available in wpDataTables Lite), chart type (line, column, pie, area, bar, scatter, etc.), which columns to include, and optional display settings (title, dimensions, colours). Returns the chart ID and shortcode. Check get-system-info first to see which chart engines are available on this installation. NOTE: Simple tables cannot be used as a chart source. WHEN TO CALL: user wants a visual chart from existing table data. DO NOT CALL before a source table exists — create or find the table first. REQUIRED INPUT: title, wpdatatable_id (from list-tables), engine (google or chartjs), type, selected_columns (orig_header strings from get-table-info). OPTIONAL: width, height, show_title, show_grid, range_type, row_range, and other display flags. TYPICAL WORKFLOW: get-system-info → list-tables → get-table-info → create-chart. RETURNS: chart_id and shortcode. VERIFY with list-charts and get-chart-info.', 'wpdatatables' ), |
| 31 |
'category' => 'wpdatatables-data', |
| 32 |
|
| 33 |
'input_schema' => array( |
| 34 |
'type' => 'object', |
| 35 |
'properties' => array( |
| 36 |
'title' => array( |
| 37 |
'type' => 'string', |
| 38 |
'description' => 'Chart title.', |
| 39 |
), |
| 40 |
'wpdatatable_id' => array( |
| 41 |
'type' => 'integer', |
| 42 |
'description' => 'Source wpDataTable ID.', |
| 43 |
), |
| 44 |
'engine' => array( |
| 45 |
'type' => 'string', |
| 46 |
'description' => 'Chart rendering engine. wpDataTables Lite supports google and chartjs (HighCharts/ApexCharts require a premium licence).', |
| 47 |
'enum' => array( 'google', 'chartjs' ), |
| 48 |
), |
| 49 |
'type' => array( |
| 50 |
'type' => 'string', |
| 51 |
'description' => 'Chart type (e.g. line, column, pie, area, bar, scatter, donut, bubble, radar, polar, gauge, histogram, candlestick, waterfall, stepped_area, stacked_bar, stacked_column, stacked_area).', |
| 52 |
), |
| 53 |
'selected_columns' => array( |
| 54 |
'type' => 'array', |
| 55 |
'description' => 'Column orig_headers to include in the chart. First column is typically the X-axis label.', |
| 56 |
'items' => array( 'type' => 'string' ), |
| 57 |
), |
| 58 |
'width' => array( |
| 59 |
'type' => 'integer', |
| 60 |
'description' => 'Chart width in pixels (default 400).', |
| 61 |
), |
| 62 |
'height' => array( |
| 63 |
'type' => 'integer', |
| 64 |
'description' => 'Chart height in pixels (default 400).', |
| 65 |
), |
| 66 |
'responsive_width' => array( |
| 67 |
'type' => 'boolean', |
| 68 |
'description' => 'Use responsive width (default true).', |
| 69 |
), |
| 70 |
'follow_filtering' => array( |
| 71 |
'type' => 'boolean', |
| 72 |
'description' => 'Chart reacts to table front-end filters (default false).', |
| 73 |
), |
| 74 |
'group_chart' => array( |
| 75 |
'type' => 'boolean', |
| 76 |
'description' => 'Group rows with the same label (default false).', |
| 77 |
), |
| 78 |
), |
| 79 |
'required' => array( 'title', 'wpdatatable_id', 'engine', 'type', 'selected_columns' ), |
| 80 |
), |
| 81 |
|
| 82 |
'output_schema' => array( |
| 83 |
'type' => 'object', |
| 84 |
'properties' => array( |
| 85 |
'chart_id' => array( 'type' => 'integer', 'description' => 'Created chart ID.' ), |
| 86 |
'shortcode' => array( 'type' => 'string', 'description' => 'WordPress shortcode to embed the chart.' ), |
| 87 |
), |
| 88 |
), |
| 89 |
|
| 90 |
'execute_callback' => 'wdtmcp_execute_create_chart', |
| 91 |
|
| 92 |
'permission_callback' => function () { |
| 93 |
return current_user_can( 'manage_options' ); |
| 94 |
}, |
| 95 |
|
| 96 |
'meta' => array( |
| 97 |
'annotations' => array( |
| 98 |
'instructions' => __( 'Call get-system-info for available engines, then get-table-info for column orig_header values. Requires wpdatatable_id, engine, type, and selected_columns.', 'wpdatatables' ), |
| 99 |
'readonly' => false, |
| 100 |
'destructive' => false, |
| 101 |
'idempotent' => false, |
| 102 |
), |
| 103 |
), |
| 104 |
) |
| 105 |
); |
| 106 |
} |
| 107 |
|
| 108 |
/** |
| 109 |
* Map a generic chart type to the engine-specific type used by wpDataTables. |
| 110 |
* |
| 111 |
* @param string $engine Chart engine key. |
| 112 |
* @param string $type Generic or engine-specific chart type. |
| 113 |
* @return string |
| 114 |
*/ |
| 115 |
function wdtmcp_resolve_chart_type( $engine, $type ) { |
| 116 |
$engine = strtolower( sanitize_text_field( $engine ) ); |
| 117 |
$type = strtolower( sanitize_text_field( $type ) ); |
| 118 |
|
| 119 |
if ( '' === $type ) { |
| 120 |
return ''; |
| 121 |
} |
| 122 |
|
| 123 |
if ( strpos( $type, $engine . '_' ) === 0 ) { |
| 124 |
return $type; |
| 125 |
} |
| 126 |
|
| 127 |
$aliases = array( |
| 128 |
'google' => array( |
| 129 |
'line' => 'google_line_chart', |
| 130 |
'area' => 'google_area_chart', |
| 131 |
'stepped_area' => 'google_stepped_area_chart', |
| 132 |
'column' => 'google_column_chart', |
| 133 |
'histogram' => 'google_histogram', |
| 134 |
'bar' => 'google_bar_chart', |
| 135 |
'stacked_bar' => 'google_stacked_bar_chart', |
| 136 |
'pie' => 'google_pie_chart', |
| 137 |
'donut' => 'google_donut_chart', |
| 138 |
'bubble' => 'google_bubble_chart', |
| 139 |
'scatter' => 'google_scatter_chart', |
| 140 |
'gauge' => 'google_gauge_chart', |
| 141 |
'candlestick' => 'google_candlestick_chart', |
| 142 |
'waterfall' => 'google_waterfall_chart', |
| 143 |
'geo' => 'google_geo_chart', |
| 144 |
), |
| 145 |
'chartjs' => array( |
| 146 |
'line' => 'chartjs_line_chart', |
| 147 |
'bar' => 'chartjs_bar_chart', |
| 148 |
'pie' => 'chartjs_pie_chart', |
| 149 |
'doughnut'=> 'chartjs_doughnut_chart', |
| 150 |
'donut' => 'chartjs_doughnut_chart', |
| 151 |
'radar' => 'chartjs_radar_chart', |
| 152 |
'polar' => 'chartjs_polar_area_chart', |
| 153 |
'bubble' => 'chartjs_bubble_chart', |
| 154 |
'scatter' => 'chartjs_scatter_chart', |
| 155 |
), |
| 156 |
'highcharts' => array( |
| 157 |
'line' => 'highcharts_line_chart', |
| 158 |
'area' => 'highcharts_area_chart', |
| 159 |
'column' => 'highcharts_column_chart', |
| 160 |
'bar' => 'highcharts_basic_bar_chart', |
| 161 |
'stacked_bar' => 'highcharts_stacked_bar_chart', |
| 162 |
'stacked_column' => 'highcharts_stacked_column_chart', |
| 163 |
'pie' => 'highcharts_pie_chart', |
| 164 |
'donut' => 'highcharts_donut_chart', |
| 165 |
'scatter' => 'highcharts_scatter_plot', |
| 166 |
), |
| 167 |
'apexcharts' => array( |
| 168 |
'line' => 'apexcharts_basic_line_chart', |
| 169 |
'area' => 'apexcharts_basic_area_chart', |
| 170 |
'column' => 'apexcharts_column_chart', |
| 171 |
'bar' => 'apexcharts_stacked_bar_chart', |
| 172 |
'stacked_bar' => 'apexcharts_stacked_bar_chart', |
| 173 |
'stacked_column' => 'apexcharts_stacked_column_chart', |
| 174 |
'pie' => 'apexcharts_pie_chart', |
| 175 |
'donut' => 'apexcharts_donut_chart', |
| 176 |
'radar' => 'apexcharts_radar_chart', |
| 177 |
'scatter' => 'apexcharts_scatter_chart', |
| 178 |
), |
| 179 |
); |
| 180 |
|
| 181 |
if ( isset( $aliases[ $engine ][ $type ] ) ) { |
| 182 |
return $aliases[ $engine ][ $type ]; |
| 183 |
} |
| 184 |
|
| 185 |
if ( substr( $type, -6 ) === '_chart' ) { |
| 186 |
return $engine . '_' . $type; |
| 187 |
} |
| 188 |
|
| 189 |
return $engine . '_' . $type . '_chart'; |
| 190 |
} |
| 191 |
|
| 192 |
/** |
| 193 |
* Execute callback for wpdatatables/create-chart. |
| 194 |
* |
| 195 |
* @param array $input |
| 196 |
* @return array|WP_Error |
| 197 |
*/ |
| 198 |
function wdtmcp_execute_create_chart( $input ) { |
| 199 |
global $wpdb; |
| 200 |
|
| 201 |
if ( ! class_exists( 'WPDataChart' ) || ! class_exists( 'WPDataTable' ) ) { |
| 202 |
return new \WP_Error( |
| 203 |
'wdtmcp_missing_class', |
| 204 |
__( 'wpDataTables core classes WPDataChart and WPDataTable are required.', 'wpdatatables' ) |
| 205 |
); |
| 206 |
} |
| 207 |
|
| 208 |
$title = isset( $input['title'] ) ? sanitize_text_field( $input['title'] ) : ''; |
| 209 |
$wpdatatable_id = isset( $input['wpdatatable_id'] ) ? (int) $input['wpdatatable_id'] : 0; |
| 210 |
$engine = isset( $input['engine'] ) ? strtolower( sanitize_text_field( $input['engine'] ) ) : ''; |
| 211 |
$type = isset( $input['type'] ) ? sanitize_text_field( $input['type'] ) : ''; |
| 212 |
$selected_columns = isset( $input['selected_columns'] ) && is_array( $input['selected_columns'] ) |
| 213 |
? array_values( array_map( 'sanitize_text_field', $input['selected_columns'] ) ) |
| 214 |
: array(); |
| 215 |
|
| 216 |
$allowed_engines = array( 'google', 'chartjs' ); |
| 217 |
|
| 218 |
if ( '' === $title ) { |
| 219 |
return new \WP_Error( 'wdtmcp_invalid_input', __( 'A chart title is required.', 'wpdatatables' ) ); |
| 220 |
} |
| 221 |
|
| 222 |
if ( $wpdatatable_id <= 0 ) { |
| 223 |
return new \WP_Error( 'wdtmcp_invalid_input', __( 'A valid wpdatatable_id is required.', 'wpdatatables' ) ); |
| 224 |
} |
| 225 |
|
| 226 |
if ( ! in_array( $engine, $allowed_engines, true ) ) { |
| 227 |
return new \WP_Error( |
| 228 |
'wdtmcp_invalid_input', |
| 229 |
sprintf( |
| 230 |
/* translators: %s: comma-separated engine list */ |
| 231 |
__( 'Engine must be one of: %s.', 'wpdatatables' ), |
| 232 |
implode( ', ', $allowed_engines ) |
| 233 |
) |
| 234 |
); |
| 235 |
} |
| 236 |
|
| 237 |
$engine_license = wdtmcp_assert_chart_engine_available( $engine, 'wpdatatables/create-chart' ); |
| 238 |
if ( is_wp_error( $engine_license ) ) { |
| 239 |
return $engine_license; |
| 240 |
} |
| 241 |
|
| 242 |
if ( '' === $type ) { |
| 243 |
return new \WP_Error( 'wdtmcp_invalid_input', __( 'A chart type is required.', 'wpdatatables' ) ); |
| 244 |
} |
| 245 |
|
| 246 |
if ( count( $selected_columns ) < 2 ) { |
| 247 |
return new \WP_Error( |
| 248 |
'wdtmcp_invalid_input', |
| 249 |
__( 'At least two selected_columns are required (label column and value column).', 'wpdatatables' ) |
| 250 |
); |
| 251 |
} |
| 252 |
|
| 253 |
$table_exists = (int) $wpdb->get_var( |
| 254 |
$wpdb->prepare( |
| 255 |
'SELECT COUNT(*) FROM ' . $wpdb->prefix . 'wpdatatables WHERE id = %d', |
| 256 |
$wpdatatable_id |
| 257 |
) |
| 258 |
); |
| 259 |
|
| 260 |
if ( $table_exists < 1 ) { |
| 261 |
return new \WP_Error( |
| 262 |
'wdtmcp_not_found', |
| 263 |
sprintf( __( 'Table with ID %d was not found.', 'wpdatatables' ), $wpdatatable_id ) |
| 264 |
); |
| 265 |
} |
| 266 |
|
| 267 |
try { |
| 268 |
$table = \WPDataTable::loadWpDataTable( $wpdatatable_id, null, true ); |
| 269 |
} catch ( \Exception $e ) { |
| 270 |
return new \WP_Error( |
| 271 |
'wdtmcp_table_load_failed', |
| 272 |
sprintf( __( 'Could not load source table: %s', 'wpdatatables' ), $e->getMessage() ) |
| 273 |
); |
| 274 |
} |
| 275 |
|
| 276 |
$available_headers = array(); |
| 277 |
foreach ( $table->getColumns() as $column ) { |
| 278 |
$available_headers[] = $column->getOriginalHeader(); |
| 279 |
} |
| 280 |
|
| 281 |
foreach ( $selected_columns as $column_header ) { |
| 282 |
if ( ! in_array( $column_header, $available_headers, true ) ) { |
| 283 |
return new \WP_Error( |
| 284 |
'wdtmcp_invalid_input', |
| 285 |
sprintf( |
| 286 |
/* translators: 1: column name, 2: table id */ |
| 287 |
__( 'Column "%1$s" was not found on table %2$d.', 'wpdatatables' ), |
| 288 |
$column_header, |
| 289 |
$wpdatatable_id |
| 290 |
) |
| 291 |
); |
| 292 |
} |
| 293 |
} |
| 294 |
|
| 295 |
$resolved_type = wdtmcp_resolve_chart_type( $engine, $type ); |
| 296 |
|
| 297 |
$chart_data = array( |
| 298 |
'title' => $title, |
| 299 |
'wpdatatable_id' => $wpdatatable_id, |
| 300 |
'engine' => $engine, |
| 301 |
'type' => $resolved_type, |
| 302 |
'selected_columns' => $selected_columns, |
| 303 |
'range_type' => 'all_rows', |
| 304 |
'follow_filtering' => isset( $input['follow_filtering'] ) ? (bool) $input['follow_filtering'] : false, |
| 305 |
'width' => isset( $input['width'] ) ? (int) $input['width'] : 400, |
| 306 |
'height' => isset( $input['height'] ) ? (int) $input['height'] : 400, |
| 307 |
'responsive_width' => isset( $input['responsive_width'] ) ? (bool) $input['responsive_width'] : true, |
| 308 |
'group_chart' => isset( $input['group_chart'] ) ? (bool) $input['group_chart'] : false, |
| 309 |
'show_grid' => true, |
| 310 |
'show_title' => true, |
| 311 |
'tooltip_enabled' => true, |
| 312 |
'background_color' => '#FFFFFF', |
| 313 |
'loader' => (bool) get_option( 'wdtGlobalChartLoader' ), |
| 314 |
); |
| 315 |
|
| 316 |
try { |
| 317 |
$chart = \WPDataChart::build( $chart_data ); |
| 318 |
if ( function_exists( 'wdtmcp_chart_save_persisting_table_rows' ) ) { |
| 319 |
wdtmcp_chart_save_persisting_table_rows( $chart ); |
| 320 |
} else { |
| 321 |
$chart->save(); |
| 322 |
} |
| 323 |
} catch ( \Exception $e ) { |
| 324 |
return new \WP_Error( |
| 325 |
'wdtmcp_chart_create_failed', |
| 326 |
sprintf( __( 'Failed to create chart: %s', 'wpdatatables' ), $e->getMessage() ) |
| 327 |
); |
| 328 |
} |
| 329 |
|
| 330 |
$chart_id = (int) $chart->getId(); |
| 331 |
if ( $chart_id <= 0 ) { |
| 332 |
return new \WP_Error( 'wdtmcp_chart_create_failed', __( 'Chart was not saved.', 'wpdatatables' ) ); |
| 333 |
} |
| 334 |
|
| 335 |
return array( |
| 336 |
'chart_id' => $chart_id, |
| 337 |
'shortcode' => $chart->getShortCode(), |
| 338 |
); |
| 339 |
} |
| 340 |
|