diff --git a/Gruntfile.js b/Gruntfile.js index e6fb298e..56775154 100755 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -18,6 +18,12 @@ module.exports = function (grunt) { 'package.json' ] }, + readmetxt: { + options: { + prefix: 'Stable tag:\\s*', + }, + src: [ 'readme.txt' ], + }, style: { options: { prefix: 'Version\\:\\s' diff --git a/classes/Visualizer/Module/Abilities.php b/classes/Visualizer/Module/Abilities.php new file mode 100644 index 00000000..54bb221a --- /dev/null +++ b/classes/Visualizer/Module/Abilities.php @@ -0,0 +1,1707 @@ +_addAction( 'wp_abilities_api_categories_init', 'registerCategory' ); + $this->_addAction( 'wp_abilities_api_init', 'registerAbilities' ); + } + + /** + * Registers the ability category. + * + * @access public + * @return void + */ + public function registerCategory() { + if ( ! function_exists( 'wp_register_ability_category' ) ) { + return; + } + + wp_register_ability_category( + self::CATEGORY, + array( + 'label' => __( 'Visualizer', 'visualizer' ), + 'description' => __( 'Create, read and refresh Visualizer charts and tables.', 'visualizer' ), + ) + ); + } + + /** + * Registers the abilities. + * + * @access public + * @return void + */ + public function registerAbilities() { + if ( ! function_exists( 'wp_register_ability' ) ) { + return; + } + + $column_schema = array( + 'type' => 'object', + 'properties' => array( + 'name' => array( + 'type' => 'string', + 'description' => __( 'Column label.', 'visualizer' ), + ), + 'type' => array( + 'type' => 'string', + 'description' => __( 'Column data type.', 'visualizer' ), + 'enum' => Visualizer_Source::getAllowedTypes(), + ), + ), + 'required' => array( 'name', 'type' ), + ); + + $embed_schema = array( + 'type' => 'object', + 'properties' => array( + 'shortcode' => array( 'type' => 'string' ), + 'block' => array( 'type' => 'string' ), + ), + ); + + $rows_schema = array( + 'type' => 'array', + 'description' => __( 'Data rows. Each row is an array of cell values in column order.', 'visualizer' ), + 'items' => array( + 'type' => 'array', + ), + ); + + $preview_schema = array( + 'type' => 'object', + 'properties' => array( + 'columns' => array( + 'type' => 'array', + 'items' => $column_schema, + ), + 'rows' => $rows_schema, + 'row_count' => array( 'type' => 'integer' ), + ), + ); + + wp_register_ability( + 'visualizer/list-charts', + array( + 'label' => __( 'List charts', 'visualizer' ), + 'description' => __( 'Lists the Visualizer charts the current user can manage, with their type, library, data source and shortcode.', 'visualizer' ), + 'category' => self::CATEGORY, + 'input_schema' => array( + 'type' => 'object', + 'default' => array(), + 'properties' => array( + 'page' => array( + 'type' => 'integer', + 'description' => __( 'Page of results.', 'visualizer' ), + 'default' => 1, + 'minimum' => 1, + ), + 'per_page' => array( + 'type' => 'integer', + 'description' => __( 'Charts per page.', 'visualizer' ), + 'default' => 20, + 'minimum' => 1, + 'maximum' => self::MAX_PER_PAGE, + ), + 'type' => array( + 'type' => 'string', + 'description' => __( 'Only return charts of this type (for example pie, line, bar, tabular).', 'visualizer' ), + ), + 'search' => array( + 'type' => 'string', + 'description' => __( 'Search term matched against the chart settings (title), like the chart library search.', 'visualizer' ), + ), + ), + ), + 'output_schema' => array( + 'type' => 'object', + 'properties' => array( + 'charts' => array( + 'type' => 'array', + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( 'type' => 'integer' ), + 'title' => array( 'type' => 'string' ), + 'type' => array( 'type' => 'string' ), + 'library' => array( 'type' => 'string' ), + 'source' => array( 'type' => 'string' ), + 'modified' => array( 'type' => 'string' ), + 'shortcode' => array( 'type' => 'string' ), + ), + ), + ), + 'total' => array( 'type' => 'integer' ), + 'total_pages' => array( 'type' => 'integer' ), + ), + ), + 'execute_callback' => array( $this, 'executeListCharts' ), + 'permission_callback' => array( $this, 'canManageCharts' ), + 'meta' => array( + 'annotations' => array( + 'readonly' => true, + 'destructive' => false, + 'idempotent' => true, + ), + 'show_in_rest' => true, + ), + ) + ); + + wp_register_ability( + 'visualizer/get-chart', + array( + 'label' => __( 'Get chart', 'visualizer' ), + 'description' => __( 'Returns the type, columns, rows, options, data source and embed markup (shortcode and block) of a chart. Source credentials are never returned.', 'visualizer' ), + 'category' => self::CATEGORY, + 'input_schema' => array( + 'type' => 'object', + 'properties' => array( + 'chart_id' => array( + 'type' => 'integer', + 'description' => __( 'The chart ID.', 'visualizer' ), + 'minimum' => 1, + ), + ), + 'required' => array( 'chart_id' ), + ), + 'output_schema' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( 'type' => 'integer' ), + 'title' => array( 'type' => 'string' ), + 'type' => array( 'type' => 'string' ), + 'library' => array( 'type' => 'string' ), + 'columns' => array( + 'type' => 'array', + 'items' => $column_schema, + ), + 'rows' => $rows_schema, + 'options' => array( + 'type' => 'object', + 'additionalProperties' => true, + ), + 'source' => array( + 'type' => 'object', + 'additionalProperties' => true, + ), + 'embed' => $embed_schema, + ), + ), + 'execute_callback' => array( $this, 'executeGetChart' ), + 'permission_callback' => array( $this, 'canEditChartInput' ), + 'meta' => array( + 'annotations' => array( + 'readonly' => true, + 'destructive' => false, + 'idempotent' => true, + ), + 'show_in_rest' => true, + ), + ) + ); + + wp_register_ability( + 'visualizer/upsert-chart', + array( + 'label' => __( 'Create or update chart', 'visualizer' ), + 'description' => __( 'Creates a chart (no chart_id) or updates one (chart_id given) from typed columns, rows and options. With dry_run=true nothing is saved and the parsed data preview is returned. Chart types that need a higher plan are rejected.', 'visualizer' ), + 'category' => self::CATEGORY, + 'input_schema' => array( + 'type' => 'object', + 'properties' => array( + 'chart_id' => array( + 'type' => 'integer', + 'description' => __( 'The chart to update. Omit to create a new chart.', 'visualizer' ), + 'minimum' => 1, + ), + 'title' => array( + 'type' => 'string', + 'description' => __( 'Chart name shown in the chart library.', 'visualizer' ), + ), + 'type' => array( + 'type' => 'string', + 'description' => __( 'Chart type, for example pie, line, bar, column, area, tabular. Required when creating.', 'visualizer' ), + ), + 'library' => array( + 'type' => 'string', + 'description' => __( 'Rendering library. Defaults to the first library the chart type supports.', 'visualizer' ), + 'enum' => array( 'GoogleCharts', 'ChartJS', 'DataTable' ), + ), + 'columns' => array( + 'type' => 'array', + 'description' => __( 'Typed columns. Required when creating; when updating, send together with rows to replace the chart data.', 'visualizer' ), + 'items' => $column_schema, + ), + 'rows' => $rows_schema, + 'options' => array( + 'type' => 'object', + 'description' => __( 'Chart settings merged over the existing ones (for example title, legend, colors).', 'visualizer' ), + 'additionalProperties' => true, + ), + 'dry_run' => array( + 'type' => 'boolean', + 'description' => __( 'Validate and return the parsed data preview without saving.', 'visualizer' ), + 'default' => false, + ), + ), + ), + 'output_schema' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( 'type' => 'integer' ), + 'created' => array( 'type' => 'boolean' ), + 'embed' => $embed_schema, + 'dry_run' => array( 'type' => 'boolean' ), + 'valid' => array( 'type' => 'boolean' ), + 'parsed_preview' => $preview_schema, + 'errors' => array( + 'type' => 'array', + 'items' => array( 'type' => 'string' ), + ), + ), + ), + 'execute_callback' => array( $this, 'executeUpsertChart' ), + 'permission_callback' => array( $this, 'canUpsertChart' ), + 'meta' => array( + 'annotations' => array( + 'readonly' => false, + 'destructive' => false, + 'idempotent' => true, + ), + 'show_in_rest' => true, + ), + ) + ); + + wp_register_ability( + 'visualizer/set-data-source', + array( + 'label' => __( 'Set chart data source', 'visualizer' ), + 'description' => __( 'Points a chart at a remote CSV/XLSX file, a JSON endpoint or a WordPress database query, imports the data and optionally saves a refresh interval. Requires a plan that includes the chosen source. With dry_run=true nothing is saved and the fetched preview is returned.', 'visualizer' ), + 'category' => self::CATEGORY, + 'input_schema' => array( + 'type' => 'object', + 'properties' => array( + 'chart_id' => array( + 'type' => 'integer', + 'description' => __( 'The chart ID.', 'visualizer' ), + 'minimum' => 1, + ), + 'source' => array( + 'type' => 'string', + 'description' => __( 'Source kind.', 'visualizer' ), + 'enum' => array( 'csv_url', 'json', 'db_query' ), + ), + 'url' => array( + 'type' => 'string', + 'description' => __( 'URL of the CSV/XLSX file (csv_url) or of the JSON endpoint (json). The CSV must have the labels in row 1 and the data types in row 2.', 'visualizer' ), + ), + 'json_root' => array( + 'type' => 'string', + 'description' => __( 'Root node that holds the rows, in the format the chart editor uses: "root" for the top level, "root>data>results" for nested nodes. Run with dry_run=true and no mapping to list the available roots.', 'visualizer' ), + ), + 'json_method' => array( + 'type' => 'string', + 'enum' => array( 'GET', 'POST' ), + 'default' => 'GET', + ), + 'json_paging' => array( + 'type' => 'string', + 'description' => __( 'Node that holds the next page URL, same format as json_root.', 'visualizer' ), + ), + 'mapping' => array( + 'type' => 'array', + 'description' => __( 'JSON sources only: the JSON keys to use as columns, in order, with their data type. Run with dry_run=true first to see the available keys.', 'visualizer' ), + 'items' => $column_schema, + ), + 'query' => array( + 'type' => 'string', + 'description' => __( 'db_query sources only: a single SELECT statement run against the WordPress database.', 'visualizer' ), + ), + 'refresh_interval' => array( + 'type' => 'number', + 'description' => __( 'Hours between automatic refreshes. -1 imports once. Only the intervals offered by the chart editor for the active plan are accepted.', 'visualizer' ), + 'default' => -1, + ), + 'dry_run' => array( + 'type' => 'boolean', + 'default' => false, + ), + ), + 'required' => array( 'chart_id', 'source' ), + ), + 'output_schema' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( 'type' => 'integer' ), + 'source' => array( 'type' => 'string' ), + 'refresh_interval' => array( 'type' => 'number' ), + 'dry_run' => array( 'type' => 'boolean' ), + 'available_roots' => array( + 'type' => 'array', + 'items' => array( 'type' => 'string' ), + ), + 'available_keys' => array( + 'type' => 'array', + 'items' => array( 'type' => 'string' ), + ), + 'parsed_preview' => $preview_schema, + 'embed' => $embed_schema, + ), + ), + 'execute_callback' => array( $this, 'executeSetDataSource' ), + 'permission_callback' => array( $this, 'canSetDataSource' ), + 'meta' => array( + 'annotations' => array( + 'readonly' => false, + 'destructive' => false, + 'idempotent' => true, + ), + 'show_in_rest' => true, + ), + ) + ); + + wp_register_ability( + 'visualizer/refresh-chart-data', + array( + 'label' => __( 'Refresh chart data', 'visualizer' ), + 'description' => __( 'Fetches the current data for a chart from its saved remote CSV/XLSX, JSON or database source and stores it.', 'visualizer' ), + 'category' => self::CATEGORY, + 'input_schema' => array( + 'type' => 'object', + 'properties' => array( + 'chart_id' => array( + 'type' => 'integer', + 'description' => __( 'The chart ID.', 'visualizer' ), + 'minimum' => 1, + ), + ), + 'required' => array( 'chart_id' ), + ), + 'output_schema' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( 'type' => 'integer' ), + 'source' => array( 'type' => 'string' ), + 'refreshed' => array( 'type' => 'boolean' ), + 'row_count' => array( 'type' => 'integer' ), + ), + ), + 'execute_callback' => array( $this, 'executeRefreshChartData' ), + 'permission_callback' => array( $this, 'canEditChartInput' ), + 'meta' => array( + 'annotations' => array( + 'readonly' => false, + 'destructive' => false, + 'idempotent' => true, + ), + 'show_in_rest' => true, + ), + ) + ); + } + + // ------------------------------------------------------------------------- + // Permissions + // ------------------------------------------------------------------------- + + /** + * Same capability as the chart library page and the charts list endpoint. + * + * @access public + * @return bool + */ + public function canManageCharts() { + return current_user_can( 'edit_posts' ); + } + + /** + * Same checks as the chart editor: edit_posts plus Visualizer_Module::can_edit_chart(). + * + * @access public + * + * @param mixed $input The ability input. + * @return bool + */ + public function canEditChartInput( $input = array() ) { + if ( ! current_user_can( 'edit_posts' ) ) { + return false; + } + + $chart_id = $this->getChartId( $input ); + + // A missing or invalid ID is reported by the execute callback. + return ! $chart_id || ! $this->getChart( $chart_id ) || self::can_edit_chart( $chart_id ); + } + + /** + * Creating needs edit_posts; updating additionally needs can_edit_chart(). + * + * @access public + * + * @param mixed $input The ability input. + * @return bool + */ + public function canUpsertChart( $input = array() ) { + return $this->canEditChartInput( $input ); + } + + /** + * Chart editor checks, plus the database import checks for SQL sources. + * + * @access public + * + * @param mixed $input The ability input. + * @return bool + */ + public function canSetDataSource( $input = array() ) { + if ( ! $this->canEditChartInput( $input ) ) { + return false; + } + + if ( is_array( $input ) && isset( $input['source'] ) && 'db_query' === $input['source'] ) { + // Same as Visualizer_Module_Chart::saveQuery(). + return current_user_can( 'administrator' ) && is_super_admin(); + } + + return true; + } + + // ------------------------------------------------------------------------- + // Execute callbacks + // ------------------------------------------------------------------------- + + /** + * Lists charts. + * + * @access public + * + * @param mixed $input The ability input. + * @return array|WP_Error + */ + public function executeListCharts( $input = array() ) { + $input = is_array( $input ) ? $input : array(); + $page = isset( $input['page'] ) ? max( 1, absint( $input['page'] ) ) : 1; + $per_page = isset( $input['per_page'] ) ? absint( $input['per_page'] ) : 20; + $per_page = min( self::MAX_PER_PAGE, max( 1, $per_page ) ); + + $query_args = array( + 'post_type' => Visualizer_Plugin::CPT_VISUALIZER, + 'post_status' => 'publish', + 'posts_per_page' => $per_page, + 'paged' => $page, + ); + + // Same restriction as Visualizer_Module_Chart::getCharts(). + if ( ! current_user_can( 'edit_others_posts' ) ) { + $query_args['author'] = get_current_user_id(); + } + + $meta_query = array(); + if ( ! empty( $input['type'] ) ) { + $type = sanitize_text_field( $input['type'] ); + if ( ! in_array( $type, Visualizer_Plugin::getChartTypes(), true ) ) { + return new WP_Error( 'visualizer_invalid_type', __( 'Unknown chart type.', 'visualizer' ) ); + } + $meta_query[] = array( + 'key' => Visualizer_Plugin::CF_CHART_TYPE, + 'value' => $type, + 'compare' => '=', + ); + } + if ( ! empty( $input['search'] ) ) { + $meta_query[] = array( + 'key' => Visualizer_Plugin::CF_SETTINGS, + 'value' => sanitize_text_field( $input['search'] ), + 'compare' => 'LIKE', + ); + } + if ( $meta_query ) { + $query_args['meta_query'] = $meta_query; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + } + + $query = new WP_Query( apply_filters( 'visualizer_query_args', $query_args ) ); + $charts = array(); + foreach ( $query->posts as $chart ) { + if ( ! $chart instanceof WP_Post ) { + continue; + } + $charts[] = array( + 'id' => $chart->ID, + 'title' => $this->getChartTitle( $chart ), + 'type' => (string) get_post_meta( $chart->ID, Visualizer_Plugin::CF_CHART_TYPE, true ), + 'library' => (string) get_post_meta( $chart->ID, Visualizer_Plugin::CF_CHART_LIBRARY, true ), + 'source' => $this->getSourceKind( $chart->ID ), + 'modified' => (string) $chart->post_modified_gmt, + 'shortcode' => $this->getShortcode( $chart->ID ), + ); + } + + return array( + 'charts' => $charts, + 'total' => (int) $query->found_posts, + 'total_pages' => (int) $query->max_num_pages, + ); + } + + /** + * Returns one chart. + * + * @access public + * + * @param mixed $input The ability input. + * @return array|WP_Error + */ + public function executeGetChart( $input = array() ) { + $chart = $this->getEditableChart( $input ); + if ( is_wp_error( $chart ) ) { + return $chart; + } + + $type = (string) get_post_meta( $chart->ID, Visualizer_Plugin::CF_CHART_TYPE, true ); + $series = apply_filters( Visualizer_Plugin::FILTER_GET_CHART_SERIES, get_post_meta( $chart->ID, Visualizer_Plugin::CF_SERIES, true ), $chart->ID, $type ); + $data = self::get_chart_data( $chart, $type ); + $settings = get_post_meta( $chart->ID, Visualizer_Plugin::CF_SETTINGS, true ); + $settings = apply_filters( Visualizer_Plugin::FILTER_GET_CHART_SETTINGS, $settings, $chart->ID, $type ); + if ( ! is_array( $settings ) ) { + $settings = array(); + } + // The chart preview image is a large data URI, not an option. + unset( $settings['chart-img'] ); + + return array( + 'id' => $chart->ID, + 'title' => $this->getChartTitle( $chart ), + 'type' => $type, + 'library' => (string) get_post_meta( $chart->ID, Visualizer_Plugin::CF_CHART_LIBRARY, true ), + 'columns' => $this->seriesToColumns( $series ), + 'rows' => $this->normalizeRows( $data ), + 'options' => $settings, + 'source' => $this->describeSource( $chart->ID ), + 'embed' => $this->getEmbed( $chart->ID ), + ); + } + + /** + * Creates or updates a chart from typed columns and rows. + * + * @access public + * + * @param mixed $input The ability input. + * @return array|WP_Error + */ + public function executeUpsertChart( $input = array() ) { + $input = is_array( $input ) ? $input : array(); + $dry_run = ! empty( $input['dry_run'] ); + $chart = null; + + if ( ! empty( $input['chart_id'] ) ) { + $chart = $this->getEditableChart( $input ); + if ( is_wp_error( $chart ) ) { + return $chart; + } + } + + $has_data = isset( $input['columns'] ) || isset( $input['rows'] ); + if ( ! $chart && ! $has_data ) { + return new WP_Error( 'visualizer_missing_data', __( 'columns and rows are required to create a chart.', 'visualizer' ) ); + } + + // Chart type and library, gated like the chart type page. + $type = $chart ? (string) get_post_meta( $chart->ID, Visualizer_Plugin::CF_CHART_TYPE, true ) : ''; + if ( isset( $input['type'] ) && '' !== $input['type'] ) { + $type = sanitize_text_field( $input['type'] ); + } + if ( '' === $type ) { + return new WP_Error( 'visualizer_missing_type', __( 'type is required to create a chart.', 'visualizer' ) ); + } + + $type_changed = ! $chart || isset( $input['type'] ) || isset( $input['library'] ); + $library = $chart ? (string) get_post_meta( $chart->ID, Visualizer_Plugin::CF_CHART_LIBRARY, true ) : ''; + if ( $type_changed ) { + $library = $this->resolveLibrary( $type, isset( $input['library'] ) ? (string) $input['library'] : ( isset( $input['type'] ) ? '' : $library ) ); + if ( is_wp_error( $library ) ) { + return $library; + } + } + + $source = null; + if ( $has_data ) { + $source = $this->buildManualSource( $input ); + if ( is_wp_error( $source ) ) { + if ( $dry_run ) { + return array( + 'dry_run' => true, + 'valid' => false, + 'errors' => $source->get_error_messages(), + ); + } + return $source; + } + } + + $options = array(); + if ( isset( $input['options'] ) ) { + if ( ! is_array( $input['options'] ) ) { + return new WP_Error( 'visualizer_invalid_options', __( 'options must be an object.', 'visualizer' ) ); + } + // Same sanitization as the settings form (Visualizer_Module_Chart::sanitizeSettings()). + $options = map_deep( $input['options'], 'sanitize_textarea_field' ); + unset( $options['chart-img'] ); + } + + if ( $dry_run ) { + $result = array( + 'dry_run' => true, + 'valid' => true, + 'errors' => array(), + ); + if ( $source ) { + $result['parsed_preview'] = $this->getPreview( $source->getSeries(), $source->getRawData() ); + } + return $result; + } + + $created = false; + $this->disableRevisionsTemporarily(); + + if ( ! $chart ) { + // Same defaults as the chart creation flow in Visualizer_Module_Chart::renderChartPages(). + $chart_id = wp_insert_post( + array( + 'post_type' => Visualizer_Plugin::CPT_VISUALIZER, + 'post_title' => 'Visualization', + 'post_author' => get_current_user_id(), + 'post_status' => 'auto-draft', + 'post_content' => $source->getData(), + ), + true + ); + if ( is_wp_error( $chart_id ) ) { + return $chart_id; + } + + add_post_meta( $chart_id, Visualizer_Plugin::CF_CHART_TYPE, $type ); + add_post_meta( $chart_id, Visualizer_Plugin::CF_DEFAULT_DATA, 0 ); + add_post_meta( $chart_id, Visualizer_Plugin::CF_SOURCE, $source->getSourceName() ); + add_post_meta( $chart_id, Visualizer_Plugin::CF_SERIES, $source->getSeries() ); + add_post_meta( $chart_id, Visualizer_Plugin::CF_CHART_LIBRARY, $library ); + add_post_meta( $chart_id, Visualizer_Plugin::CF_SETTINGS, array( 'focusTarget' => 'datum' ) ); + + do_action( 'visualizer_pro_new_chart_defaults', $chart_id ); + + $chart = get_post( $chart_id ); + if ( ! $chart instanceof WP_Post ) { + return new WP_Error( 'visualizer_create_failed', __( 'The chart could not be created.', 'visualizer' ) ); + } + Visualizer_Module_Utility::set_defaults( $chart ); + $created = true; + } else { + if ( $type_changed ) { + update_post_meta( $chart->ID, Visualizer_Plugin::CF_CHART_TYPE, $type ); + update_post_meta( $chart->ID, Visualizer_Plugin::CF_CHART_LIBRARY, $library ); + } + if ( $source ) { + $this->clearSourceMeta( $chart->ID, true ); + $this->persistSource( $chart->ID, $source ); + } + } + + // Settings, stored the way the settings form stores them. + $settings = get_post_meta( $chart->ID, Visualizer_Plugin::CF_SETTINGS, true ); + if ( ! is_array( $settings ) ) { + $settings = array(); + } + $settings = array_merge( $settings, $options ); + + $post_update = array( 'ID' => $chart->ID ); + if ( isset( $input['title'] ) && '' !== trim( (string) $input['title'] ) ) { + $title = sanitize_text_field( $input['title'] ); + $settings['backend-title'] = $title; + $post_update['post_title'] = $title; + } + + $internal_title = ''; + if ( ! empty( $settings['title'] ) ) { + $internal_title = is_array( $settings['title'] ) ? ( isset( $settings['title']['text'] ) ? $settings['title']['text'] : '' ) : $settings['title']; + } + $settings['internal_title'] = '' !== (string) $internal_title ? $internal_title : $chart->ID; + if ( empty( $settings['pieResidueSliceLabel'] ) ) { + $settings['pieResidueSliceLabel'] = esc_html__( 'Other', 'visualizer' ); + } + update_post_meta( $chart->ID, Visualizer_Plugin::CF_SETTINGS, $settings ); + + if ( $created ) { + $post_update['post_status'] = 'publish'; + } + if ( count( $post_update ) > 1 ) { + wp_update_post( $post_update ); + } + + $this->clearChartCache( $chart->ID ); + + return array( + 'id' => $chart->ID, + 'created' => $created, + 'embed' => $this->getEmbed( $chart->ID ), + ); + } + + /** + * Configures a remote CSV/XLSX, JSON or database source for a chart. + * + * @access public + * + * @param mixed $input The ability input. + * @return array|WP_Error + */ + public function executeSetDataSource( $input = array() ) { + $input = is_array( $input ) ? $input : array(); + $chart = $this->getEditableChart( $input ); + if ( is_wp_error( $chart ) ) { + return $chart; + } + + $kind = isset( $input['source'] ) ? sanitize_key( $input['source'] ) : ''; + $dry_run = ! empty( $input['dry_run'] ); + + $features = array( + 'csv_url' => 'import-url', + 'json' => 'import-url', + 'db_query' => 'db-query', + ); + if ( ! isset( $features[ $kind ] ) ) { + return new WP_Error( 'visualizer_invalid_source', __( 'source must be one of csv_url, json or db_query.', 'visualizer' ) ); + } + if ( ! $this->isFeatureAvailable( $features[ $kind ] ) ) { + return $this->planError( 'visualizer_feature_unavailable', __( 'This data source is not available on the current Visualizer plan.', 'visualizer' ), $features[ $kind ] ); + } + + $schedule_types = array( + 'csv_url' => 'csv', + 'json' => 'json', + 'db_query' => 'db', + ); + $hours = $this->resolveInterval( $input, $schedule_types[ $kind ], $chart->ID ); + if ( is_wp_error( $hours ) ) { + return $hours; + } + + switch ( $kind ) { + case 'csv_url': + return $this->setCsvUrlSource( $chart, $input, $hours, $dry_run ); + case 'json': + return $this->setJsonSource( $chart, $input, $hours, $dry_run ); + default: + return $this->setQuerySource( $chart, $input, $hours, $dry_run ); + } + } + + /** + * Re-fetches the data of a chart from its saved source. + * + * @access public + * + * @param mixed $input The ability input. + * @return array|WP_Error + */ + public function executeRefreshChartData( $input = array() ) { + $chart = $this->getEditableChart( $input ); + if ( is_wp_error( $chart ) ) { + return $chart; + } + + $source_class = (string) get_post_meta( $chart->ID, Visualizer_Plugin::CF_SOURCE, true ); + + switch ( $source_class ) { + case 'Visualizer_Source_Query': + case 'Visualizer_Source_Json': + // Handled by Visualizer_Module_Setup::refresh_db_for_chart(). + delete_post_meta( $chart->ID, Visualizer_Plugin::CF_ERROR ); + apply_filters( 'visualizer_schedule_refresh_chart', $chart, $chart->ID, true ); + $error = get_post_meta( $chart->ID, Visualizer_Plugin::CF_ERROR, true ); + if ( ! empty( $error ) ) { + return new WP_Error( 'visualizer_refresh_failed', sanitize_text_field( $error ) ); + } + break; + case 'Visualizer_Source_Csv_Remote': + case 'Visualizer_Source_Xlsx_Remote': + $url = $this->getRemoteFileUrl( $chart ); + if ( '' === $url ) { + return new WP_Error( 'visualizer_no_source', __( 'The chart has no saved source URL.', 'visualizer' ) ); + } + $source = $this->fetchSource( new $source_class( $url ) ); + if ( is_wp_error( $source ) ) { + return $source; + } + $this->disableRevisionsTemporarily(); + $this->persistSource( $chart->ID, $source ); + break; + default: + return new WP_Error( 'visualizer_not_refreshable', __( 'The chart data was entered manually or uploaded as a file, so there is no source to refresh from.', 'visualizer' ) ); + } + + $this->clearChartCache( $chart->ID ); + + $chart = get_post( $chart->ID ); + $rows = $chart instanceof WP_Post ? self::get_chart_data( $chart, '' ) : array(); + + return array( + 'id' => (int) $input['chart_id'], + 'source' => $this->getSourceKind( (int) $input['chart_id'] ), + 'refreshed' => true, + 'row_count' => is_array( $rows ) ? count( $rows ) : 0, + ); + } + + // ------------------------------------------------------------------------- + // Source helpers + // ------------------------------------------------------------------------- + + /** + * Remote CSV/XLSX source, same flow as the "Import from URL" form in Visualizer_Module_Chart::uploadData(). + * + * @param WP_Post $chart The chart. + * @param array $input The ability input. + * @param float $hours The refresh interval. + * @param bool $dry_run Whether to skip saving. + * @return array|WP_Error + */ + private function setCsvUrlSource( $chart, $input, $hours, $dry_run ) { + $url = $this->validateUrl( $input ); + if ( is_wp_error( $url ) ) { + return $url; + } + + $extension = strtolower( (string) pathinfo( (string) wp_parse_url( $url, PHP_URL_PATH ), PATHINFO_EXTENSION ) ); + $source = 'xlsx' === $extension ? new Visualizer_Source_Xlsx_Remote( $url ) : new Visualizer_Source_Csv_Remote( $url ); + $source = $this->fetchSource( $source ); + if ( is_wp_error( $source ) ) { + return $source; + } + + if ( $dry_run ) { + return array( + 'id' => $chart->ID, + 'source' => 'csv_url', + 'dry_run' => true, + 'parsed_preview' => $this->getPreview( $source->getSeries(), $source->getRawData() ), + ); + } + + $this->disableRevisionsTemporarily(); + $this->clearSourceMeta( $chart->ID, $hours < 0 ); + $this->persistSource( $chart->ID, $source ); + if ( $hours >= 0 ) { + apply_filters( 'visualizer_pro_chart_schedule', $chart->ID, $url, $hours ); + } + $this->clearChartCache( $chart->ID ); + + return array( + 'id' => $chart->ID, + 'source' => 'csv_url', + 'refresh_interval' => $hours, + 'dry_run' => false, + 'embed' => $this->getEmbed( $chart->ID ), + ); + } + + /** + * JSON source, same flow as Visualizer_Module_Chart::setJsonData(). + * + * @param WP_Post $chart The chart. + * @param array $input The ability input. + * @param float $hours The refresh interval. + * @param bool $dry_run Whether to skip saving. + * @return array|WP_Error + */ + private function setJsonSource( $chart, $input, $hours, $dry_run ) { + $url = $this->validateUrl( $input ); + if ( is_wp_error( $url ) ) { + return $url; + } + + $params = array( + 'url' => $url, + 'root' => ( isset( $input['json_root'] ) && is_string( $input['json_root'] ) && '' !== trim( $input['json_root'] ) ) ? sanitize_text_field( $input['json_root'] ) : 'root', + 'method' => ( isset( $input['json_method'] ) && 'POST' === strtoupper( (string) $input['json_method'] ) ) ? 'POST' : 'GET', + ); + if ( ! empty( $input['json_paging'] ) && is_string( $input['json_paging'] ) ) { + $params['paging'] = sanitize_text_field( $input['json_paging'] ); + } + + // Credentials saved from the chart editor are reused for the same host only; they are never accepted or returned here. + $saved_headers = get_post_meta( $chart->ID, Visualizer_Plugin::CF_JSON_HEADERS, true ); + $saved_url = (string) get_post_meta( $chart->ID, Visualizer_Plugin::CF_JSON_URL, true ); + $auth = array(); + if ( is_array( $saved_headers ) && ! empty( $saved_headers['auth'] ) && '' !== $saved_url && wp_parse_url( $saved_url, PHP_URL_HOST ) === wp_parse_url( $url, PHP_URL_HOST ) && wp_parse_url( $saved_url, PHP_URL_SCHEME ) === wp_parse_url( $url, PHP_URL_SCHEME ) ) { + $auth = $saved_headers['auth']; + if ( is_array( $auth ) && isset( $auth['username'], $auth['password'] ) ) { + $params['username'] = $auth['username']; + $params['password'] = $auth['password']; + } elseif ( is_string( $auth ) ) { + $params['auth'] = $auth; + } + } + + // First pass: discover the keys available under the chosen root. + $probe = $this->fetchSource( new Visualizer_Source_Json( $params ) ); + if ( is_wp_error( $probe ) ) { + return $probe; + } + $raw = $probe->getRawData(); + $first = $raw ? reset( $raw ) : null; + $keys = is_array( $first ) ? array_map( 'strval', array_keys( $first ) ) : array(); + $mapping = isset( $input['mapping'] ) ? $input['mapping'] : array(); + if ( ! $keys && ( $mapping || ! $dry_run ) ) { + return new WP_Error( 'visualizer_fetch_failed', __( 'Unable to fetch data from the endpoint. Check the URL and the root.', 'visualizer' ) ); + } + + if ( ! $mapping ) { + if ( $dry_run ) { + $roots = ( new Visualizer_Source_Json( $params ) )->fetchRoots(); + return array( + 'id' => $chart->ID, + 'source' => 'json', + 'dry_run' => true, + 'available_roots' => is_array( $roots ) ? array_values( array_map( 'strval', $roots ) ) : array(), + 'available_keys' => $keys, + ); + } + return new WP_Error( 'visualizer_missing_mapping', __( 'mapping is required for JSON sources. Run with dry_run=true to list the available keys.', 'visualizer' ) ); + } + + $columns = $this->validateColumns( $mapping ); + if ( is_wp_error( $columns ) ) { + return $columns; + } + $params['header'] = array(); + $params['type'] = array(); + foreach ( $columns as $column ) { + if ( ! in_array( $column['name'], $keys, true ) ) { + /* translators: %s: JSON key. */ + return new WP_Error( 'visualizer_invalid_mapping', sprintf( __( 'The key "%s" does not exist in the JSON data.', 'visualizer' ), $column['name'] ) ); + } + $params['header'][] = $column['name']; + $params['type'][ $column['name'] ] = $column['type']; + } + + $source = new Visualizer_Source_Json( $params ); + try { + $source->fetchFromEditableTable(); + } catch ( Exception $e ) { + return new WP_Error( 'visualizer_fetch_failed', __( 'The data could not be parsed with the given column types.', 'visualizer' ) ); + } + if ( ! $source->getSeries() ) { + return new WP_Error( 'visualizer_invalid_mapping', __( 'No columns could be mapped.', 'visualizer' ) ); + } + + if ( $dry_run ) { + return array( + 'id' => $chart->ID, + 'source' => 'json', + 'dry_run' => true, + 'available_keys' => $keys, + 'parsed_preview' => $this->getPreview( $source->getSeries(), $source->getRawData() ), + ); + } + + $this->disableRevisionsTemporarily(); + $this->clearSourceMeta( $chart->ID, true ); + update_post_meta( $chart->ID, Visualizer_Plugin::CF_EDITABLE_TABLE, true ); + $this->persistSource( $chart->ID, $source ); + update_post_meta( $chart->ID, Visualizer_Plugin::CF_JSON_URL, $params['url'] ); + update_post_meta( $chart->ID, Visualizer_Plugin::CF_JSON_ROOT, $params['root'] ); + + $headers = array( 'method' => $params['method'] ); + if ( $auth ) { + $headers['auth'] = $auth; + } + add_post_meta( $chart->ID, Visualizer_Plugin::CF_JSON_HEADERS, $headers ); + if ( ! empty( $params['paging'] ) ) { + add_post_meta( $chart->ID, Visualizer_Plugin::CF_JSON_PAGING, $params['paging'] ); + } + + // Same as Visualizer_Module_Chart::setJsonSchedule(). + $schedules = get_option( Visualizer_Plugin::CF_JSON_SCHEDULE, array() ); + $schedules = is_array( $schedules ) ? $schedules : array(); + if ( $hours >= 0 ) { + add_post_meta( $chart->ID, Visualizer_Plugin::CF_JSON_SCHEDULE, $hours ); + $schedules[ $chart->ID ] = time() + $hours * HOUR_IN_SECONDS; + } else { + unset( $schedules[ $chart->ID ] ); + } + update_option( Visualizer_Plugin::CF_JSON_SCHEDULE, $schedules ); + + $this->clearChartCache( $chart->ID ); + + return array( + 'id' => $chart->ID, + 'source' => 'json', + 'refresh_interval' => $hours, + 'dry_run' => false, + 'embed' => $this->getEmbed( $chart->ID ), + ); + } + + /** + * WordPress database query source, same flow as Visualizer_Module_Chart::saveQuery(). + * + * @param WP_Post $chart The chart. + * @param array $input The ability input. + * @param float $hours The refresh interval. + * @param bool $dry_run Whether to skip saving. + * @return array|WP_Error + */ + private function setQuerySource( $chart, $input, $hours, $dry_run ) { + // Repeated here because the permission callback only sees the raw input. + if ( ! current_user_can( 'administrator' ) || ! is_super_admin() ) { + return new WP_Error( 'visualizer_forbidden', __( 'Action not allowed for this user.', 'visualizer' ) ); + } + + $query = isset( $input['query'] ) && is_string( $input['query'] ) ? trim( trim( $input['query'] ), ';' ) : ''; + if ( '' === $query ) { + return new WP_Error( 'visualizer_missing_query', __( 'query is required for db_query sources.', 'visualizer' ) ); + } + + // No connection parameters: the query always runs against the WordPress database. + $source = new Visualizer_Source_Query( $query, $chart->ID, array( 'db_type' => Visualizer_Plugin::WP_DB_NAME ) ); + $source->fetch( false ); + $error = $source->get_error(); + if ( ! empty( $error ) ) { + return new WP_Error( 'visualizer_query_failed', sanitize_text_field( $error ) ); + } + if ( ! $source->getSeries() ) { + return new WP_Error( 'visualizer_query_failed', __( 'The query returned no rows.', 'visualizer' ) ); + } + + if ( $dry_run ) { + return array( + 'id' => $chart->ID, + 'source' => 'db_query', + 'dry_run' => true, + 'parsed_preview' => $this->getPreview( $source->getSeries(), $source->getRawData() ), + ); + } + + $this->disableRevisionsTemporarily(); + $this->clearSourceMeta( $chart->ID, true ); + update_post_meta( $chart->ID, Visualizer_Plugin::CF_DB_QUERY, $query ); + update_post_meta( $chart->ID, Visualizer_Plugin::CF_DB_SCHEDULE, $hours ); + $this->persistSource( $chart->ID, $source ); + + $schedules = get_option( Visualizer_Plugin::CF_DB_SCHEDULE, array() ); + $schedules = is_array( $schedules ) ? $schedules : array(); + $schedules[ $chart->ID ] = time() + $hours * HOUR_IN_SECONDS; + update_option( Visualizer_Plugin::CF_DB_SCHEDULE, $schedules ); + + $this->clearChartCache( $chart->ID ); + + return array( + 'id' => $chart->ID, + 'source' => 'db_query', + 'refresh_interval' => $hours, + 'dry_run' => false, + 'embed' => $this->getEmbed( $chart->ID ), + ); + } + + /** + * Builds a CSV source from typed columns and rows, the format the manual data editor submits. + * + * @param array $input The ability input. + * @return Visualizer_Source|WP_Error + */ + private function buildManualSource( $input ) { + $columns = $this->validateColumns( isset( $input['columns'] ) ? $input['columns'] : null ); + if ( is_wp_error( $columns ) ) { + return $columns; + } + + $rows = isset( $input['rows'] ) ? $input['rows'] : null; + if ( ! is_array( $rows ) || ! $rows ) { + return new WP_Error( 'visualizer_invalid_rows', __( 'rows must be a non-empty array of arrays.', 'visualizer' ) ); + } + if ( count( $rows ) > self::MAX_ROWS ) { + /* translators: %d: maximum number of rows. */ + return new WP_Error( 'visualizer_too_many_rows', sprintf( __( 'A maximum of %d rows is accepted.', 'visualizer' ), self::MAX_ROWS ) ); + } + + $errors = new WP_Error(); + $lines = array( wp_list_pluck( $columns, 'name' ), wp_list_pluck( $columns, 'type' ) ); + foreach ( array_values( $rows ) as $index => $row ) { + if ( ! is_array( $row ) || count( $row ) > count( $columns ) ) { + /* translators: %d: row number. */ + $errors->add( 'visualizer_invalid_rows', sprintf( __( 'Row %d is not an array or has more cells than columns.', 'visualizer' ), $index + 1 ) ); + continue; + } + $line = array(); + foreach ( array_values( $row ) as $position => $cell ) { + if ( is_bool( $cell ) ) { + $cell = $cell ? 'true' : 'false'; + } elseif ( null === $cell ) { + $cell = ''; + } elseif ( ! is_scalar( $cell ) ) { + /* translators: %d: row number. */ + $errors->add( 'visualizer_invalid_rows', sprintf( __( 'Row %d contains a value that is not a string, number or boolean.', 'visualizer' ), $index + 1 ) ); + continue 2; + } + $cell = sanitize_text_field( (string) $cell ); + if ( 'number' === $columns[ $position ]['type'] && '' !== $cell && ! is_numeric( str_replace( ',', '', $cell ) ) ) { + /* translators: 1: row number, 2: column label. */ + $errors->add( 'visualizer_invalid_rows', sprintf( __( 'Row %1$d: "%2$s" expects a number.', 'visualizer' ), $index + 1, $columns[ $position ]['name'] ) ); + continue 2; + } + $line[] = $cell; + } + $lines[] = array_pad( $line, count( $columns ), '' ); + + if ( count( $errors->get_error_messages() ) >= 20 ) { + break; + } + } + if ( $errors->has_errors() ) { + return $errors; + } + + $tmpfile = wp_tempnam( Visualizer_Plugin::NAME ); + // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen + $handle = $tmpfile ? fopen( $tmpfile, 'w' ) : false; + if ( ! $handle ) { + return new WP_Error( 'visualizer_tmp_file', __( 'A temporary file could not be created.', 'visualizer' ) ); + } + foreach ( $lines as $line ) { + fputcsv( $handle, $line, VISUALIZER_CSV_DELIMITER, VISUALIZER_CSV_ENCLOSURE ); + } + fclose( $handle ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose + + $source = $this->fetchSource( new Visualizer_Source_Csv( $tmpfile ) ); + wp_delete_file( $tmpfile ); + + if ( ! is_wp_error( $source ) && count( $source->getSeries() ) !== count( $columns ) ) { + return new WP_Error( 'visualizer_invalid_columns', __( 'The columns could not be parsed. Check that every column has a label and a type.', 'visualizer' ) ); + } + + return $source; + } + + /** + * Runs fetch() on a source and converts failures to WP_Error. + * + * @param Visualizer_Source $source The source. + * @return Visualizer_Source|WP_Error + */ + private function fetchSource( $source ) { + try { + $fetched = $source->fetch(); + } catch ( Exception $e ) { + $fetched = false; + } + + $error = $source->get_error(); + if ( ! $fetched || ! empty( $error ) ) { + return new WP_Error( 'visualizer_fetch_failed', ! empty( $error ) ? sanitize_text_field( $error ) : __( 'Could not parse data. Check the format and try again.', 'visualizer' ) ); + } + + return $source; + } + + /** + * Stores the series and data of a fetched source, as the editors do after an import. + * + * @param int $chart_id The chart ID. + * @param Visualizer_Source $source The fetched source. + * @return void + */ + private function persistSource( $chart_id, $source ) { + wp_update_post( + array( + 'ID' => $chart_id, + 'post_content' => $source->getData(), + ) + ); + update_post_meta( $chart_id, Visualizer_Plugin::CF_SERIES, $source->getSeries() ); + update_post_meta( $chart_id, Visualizer_Plugin::CF_SOURCE, $source->getSourceName() ); + update_post_meta( $chart_id, Visualizer_Plugin::CF_DEFAULT_DATA, 0 ); + delete_post_meta( $chart_id, Visualizer_Plugin::CF_ERROR ); + } + + /** + * Removes the parameters of the previous source, as Visualizer_Module_Chart::uploadData() does before an import. + * + * @param int $chart_id The chart ID. + * @param bool $remove_schedule Whether to remove the remote file schedule too. + * @return void + */ + private function clearSourceMeta( $chart_id, $remove_schedule ) { + if ( $remove_schedule ) { + apply_filters( 'visualizer_pro_remove_schedule', $chart_id ); + delete_post_meta( $chart_id, Visualizer_Plugin::CF_CHART_URL ); + delete_post_meta( $chart_id, Visualizer_Plugin::CF_CHART_SCHEDULE ); + } + + delete_post_meta( $chart_id, Visualizer_Plugin::CF_FILTER_CONFIG ); + delete_post_meta( $chart_id, '__transient-' . Visualizer_Plugin::CF_FILTER_CONFIG ); + delete_post_meta( $chart_id, '__transient-' . Visualizer_Plugin::CF_DB_QUERY ); + delete_post_meta( $chart_id, Visualizer_Plugin::CF_DB_QUERY ); + delete_post_meta( $chart_id, Visualizer_Plugin::CF_DB_SCHEDULE ); + delete_post_meta( $chart_id, Visualizer_Plugin::CF_REMOTE_DB_PARAMS ); + + delete_post_meta( $chart_id, Visualizer_Plugin::CF_JSON_URL ); + delete_post_meta( $chart_id, Visualizer_Plugin::CF_JSON_ROOT ); + delete_post_meta( $chart_id, Visualizer_Plugin::CF_JSON_PAGING ); + delete_post_meta( $chart_id, Visualizer_Plugin::CF_JSON_HEADERS ); + delete_post_meta( $chart_id, Visualizer_Plugin::CF_JSON_SCHEDULE ); + + delete_post_meta( $chart_id, Visualizer_Plugin::CF_ERROR ); + delete_post_meta( $chart_id, Visualizer_Plugin::CF_EDITOR ); + delete_post_meta( $chart_id, Visualizer_Plugin::CF_EDITABLE_TABLE ); + } + + /** + * Clears the cached chart output. + * + * @param int $chart_id The chart ID. + * @return void + */ + private function clearChartCache( $chart_id ) { + delete_transient( Visualizer_Plugin::CF_CHART_CACHE . '_' . $chart_id ); + } + + // ------------------------------------------------------------------------- + // Validation helpers + // ------------------------------------------------------------------------- + + /** + * Extracts the chart ID from the input. + * + * @param mixed $input The ability input. + * @return int + */ + private function getChartId( $input ) { + return ( is_array( $input ) && isset( $input['chart_id'] ) ) ? absint( $input['chart_id'] ) : 0; + } + + /** + * Returns the chart post, or null when the ID is not a chart. + * + * @param int $chart_id The chart ID. + * @return WP_Post|null + */ + private function getChart( $chart_id ) { + $chart = $chart_id ? get_post( $chart_id ) : null; + + return ( $chart instanceof WP_Post && Visualizer_Plugin::CPT_VISUALIZER === $chart->post_type ) ? $chart : null; + } + + /** + * Returns the chart from the input when the current user can edit it. + * + * @param mixed $input The ability input. + * @return WP_Post|WP_Error + */ + private function getEditableChart( $input ) { + $chart = $this->getChart( $this->getChartId( $input ) ); + if ( ! $chart ) { + return new WP_Error( 'visualizer_chart_not_found', __( 'Chart not found.', 'visualizer' ) ); + } + if ( ! self::can_edit_chart( $chart->ID ) ) { + return new WP_Error( 'visualizer_forbidden', __( 'You do not have permission to perform this action.', 'visualizer' ) ); + } + + return $chart; + } + + /** + * Validates a list of {name, type} columns. + * + * @param mixed $columns The columns input. + * @return array|WP_Error + */ + private function validateColumns( $columns ) { + if ( ! is_array( $columns ) || ! $columns ) { + return new WP_Error( 'visualizer_invalid_columns', __( 'columns must be a non-empty array of {name, type} objects.', 'visualizer' ) ); + } + if ( count( $columns ) > self::MAX_COLUMNS ) { + /* translators: %d: maximum number of columns. */ + return new WP_Error( 'visualizer_too_many_columns', sprintf( __( 'A maximum of %d columns is accepted.', 'visualizer' ), self::MAX_COLUMNS ) ); + } + + $valid = array(); + $names = array(); + foreach ( $columns as $column ) { + $name = ( is_array( $column ) && isset( $column['name'] ) && is_scalar( $column['name'] ) ) ? sanitize_text_field( wp_strip_all_tags( (string) $column['name'] ) ) : ''; + $type = ( is_array( $column ) && isset( $column['type'] ) && is_string( $column['type'] ) ) ? $column['type'] : ''; + if ( '' === $name || '0' === $name ) { + return new WP_Error( 'visualizer_invalid_columns', __( 'Every column needs a non-empty name.', 'visualizer' ) ); + } + if ( in_array( $name, $names, true ) ) { + /* translators: %s: column label. */ + return new WP_Error( 'visualizer_invalid_columns', sprintf( __( 'Duplicate column name "%s".', 'visualizer' ), $name ) ); + } + if ( ! in_array( $type, Visualizer_Source::getAllowedTypes(), true ) ) { + /* translators: 1: column label, 2: list of types. */ + return new WP_Error( 'visualizer_invalid_columns', sprintf( __( 'Column "%1$s" has an invalid type. Allowed types: %2$s.', 'visualizer' ), $name, implode( ', ', Visualizer_Source::getAllowedTypes() ) ) ); + } + $names[] = $name; + $valid[] = array( + 'name' => $name, + 'type' => $type, + ); + } + + return $valid; + } + + /** + * Validates the chart type against the active plan and resolves the library. + * + * @param string $type The chart type. + * @param string $library The requested library, or empty for the default. + * @return string|WP_Error + */ + private function resolveLibrary( $type, $library ) { + $types = Visualizer_Module_Admin::_getChartTypesLocalized(); + if ( ! isset( $types[ $type ] ) || ! is_array( $types[ $type ] ) ) { + /* translators: %s: list of chart types. */ + return new WP_Error( 'visualizer_invalid_type', sprintf( __( 'Unknown chart type. Available types: %s.', 'visualizer' ), implode( ', ', array_keys( $types ) ) ) ); + } + if ( ! Visualizer_Module_Admin::checkChartStatus( $type ) ) { + return $this->planError( 'visualizer_type_unavailable', __( 'This chart type is not available on the current Visualizer plan.', 'visualizer' ), 'pro-chart-types' ); + } + + $supported = isset( $types[ $type ]['supports'] ) ? (array) $types[ $type ]['supports'] : array( 'Google Charts' ); + $supported = array_values( array_map( array( $this, 'removeSpaces' ), $supported ) ); + if ( '' === $library ) { + return $supported[0]; + } + if ( ! in_array( $library, $supported, true ) ) { + /* translators: %s: list of libraries. */ + return new WP_Error( 'visualizer_invalid_library', sprintf( __( 'This chart type supports these libraries on the current plan: %s.', 'visualizer' ), implode( ', ', $supported ) ) ); + } + + return $library; + } + + /** + * Removes the spaces of a library label ("Google Charts" is stored as "GoogleCharts"). + * + * @access public + * + * @param string $label The library label. + * @return string + */ + public function removeSpaces( $label ) { + return str_replace( ' ', '', (string) $label ); + } + + /** + * Same plan check as the upsell overlays in the chart editor (Visualizer_Module_Sources::addProUpsell()). + * + * @param string $feature The feature key. + * @return bool + */ + private function isFeatureAvailable( $feature ) { + if ( in_array( $feature, (array) self::get_features_for_license( 2 ), true ) ) { + return (bool) apply_filters( 'visualizer_is_business', false ); + } + if ( in_array( $feature, (array) self::get_features_for_license( 1 ), true ) ) { + return (bool) self::is_pro(); + } + + return true; + } + + /** + * Validates refresh_interval against the intervals the chart editor offers for the active plan. + * + * @param array $input The ability input. + * @param string $type The schedule type (csv, json, db). + * @param int $chart_id The chart ID. + * @return float|WP_Error + */ + private function resolveInterval( $input, $type, $chart_id ) { + if ( ! isset( $input['refresh_interval'] ) || ! is_numeric( $input['refresh_interval'] ) || (float) $input['refresh_interval'] < 0 ) { + return -1.0; + } + + $hours = (float) $input['refresh_interval']; + $allowed = apply_filters( 'visualizer_chart_schedules', array( '-1' => __( 'One-time', 'visualizer' ) ), $type, $chart_id ); + $allowed = is_array( $allowed ) ? array_keys( $allowed ) : array(); + foreach ( $allowed as $option ) { + if ( is_numeric( $option ) && abs( (float) $option - $hours ) < 0.00001 ) { + return $hours; + } + } + + /* translators: %s: list of intervals in hours. */ + $message = sprintf( __( 'This refresh interval is not available on the current Visualizer plan. Allowed values (hours): %s.', 'visualizer' ), implode( ', ', $allowed ) ); + + // The plan is the limit only when it offers no recurring interval; otherwise the value itself is wrong. + if ( count( $allowed ) > 1 ) { + return new WP_Error( 'visualizer_interval_unavailable', $message ); + } + + return $this->planError( 'visualizer_interval_unavailable', $message, 'refresh-interval' ); + } + + /** + * Builds the error for a request the active plan does not cover, with the upgrade link. + * + * @param string $code The error code. + * @param string $message The error message. + * @param string $area The gated feature, used as the campaign of the upgrade link. + * @return WP_Error + */ + private function planError( $code, $message, $area ) { + $upgrade_url = tsdk_translate_link( tsdk_utmify( Visualizer_Plugin::PRO_TEASER_URL, $area, 'mcp' ) ); + + return new WP_Error( + $code, + /* translators: 1: the error message, 2: the upgrade URL. */ + sprintf( __( '%1$s Upgrade: %2$s', 'visualizer' ), $message, $upgrade_url ), + array( 'upgrade_url' => $upgrade_url ) + ); + } + + /** + * Validates the url input. + * + * @param array $input The ability input. + * @return string|WP_Error + */ + private function validateUrl( $input ) { + $url = ( isset( $input['url'] ) && is_string( $input['url'] ) ) ? wp_http_validate_url( esc_url_raw( trim( $input['url'] ) ) ) : false; + if ( ! $url ) { + return new WP_Error( 'visualizer_invalid_url', __( 'The URL you entered is invalid. Please enter a valid URL.', 'visualizer' ) ); + } + + return (string) $url; + } + + // ------------------------------------------------------------------------- + // Output helpers + // ------------------------------------------------------------------------- + + /** + * Returns the chart name as shown in the chart library. + * + * @param WP_Post $chart The chart. + * @return string + */ + private function getChartTitle( $chart ) { + $settings = get_post_meta( $chart->ID, Visualizer_Plugin::CF_SETTINGS, true ); + if ( is_array( $settings ) && ! empty( $settings['backend-title'] ) && is_string( $settings['backend-title'] ) ) { + return $settings['backend-title']; + } + + return '#' . $chart->ID; + } + + /** + * Returns the shortcode of a chart. + * + * @param int $chart_id The chart ID. + * @return string + */ + private function getShortcode( $chart_id ) { + return sprintf( '[visualizer id="%d"]', $chart_id ); + } + + /** + * Returns the embed descriptor of a chart. + * + * @param int $chart_id The chart ID. + * @return array{shortcode: string, block: string} + */ + private function getEmbed( $chart_id ) { + return array( + 'shortcode' => $this->getShortcode( $chart_id ), + 'block' => sprintf( '', $chart_id ), + ); + } + + /** + * Maps a source class to the source kind used by the abilities. + * + * @param int $chart_id The chart ID. + * @return string + */ + private function getSourceKind( $chart_id ) { + $kinds = array( + 'Visualizer_Source_Csv' => 'manual', + 'Visualizer_Source_Xlsx' => 'manual', + 'Visualizer_Source_Csv_Remote' => 'csv_url', + 'Visualizer_Source_Xlsx_Remote' => 'csv_url', + 'Visualizer_Source_Json' => 'json', + 'Visualizer_Source_Query' => 'db_query', + ); + $class = (string) get_post_meta( $chart_id, Visualizer_Plugin::CF_SOURCE, true ); + + return isset( $kinds[ $class ] ) ? $kinds[ $class ] : $class; + } + + /** + * Describes the saved source of a chart without any credentials. + * + * @param int $chart_id The chart ID. + * @return array + */ + private function describeSource( $chart_id ) { + $kind = $this->getSourceKind( $chart_id ); + $source = array( 'kind' => $kind ); + + switch ( $kind ) { + case 'csv_url': + $chart = get_post( $chart_id ); + $source['url'] = $chart instanceof WP_Post ? $this->getRemoteFileUrl( $chart ) : ''; + $source['refresh_interval'] = $this->getInterval( $chart_id, Visualizer_Plugin::CF_CHART_SCHEDULE ); + break; + case 'json': + $headers = get_post_meta( $chart_id, Visualizer_Plugin::CF_JSON_HEADERS, true ); + $source['url'] = (string) get_post_meta( $chart_id, Visualizer_Plugin::CF_JSON_URL, true ); + $source['json_root'] = (string) get_post_meta( $chart_id, Visualizer_Plugin::CF_JSON_ROOT, true ); + $source['json_method'] = ( is_array( $headers ) && ! empty( $headers['method'] ) ) ? (string) $headers['method'] : 'GET'; + $source['has_saved_auth'] = is_array( $headers ) && ! empty( $headers['auth'] ); + $source['refresh_interval'] = $this->getInterval( $chart_id, Visualizer_Plugin::CF_JSON_SCHEDULE ); + break; + case 'db_query': + $source['is_remote_db'] = ! empty( get_post_meta( $chart_id, Visualizer_Plugin::CF_REMOTE_DB_PARAMS, true ) ); + $source['refresh_interval'] = $this->getInterval( $chart_id, Visualizer_Plugin::CF_DB_SCHEDULE ); + // The SQL is only shown to users who may edit it in the chart editor. + if ( current_user_can( 'administrator' ) && is_super_admin() ) { + $source['query'] = (string) get_post_meta( $chart_id, Visualizer_Plugin::CF_DB_QUERY, true ); + } + break; + } + + return $source; + } + + /** + * Returns a saved refresh interval in hours, -1 when none is saved. + * + * @param int $chart_id The chart ID. + * @param string $meta_key The schedule meta key. + * @return float + */ + private function getInterval( $chart_id, $meta_key ) { + $hours = get_post_meta( $chart_id, $meta_key, true ); + + return is_numeric( $hours ) ? (float) $hours : -1.0; + } + + /** + * Returns the URL of a remote CSV/XLSX chart. + * + * @param WP_Post $chart The chart. + * @return string + */ + private function getRemoteFileUrl( $chart ) { + $url = (string) get_post_meta( $chart->ID, Visualizer_Plugin::CF_CHART_URL, true ); + if ( '' === $url ) { + // Same fallback as Visualizer_Source_Csv_Remote::_repopulate(). + $content = self::decode_content( html_entity_decode( $chart->post_content ) ); + $url = ( is_array( $content ) && isset( $content['source'] ) && is_string( $content['source'] ) ) ? $content['source'] : ''; + } + + return wp_http_validate_url( $url ) ? $url : ''; + } + + /** + * Converts stored series to columns. + * + * @param mixed $series The series. + * @return array + */ + private function seriesToColumns( $series ) { + $columns = array(); + if ( is_array( $series ) ) { + foreach ( $series as $serie ) { + if ( ! is_array( $serie ) ) { + continue; + } + $columns[] = array( + 'name' => isset( $serie['label'] ) ? (string) $serie['label'] : '', + 'type' => isset( $serie['type'] ) ? (string) $serie['type'] : 'string', + ); + } + } + + return $columns; + } + + /** + * Returns data rows as a list of lists. + * + * @param mixed $data The chart data. + * @return array> + */ + private function normalizeRows( $data ) { + $rows = array(); + if ( is_array( $data ) ) { + foreach ( $data as $row ) { + if ( is_array( $row ) ) { + $rows[] = array_values( $row ); + } + } + } + + return $rows; + } + + /** + * Returns the parsed preview of a fetched source. + * + * @param mixed $series The parsed series. + * @param mixed $data The parsed data. + * @return array + */ + private function getPreview( $series, $data ) { + $rows = $this->normalizeRows( $data ); + + return array( + 'columns' => $this->seriesToColumns( $series ), + 'rows' => array_slice( $rows, 0, self::PREVIEW_ROWS ), + 'row_count' => count( $rows ), + ); + } +} diff --git a/classes/Visualizer/Module/Setup.php b/classes/Visualizer/Module/Setup.php index 2ef61c60..6ce00659 100644 --- a/classes/Visualizer/Module/Setup.php +++ b/classes/Visualizer/Module/Setup.php @@ -45,6 +45,7 @@ public function __construct( Visualizer_Plugin $plugin ) { register_activation_hook( VISUALIZER_BASEFILE, array( $this, 'activate' ) ); register_deactivation_hook( VISUALIZER_BASEFILE, array( $this, 'deactivate' ) ); $this->_addAction( 'visualizer_schedule_refresh_db', 'refreshDbChart' ); + $this->_addAction( 'init', 'maybe_reschedule_refresh_db' ); $this->_addFilter( 'visualizer_schedule_refresh_chart', 'refresh_db_for_chart', 10, 3 ); $this->_addAction( 'admin_init', 'adminInit' ); @@ -490,7 +491,11 @@ private function schedule_refresh_db_action(): void { $interval = $this->get_schedule_interval_seconds( $interval_key ); $timestamp = strtotime( 'midnight' ) - get_option( 'gmt_offset' ) * HOUR_IN_SECONDS; - if ( function_exists( 'as_next_scheduled_action' ) && function_exists( 'as_schedule_recurring_action' ) ) { + if ( + visualizer_can_use_action_scheduler() + && function_exists( 'as_next_scheduled_action' ) + && function_exists( 'as_schedule_recurring_action' ) + ) { $next = as_next_scheduled_action( $hook, array(), $group ); if ( false === $next ) { as_schedule_recurring_action( $timestamp, $interval, $hook, array(), $group ); @@ -503,6 +508,29 @@ private function schedule_refresh_db_action(): void { wp_schedule_event( $timestamp, $interval_key, $hook ); } + /** + * Keep the DB refresh scheduled when Action Scheduler is not available. + * + * The migration to Action Scheduler clears the WP-Cron event, so a site that + * already migrated and then lost the library would have nothing left running + * the refresh. Re-arms WP-Cron in that case; no-op whenever the library is up. + */ + public function maybe_reschedule_refresh_db(): void { + if ( + visualizer_can_use_action_scheduler() + && function_exists( 'as_next_scheduled_action' ) + && function_exists( 'as_schedule_recurring_action' ) + ) { + return; + } + + if ( wp_next_scheduled( 'visualizer_schedule_refresh_db' ) ) { + return; + } + + $this->schedule_refresh_db_action(); + } + /** * Unschedule the recurring DB refresh action. */ diff --git a/composer.json b/composer.json index 1153b42d..fc50bfe0 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,7 @@ "source": "https://github.com/Codeinwp/visualizer" }, "require": { - "codeinwp/themeisle-sdk": "^3.3", + "codeinwp/themeisle-sdk": "^3.3.65", "neitanod/forceutf8": "~2.0", "openspout/openspout": "^3.7", "woocommerce/action-scheduler": "^3.8" diff --git a/composer.lock b/composer.lock index 79c20cde..aef72b51 100644 --- a/composer.lock +++ b/composer.lock @@ -4,20 +4,20 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "881b5f99c0b72f47e79eb09bdac7fbea", + "content-hash": "78fe9ab724950bb3c973311378d483e5", "packages": [ { "name": "codeinwp/themeisle-sdk", - "version": "3.3.61", + "version": "3.3.65", "source": { "type": "git", "url": "https://github.com/Codeinwp/themeisle-sdk.git", - "reference": "9fe698b52dec768a0dd8b500fb51efe40962ee99" + "reference": "f650fe856d52ce4e5754557d89ba2f3127ad54d8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeinwp/themeisle-sdk/zipball/9fe698b52dec768a0dd8b500fb51efe40962ee99", - "reference": "9fe698b52dec768a0dd8b500fb51efe40962ee99", + "url": "https://api.github.com/repos/Codeinwp/themeisle-sdk/zipball/f650fe856d52ce4e5754557d89ba2f3127ad54d8", + "reference": "f650fe856d52ce4e5754557d89ba2f3127ad54d8", "shasum": "" }, "require-dev": { @@ -43,9 +43,9 @@ ], "support": { "issues": "https://github.com/Codeinwp/themeisle-sdk/issues", - "source": "https://github.com/Codeinwp/themeisle-sdk/tree/v3.3.61" + "source": "https://github.com/Codeinwp/themeisle-sdk/tree/v3.3.65" }, - "time": "2026-08-24T15:59:27+00:00" + "time": "2026-09-29T09:35:47+00:00" }, { "name": "neitanod/forceutf8", @@ -1066,11 +1066,11 @@ }, { "name": "phpstan/phpstan", - "version": "2.2.13", + "version": "2.2.14", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/9ba9ac76ee9c5cf5b56d58eb5deec6315b7a0260", - "reference": "9ba9ac76ee9c5cf5b56d58eb5deec6315b7a0260", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/9c672e7a8e791dfc3d30e55f683e73fc0b63a3ac", + "reference": "9c672e7a8e791dfc3d30e55f683e73fc0b63a3ac", "shasum": "" }, "require": { @@ -1126,7 +1126,7 @@ "type": "github" } ], - "time": "2026-09-03T20:38:19+00:00" + "time": "2026-09-12T21:39:33+00:00" }, { "name": "phpunit/php-code-coverage", diff --git a/index.php b/index.php index dd010a6e..f99c6d50 100644 --- a/index.php +++ b/index.php @@ -145,6 +145,9 @@ function () { // AI Chart Builder module (AJAX endpoints for the React wizard). $plugin->setModule( Visualizer_Module_AIBuilder::NAME ); + // Abilities API integration (no-op when the API is not available). + $plugin->setModule( Visualizer_Module_Abilities::NAME ); + $vendor_file = VISUALIZER_ABSPATH . '/vendor/autoload.php'; if ( is_readable( $vendor_file ) ) { include_once $vendor_file; @@ -152,7 +155,7 @@ function () { $action_scheduler_file = VISUALIZER_ABSPATH . '/vendor/woocommerce/action-scheduler/action-scheduler.php'; - if ( is_readable( $action_scheduler_file ) ) { + if ( visualizer_can_use_action_scheduler() && is_readable( $action_scheduler_file ) ) { require_once $action_scheduler_file; } @@ -180,6 +183,25 @@ function () { ); } ); + add_filter( + 'visualizer_ai_connect_metadata', + function () { + return array( + 'name' => 'Visualizer', + 'notice_cases' => array( + __( 'build a chart from your numbers', 'visualizer' ), + __( 'update a table with new data', 'visualizer' ), + __( 'refresh charts from their data source', 'visualizer' ), + ), + 'prompts' => array( + __( 'Create a Visualizer pie chart "Traffic by channel" with Organic 5400, Direct 2100 and Social 1300, and add it to my Analytics page.', 'visualizer' ), + __( 'Refresh every chart that pulls from a remote CSV, JSON or database source.', 'visualizer' ), + __( 'Change my "Monthly sales" chart to a bar chart and add September: 12,400.', 'visualizer' ), + ), + 'ability_prefix' => 'visualizer', + ); + } + ); if ( ! defined( 'TI_E2E_TESTING' ) && 'yes' === get_option( 'visualizer_logger_flag', 'no' ) ) { add_filter( 'themeisle_sdk_enable_telemetry', '__return_true' ); @@ -217,6 +239,21 @@ function ( $products ) { } } +/** + * Whether the bundled Action Scheduler can run against this wpdb. + * + * The bundled library calls wpdb::db_server_info() unguarded when it claims a + * queue batch, and core only added that method in WordPress 5.5. Loading it + * without the method available fatals, so those sites stay on WP-Cron instead. + * + * @return bool + */ +function visualizer_can_use_action_scheduler() { + global $wpdb; + + return isset( $wpdb ) && is_callable( array( $wpdb, 'db_server_info' ) ); +} + /** * Registers with the SDK * diff --git a/readme.txt b/readme.txt index a1c6e364..ef7599fc 100755 --- a/readme.txt +++ b/readme.txt @@ -4,7 +4,7 @@ Tags: charts, tables, datatable, data-visualization, ai-charts Requires at least: 5.2 Tested up to: 7.1 Requires PHP: 7.4 -Stable tag: trunk +Stable tag: 4.0.8 License: GPL v2.0 or later License URI: http://www.opensource.org/licenses/gpl-license.php @@ -253,18 +253,12 @@ Please follow the reporting protocols outlined on our [Security Page](https://th - Fixed script execution from saved JSON chart credentials. Thanks Yaswanth Reddy Sunkara for the report. - Updated dependencies - - - ##### [Version 4.0.7](https://github.com/Codeinwp/visualizer/compare/v4.0.6...v4.0.7) (2026-07-30) - Fixed charts using a database source saving the results of the previous query, so a saved chart no longer differs from the data shown in the editor. - Updated dependencies - Enhanced security — fixed unauthorized chart_data REST access. - - - ##### [Version 4.0.6](https://github.com/Codeinwp/visualizer/compare/v4.0.5...v4.0.6) (2026-07-22) - Fixed scheduled imports from remote CSV files so charts continue to refresh automatically in the background. @@ -272,1072 +266,4 @@ Please follow the reporting protocols outlined on our [Security Page](https://th - Improved the overall security of the product. Thanks to **creeper_kirby** for responsibly reporting the issue. - Updated dependencies. - - - -##### [Version 4.0.5](https://github.com/Codeinwp/visualizer/compare/v4.0.4...v4.0.5) (2026-07-10) - -- Enhanced security - - - - -##### [Version 4.0.4](https://github.com/Codeinwp/visualizer/compare/v4.0.3...v4.0.4) (2026-06-30) - -- Enhanced security - - - - -##### [Version 4.0.3](https://github.com/Codeinwp/visualizer/compare/v4.0.2...v4.0.3) (2026-06-09) - -- Improved layout compatibility with WP 7.0 - - - - -##### [Version 4.0.2](https://github.com/Codeinwp/visualizer/compare/v4.0.1...v4.0.2) (2026-05-19) - -- Fixed broken candlestick chart -- Updated dependencies -- Enhanced security (CVE-2026-65526 fixed) - - - - -##### [Version 4.0.1](https://github.com/Codeinwp/visualizer/compare/v4.0.0...v4.0.1) (2026-03-31) - -- Internal update to ensure compatibility and improve system consistency. - - - - -#### [Version 4.0.0](https://github.com/Codeinwp/visualizer/compare/v3.11.15...v4.0.0) (2026-03-31) - -### New Features -- Introducing AI chart creation powered by D3.js -- You can now import XLSX files natively -- The charts library now includes a grid and list view for easier browsing -- Added a new Elementor widget for displaying Visualizer charts -- Improved auto-sync reliability by replacing WP-Cron with Action Scheduler - -### Improvements -- Cleaned up legacy block controls in the Block Editor - -### Bug Fixes -- Resolved an issue where lazy charts didn’t render when already in the viewport on page load -- Fixed a fatal error on servers with broken FTP filesystem configuration -- Addressed errors occurring in the getCSV method -- Fixed an intermittent sidebar scroll issue in Chrome -- Prevented a double clipboard alert when copying -- Fixed a bubble chart crash on window resize when using named series in manual configuration - - - - -##### [Version 3.11.15](https://github.com/Codeinwp/visualizer/compare/v3.11.14...v3.11.15) (2026-03-04) - -- Enhanced security - - - - -##### [Version 3.11.14](https://github.com/Codeinwp/visualizer/compare/v3.11.13...v3.11.14) (2025-11-28) - -- Enhanced security - - - - -##### [Version 3.11.13](https://github.com/Codeinwp/visualizer/compare/v3.11.12...v3.11.13) (2025-11-06) - -- Enhanced security - - - - -##### [Version 3.11.12](https://github.com/Codeinwp/visualizer/compare/v3.11.11...v3.11.12) (2025-09-04) - -- Updated dependencies -- Fixed issue with the number format not being applied properly to the last value of the Google chart - - - - -##### [Version 3.11.11](https://github.com/Codeinwp/visualizer/compare/v3.11.10...v3.11.11) (2025-05-27) - -- Fixed chart rendering on page load with multiple charts -- Updated dependencies - - - - -##### [Version 3.11.10](https://github.com/Codeinwp/visualizer/compare/v3.11.9...v3.11.10) (2025-04-16) - -- Fixed special characters not rendering correctly in Legend labels -- Updated dependencies - - - - -##### [Version 3.11.9](https://github.com/Codeinwp/visualizer/compare/v3.11.8...v3.11.9) (2025-02-13) - -- Enhanced security -- Fixed extra column issue with a simple editor - - - - -##### [Version 3.11.8](https://github.com/Codeinwp/visualizer/compare/v3.11.7...v3.11.8) (2024-11-13) - -- Updated WordPress core tested up to version - - - - -##### [Version 3.11.7](https://github.com/Codeinwp/visualizer/compare/v3.11.6...v3.11.7) (2024-11-07) - -- Updated dependencies -- Enhanced security - - - - -##### [Version 3.11.6](https://github.com/Codeinwp/visualizer/compare/v3.11.5...v3.11.6) (2024-08-30) - -- Fixed chart overlapping issue with the Gutenberg editor - - - - -##### [Version 3.11.5](https://github.com/Codeinwp/visualizer/compare/v3.11.4...v3.11.5) (2024-07-17) - -- Updated promotional links and buttons - - - - -##### [Version 3.11.4](https://github.com/Codeinwp/visualizer/compare/v3.11.3...v3.11.4) (2024-07-09) - -- Fixed broken link in the Map settings of the Geo Chart -- Fixed the default cron job sync time for data synchronization [PRO] - - - - -##### [Version 3.11.3](https://github.com/Codeinwp/visualizer/compare/v3.11.2...v3.11.3) (2024-05-27) - -- Fixed the rendering of Visualizer widgets in the Customizer - - - - -##### [Version 3.11.2](https://github.com/Codeinwp/visualizer/compare/v3.11.1...v3.11.2) (2024-05-23) - -- Fixed Visualizer block widget not loading -- Improved the popup rendering -- Enhanced security - - - - -##### [Version 3.11.1](https://github.com/Codeinwp/visualizer/compare/v3.11.0...v3.11.1) (2024-05-14) - -- Fixed the permissions of the plans - - - - -#### [Version 3.11.0](https://github.com/Codeinwp/visualizer/compare/v3.10.15...v3.11.0) (2024-05-13) - -#### Enhancements -- Added a manual data edit feature to the free version of the plugin -- Improved the flow of chart creation -- Added a chart information panel: backend chart title & quick shortcode access -- Line and Pie charts from the ChartJS library are now available for free users, along with library selection and relative images -- Improved wording in onboarding -- Improved workflow for creating charts via the Visualizer Gutenberg block -- Moved chart library selection to the top of the page and showing charts from the available library only -- Unified chart edition (removed duplicated chart settings in block editor) -- Added optional feature usage data -- Enhanced security -- Improved the UI of the database query option (visible database prefix, sample query, autocompleting table names) [PRO] - - - - -##### [Version 3.10.15](https://github.com/Codeinwp/visualizer/compare/v3.10.14...v3.10.15) (2024-05-10) - -- Updated internal dependencies - - - - -##### [Version 3.10.14](https://github.com/Codeinwp/visualizer/compare/v3.10.13...v3.10.14) (2024-05-10) - -- Updated internal dependencies - - - - -##### [Version 3.10.13](https://github.com/Codeinwp/visualizer/compare/v3.10.12...v3.10.13) (2024-04-17) - -### Fixes -- **Updated internal dependencies:** Enhanced performance and security. - - - - -##### [Version 3.10.12](https://github.com/Codeinwp/visualizer/compare/v3.10.11...v3.10.12) (2024-04-15) - -### Fixes -- **Updated internal dependencies:** Enhanced performance and security. - - - - -##### [Version 3.10.11](https://github.com/Codeinwp/visualizer/compare/v3.10.10...v3.10.11) (2024-04-01) - -### Fixes -- Updated internal dependencies -- Improved E2E testing - - - - -##### [Version 3.10.10](https://github.com/Codeinwp/visualizer/compare/v3.10.9...v3.10.10) (2024-03-28) - -### Fixes -- Fixed blueprint schema for Live Preview - - - - -##### [Version 3.10.9](https://github.com/Codeinwp/visualizer/compare/v3.10.8...v3.10.9) (2024-03-27) - -### Fixes -- Visualizer live preview fixed -- Updated internal dependencies - - - - -##### [Version 3.10.8](https://github.com/Codeinwp/visualizer/compare/v3.10.7...v3.10.8) (2024-03-14) - -### Bug Fixes -- Fix Google chart rendering condition for Classic Plugin -- Changes the speed and position of the loading bar - -### Improvements -- Add a Live Preview button for the plugin page on wordpress.org -- Added NPS survey - - - - -##### [Version 3.10.7](https://github.com/Codeinwp/visualizer/compare/v3.10.6...v3.10.7) (2024-02-06) - -### Fixes -- Rendering charts that are under tabs/hidden containers -- Updated dependencies -- Updated CTA buttons -- Updated UTM campaign links - - - - -##### [Version 3.10.6](https://github.com/Codeinwp/visualizer/compare/v3.10.5...v3.10.6) (2024-01-18) - -### Fixes -- Enhanced security - - - - -##### [Version 3.10.5](https://github.com/Codeinwp/visualizer/compare/v3.10.4...v3.10.5) (2023-12-19) - -### Improvements -- **Added chart responsive support** -- **SDK Updates** - -### Bug Fixes -- **Fixed chart data filter issue** -- **Fixed animation issue fusion builder** -- **Fixed live import issue with Gutenberg block** -- **Fixed tooltip text rendering issue in ChartJS** -- **Fixed onboarding chart import issue** -- **Removed PHP deprecated function** -- **Fixed compatibility issue with WP VIP** -- **Fixed PHP 8.1 deprecated error** - - - - -##### [Version 3.10.4](https://github.com/Codeinwp/visualizer/compare/v3.10.3...v3.10.4) (2023-08-17) - -- Updated dependencies -- Updated WordPress version required to 5.2 or higher - - - - -##### [Version 3.10.3](https://github.com/Codeinwp/visualizer/compare/v3.10.2...v3.10.3) (2023-06-13) - -- Removed branding label - - - - -##### [Version 3.10.2](https://github.com/Codeinwp/visualizer/compare/v3.10.1...v3.10.2) (2023-06-05) - -- Added About Us page integration -- Updated dependencies - - - - -##### [Version 3.10.1](https://github.com/Codeinwp/visualizer/compare/v3.10.0...v3.10.1) (2023-05-24) - -- Fixed data tables horizontal scrolling issue -- Added legend position default label text -- Added tooltip default label text -- Fixed Gutenberg block error -- Fixed table chart rendering issue in Gutenberg block -- Fixed compatibility issue with the FSE editor -- Fixed compatibility issue with WP 5.6 -- Fixed console error when importing charts from others -- Fixed pagination enable/disable issue -- Fixed editor rows limitation issue - - - - -#### [Version 3.10.0](https://github.com/Codeinwp/visualizer/compare/v3.9.7...v3.10.0) (2023-04-04) - -- Added a setup wizard onboarding page -- Fixed empty chart issue - - - - -##### [Version 3.9.7](https://github.com/Codeinwp/visualizer/compare/v3.9.6...v3.9.7) (2023-03-30) - -- Improved chart actions icons -- Updated WordPress core tested up to version - - - - -##### [Version 3.9.6](https://github.com/Codeinwp/visualizer/compare/v3.9.5...v3.9.6) (2023-03-20) - -- Fixed custom format issue in ChartJS - - - - -##### [Version 3.9.5](https://github.com/Codeinwp/visualizer/compare/v3.9.4...v3.9.5) (2023-01-30) - -- Improved security by escaping shortcode attribute before render -- Compatibility with PHP 8.2 - - - - -##### [Version 3.9.4](https://github.com/Codeinwp/visualizer/compare/v3.9.3...v3.9.4) (2023-01-12) - -- Fixed the lock of PRO features bypass - - - - -##### [Version 3.9.3](https://github.com/Codeinwp/visualizer/compare/v3.9.2...v3.9.3) (2023-01-05) - -- Fixed the chart refresh problem when importing from CSV [#970](https://github.com/Codeinwp/visualizer/issues/970) - - - - -##### [Version 3.9.2](https://github.com/Codeinwp/visualizer/compare/v3.9.1...v3.9.2) (2022-12-21) - -- Fixed Geo chart [PRO] flickering issue -- Added disable legend option for ChartJS chart -- Enhanced security - - - - -##### [Version 3.9.1](https://github.com/Codeinwp/visualizer/compare/v3.9.0...v3.9.1) (2022-11-17) - -- Fixed PHP error when Polylang plugin is activated [#960](https://github.com/Codeinwp/visualizer/issues/960) - - - - -#### [Version 3.9.0](https://github.com/Codeinwp/visualizer/compare/v3.8.1...v3.9.0) (2022-11-10) - -- Fixed table chart decimal number issue Decimal Numbers are ignored and not displayed [#955](https://github.com/Codeinwp/visualizer/issues/955) -- Fix the scrollbar issue that happens on WP 6.1 version [WP 6.1] Chart permissions selector is not scrollable [#950](https://github.com/Codeinwp/visualizer/issues/950) -- Fix PHP Warning Warning when copy (frontend action) is enabled [#949](https://github.com/Codeinwp/visualizer/issues/949) -- Compatibility with the WPML translation plugin for translating charts [PRO feature] add support for string translations [#731](https://github.com/Codeinwp/visualizer/issues/731) -- Integration with Woocommerce Data endpoints for creating charts [PRO feature] -- Show new features on chart library page - - - - -##### [Version 3.8.1](https://github.com/Codeinwp/visualizer/compare/v3.8.0...v3.8.1) (2022-10-12) - - -- Change copy shortcode icon to clone a chart We can change copy Short Code icon [#945](https://github.com/Codeinwp/visualizer/issues/945) -- Fix temp file creation issue on instawp selecting the line chart is not working on instawp [#940](https://github.com/Codeinwp/visualizer/issues/940) -- Fix control Type error -- Fixed simple editor scrolling issue -- Fix filter broken layout -- Fix chartJs javascript error when rendering multiple charts - - - - -#### [Version 3.8.0](https://github.com/Codeinwp/visualizer/compare/v3.7.12...v3.8.0) (2022-09-21) - -- Add data filter support for the charts - - - - -##### [Version 3.7.12](https://github.com/Codeinwp/visualizer/compare/v3.7.11...v3.7.12) (2022-09-07) - -- Fixed compatibility issue with some themes and plugins [#933](https://github.com/Codeinwp/visualizer/issues/933) - - - - -##### [Version 3.7.11](https://github.com/Codeinwp/visualizer/compare/v3.7.10...v3.7.11) (2022-08-30) - -- Fix update axis baseline color [#925](https://github.com/Codeinwp/visualizer/issues/925) -- Update more features images [#926](https://github.com/Codeinwp/visualizer/issues/926) -- Fix update minor grid line field [#924](https://github.com/Codeinwp/visualizer/issues/924) -- Add new filter for container attributes [#922](https://github.com/Codeinwp/visualizer/issues/922) -- Fix axis data format issue [#920](https://github.com/Codeinwp/visualizer/issues/920) -- Chart unique title [#915](https://github.com/Codeinwp/visualizer/issues/915) -- Fix block back button issue [#930](https://github.com/Codeinwp/visualizer/issues/930) -- Userguide broken documentation link [#918](https://github.com/Codeinwp/visualizer/issues/918) - - - - -##### [Version 3.7.10](https://github.com/Codeinwp/visualizer/compare/v3.7.9...v3.7.10) (2022-07-05) - - -- Fix watermark/copyright opacity wrong value [#910](https://github.com/Codeinwp/visualizer/issues/910) - - - - -##### [Version 3.7.9](https://github.com/Codeinwp/visualizer/compare/v3.7.8...v3.7.9) (2022-06-22) - -* Fix branding bad HTML - - - - -##### [Version 3.7.8](https://github.com/Codeinwp/visualizer/compare/v3.7.7...v3.7.8) (2022-06-21) - -- Add the description field in the pie chart -- Fix Label other is not visible on the pie chart -- Fix the pagination issue in the chart library -- Update dependencies - - - - -##### [Version 3.7.7](https://github.com/Codeinwp/visualizer/compare/v3.7.6...v3.7.7) (2022-05-31) - -* Harden security by enforcing escaped urls. - - - - -##### [Version 3.7.6](https://github.com/Codeinwp/visualizer/compare/v3.7.5...v3.7.6) (2022-05-26) - -- Fix PHP 8.0 or above compatibility issue -- Fix the space missing between the id and class attributes of the charts -- Fix compatibility issue with Ultimate Coupons for WooCommerce Free plugin -- Fix charts placed in accordion (tabs) that do not load properly -- Fix conflict with translation plugins - - - - -##### [Version 3.7.5](https://github.com/Codeinwp/visualizer/compare/v3.7.4...v3.7.5) (2022-03-17) - -- Fix display long hAxis text issue -- Improve features discoverability. - - - - -##### [Version 3.7.4](https://github.com/Codeinwp/visualizer/compare/v3.7.3...v3.7.4) (2022-02-07) - -- Fix multiple lazy load charts issue -- Tested up to WordPress 5.9 version - - - - -##### [Version 3.7.3](https://github.com/Codeinwp/visualizer/compare/v3.7.2...v3.7.3) (2022-01-19) - -- Fix multiple charts lazy rendering issue -- Upgrade jQuery UI core library CDN -- Fix console error when rendering multiple charts -- Keep only Line / Pie / bar / Table charts in free version - - - - -##### [Version 3.7.2](https://github.com/Codeinwp/visualizer/compare/v3.7.1...v3.7.2) (2021-12-20) - -- Add latest chartJS library support in the bar chart -- Fix horizontal axis showing date format in Bar chart -- Fix orientation rotate issue in mobile -- CSS performance improvements -- Improve JS performance - - - - -##### [Version 3.7.1](https://github.com/Codeinwp/visualizer/compare/v3.7.0...v3.7.1) (2021-11-17) - -- [Feat] Allow charts to render in dashboard areas via 3rd party plugins like Dashboard Widgets Suite -- [Fix] Changing Line Chart series style to dot on some series changes the order of tooltip information -- [Fix] Problem with X-Axis of Line chart on Safari if date/datetime is used as values -- [Fix] Multiple Visualizer charts causing rendering issues (blank page) -- [Fix] Charts are not resizing on landscape view on mobile -- [Fix] Notices/Warnings appear on the front-end if the user is not logged in for some charts -- [Fix] Radar/Spider Chart Tooltip in PRO does not show the data value - - - - -#### [Version 3.7.0](https://github.com/Codeinwp/visualizer/compare/v3.6.1...v3.7.0) (2021-10-07) - -#### Features -- Add option to enable/disable save the chart image -- Enhanced caching - -#### Fixes -- Fix undefined variable errors when using Gutenberg blocks -- Fix broken chart in iOS and Safari -- Fix custom refresh time support for DB chart -- Fix table placed inside accordion cannot be scrolled down (refreshes all the time) -- Fix pagination doesn't work in the library -- Fix HTML entities in JSON break Visualizer Table Chart -- Fix string type columns affect the target of the formatting applied to a number column - - - - -##### [Version 3.6.1](https://github.com/Codeinwp/visualizer/compare/v3.6.0...v3.6.1) (2021-08-04) - -- Fix broken dropdown style issue -- Add block support in widget area -- Add wp alpha color picker support -- Fix broken chart layout issue when add long title - - - - -#### [Version 3.6.0](https://github.com/Codeinwp/visualizer/compare/v3.5.1...v3.6.0) (2021-07-08) - -### Features -* Adds option to render chart as image -* Automatically render chart as image on AMP - -### Fixes -* [Chart Type: TABLE] When adding a blank header, the last column of the table disappears -* Styling is not applied to Chart Area for Table Chart ( Margin, Width, Height) -* Improve compatibility with Site Kit by Google -* Loading icon doesn't disappear until you edit the chart again [Line Chart] -* Broken chart if used inside an accordion (expand/collapse) - - - - -##### [Version 3.5.1](https://github.com/Codeinwp/visualizer/compare/v3.5.0...v3.5.1) (2021-05-25) - - -- Fix compatibility with 3rd party plugins which use the same js library as Visualizer [#801](https://github.com/Codeinwp/visualizer/issues/801) -- Fix fatal error when a new chart is created on PHP 8 [#795](https://github.com/Codeinwp/visualizer/issues/795) -- Chart doesn't get refreshed automatically when the JSON endpoint is used as the data source - -#### Features: -- Adds option to add schema.org Dataset for license and creator [#794](https://github.com/Codeinwp/visualizer/issues/794) - - - - -#### [Version 3.5.0](https://github.com/Codeinwp/visualizer/compare/v3.4.11...v3.5.0) (2021-04-28) - -#### Fixes -- Permissions tab disappears after a user modifies a chart's data -- Conflict with Edit Flow plugin -- Improve compatibility with Ninja Forms -- Fix for color for Minor Grid Lines not working - -#### Features -- Add support for key:value header to access JSON endpoints -- Option to download CSV data of the chart without the row with series type - - - -= 3.4.11 - 2021-02-16 = - -[Feat] Tested up to WP 5.6 - - -= 3.4.10 - 2020-12-09 = - -* [Fix] Compatibility with Composer 2.0 - - -= 3.4.9 - 2020-11-26 = - -* [Fix] Pie chart slices offset problem in the Gutenberg editor -* [Fix] Warning is thrown when remote file is used as source -* [Fix] Bubble chart legend position not working in the Gutenberg editor - - -= 3.4.8 - 2020-09-29 = - -* [Fix] Option to edit charts imported from JSON - - -= 3.4.7 - 2020-09-21 = - -* - [Fix] Import from JSON: Reordering columns only reorders the label, not the associated data -* - [Fix] numberFormat option doesn't apply in Bar charts when using annotations -* - [Fix] Google GEO chart is not rendering if 3rd data column is added - - -= 3.4.6 - 2020-08-11 = - -* - [Feat] Show chart ID in the chart library -* - [Fix] Compatibility with WP 5.5 -* - [Fix] Google charts: Series number format not applying in the Gutenberg editor -* - [Fix] Google Table chart does not display chart if boolean values are specified -* - [Fix] Duplicated enque for jsapi loader - - -= 3.4.5 - 2020-07-08 = - -* [Feat] New Google Table Charts -* [Feat] Option for lazy loading Google Charts -* [Feat] Option to easily copy chart shortcode code -* [Fix] Remove Inside the Chart option for the legend position for Google Pie charts - - -= 3.4.4 - 2020-06-16 = - -* [Feat] Option to download charts as .png images -* [Fix] Make UI more intuitive when a chart is missing in the editor -* [Fix] Clicking Copy chart shows the Copied message multiple times -* [Fix] Conflict with Modern Events Calendar plugin -* [Fix] Chart size ( width and height ) options not working for ChartJS charts -* [Fix] Resizing the window causes annotation-based charts to throw an error -* [Fix] Remove Inside the Chart option as legend position for Google Pie charts -* [Fix] Clicking Advanced Options panel breaks Bubble chart -* [Fix] Missing posts revisions on chart update - - -= 3.4.3 - 2020-05-04 = - -* [Feat] Improved chart creation UX -* [Feat] New option to control the number of charts per page in the Charts Library -* [Feat] New option for filtering/ordering charts in the Charts Library -* [Feat] Support for custom codes for the boolean data type in Table charts -* [Fix] Support for displaying apostrophes in charts -* [Fix] Visualizer button layout in the Classic block -* [Fix] Bulk activation of plugin aborts activation of subsequent plugins - - -= 3.4.2 - 2020-02-17 = - -* New Cypress tests for the Gutenberg block - - -= 3.4.1 - 2020-02-14 = - -* [Fix] Insert chart button in the classic block -* [Fix for Pro version] Import from chart did not work - - -= 3.4.0 - 2020-02-13 = - -* [Feat] Support for authentication for JSON import -* [Feat] New chart type: Bubble -* [Feat] Combine one-time import and schedule import into a single control for an online .csv file import -* [Feat] Add support for annotations and other roles -* [Feat] For every chart show the last updated date and any error that exists -* [Feat] Tested up to WP 5.3 -* [Fix] When new data is imported using csv/url, the manual editor still show old data -* [Fix] Having SCRIPT_DEBUG on causes issues in real time update of charts -* [Fix] Table chart: Error appears when trying to import from JSON -* [Fix] PHP Fatal error: Uncaught Error: Cannot unset string offsets -* [Fix] Long responsive table can overflow on smaller screens - - -= 3.3.4 - 2019-11-15 = - -* Fix issue with table chart not loading in the block editor - - -= 3.3.3 - 2019-11-12 = - -* Tested upto WordPress 5.3 - - -= 3.3.2 - 2019-10-03 = - -* Add support for Dataset schema -* Horizontal Axis formatting should apply to tooltips - - -### 3.3.1 - 2019-09-28 ### - -* Increase minimum requirement to PHP 5.6 -* Fixed issue with loading customization.js on multisites -* Fixed issue with manually editing a remotely loaded chart -* Fixed issues with cloning -* Fixed issues with ChartJS assigning default colors -* Fix security issues in block editor - - -### 3.3.0 - 2019-08-14 ### - -* Add support for ChartJS -* Add alpha color picker for supported charts -* Fix issue with some options of DataTable -* Include DataTable charts in block editor -* Fix issue with import from JSON not working with some sources -* Add menu and onboarding page -* Fix issue with frontend action checkboxes -* Improve UX in advanced settings - - -### 3.2.1 - 2019-05-05 ### - -* Fix issue with async loading of scripts - - -### 3.2.0 - 2019-05-03 ### - -* Add support for charts in AMP requests -* Add support to show charts from JSON/REST endpoints -* Fix loading of Google Visualization javascript files -* Add simple editors for editing chart data -* Tested up to WP 5.2 - - -### 3.1.3 - 2019-02-24 ### - -* Fix issue with changing column settings of the last column in table chart -* Add support for query language to get subset of data from Google Spreadsheet -* Fix conflict with jquery 3.3.x -* Migrated PHPExcel to PhpSpreadsheet -* Front end action 'print' should print the chart and fall back to printing the data -* Fix issue with table chart not showing in IE -* Fix issue with multiple instances of same chart not showing -* Fix issue with date type column does not work with Combo charts -* Tested with WP 5.1 - - -### 3.1.2 - 2018-12-06 ### - -* Fix bug "Warning: A non-numeric value encountered" -* Tested with WP 5.0 - - -### 3.1.1 - 2018-12-05 ### - -* Fix issue with Gutenberg support -* Fix issue with loading new Table chart -* Fix options that don't work correctly with some charts - - -### 3.1.0 - 2018-12-03 ### - -* Add Table chart -* Fix date format in sample files - - -### 3.0.12 - 2018-10-11 ### - -* Added filter to enable users to change schedule of charts. -* Fixed bug with line chart with timeofday column. -* Fixed bug with scheduled charts that sometimes did not show updated data. -* Javascript can be customized on a per user basis that will not be wiped out on update. - - -### 3.0.11 - 2018-08-15 ### - -* Fixed issue with the Series Settings options for the Table Chart -* Fixed issue with chart showing "Table has no columns" with remote sources - - -### 3.0.10 - 2018-07-20 ### - -* Fixed problem with chart reverting to the default values -* Fixed problem with Boolean column type -* Fixed problem with the Geo chart type not saving colors options - - -### 3.0.9 - 2018-07-12 ### - -* New chart title option for the back-end of the charts that don't allow a title on the front-end -* Store the png images of the charts in a global array that can be used in JS -* Added options for charts animations - - -### 3.0.8 - 2018-06-27 ### - -* Added revision support for the chart post type -* Added both % and Value to the Pie Slice -* Use the blog locale for Visualizer's options -* Fixed issue with data being fetched from the remote source every single time the chart was shown -* Fixed issue with scheduled charts not being updated if one of the scheduled charts is deleted - - -### 3.0.7 - 2018-03-26 ### - -* Adds insert button in chart library. -* Remove frontend assets where they are not needed. -* Improve non-English charts compatibility. -* Adds a filter to change charts locale. - - -### 3.0.6 - 2018-02-27 ### - -* Fix UTF-8 support while saving the data. -* Improve editing experience. -* Improves compatibility with Premium version. -* Adds chart button into TinyMCE editor. - - -### 3.0.5 - 2018-01-05 ### - -* Fix chart rendering bug in firefox. -* Fix review notification. - - -### 3.0.4 - 2017-11-27 ### - -* Fix for review message notification. - - -### 3.0.3 - 2017-11-16 ### - -* Adds compatibility with WordPress 4.9. - - -### 3.0.2 - 2017-10-10 ### - -* Fix dependency for composer dependencies. - - -### 3.0.1 - 2017-10-06 ### - -* Improved compatibility with various theme and plugins. -* Fix for chart type selection when creation from media popup. - - -### 3.0.0 - 2017-09-05 ### - -* Adds support manual configuration according to Google Visualization API. -* Improves compatibility with more features from the pro version. - - -### 2.2.0 - 2017-08-16 ### - -* Added custom number format for pie chart. -* Added frontend actions buttons ( Print, Export to CSV, Export to Excel, Copy) - - -### 2.1.9 - 2017-07-10 ### - -* Fixed display error with hex color. - - -### 2.1.8 - 2017-07-03 ### - -* Added chart title into library. -* Fixed SDK issues with dashboard widget. - - -### 2.1.7 ### - -* Updated sdk loading logic. - - - -### 2.1.4 ### -* Fixed issues with non-latin chars on CSV files to import. - -### 2.1.2 ### -* Fixed priority issue with wp_enqueue_media -* Added latest version of sdk - -### 2.1.1 ### -* Fixed charts bliking on some themes. - -### 2.1.0 ### -* Fixed geomap issue with apikey. -* Fixed responsive issues on tabbed interface and page builders. -* Added compatibility with premium import from posts/page feature. - -### 2.0.4 ### -* Fixed resize issue in the library page. - -### 2.0.0 ### -* Improved design and layout to support multiple datasources. -* Added new integrations in the Pro version. -* Added opt-in for tracking. - -### 1.7.6 ### -* Fixed issue when using the same shortcode multiple times on the same page. - -### 1.7.5 ### -* Removed footer banner upsell -* Fixed series settings issue -* Fixed issue with comas being used in numbers - -### 1.7.2 ### -* Improved charts responsive system - -### 1.7.1 ### -* Fixed grid lines error links - -### 1.7.0 ### -* Fixed responsive issues -* Fixed issues with zero margin values -* Fixed import issue - -### 1.6.6 ### -* Fixed charts resizing on tabbed content - -### 1.6.5 ### -* Fixed responsive issue -* Fixed no axis text color for line and bar charts - - -### 1.6.0 ### -* Fixed security issue when importing charts -* Removed pointer for the pro version -* Fixed charts import from media library -* Added support to show legend on the left side - - -### 1.5.6 ### -* Added support for 3 more chart types -* Fixed issue with charts not saving - - -### 1.5.5 ### -* Added export for charts -* Enable default value for focus target. Fixed issue with hover which was not working on some machines. - - -### 1.5.4 ### -* Added free search text over graphs - -### 1.5.2 ### -* Added step2 and 3 into step 1 - -### 1.5.1 ### -* Fixed bug with from web button - -### 1.5 ### -* Added support for live editor -* Added support for importing data from other charts -* Added filter for chart settings -* Fixed bug when zero was not working on the charts - -### 1.4.2.3 ### -* Implemented ability to edit horizontal and vertical axis number format for bar and column charts - -### 1.4.2.2 ### -* Added ability to pass a class for chart wrapper div -* Added proper label for custom post type - -### 1.4.2.1 ### -* Fixed issue with download_url function which not exists at front end -* Added functionality which prevents direct access to the plugin folder - -### 1.4.2 ### -* Fixed remote CSV uploading issue when allow_url_fopen option is disabled in php.ini -* Replaced flattr image on widget and added donate link to plugin meta data -* Added notification message at library page when allow_url_fopen option is disabled - -### 1.4.1.1 ### -* Removed CSV parser escape constant to prevent warnings which appears when PHP 5.2.x or less is used - -### 1.4.1 ### -* Fixed issue which prevents the plugin working on SSL backend -* Fixed issue with CSV file uploading in IE and other browsers -* Fixed issue with empty series, which appears due to leading space in a source file -* Added ability to define custom delimiter, enclosure and escape variables for CSV parsing - -### 1.4 ### -* Implemented aggregation target and selection mode options for candlestick chart -* Implemented focus target and data opacity for columnar chars -* Implemented data opacity and interpolate nulls settings for line chart -* Implemented ability to edit tooltip settings -* Implemented new settings for linear charts like selection mode and aggregation target -* Implemented area and point opacity settings for area chart -* Implemented new settings for pie chart like pie hole, start angle and slice offset -* Implemented ability to select a color for chart title and legend items -* Fixed number formatting settings for linear charts, from now it works only for axis labels -* Reworked general settings section by moving title and font settings into separate groups - -### 1.3.0.2 ### -* Replaced links to github wiki - -### 1.3.0.1 ### -* Added Flattr button - -### 1.3.0 ### -* Implemented ability to set number and date formatters -* Implemented ability to select transparent background for a chart -* Fixed JS bugs which appear when post type editor support is disabled -* Fixed issue with NULL values for numeric series -* Fixed invalid charts rendering at "Add Media" library -* Fixed compatibility issue with another Google API related plugins -* Added "rate the plugin" box - -### 1.2.0 ### -* Implemented minor grid lines settings. -* Implemented view window settings. -* Horizontal and vertical axes settings were split into separate groups. - -### 1.1.4 ### -* Bug with float values has been fixed. - -### 1.1.3 ### -* Issue with "fseek warning" for Google Spreadsheet document source, was fixed. - -### 1.1.2 ### -* Compatibility issues with WordPress version 3.6 has been fixed. - -### 1.1.1 ### -* Active type tab in the charts library was fixed. -* Library styles were updated. - -### 1.1.0 ### -* Auto population was added for remote CSV file source. -* Ability to hook chart series and data was implemented. -* Ability to upload CSV files from web was implemented. - -### 1.0.1 ### -* The bug with CSV file uploading was fixed. - -### 1.0.0 ### -* The first version of what wil be the best wp charts plugin. +For the full changelog, please see [CHANGELOG.md](https://github.com/Codeinwp/visualizer/blob/development/CHANGELOG.md).