'chartjs',
'type' => 'line',
'parse_in' => 'rows',
'labels' => true,
'shared' => false,
'subtitle' => '',
'y_title' => '',
'y_units' => '',
'y_min' => false,
'y_min_value' => 0,
'x_title' => '',
'x_units' => '',
'height' => 600,
'legend' => true,
'source' => '',
'source_url' => '',
'data' => [],
'set_names' => [],
'data_point_colors' => false,
'mean_point' => true,
'sample_points' => false,
'constrain_y_axis' => false,
'include_source' => false,
];
public $get_chart_default_args = [
'show' => 'chart',
'width' => 'responsive',
'share' => '',
];
public $parse_options = [
'columns',
'rows',
];
public $options_set;
public $plugin_url;
public $is_iframe = false;
public $instance = 1;
public $iframe_csp_nonce = '';
public $settings = [
'library' => 'chartjs',
'show_library' => 'no',
'performance' => 'default',
'image_multiplier' => '2',
'image_width' => '900',
'default_height' => '400',
'embeds' => '',
'defer_rendering' => 'enabled',
'default_theme' => '_default',
'locale' => 'en-US',
'csv_delimiter' => ',',
];
public $csv_delimiters = [
',' => 'Comma',
"\t" => 'Tab',
' ' => 'Space',
';' => 'Semicolon',
];
public $library_class;
public $icon = 'PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjY0IDY0IDI0IDI0IiB3aWR0aD0iMjRweCIgaGVpZ2h0PSIyNHB4Ij4KICA8cGF0aCBmaWxsPSJjdXJyZW50Q29sb3IiIGQ9Ik0gNzYuNjU3IDY1LjUgTCA3Ni42NTcgNzUuMzQ0IEwgODYuNSA3NS4zNDQgQyA4Ni41IDY5LjkwOSA4Mi4wOTEgNjUuNSA3Ni42NTcgNjUuNSBaIE0gNzQuNjg4IDc2LjMyOCBMIDc0LjY4OCA2Ni44MzMgQyA2OS41NTcgNjcuMTc0IDY1LjUgNzEuNDM5IDY1LjUgNzYuNjU3IEMgNjUuNSA4Mi4wOTEgNjkuOTA5IDg2LjUgNzUuMzQzIDg2LjUgQyA4MC41NjIgODYuNSA4NC44MjggODIuNDQzIDg1LjE2NyA3Ny4zMTMgTCA3NC42ODggNzcuMzEzIEwgNzQuNjg4IDc2LjMyOCBaIi8+Cjwvc3ZnPg==';
public $logo = 'PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPHN2ZyBpZD0idXVpZC03OGNkNDE3YS0zN2NjLTQ0NTAtODU2ZC00MzBlMTliYjhmOTEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDQzNyAzNzAuMDU2Ij4KICA8ZyBpZD0idXVpZC1hMDM3MTk0ZS1hN2QzLTQ4NTktOGEwZi0wMmIzMWI0YmQ3MzEiIHRyYW5zZm9ybT0ibWF0cml4KDEsIDAsIDAsIDEsIDM4LjQwMzk5OSwgNTUuOTM3MDA0KSI+CiAgICA8cmVjdCB4PSIyODEuNDAxIiB5PSIxNDguNTExIiB3aWR0aD0iODguNzEiIGhlaWdodD0iMTQxLjQ1IiBzdHlsZT0ic3Ryb2tlLXdpZHRoOiAxOyIvPgogICAgPHJlY3QgeD0iLTkuOTE5IiB5PSIyNjQuODIxIiB3aWR0aD0iODguNzEiIGhlaWdodD0iMjUuMTMiIHN0eWxlPSJzdHJva2Utd2lkdGg6IDE7Ii8+CiAgICA8cmVjdCB4PSItOS45MTkiIHk9Ii0zMS43NzkiIHdpZHRoPSI4OC43MSIgaGVpZ2h0PSIyODQuMjEiIHN0eWxlPSJzdHJva2Utd2lkdGg6IDE7Ii8+CiAgICA8cmVjdCB4PSIyODEuNDAxIiB5PSItMzEuNzc5IiB3aWR0aD0iODguNzEiIGhlaWdodD0iMTY3Ljg3IiBzdHlsZT0ic3Ryb2tlLXdpZHRoOiAxOyIvPgogICAgPHJlY3QgeD0iMTg2LjI4MSIgeT0iMTk0LjU1MSIgd2lkdGg9IjgyLjc5IiBoZWlnaHQ9Ijk1LjQiIHN0eWxlPSJzdHJva2Utd2lkdGg6IDE7Ii8+CiAgICA8cmVjdCB4PSI5MS4wODEiIHk9IjIzNS40MTEiIHdpZHRoPSI4Mi45NCIgaGVpZ2h0PSI1NC41NSIgc3R5bGU9InN0cm9rZS13aWR0aDogMTsiLz4KICAgIDxwb2x5Z29uIHBvaW50cz0iMjgxLjQwMSAtMzEuNzc5IDE4MC4xMDEgNjUuMTExIDE4MC4xMDEgMTc2LjcwMSAyODEuNDAxIDc5LjcxMSAyODEuNDAxIC0zMS43NzkiIHN0eWxlPSJzdHJva2Utd2lkdGg6IDE7Ii8+CiAgICA8cG9seWdvbiBwb2ludHM9Ijc4LjgwMSA3OS43MTEgMTgwLjEwMSAxNzYuNzAxIDE4MC4xMDEgNjUuMTExIDc4LjgwMSAtMzEuNzc5IDc4LjgwMSA3OS43MTEiIHN0eWxlPSJzdHJva2Utd2lkdGg6IDE7Ii8+CiAgPC9nPgo8L3N2Zz4=';
private $admin;
private $parse;
private $block;
private $fs;
private $libraries = [
'chartjs' => 'Chart.js',
];
/**
* Constructor
*/
public function __construct() {
// Initiate Freemius first so it loads as early as possible
// Must run synchronously here at file load so Freemius detects m-chart.php as the plugin file
$this->freemius();
$this->plugin_url = $this->plugin_url();
add_action( 'init', [ $this, 'init' ] );
add_action( 'plugins_loaded', [ $this, 'plugins_loaded' ] );
add_action( 'save_post', [ $this, 'save_post' ] );
// Doing this early as possible because it sets is_iframe which we might need to use for other things
add_action( 'template_redirect', [ $this, 'template_redirect' ], 0 );
add_action( 'm_chart_update_post_meta', [ $this, 'm_chart_update_post_meta' ], 10, 2 );
// Doing this before the default so it's already done before anything else
add_filter( 'm_chart_get_chart_image_tag', [ $this, 'm_chart_get_chart_image_tag' ], 9, 3 );
add_filter( 'the_content', [ $this, 'the_content' ] );
add_filter( 'm_chart_image_support', [ $this, 'm_chart_image_support' ], 10, 2 );
add_filter( 'm_chart_instant_preview_support', [ $this, 'm_chart_instant_preview_support' ], 10, 2 );
add_filter( 'm_chart_library_class', [ $this, 'm_chart_library_class' ], 10, 2 );
add_shortcode( 'm-chart', [ $this, 'chart_shortcode' ] );
// Backwards-compatible alias for content created before the rename
add_shortcode( 'chart', [ $this, 'chart_shortcode' ] );
// Initiate the block class
$this->block();
}
/**
* Initiate and return the Freemius SDK instance
*
* Lazily builds the instance on first call and caches it
* The SDK lives in components/external/freemius rather than the usual plugin root /freemius
* M Chart core (m-chart) is the free version so is_premium is false
* M Chart Pro (m-chart-pro) is a separate plugin sold under the same Freemius product via parallel activation
*
* @return Freemius the Freemius SDK instance for M Chart
*/
public function freemius() {
if ( isset( $this->fs ) ) {
return $this->fs;
}
require_once __DIR__ . '/external/freemius/start.php';
$this->fs = fs_dynamic_init( [
'id' => '30258',
'slug' => 'm-chart',
'premium_slug' => 'm-chart-pro',
'type' => 'plugin',
'public_key' => 'pk_b1eafc082abd31243874041aaaaab',
'is_premium' => false,
'has_premium_version' => true,
'has_addons' => false,
'has_paid_plans' => true,
'is_org_compliant' => true,
'parallel_activation' => [
'enabled' => true,
'premium_version_basename' => 'm-chart-pro/m-chart-pro.php',
],
'menu' => [
// Anchor on the Charts CPT itself so the Freemius pages are plugin-level (m-chart-account, m-chart-pricing)
// The CPT slug contains ".php?" so get_slug() falls back to the m-chart affix rather than an m-chart-settings- prefix
'slug' => 'edit.php?post_type=m-chart',
// Surface the Freemius Account page as a submenu link under the Charts menu
'account' => true,
'contact' => false,
'support' => false,
],
] );
$this->freemius_filters();
do_action( 'm_chart_fs_loaded' );
return $this->fs;
}
/**
* Register Freemius filters that brand the pricing and opt-in screens
*
* pricing/css_path points Freemius at our stylesheet which is enqueued on the pricing page after its own CSS
* pricing/show_annual_in_monthly shows the real annual price rather than the monthly equivalent of the annual plan
*
* The css_path and plugin_icon paths run through Freemius's fs_asset_url which maps a path to a URL by stripping WP_PLUGIN_DIR
* __DIR__ resolves to the plugin's real path which differs from WP_PLUGIN_DIR when the plugin is symlinked
* plugin_basename normalizes it back to the plugins-dir-relative path so the URL resolves in every install
*
* The opt-in / connect screen has no css_path filter so we inline our stylesheet on the connect/before action instead
* It prints after Freemius's own connect.css so equal-specificity overrides win on source order
*/
private function freemius_filters() {
$this->fs->add_filter( 'pricing/css_path', function () {
return WP_PLUGIN_DIR . '/' . plugin_basename( __DIR__ . '/css/m-chart-freemius-pricing.css' );
} );
// Re-point the pricing page contact links at M Chart's own support/contact pages
// Freemius wraps the pricing output in this filter so the inline script is scoped to that screen
$this->fs->add_filter( 'templates/pricing.php', function ( $html ) {
$script_path = __DIR__ . '/js/m-chart-freemius-pricing.js';
if ( ! file_exists( $script_path ) ) {
return $html;
}
return $html
. "\n";
} );
$this->fs->add_filter( 'plugin_icon', function () {
return WP_PLUGIN_DIR . '/' . plugin_basename( __DIR__ . '/images/m-chart-logo-square-rounded.svg' );
} );
$this->fs->add_filter( 'pricing/show_annual_in_monthly', '__return_false' );
$this->fs->add_action( 'connect/before', function () {
$css_path = __DIR__ . '/css/m-chart-freemius-connect.css';
if ( ! file_exists( $css_path ) ) {
return;
}
echo "\n";
} );
// Swap Freemius's casual interjection headings for professional wording
// These are admin-notice titles keyed by id and the trailing "!" / "..." is appended by the SDK so it carries over
// Values are left unescaped since Freemius escapes them at the output site
// Deferred to init since freemius() runs at construct and the __() calls would load the text domain before WordPress is ready
add_action( 'init', function () {
fs_override_i18n( [
'yee-haw' => __( 'Success', 'm-chart' ),
'woot' => __( 'Success', 'm-chart' ),
'right-on' => __( 'Success', 'm-chart' ),
'oops' => __( 'Error', 'm-chart' ),
'hmm' => __( 'Heads up', 'm-chart' ),
'hey' => __( 'Hello', 'm-chart' ),
], $this->slug );
} );
}
/**
* Admin object accessor
*/
public function admin() {
if ( ! $this->admin ) {
require_once __DIR__ . '/class-m-chart-admin.php';
$this->admin = new M_Chart_Admin();
}
return $this->admin;
}
/**
* Block object accessor
*/
public function block() {
if ( ! $this->block ) {
require_once __DIR__ . '/class-m-chart-block.php';
$this->block = new M_Chart_Block();
}
return $this->block;
}
/**
* Library object accessor
*/
public function library( $library = false ) {
if ( ! $library ) {
$library = $this->get_library();
}
return apply_filters( 'm_chart_library_class', $this->library_class, $library );
}
/**
* Parse object accessor
*/
public function parse() {
if ( ! $this->parse ) {
require_once __DIR__ . '/class-m-chart-parsed-data-point.php';
require_once __DIR__ . '/class-m-chart-parse.php';
$this->parse = new M_Chart_Parse();
}
return $this->parse;
}
/**
* Do init stuff
*/
public function init() {
// Register the units taxonomy
register_taxonomy(
$this->slug . '-units',
[ $this->slug ],
[
'hierarchical' => true,
'labels' => [
'name' => esc_html__( 'Chart Units', 'm-chart' ),
'singular_name' => esc_html__( 'Chart Unit', 'm-chart' ),
'search_items' => esc_html__( 'Search Chart Units', 'm-chart' ),
'all_items' => esc_html__( 'All Chart Units', 'm-chart' ),
'parent_item' => esc_html__( 'Parent Chart Unit', 'm-chart' ),
'parent_item_colon' => esc_html__( 'Parent Chart Unit:', 'm-chart' ),
'edit_item' => esc_html__( 'Edit Chart Unit', 'm-chart' ),
'update_item' => esc_html__( 'Update Chart Unit', 'm-chart' ),
'add_new_item' => esc_html__( 'Add New Chart Unit', 'm-chart' ),
'new_item_name' => esc_html__( 'Chart Unit Name', 'm-chart' ),
'menu_name' => esc_html__( 'Chart Units', 'm-chart' ),
],
'show_ui' => true,
'query_var' => true,
'rewrite' => [
'slug' => $this->slug . '-units',
],
]
);
// Register the library taxonomy
register_taxonomy(
$this->slug . '-library',
[ $this->slug ],
[
'hierarchical' => false,
'show_ui' => false,
'query_var' => true,
'rewrite' => [
'slug' => $this->slug . '-library',
],
]
);
// Register the charts custom post type
register_post_type(
$this->slug,
[
'labels' => [
'name' => esc_html__( 'Charts', 'm-chart' ),
// Top-level admin menu label defaults to 'name' so we set it explicitly to brand the menu as M Chart
'menu_name' => esc_html__( 'M Chart', 'm-chart' ),
// First submenu item label defaults to menu_name so we set it back to Charts (parent reads M Chart, list reads Charts)
'all_items' => esc_html__( 'Charts', 'm-chart' ),
'singular_name' => esc_html__( 'Chart', 'm-chart' ),
'add_new' => esc_html__( 'Add Chart', 'm-chart' ),
'add_new_item' => esc_html__( 'Add Chart', 'm-chart' ),
'edit' => esc_html__( 'Edit', 'm-chart' ),
'edit_item' => esc_html__( 'Edit Chart', 'm-chart' ),
'new_item' => esc_html__( 'New Chart', 'm-chart' ),
'view' => esc_html__( 'View', 'm-chart' ),
'view_item' => esc_html__( 'View Chart', 'm-chart' ),
'search_items' => esc_html__( 'Search Charts', 'm-chart' ),
'not_found' => esc_html__( 'No Charts found', 'm-chart' ),
'not_found_in_trash' => esc_html__( 'No Charts found in the Trash', 'm-chart' ),
],
'register_meta_box_cb' => is_admin() ? [ $this->admin(), 'meta_boxes' ] : null,
'public' => true,
'show_in_rest' => true,
'hierarchical' => false,
'exclude_from_search' => false,
'menu_position' => 9,
'menu_icon' => 'data:image/svg+xml;base64,' . $this->logo,
'query_var' => true,
'can_export' => true,
'has_archive' => 'charts',
'description' => esc_html__( 'Manage data sets and display them as charts in WordPress.', 'm-chart' ),
'rewrite' => [
'slug' => 'chart',
],
'supports' => [
'author',
'title',
'excerpt',
'comments',
],
'taxonomies' => [
'category',
'post_tag',
$this->slug . '-units',
],
]
);
// Register the graphing library scripts
wp_register_script(
'chartjs',
$this->plugin_url . '/components/external/chartjs/chart.js',
[],
$this->version
);
wp_register_script(
'chartjs-helper',
$this->plugin_url . '/components/js/m-chart-chartjs-helper.min.js',
[ 'chartjs' ],
$this->version
);
wp_register_script(
'chartjs-datalabels',
$this->plugin_url . '/components/external/chartjs/chartjs-plugin-datalabels.js',
[ 'chartjs' ],
$this->version
);
wp_register_script(
'chartjs-treemap',
$this->plugin_url . '/components/external/chartjs/chartjs-chart-treemap.min.js',
[ 'chartjs' ],
$this->version
);
wp_register_script(
'chartjs-boxplot',
$this->plugin_url . '/components/external/chartjs/chartjs-chart-boxplot.min.js',
[ 'chartjs' ],
$this->version
);
// Add endpoint needed for iframe embed support
add_rewrite_endpoint( 'embed', EP_PERMALINK );
// Check if we need to run any upgrades
$current_version = get_site_option( 'm_chart_version' );
if ( version_compare( $current_version, '1.7', 'lt' ) ) {
$this->upgrade_to_1_7();
}
if ( version_compare( $current_version, '1.7.4', 'lt' ) ) {
$this->upgrade_to_1_7_4();
}
}
/**
* Do plugins loaded stuff
*/
public function plugins_loaded() {
load_plugin_textdomain( 'm-chart', false, plugin_basename( dirname( __FILE__ ) ) . '/languages/' );
}
/**
* WP VIP's CDN was breaking the chart library's ability to handle embedded SVGs so this should circumvent that
* If you wanted to say, watermark your charts, SVGs suddenly become very important
*
* @param string $path option additional path to be used (e.g. components)
*
* @return string URL to the plugin directory with path if parameter was passed
*/
public function plugin_url( $path = '' ) {
if ( is_admin() ) {
$url_base = parse_url( admin_url() );
} else {
$url_base = parse_url( home_url() );
}
$url_path = parse_url( plugins_url( $path, __DIR__ ) );
// Check for a port value if one exists we make sure it's honored
$port = '';
if ( isset( $url_base['port'] ) && 80 != $url_base['port'] ) {
$port = ':' . $url_base['port'];
}
$scheme = $url_base['scheme'] ?? 'https';
$host = $url_base['host'] ?? '';
$path = $url_path['path'] ?? '';
$query = $url_path['query'] ?? '';
return $scheme . '://' . $host . $port . preg_replace( '#/$#', '', $path ) . ( '' === $query ? '' : '?' . $query );
}
/**
* Get chart post meta
*
* @param int $post_id WP post ID of the post you want post meta from
* @param string $field optional field to be returend instead of all post meta
*
* @return array $post_meta the meta for a give post ID
*/
public function get_post_meta( $post_id, $field = false ) {
$raw_post_meta = get_post_meta( $post_id, $this->slug, true );
$post_meta = $raw_post_meta;
$defaults = $this->chart_meta_fields;
// Make sure the correct library is set in the default chart meta fields
if ( ! $post_meta ) {
// get_current_screen() is only defined in admin context
// This guards the call so REST and front-end contexts don't fatal
$current_screen = is_admin() && function_exists( 'get_current_screen' )
? get_current_screen()
: null;
// If we're we're adding a new chart and a library is specified in the get vars we use it
if (
is_admin()
&& null != $current_screen
&& 'post' == $current_screen->base
&& 'add' == $current_screen->action
&& isset( $_GET['library'] )
&& $this->is_valid_library( $_GET['library'] )
) {
$defaults['library'] = $_GET['library'];
} else {
$defaults['library'] = $this->get_library();
}
}
// Default chart height comes from the plugin settings so new charts honor the site default
// Saved charts already have a height so wp_parse_args leaves theirs alone
$defaults['height'] = (int) $this->get_settings( 'default_height' );
$post_meta = wp_parse_args( $post_meta, $defaults );
// Theme default is based off of an option so we'll handle that here
if ( ! isset( $post_meta['theme'] ) ) {
$settings = $this->get_settings();
$post_meta['theme'] = $settings['default_theme'];
}
// If there's no subtitle set we'll set an empty one
if ( ! isset( $post_meta['subtitle'] ) ) {
$post_meta['subtitle'] = '';
}
// If there's no y min value set we'll set it to 0
if ( ! isset( $post_meta['y_min_value'] ) ) {
$post_meta['y_min_value'] = 0;
}
// If the data has the old legacy format we need to update it
if ( isset( $post_meta['data'] ) && ! isset( $post_meta['data']['sets'] ) ) {
$data = $post_meta['data'];
$post_meta['data'] = [];
$post_meta['data']['sets'][] = $data;
}
// If there's no set_names value set we'll set it to an empty array
if ( ! isset( $post_meta['set_names'] ) ) {
$post_meta['set_names'] = [];
}
$post_meta = apply_filters( 'm_chart_get_post_meta', $post_meta, $raw_post_meta, $post_id );
if ( $field && isset( $post_meta[ $field ] ) ) {
return $post_meta[ $field ];
} elseif ( $field ) {
return null;
}
return $post_meta;
}
/**
* Update the post meta and set unit terms if appropriate
*
* @param int $post_id WP post ID of the post you want to attach post meta to
* @param array $meta an array of the post meta you want to attach to the post
*/
public function update_post_meta( $post_id, $meta ) {
// Make sure the meta is formatted correctly and validated
$parsed_meta = $this->validate_post_meta( $meta );
// Set unit terms
$terms = [];
if ( '' != $parsed_meta['y_units'] ) {
$terms[] = $parsed_meta['y_units'];
}
if ( '' != $parsed_meta['x_units'] ) {
$terms[] = $parsed_meta['x_units'];
}
wp_set_object_terms( $post_id, $terms, $this->slug . '-units' );
// Set library as a post_tag
wp_set_object_terms( $post_id, $parsed_meta['library'], $this->slug . '-library' );
// Save meta to the post
update_post_meta( $post_id, $this->slug, $parsed_meta );
do_action( 'm_chart_update_post_meta', $post_id, $parsed_meta, $meta );
}
/**
* Parses a $meta array and returns a cleaned and validated array
*
* @param array $meta an array of the post meta you want to attach to the post
*
* @return array of cleaned/validated post meta
*/
public function validate_post_meta( $meta ) {
// Need to set checkboxes before checking or they can't be deselected
$chart_meta['labels'] = false;
$chart_meta['y_min'] = false;
$chart_meta['legend'] = false;
$chart_meta['data_point_colors'] = false;
$chart_meta['mean_point'] = false;
$chart_meta['sample_points'] = false;
$chart_meta['constrain_y_axis'] = false;
$chart_meta['include_source'] = false;
// Filter values so we know the data is clean
foreach ( $this->chart_meta_fields as $field => $default ) {
if ( isset( $meta[ $field ] ) ) {
if ( 'source_url' == $field && '' != $meta[ $field ] ) {
$chart_meta[ $field ] = esc_url_raw( $meta[ $field ] );
} elseif ( 'data' == $field ) {
$chart_meta[ $field ]['sets'] = $meta[ $field ];
} elseif ( 'set_names' == $field ) {
$chart_meta[ $field ] = array_values( $meta[ $field ] );
} elseif ( in_array( $field, [ 'labels', 'y_min', 'legend', 'data_point_colors', 'mean_point', 'sample_points', 'constrain_y_axis', 'include_source' ] ) ) {
$chart_meta[ $field ] = (bool) $meta[ $field ];
} elseif ( 'height' == $field ) {
$chart_meta[ $field ] = absint( $meta[ $field ] );
if ( $chart_meta[ $field ] > 1500 ) {
$chart_meta[ $field ] = 1500;
} elseif ( $chart_meta[ $field ] < 300 ) {
$chart_meta[ $field ] = 300;
}
} elseif ( 'y_min_value' == $field ) {
$chart_meta[ $field ] = floatval( $meta[ $field ] );
} else {
$chart_meta[ $field ] = wp_filter_nohtml_kses( $meta[ $field ] );
}
} elseif ( ! isset( $chart_meta[ $field ] ) ) {
// Fall back on the default value if there wasn't one in the given meta
$chart_meta[ $field ] = $default;
}
}
// The theme meta it isn't included in the chart_meta_fields class var so we handle it here
if ( isset( $meta['theme'] ) && preg_match( '#^[a-zA-Z0-9-_]+$#', $meta['theme'] ) ) {
$chart_meta['theme'] = $meta['theme'];
}
// If the data value is not an array we asume it is JSON encoded (i.e. from Jspreadsheet CE)
if ( ! is_array( $chart_meta['data']['sets'] ) && '' != $chart_meta['data']['sets'] ) {
$decoded = json_decode( stripslashes( $chart_meta['data']['sets'] ) );
$chart_meta['data']['sets'] = is_array( $decoded ) ? $decoded : [];
}
// Validate the data array
foreach ( $chart_meta['data']['sets'] as $key => $data ) {
$chart_meta['data'][ $key ] = $this->validate_data( $data );
}
// Allow plugins to validate their own custom meta
$chart_meta = apply_filters( 'm_chart_validate_post_meta', $chart_meta, $meta );
return $chart_meta;
}
/**
* Runs all of the raw data array values through wp_filter_nohtml_kses
*
* @param array $data an array of data as passed by the user
*
* @return array of validated data
*/
function validate_data( $data ) {
if ( ! is_array( $data ) ) {
return wp_filter_nohtml_kses( $data );
}
foreach ( $data as $key => $value ) {
if ( is_array( $value ) ) {
$data[ $key ] = $this->validate_data( $value );
} else {
$value = $value ?? '';
$data[ $key ] = wp_filter_nohtml_kses( $value );
}
}
return $data;
}
/**
* Hook to save_post action and save chart related post meta
*
* @param int $post_id WP post ID of the post
*/
public function save_post( $post_id ) {
// We do this in the main class because otherwise it won't get hooked soon enough
$this->admin()->save_post( $post_id );
}
/**
* Returns a chart
*
* @param int $post_id WP post ID of the chart you want
* @param array $args an array of args
*
* @return string HTML and Javascript needed to display a chart (or if appropriate an HTML image tag)
*/
public function get_chart( $post_id = null, $args = [] ) {
if ( ! $post_id ) {
$post_id = get_the_ID();
}
do_action( 'm_chart_get_chart_start', $post_id, $args );
// Normalize historic usage of 'img' argument
if ( isset( $args['img'] ) && 'yes' == $args['img'] ) {
$args['show'] = 'image';
unset( $args['img'] );
}
$args = wp_parse_args( $args, $this->get_chart_default_args );
$library = $this->get_post_meta( $post_id, 'library' );
// If it's not a valid library we don't proceed
if ( ! $this->is_valid_library( $library ) ) {
return;
}
// Make sure we isntantiate the library so any library specific filters/setup get run
$this->library( $library );
// If they want the table of data we'll return that
if ( 'table' == $args['show'] ) {
return $this->build_table( $post_id );
}
// If they want the image version or the request is happening from a feed we return the image tag
if (
'image' == $args['show']
|| is_feed()
|| $this->is_amp_endpoint()
|| apply_filters( 'm_chart_show_image', false, $post_id, $args )
) {
$image = $this->get_chart_image( $post_id );
// Default behavior is to return the full size image but with the width/height values halved
// This should result in an image that looks nice on retina or better screens
$image['width'] = $image['width'] / 2;
$image['height'] = $image['height'] / 2;
$image = apply_filters( 'm_chart_get_chart_image_tag', $image, $post_id, $args );
$classes = $this->slug . ' ' . $this->slug . '-' . $post_id;
if ( $this->is_amp_endpoint() ) {
ob_start();
?>
plugin_url . '/components/css/m-chart-frontend.css', [], $this->version ); ?>
get_settings();
if (
! is_admin()
&& 'enabled' == $settings['embeds']
&& ! $this->is_iframe
) {
return $this->get_chart_iframe( $post_id, $args );
}
// If we haven't enqueued the right library yet lets do it
if ( ! wp_script_is( $library, 'enqueued' ) ) {
wp_enqueue_script( $library );
wp_enqueue_script( 'chartjs-helper' );
}
// Ship the visually hidden rule the screen reader data table relies on
// so it never depends on the active theme defining .screen-reader-text
wp_enqueue_style( 'm-chart-frontend', $this->plugin_url . '/components/css/m-chart-frontend.css', [], $this->version );
$template = __DIR__ . '/templates/' . $library . '-chart.php';
ob_start();
do_action( 'm_chart_get_chart_begin', $post_id, $args );
require apply_filters( 'm_chart_chart_template', $template, $library, $post_id );
do_action( 'm_chart_get_chart_end', $post_id, $args );
$this->instance++;
return ob_get_clean();
}
/**
* Returns a charts data as an HTML table
*
* @param int $post_id WP post ID of the chart you want
*
* @return string HTML table
*/
public function build_table( $post_id ) {
$post = get_post( $post_id );
$post_meta = $this->get_post_meta( $post_id );
$library = m_chart()->get_post_meta( $post->ID, 'library' );
$table = '';
$multiple = count( $post_meta['data']['sets'] ) > 1 ? true : false;
foreach ( $post_meta['data']['sets'] as $set => $data ) {
$classes = $this->slug . '-table ' . $this->slug . '-table-' . $post_id . '-' . $set;
m_chart()->parse()->parse_data( $data, $post_meta['parse_in'] );
$template = __DIR__ . '/templates/table.php';
ob_start();
require apply_filters( 'm_chart_table_template', $template, $library, $post->ID );
$table .= ob_get_clean();
}
return $table;
}
/**
* Return an array of image values for a chart
*
* @param int $post_id WP post ID of the chart you want an image for
*
* @return array an array of image values url, width, height, etc...
*/
public function get_chart_image( $post_id ) {
$settings = $this->get_settings();
// If we aren't generating images we'll return false
if ( 'default' != $settings['performance'] ) {
return false;
}
if ( ! $thumbnail_id = get_post_meta( $post_id, '_thumbnail_id', true ) ) {
return false;
}
if ( ! $thumbnail = wp_get_attachment_image_src( $thumbnail_id, 'full' ) ) {
return false;
}
return [
'url' => $thumbnail[0],
'file' => basename( $thumbnail[0] ),
'width' => $thumbnail[1],
'height' => $thumbnail[2],
'name' => get_the_title( $thumbnail_id ),
];
}
/**
* Filter the m_chart_get_chart_image_tag hook and return a plaecholder if appropriate
*
* @param array|bool an array of image values or false if no image could be found
* @param int $post_id WP post ID of the chart you want an image for
*
* @return array an array of image values url, width, height, etc...
*/
public function m_chart_get_chart_image_tag( $img, $post_id ) {
if ( $img ) {
return $img;
}
$url = $this->plugin_url . '/components/images/chart-placeholder.png';
return [
'url' => $url,
'file' => basename( $url ),
'width' => 640,
'height' => 480,
'name' => get_the_title( $post_id ),
];
}
/**
* Filter the the_content hook and return chart code if this is a chart
*
* @param string $content content from the current post
*
* @return string original content or chart code
*/
public function the_content( $content ) {
// Make sure we're dealing with a chart
if ( get_post_type() != $this->slug ) {
// We aren't dealing with a chart so we'll just stop here
return $content;
}
// Call the get_chart method and let it do it's thing
return $this->get_chart();
}
/**
* Hook to the m_chart_image_support filter and indicate that Chart.js supports images
*
* @param string $supports_images yes/no whether the library supports image generation
* @param string $library the library in question
*
* @return string yes/no whether the library supports images
*/
public function m_chart_image_support( $supports_images, $library ) {
if ( $library != $this->chart_meta_fields['library'] ) {
return $supports_images;
}
return 'yes';
}
/**
* Hook to the m_chart_instant_preview_support filter and indicate that Chart.js supports instant previews
*
* @param string $supports_images yes/no whether the library supports instant previews
* @param string $library the library in question
*
* @return string yes/no whether the library supports instant previews
*/
public function m_chart_instant_preview_support( $supports_instant_preview, $library ) {
if ( $library != $this->chart_meta_fields['library'] ) {
return $supports_instant_preview;
}
return 'yes';
}
/**
* Hook to the m_chart_library_class filter and return the library class if appropriate
*
* @param string $library_class the library class
* @param string $library the library in question
*
* @return class the library class for this library
*/
public function m_chart_library_class( $library_class, $library ) {
// If Chart.js wasn't requested we'll stop here ()
if ( $library != $this->chart_meta_fields['library'] ) {
return $library_class;
}
if ( ! $this->library_class instanceof M_Chart_Chartjs ) {
require_once __DIR__ . '/class-m-chart-chartjs.php';
$this->library_class = new M_Chart_Chartjs();
}
return $this->library_class;
}
/**
* Return an iframe for a given chart
*
* @param int $post_id WP post ID of the chart you want
* @param array $args an array of args
*
* @return string HTML needed to display a chart via an iframe
*/
public function get_chart_iframe( $post_id, $args = [] ) {
$post_meta = $this->get_post_meta( $post_id );
$src_url = add_query_arg( $args, get_permalink( $post_id ) . 'embed/' );
$defer_lazy = 'enabled' === $this->get_settings( 'defer_rendering' );
ob_start();
?>
instance++;
return ob_get_clean();
}
/**
* Return a chart via the [m-chart id="x"] short code
*
* Also registered under the legacy [chart] name for backwards compatibility
*
* @param array $args an array of arguments passed by the WP short code API
*
* @return string the chart requested in Javascript or HTML form
*/
public function chart_shortcode( $args ) {
$default_args = $this->get_chart_default_args;
$default_args['id'] = '';
$args = shortcode_atts( $default_args, $args );
$post_id = $args['id'];
unset( $args['id'] );
// Did we get what looks like a chart ID?
if ( ! is_numeric( $post_id ) ) {
return;
}
// Make sure the chart actually exists and that it's a chart so we don't fatal later
if ( $this->slug != get_post_type( $post_id ) ) {
return;
}
// Is it published?
if ( 'publish' != get_post_status( $post_id ) ) {
return;
}
return $this->get_chart( $post_id, $args );
}
/**
* Helper function that prevents issues with stripslashes and unicode characters
* stripslashes will strip off the slash before unicode characters which is sucky, this prevents that
*
* @param string $string a string that may have unicode characters as well as unecessary escaping
*
* @return string a string with any unecessary escaping removed
*/
public function unicode_aware_stripslashes( $string ) {
return stripslashes( preg_replace( '#\\\u[a-fA-F0-9]{4}#', '\\\\$0', $string ) );
}
/**
* Helper function that returns the chart unit terms
*
* @return array an array of generated and/or compiled unit terms
*/
public function get_unit_terms() {
$terms = get_terms( $this->slug . '-units', [ 'hide_empty' => false ] );
if ( empty( $terms ) ) {
$terms = $this->generate_unit_terms();
}
return $this->compile_unit_terms( $terms );
}
/**
* Helper function that compiles the chart unit terms for use
*
* @param array an array of unit terms
*
* @return array an array of compiled unit terms
*/
public function compile_unit_terms( $terms ) {
$compiled_terms = [];
$parents = [];
foreach ( $terms as $unit ) {
if ( 0 == $unit->parent ) {
$parents[ $unit->term_id ] = $unit->name;
}
}
foreach ( $terms as $unit ) {
if ( 0 != $unit->parent && isset( $parents[ $unit->parent ] ) ) {
$compiled_terms[ $parents[ $unit->parent ] ][] = $unit;
}
}
ksort( $compiled_terms );
return $compiled_terms;
}
/**
* Helper function that populates the unit terms taxonomy with a default set of terms
*
* @return array an array of the newly generated unit terms
*/
public function generate_unit_terms() {
// Load the default terms array
$default_terms = require __DIR__ . '/array-default-unit-terms.php';
$terms = [];
foreach ( $default_terms as $parent_term => $child_terms ) {
$parent = (object) wp_insert_term( $parent_term, $this->slug . '-units' );
$terms[] = get_term( $parent->term_id, $this->slug . '-units' );
foreach ( $child_terms as $child_term ) {
$term = (object) wp_insert_term( $child_term, $this->slug . '-units', [ 'parent' => $parent->term_id ] );
$terms[] = get_term( $term->term_id, $this->slug . '-units' );
}
}
return $terms;
}
/**
* Looks for the embed endpoint and serves up the requested chart if appropriate
*/
public function template_redirect() {
global $wp_query;
// Make sure this is a chart with the embed endpoint in the URL
if ( ! isset( $wp_query->query['post_type'] ) || ! isset( $wp_query->query['embed'] ) || 'm-chart' != $wp_query->query['post_type'] ) {
return;
}
$post = get_post();
if ( ! $post ) {
wp_die(
esc_html__( 'The chart could not be found', 'm-chart' ),
esc_html__( 'Chart not found', 'm-chart' ),
[ 'response' => 404 ]
);
}
$settings = $this->get_settings();
if ( 'enabled' != $settings['embeds'] ) {
wp_die(
esc_html__( 'Embeds of this type are not enabled', 'm-chart' ),
esc_html__( 'Embeds disabled', 'm-chart' ),
[ 'response' => 403 ]
);
exit;
}
$this->is_iframe = true;
$scripts = [
'jquery',
$this->get_post_meta( $post->ID, 'library' ),
];
unset( $_GET['action'], $_GET['share'] );
// This prevents issues when embedding with outside sites
header_remove( 'X-Frame-Options' );
status_header( 200 );
require __DIR__ . '/templates/iframe.php';
exit;
}
/**
* Hook to the m_chart_update_post_meta action and call the required library specific function
*
* @param int $post_id WP post ID of the post you want chart args for
* @param array $parsed_meta the parsed chart meta passed by the action hook
*/
public function m_chart_update_post_meta( $post_id, $parsed_meta ) {
$this->library( $parsed_meta['library'] )->m_chart_update_post_meta( $post_id, $parsed_meta );
}
/**
* If the passed library is valid return TRUE otherwise FALSE
*
* @param string library string (i.e. 'highcharts')
*
* @return bool
*/
public function is_valid_library( $library ) {
$libraries = $this->get_libraries();
if ( ! isset( $libraries[ $library ] ) ) {
return false;
}
return true;
}
/**
* If current page is an AMP page returns true
*
* @return bool
*/
public function is_amp_endpoint() {
if ( function_exists( 'is_amp_endpoint' ) && is_amp_endpoint() ) {
return true;
}
return false;
}
/**
* Return the M Chart settings
*
* @return array current settings
*/
public function get_settings( $setting = false ) {
// Allow third party libraries to modify the default settings
$default_settings = apply_filters( 'm_chart_default_settings', $this->settings );
$settings = (array) get_option( $this->slug, $default_settings );
$settings = wp_parse_args( $settings, $default_settings );
// Make sure the set library is still valid
if ( ! $this->is_valid_library( $settings['library'] ) ) {
$settings['library'] = 'chartjs';
}
$settings = apply_filters( 'm_chart_get_settings', $settings );
if ( $setting && isset( $settings[ $setting ] ) ) {
return $settings[ $setting ];
} elseif ( $setting ) {
return null;
}
return $settings;
}
/**
* Return an array of available charting libraries
*
* @return array current settings
*/
public function get_libraries() {
return apply_filters( 'm_chart_get_libraries', $this->libraries );
}
/**
* Return the chart types that support multiple data sets (multi-sheet tab bar)
*
* This is the single authoritative source for this list
* The value is passed to window.m_chart_admin.multi_sheet_types so the React UI always stays in sync with PHP
* Filtered via 'm_chart_multi_sheet_types' so library plugins can add or remove types
*
* @return array
*/
public function get_multi_sheet_types() {
return apply_filters( 'm_chart_multi_sheet_types', [
'scatter',
'bubble',
'radar',
'radar-area',
'boxplot',
'violin',
] );
}
/**
* Return the current library
*
* @return string current library
*/
public function get_library() {
if ( isset( $_GET['library'] ) && $this->is_valid_library( $_GET['library'] ) ) {
return $_GET['library'];
}
return $this->get_settings( 'library' );
}
/**
* Return the locale array
*
* When PHP's Intl extension is available, labels are generated in each locale's native language using Locale::getDisplayName()
* Falls back to the English labels in array-locale-codes.php otherwise
*
* @return array locales as used by Intl.NumberFormat
*/
public function get_locales() {
$locales = require __DIR__ . '/array-locale-codes.php';
if ( ! class_exists( 'Locale' ) ) {
return $locales;
}
foreach ( $locales as $code => $english_label ) {
$native = Locale::getDisplayName( $code, $code );
if ( $native ) {
$locales[ $code ] = esc_html( $native );
}
}
return $locales;
}
/**
* Return a recursively merged array out of the two given
*
* @param array a multi dimensional array that will be merged into
* @param array a multi dimensional array that will be merged recursively into the first one
*
* @return array the merged array
*/
public function array_merge_recursive( &$a, $b ) {
foreach ( $b as $child => $value ) {
if ( isset( $a[ $child ] ) ) {
// New value exists so we'll need to move a level down
if ( is_array( $a[ $child ] ) && is_array( $value ) ) {
$this->array_merge_recursive( $a[ $child ], $value );
} else {
// New value is not an array so we override the old value with the new one
$a[ $child ] = $value;
}
} else {
// New value doesn't exist so we can just add it
$a[ $child ] = $value;
}
}
return $a;
}
/**
* Do things needed for version 1.7
*/
public function upgrade_to_1_7() {
// Get all charts
$charts = get_posts(
[
'post_type' => m_chart()->slug,
'posts_per_page' => -1,
'post_status' => 'any',
'tax_query' => [
[
'taxonomy' => 'post_tag',
'field' => 'slug',
'terms' => 'highcharts',
'operator' => 'NOT IN',
],
],
]
);
// Add highcharts post_tag to all charts
foreach ( $charts as $chart ) {
wp_set_object_terms( $chart->ID, 'highcharts', 'post_tag' );
}
// Update version
update_site_option( 'm_chart_version', '1.7' );
}
/**
* Do things needed for version 1.7.4
*/
public function upgrade_to_1_7_4() {
// Get all charts tagged with highcharts
$highcharts_charts = get_posts(
[
'post_type' => m_chart()->slug,
'posts_per_page' => -1,
'post_status' => 'any',
'tax_query' => [
[
'taxonomy' => 'post_tag',
'field' => 'slug',
'terms' => 'highcharts',
'operator' => 'IN',
],
],
]
);
foreach ( $highcharts_charts as $chart ) {
// Add highcharts as a term to the m-chart-library taxonomy
wp_set_object_terms( $chart->ID, 'highcharts', m_chart()->slug . '-library' );
// Remove highcharts as a term from the post_tag taxonomy
wp_remove_object_terms( $chart->ID, 'highcharts', 'post_tag' );
}
// Get all charts tagged with chartjs
$chartjs_charts = get_posts(
[
'post_type' => m_chart()->slug,
'posts_per_page' => -1,
'post_status' => 'any',
'tax_query' => [
[
'taxonomy' => 'post_tag',
'field' => 'slug',
'terms' => 'chartjs',
'operator' => 'IN',
],
],
]
);
foreach ( $chartjs_charts as $chart ) {
// Add chartjs as a term to the m-chart-library taxonomy
wp_set_object_terms( $chart->ID, 'chartjs', m_chart()->slug . '-library' );
// Remove chartjs as a term from the post_tag taxonomy
wp_remove_object_terms( $chart->ID, 'chartjs', 'post_tag' );
}
// Update version
update_site_option( 'm_chart_version', '1.7.4' );
}
}
/**
* Plugin object accessor
*/
function m_chart() {
global $m_chart;
if ( ! $m_chart instanceof M_Chart ) {
$m_chart = new M_Chart();
}
return $m_chart;
}
/**
* Freemius SDK accessor
*
* Thin global wrapper that delegates to the core class so Freemius ecosystem code and conventions keep working
* The actual configuration lives in M_Chart::freemius()
*
* @return Freemius the Freemius SDK instance for M Chart
*/
function m_chart_fs() {
return m_chart()->freemius();
}