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

M Chart, version 2.2. 484 lines.

- Page: https://pluginprobe.com/plugins/m-chart/2.2/code/components/class-m-chart-parse.php
- Raw: https://pluginprobe.com/plugins/m-chart/2.2/raw/components/class-m-chart-parse.php
- Modified: 2026-05-21T17:40:52+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-parse.php#L10-L20`.

```php
<?php

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

class M_Chart_Parse {
	const LABELS_NONE         = 'none';
	const LABELS_FIRST_ROW    = 'first_row';
	const LABELS_FIRST_COLUMN = 'first_column';
	const LABELS_BOTH         = 'both';
	const PARSE_ROWS          = 'rows';
	const PARSE_COLUMNS       = 'columns';

	public array $data                   = [];
	public array $value_labels           = [];
	public string $value_labels_position = '';
	public array $set_data               = [];
	public array $raw_data               = [];
	public string $parse_in              = '';

	private ?NumberFormatter $formatter = null;

	/**
	 * Initializes the parse class
	 */
	public function __construct() {

	}

	/**
	 * Parses a chart's data array for labels, data sets, and raw values
	 *
	 * @param array  $data     the raw two-dimensional data array from the chart post
	 * @param string $parse_in whether to parse by rows or columns (PARSE_ROWS or PARSE_COLUMNS)
	 *
	 * @return static $this
	 */
	public function parse_data( array $data, string $parse_in ): static {
		$this->data                  = $data;
		$this->parse_in              = $parse_in;
		$this->value_labels_position = $this->get_value_labels_position();
		$this->parse_value_labels();
		$this->parse_set_data();
		
		return $this;
	}

	/**
	 * Populates $this->value_labels by reading label values from the data array based on the detected labels position
	 */
	private function parse_value_labels(): void {
		$this->value_labels = [];

		switch ( $this->value_labels_position ) {
			case self::LABELS_NONE:
				return;

			case self::LABELS_FIRST_COLUMN:
				foreach ( (array) $this->data as $columns ) {
					if ( '' != trim( (string) $columns[0] ) ) {
						$this->value_labels[] = $this->clean_labels( $columns[0] );
					}
				}
				break;

			case self::LABELS_FIRST_ROW:
				foreach ( (array) $this->data[0] as $column ) {
					if ( '' != trim( (string) $column ) ) {
						$this->value_labels[] = $this->clean_labels( $column );
					}
				}
				break;

			case self::LABELS_BOTH:
				foreach ( (array) $this->data as $columns ) {
					if ( '' != trim( (string) $columns[0] ) ) {
						$this->value_labels[ self::LABELS_FIRST_COLUMN ][] = $this->clean_labels( $columns[0] );
					}
				}

				foreach ( (array) $this->data[0] as $column ) {
					if ( '' != trim( (string) $column ) ) {
						$this->value_labels[ self::LABELS_FIRST_ROW ][] = $this->clean_labels( $column );
					}
				}
				break;
		}

		$this->value_labels = apply_filters( 'm_chart_value_labels', $this->value_labels, $this->value_labels_position, $this->data );
	}

	/**
	 * Helper function returns a string describing where the value labels are (first_row, first_column, both)
	 *
	 * @return string the position of the labels in the given data set
	 */
	private function get_value_labels_position(): string {
		if ( ! isset( $this->data[0][0] ) && ! isset( $this->data[1][0] ) ) {
			return self::LABELS_NONE;
		}

		if ( '' == $this->data[0][0] ) {
			return self::LABELS_BOTH;
		}

		// Structural pre-check
		// The simple single-series shape from creating-a-chart.md is unambiguous 2 effective columns (rows mode) or 2 effective rows (columns mode)
		// Works regardless of whether the labels look numeric (years, ordinals, etc)
		if ( self::PARSE_ROWS === $this->parse_in && 2 === $this->effective_max_columns() ) {
			return self::LABELS_FIRST_COLUMN;
		}

		if ( self::PARSE_COLUMNS === $this->parse_in && 2 === $this->effective_row_count() ) {
			return self::LABELS_FIRST_ROW;
		}

		// Existing content-based heuristic for 3+ effective columns/rows
		if ( ! is_numeric( trim( (string) $this->data[0][0] ) ) ) {
			// If the first row has multiple non-numeric headers and the data rows start with numeric values the entire first row is column labels (e.g. scatter format)
			if (
				   isset( $this->data[0][1] ) && ! is_numeric( trim( (string) $this->data[0][1] ) )
				&& isset( $this->data[1][0] ) &&   is_numeric( trim( (string) $this->data[1][0] ) )
			) {
				return self::LABELS_FIRST_ROW;
			}

			return self::LABELS_FIRST_COLUMN;
		}

		return self::LABELS_FIRST_ROW;
	}

	/**
	 * Max effective column count across rows
	 * Rightmost non-empty cell index + 1, taken as a max over all rows
	 * Trailing empty cells (typical of Jspreadsheet's minDimensions padding) don't count
	 *
	 * @return int
	 */
	private function effective_max_columns(): int {
		if ( ! is_array( $this->data ) ) {
			return 0;
		}

		$max = 0;

		foreach ( $this->data as $row ) {
			if ( ! is_array( $row ) ) {
				continue;
			}

			for ( $i = count( $row ) - 1; $i >= 0; $i-- ) {
				if ( '' !== trim( (string) ( $row[ $i ] ?? '' ) ) ) {
					if ( $i + 1 > $max ) {
						$max = $i + 1;
					}

					break;
				}
			}
		}

		return $max;
	}

	/**
	 * Count of rows that contain at least one non-empty cell
	 * Trailing empty rows (typical of Jspreadsheet's minDimensions padding) don't count
	 *
	 * @return int
	 */
	private function effective_row_count(): int {
		if ( ! is_array( $this->data ) ) {
			return 0;
		}

		$last = -1;

		foreach ( $this->data as $i => $row ) {
			if ( ! is_array( $row ) ) {
				continue;
			}

			foreach ( $row as $cell ) {
				if ( '' !== trim( (string) ( $cell ?? '' ) ) ) {
					$last = $i;

					break;
				}
			}
		}

		return $last + 1;
	}

	/**
	 * Helper function cleans data point values
	 *
	 * @param mixed $data_point a data point that may need to be cleaned or typed as an int
	 *
	 * @return float|string a float of the cleaned data point or string if the cleaned value was not numeric
	 */
	public function clean_data_point( mixed $data_point ): float|string {
		$data_point = trim( (string) $data_point );

		if ( preg_match( '/-?\d[\d,]*(?:\.\d+)?/', $data_point, $matches ) ) {
			return floatval( str_replace( ',', '', $matches[0] ) );
		}

		return $data_point;
	}

	/**
	 * Helper function parses a data point into an M_Chart_Parsed_Data_Point value object for localized display
	 * Splits the cell string into prefix, numeric value, and suffix 
	 * This means the number can be reformatted for any locale while preserving surrounding context
	 *
	 * @param mixed $data_point a raw cell value
	 *
	 * @return M_Chart_Parsed_Data_Point
	 */
	public function parse_data_point( mixed $data_point ): M_Chart_Parsed_Data_Point {
		$data_point = trim( (string) $data_point );

		if ( preg_match( '/(-?\d[\d,]*(?:\.\d+)?)/', $data_point, $matches, PREG_OFFSET_CAPTURE ) ) {
			$number = $matches[1][0];
			$offset = $matches[1][1];

			return M_Chart_Parsed_Data_Point::numeric(
				floatval( str_replace( ',', '', $number ) ),
				substr( $data_point, 0, $offset ),
				substr( $data_point, $offset + strlen( $number ) )
			);
		}

		return M_Chart_Parsed_Data_Point::text( $data_point );
	}

	/**
	 * Helper function cleans out label values
	 *
	 * @param mixed $label a label string
	 *
	 * @return string the label string cleaned of any problem content
	 */
	public function clean_labels( mixed $label ): string {
		$label = trim( html_entity_decode( (string) $label, ENT_QUOTES ) );

		// PHP's strip_tags() is case-insensitive, recursive, handles unclosed/malformed tags, and is XSS-safe
		return strip_tags( $label );
	}

	/**
	 * Populates $this->set_data and $this->raw_data by delegating to the appropriate collector based on the parse direction and labels position, then normalizing both arrays
	 */
	private function parse_set_data(): void {
		if ( self::PARSE_ROWS == $this->parse_in && self::LABELS_FIRST_COLUMN == $this->value_labels_position ) {
			[ $set_data_array, $raw_data_array ] = $this->collect_rows_first_column();
		} elseif ( self::PARSE_ROWS == $this->parse_in && self::LABELS_BOTH == $this->value_labels_position ) {
			[ $set_data_array, $raw_data_array ] = $this->collect_rows_both();
		} elseif ( self::PARSE_COLUMNS == $this->parse_in && self::LABELS_BOTH == $this->value_labels_position ) {
			[ $set_data_array, $raw_data_array ] = $this->collect_columns_both();
		} else {
			[ $set_data_array, $raw_data_array ] = $this->collect_default();
		}

		$set_data_array = $this->normalize_data_array( $set_data_array );
		$raw_data_array = $this->normalize_data_array( $raw_data_array );

		$this->set_data = apply_filters( 'm_chart_set_data', $set_data_array, $this->data, $this->parse_in );
		$this->raw_data = apply_filters( 'm_chart_raw_data', $raw_data_array, $this->data, $this->parse_in );
	}

	/**
	 * Collects data when parsing rows with labels in the first column
	 *
	 * @return array {0: array, 1: array} Two-element array of [set_data, raw_data]
	 */
	private function collect_rows_first_column(): array {
		$set_data_array = [];
		$raw_data_array = [];

		foreach ( $this->data as $row ) {
			foreach ( $row as $key => $column ) {
				if ( '' == $column || 0 == $key ) {
					continue;
				}

				$set_data_array[] = $this->clean_data_point( $column );
				$raw_data_array[] = $this->parse_data_point( $column );
			}
		}

		return [ $set_data_array, $raw_data_array ];
	}

	/**
	 * Collects data when parsing rows with labels in both the first row and first column
	 *
	 * @return array {0: array, 1: array} Two-element array of [set_data, raw_data]
	 */
	private function collect_rows_both(): array {
		$set_data_array = [];
		$raw_data_array = [];
		$limit          = count( $this->data );
		$this_sets      = [];
		$this_raw       = [];

		for ( $i = 1; $i < $limit; $i++ ) {
			foreach ( $this->data[ $i ] as $c_key => $column ) {
				if ( 0 != $c_key ) {
					$data_point = $this->clean_data_point( $column );
					$key        = $i - 1;

					if ( ! isset( $this_sets[ $key ]['is_null'] ) ) {
						$this_sets[ $key ]['is_null'] = true;
					}

					if ( is_numeric( $data_point ) ) {
						$this_sets[ $key ]['is_null'] = false;
					}

					$this_sets[ $key ]['data'][] = $data_point;
					$this_raw[ $key ][]          = $this->parse_data_point( $column );
				}
			}
		}

		foreach ( $this_sets as $key => $set ) {
			if ( false == $set['is_null'] ) {
				$set_data_array[ $key ] = $set['data'];
				$raw_data_array[ $key ] = $this_raw[ $key ];
			}
		}

		return [ $set_data_array, $raw_data_array ];
	}

	/**
	 * Collects data when parsing columns with labels in both the first row and first column
	 *
	 * @return array {0: array, 1: array} Two-element array of [set_data, raw_data]
	 */
	private function collect_columns_both(): array {
		$set_data_array = [];
		$raw_data_array = [];
		$limit          = count( $this->data );
		$this_sets      = [];
		$this_raw       = [];

		for ( $i = 1; $i < $limit; $i++ ) {
			foreach ( $this->data[ $i ] as $key => $column ) {
				if ( 0 == $key ) {
					continue;
				}

				$data_point = $this->clean_data_point( $column );
				$a_key      = $key - 1;

				if ( ! isset( $this_sets[ $a_key ]['is_null'] ) ) {
					$this_sets[ $a_key ]['is_null'] = true;
				}

				if ( is_numeric( $data_point ) ) {
					$this_sets[ $a_key ]['is_null'] = false;
				}

				$this_sets[ $a_key ]['data'][] = $data_point;
				$this_raw[ $a_key ][]          = $this->parse_data_point( $column );
			}
		}

		foreach ( $this_sets as $key => $set ) {
			if ( false == $set['is_null'] ) {
				$set_data_array[ $key ] = $set['data'];
				$raw_data_array[ $key ] = $this_raw[ $key ];
			}
		}

		return [ $set_data_array, $raw_data_array ];
	}

	/**
	 * Collects data for the default case (first-row labels only, or no labels)
	 *
	 * @return array {0: array, 1: array} Two-element array of [set_data, raw_data]
	 */
	private function collect_default(): array {
		$set_data_array = [];
		$raw_data_array = [];

		if ( ! isset( $this->data[1] ) ) {
			return [ $set_data_array, $raw_data_array ];
		}

		foreach ( $this->data as $key => $columns ) {
			foreach ( $columns as $column ) {
				if ( '' == $column || 0 == $key ) {
					continue;
				}

				$set_data_array[] = $this->clean_data_point( $column );
				$raw_data_array[] = $this->parse_data_point( $column );
			}
		}

		return [ $set_data_array, $raw_data_array ];
	}

	/**
	 * Helper function normalizes the data array so that the number of data values matches the number of value labels
	 *
	 * @param array $data_array an already parsed array of data
	 *
	 * @return array a normalized array of parsed data
	 */
	private function normalize_data_array( array $data_array ): array {
		if ( self::PARSE_ROWS == $this->parse_in && self::LABELS_BOTH == $this->value_labels_position ) {
			$first_row_labels = $this->value_labels[ self::LABELS_FIRST_ROW ] ?? [];
			$label_count      = is_array( $first_row_labels ) ? count( $first_row_labels ) - 1 : 0;

			foreach ( $data_array as $key => $data ) {
				foreach ( $data as $t_key => $value ) {
					if ( $t_key > $label_count ) {
						unset( $data_array[ $key ][ $t_key ] );
					}
				}
			}
		} elseif ( self::PARSE_COLUMNS == $this->parse_in && self::LABELS_BOTH == $this->value_labels_position ) {
			$first_col_labels = $this->value_labels[ self::LABELS_FIRST_COLUMN ] ?? [];
			$label_count      = is_array( $first_col_labels ) ? count( $first_col_labels ) - 1 : 0;

			foreach ( $data_array as $key => $data ) {
				foreach ( $data as $t_key => $value ) {
					if ( $t_key > $label_count ) {
						unset( $data_array[ $key ][ $t_key ] );
					}
				}
			}
		}

		return $data_array;
	}

	/**
	 * Formats an M_Chart_Parsed_Data_Point for table display
	 * Numeric cells are formatted with the locale-aware NumberFormatter non-numeric cells are returned as plain text
	 *
	 * @param ?M_Chart_Parsed_Data_Point $raw the parsed data point to format, or null for an empty cell
	 *
	 * @return string the formatted cell value
	 */
	public function format_raw( ?M_Chart_Parsed_Data_Point $raw ): string {
		if ( null === $raw ) {
			return '';
		}

		// If the value is a number return the formatted and prefixed/suffixed version of it
		if ( $raw->is_numeric() ) {
			$formatter = $this->get_formatter();
			$number    = $formatter ? $formatter->format( $raw->value ) : (string) $raw->value;
			
			return $raw->prefix . $number . $raw->suffix;
		}

		return $raw->text;
	}

	/**
	 * Returns a locale-aware NumberFormatter, creating and caching it on first use
	 *
	 * @return ?NumberFormatter a NumberFormatter instance, or null if the intl extension is unavailable
	 */
	private function get_formatter(): ?NumberFormatter {
		if ( null === $this->formatter ) {
			$locale          = m_chart()->get_settings( 'locale' );
			$this->formatter = class_exists( 'NumberFormatter' ) ? new NumberFormatter( $locale, NumberFormatter::DECIMAL ) : null;
		}
		
		return $this->formatter;
	}
}

```
