# m-chart/2.2/components/class-m-chart.php

M Chart, version 2.2. 1,433 lines.

- Page: https://pluginprobe.com/plugins/m-chart/2.2/code/components/class-m-chart.php
- Raw: https://pluginprobe.com/plugins/m-chart/2.2/raw/components/class-m-chart.php
- Modified: 2026-07-01T03:15:44+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/m-chart/2.2/code/components/class-m-chart.php#L10-L20`.

```php
<?php

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

class M_Chart {
	public $version           = '2.2';
	public $slug              = 'm-chart';
	public $plugin_name       = 'Chart';
	public $chart_meta_fields = [
		'library'           => '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
				. "<script id=\"m-chart-freemius-pricing\">\n"
				. file_get_contents( $script_path ) // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- local plugin asset
				. "</script>\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 "<style id=\"m-chart-freemius-connect\">\n";
			echo file_get_contents( $css_path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- local plugin asset
			echo "</style>\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();
				?>
<figure class="m-chart-image-figure">
	<amp-img src="<?php echo esc_url( $image['url'] ); ?>"
		width="<?php echo absint( $image['width'] ); ?>" height="<?php echo absint( $image['height'] ); ?>"
		alt="<?php echo esc_attr( get_the_title( $post_id ) ); ?>"
		class="<?php echo esc_attr( $classes ); ?>"></amp-img>
	<?php if ( has_action( 'm_chart_screen_reader_text' ) ) : ?>
	<?php wp_enqueue_style( 'm-chart-frontend', $this->plugin_url . '/components/css/m-chart-frontend.css', [], $this->version ); ?>
	<div class="screen-reader-text">
		<?php do_action( 'm_chart_screen_reader_text', $post_id, $args ); ?>
	</div>
	<?php endif; ?>
</figure>
				<?php
				return ob_get_clean();
			} else {
				ob_start();
				?>
<figure class="m-chart-image-figure">
	<img src="<?php echo esc_url( $image['url'] ); ?>"
		width="<?php echo absint( $image['width'] ); ?>" height="<?php echo absint( $image['height'] ); ?>"
		alt="<?php echo esc_attr( get_the_title( $post_id ) ); ?>"
		class="<?php echo esc_attr( $classes ); ?>" />
	<?php if ( has_action( 'm_chart_screen_reader_text' ) ) : ?>
	<?php wp_enqueue_style( 'm-chart-frontend', $this->plugin_url . '/components/css/m-chart-frontend.css', [], $this->version ); ?>
	<div class="screen-reader-text">
		<?php do_action( 'm_chart_screen_reader_text', $post_id, $args ); ?>
	</div>
	<?php endif; ?>
</figure>
				<?php
				return ob_get_clean();
			}
		}

		$settings = $this->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();
		?>
<iframe id="m-chart-container-<?php echo absint( $post_id ); ?>-<?php echo absint( $this->instance ); ?>"
	class="m-chart-iframe" width="100%" height="<?php echo absint( $post_meta['height'] + 1 ); ?>"
	title="<?php echo esc_attr( get_the_title( $post_id ) ); ?>"
	src="<?php echo esc_url( $src_url ); ?>"<?php echo $defer_lazy ? ' loading="lazy"' : ''; ?>></iframe>
		<?php
		if ( 'show' == $args['share'] ) {
			unset( $args['share'] );
			require apply_filters( 'm_chart_share_template', __DIR__ . '/templates/share.php' );
		}
		$this->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();
}

```
