View source
<?php
namespace Drupal\charts\Element;
use Drupal\charts\Plugin\chart\Library\ChartInterface;
use Drupal\charts\Settings\ChartsDefaultColors;
use Drupal\Component\Utility\Environment;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\NestedArray;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element\FormElement;
class ChartDataCollectorTable extends FormElement {
const FIRST_COLUMN = 'first_column';
const FIRST_ROW = 'first_row';
public function getInfo() {
$class = get_class($this);
return [
'#input' => TRUE,
'#import_csv' => TRUE,
'#import_csv_separator' => ',',
'#initial_rows' => 5,
'#initial_columns' => 2,
'#table_wrapper' => '',
'#table_wrapper_attributes' => [],
'#table_attributes' => [],
'#table_drag' => TRUE,
'#process' => [
[
$class,
'processDataCollectorTable',
],
],
'#element_validate' => [
[
$class,
'validateDataCollectorTable',
],
],
'#theme_wrappers' => [
'container',
],
];
}
public static function processDataCollectorTable(array &$element, FormStateInterface $form_state, array &$complete_form) {
$parents = $element['#parents'];
$id_prefix = implode('-', $parents);
$wrapper_id = Html::getUniqueId($id_prefix . '-ajax-wrapper');
$value = $element['#value'];
$required = !empty($element['#required']);
$user_input = $form_state
->getUserInput();
$element_state = self::getElementState($parents, $form_state);
if (empty($element_state['data_collector_table']) || empty($element_state['table_categories_identifier'])) {
$identifier_value = $value['table_categories_identifier'] ?? self::FIRST_COLUMN;
$element_state['table_categories_identifier'] = $identifier_value;
$element_state['data_collector_table'] = $value['data_collector_table'] ?? [];
$element_state['data_collector_table'] = $element_state['data_collector_table'] ?: self::initializeEmptyTable($element, $identifier_value);
self::setElementState($parents, $form_state, $element_state);
}
else {
$element_state['table_categories_identifier'] = $value['table_categories_identifier'];
}
$element = [
'#tree' => TRUE,
'#prefix' => '<div id="' . $wrapper_id . '">',
'#suffix' => '</div>',
'#wrapper_id' => $wrapper_id,
] + $element;
$element['table_categories_identifier'] = [
'#type' => 'radios',
'#title' => t('Categories are identified by'),
'#options' => [
self::FIRST_COLUMN => t('First column'),
self::FIRST_ROW => t('First row'),
],
'#description' => t('Select whether the first row or column hold the categories data'),
'#required' => $required,
'#default_value' => $element_state['table_categories_identifier'],
'#ajax' => [
'callback' => [
get_called_class(),
'ajaxRefresh',
],
'progress' => [
'type' => 'throbber',
],
'wrapper' => $wrapper_id,
'effect' => 'fade',
],
];
$table = [
'#type' => 'table',
'#tree' => TRUE,
'#header' => [],
'#responsive' => FALSE,
'#attributes' => [
'class' => 'data-collector-table',
],
];
$table_drag = $element['#table_drag'];
$table_drag_group = Html::cleanCssIdentifier($id_prefix . '-order-weight');
if ($table_drag) {
$table['#tabledrag'] = [
[
'action' => 'order',
'relationship' => 'sibling',
'group' => $table_drag_group,
],
];
}
if ($element['#table_wrapper'] && $element['#table_wrapper'] === 'container') {
$element['table_wrapper'] = [
'#type' => 'container',
'#attributes' => $element['#table_wrapper_attributes'],
'#tree' => FALSE,
];
$element['table_wrapper']['data_collector_table'] =& $table;
}
else {
$element['data_collector_table'] =& $table;
}
$rows = count($element_state['data_collector_table']);
$max_weight = count($element_state['data_collector_table']);
$max_row = max(array_keys($element_state['data_collector_table']));
$is_first_column = $element_state['table_categories_identifier'] === self::FIRST_COLUMN;
$first_row_key = NULL;
foreach ($element_state['data_collector_table'] as $i => $row) {
$first_row_key = $first_row_key === NULL ? $i : $first_row_key;
$table_first_row = $i === $first_row_key;
$add_color_first_row = $is_first_column && $table_first_row;
$first_col_key = NULL;
$row_form =& $table[$i];
$row_form['#attributes']['class'][] = 'data-collector-table--row';
foreach ($row as $j => $column) {
if ($j === 'weight') {
continue;
}
$first_col_key = $first_col_key === NULL ? $j : $first_col_key;
$table_first_col = $j === $first_col_key;
$is_category_cell = $table_first_col && $table_first_row;
$row_form[$j]['data'] = [
'#type' => 'textfield',
'#title' => t('Data for column @col - Row @row', [
'@row' => $i,
'@col' => $j,
]),
'#title_display' => 'invisible',
'#size' => 10,
'#default_value' => is_array($column) ? $column['data'] : $column,
'#wrapper_attributes' => [
'class' => [
'data-collector-table--row--cell',
],
],
];
if (!$is_category_cell && ($add_color_first_row || !$is_first_column && $j === $first_col_key)) {
$row_form[$j]['#wrapper_attributes'] = [
'class' => [
'container-inline',
],
];
$row_form[$j]['color'] = [
'#type' => 'textfield',
'#title' => t('Color'),
'#title_display' => 'invisible',
'#attributes' => [
'TYPE' => 'color',
],
'#size' => 10,
'#maxlength' => 7,
'#default_value' => $column['color'] ?? ChartsDefaultColors::randomColor(),
];
}
}
if ($table_drag) {
$row_form['#attributes']['class'][] = 'draggable';
if ($i + 1 === $rows) {
$default_weight = $max_weight;
}
else {
$default_weight = $max_row + 1;
}
$row_form['weight'] = [
'#type' => 'weight',
'#title' => t('Weight'),
'#title_display' => 'invisible',
'#delta' => $max_weight,
'#default_value' => $element_state['data_collector_table'][$i]['weight'] ?? $default_weight,
'#attributes' => [
'class' => [
$table_drag_group,
],
],
];
if (isset($user_input['data_collector_table'][$i])) {
$input_weight = $user_input['data_collector_table'][$i]['weight'];
if ($user_input['data_collector_table'][$i]['weight'] > $max_weight) {
$input_weight = $max_weight;
}
$row_form['weight']['#value'] = $input_weight;
$row_form['#weight'] = $input_weight;
}
else {
$row_form['#weight'] = $default_weight;
}
}
$row_form['delete'] = self::buildOperationButton('delete', 'row', $id_prefix, $wrapper_id, $i, [], [
'class' => [
'data-collector-table--row--delete',
],
]);
}
$colspan = 1;
if ($table_drag) {
uasort($table, [
'\\Drupal\\Component\\Utility\\SortArray',
'sortByWeightProperty',
]);
$colspan = 2;
}
$table['_delete_column_buttons'] = [
'#attributes' => [
'class' => [
'data-collector-table--column-deletes-row',
],
],
];
$first_row = current($element_state['data_collector_table']);
$columns = self::excludeWeightColumnFromRow($first_row);
$max_column = max(array_keys($first_row));
foreach ($columns as $column) {
$table['_delete_column_buttons'][$column] = self::buildOperationButton('delete', 'column', $id_prefix, $wrapper_id, $column, [], [
'class' => [
'data-collector-table--column--delete',
],
]);
if ($column === $max_column) {
$table['_delete_column_buttons'][$column]['#wrapper_attributes']['colspan'] = $colspan;
}
}
$table['_delete_column_buttons'][$max_column + 1] = [
'#markup' => '',
];
$table['_operations'] = [
'#attributes' => [
'class' => [
'data-collector-table--oprations-row',
],
],
];
$table['_operations']['wrapper'] = [
'#type' => 'container',
'#wrapper_attributes' => [
'colspan' => count($columns) + $colspan,
],
];
$table['_operations']['wrapper']['add_column'] = self::buildOperationButton('add', 'column', $id_prefix, $wrapper_id, NULL);
$table['_operations']['wrapper']['add_row'] = self::buildOperationButton('add', 'row', $id_prefix, $wrapper_id, NULL);
if ($element['#import_csv']) {
$element['import'] = [
'#type' => 'details',
'#title' => t('Import Data from CSV'),
'#description' => t('Note importing data from CSV will overwrite all the current data entry in the table.'),
'#open' => FALSE,
];
$element['import']['csv'] = [
'#name' => 'files[' . $id_prefix . ']',
'#title' => t('File upload'),
'#title_display' => 'invisible',
'#type' => 'file',
'#upload_validators' => [
'file_validate_extensions' => [
'csv',
],
'file_validate_size' => [
Environment::getUploadMaxSize(),
],
],
];
$element['import']['upload'] = [
'#type' => 'submit',
'#value' => t('Upload CSV'),
'#name' => $id_prefix . '-import-csv',
'#attributes' => [
'class' => [
Html::cleanCssIdentifier($id_prefix . '--import-csv'),
],
],
'#submit' => [
[
get_called_class(),
'importCsvToTableSubmit',
],
],
'#limit_validation_errors' => [
array_merge($parents, [
'import',
'csv',
]),
array_merge($parents, [
'import',
'upload',
]),
],
'#ajax' => [
'callback' => [
get_called_class(),
'ajaxRefresh',
],
'progress' => [
'type' => 'throbber',
],
'wrapper' => $wrapper_id,
'effect' => 'fade',
],
'#operation' => 'csv',
'#csv_separator' => $element['#import_csv_separator'] ?? ',',
];
}
return $element;
}
public static function validateDataCollectorTable(array $element, FormStateInterface $form_state) {
$parents = $element['#parents'];
$value = $form_state
->getValue($parents);
foreach ($value['data_collector_table'] as $row_key => $row) {
if (!is_numeric($row_key)) {
unset($value['data_collector_table'][$row_key]);
continue;
}
foreach ($row as $column_key => $column) {
if (!is_numeric($column_key)) {
unset($value['data_collector_table'][$row_key][$column_key]);
}
}
}
unset($value['import']);
$form_state
->setValue($parents, $value);
if ($element['#required'] && empty($value['table_categories_identifier'])) {
$form_state
->setError($element['table_categories_identifier'], t('Please select how categories should be identiefied.'));
}
}
public static function ajaxRefresh(array $form, FormStateInterface $form_state) {
$triggering_element = $form_state
->getTriggeringElement();
$operation = $triggering_element['#operation'] ?? '';
if ($operation === 'csv' || !$operation && $triggering_element['#type'] === 'radio') {
$length = -2;
}
else {
$length = $operation === 'add' ? -4 : -3;
}
$element_parents = array_slice($triggering_element['#array_parents'], 0, $length);
return NestedArray::getValue($form, $element_parents);
}
public static function tableOperationSubmit(array $form, FormStateInterface $form_state) {
$triggering_element = $form_state
->getTriggeringElement();
$operation_on = $triggering_element['#operation_on'];
$operation = $triggering_element['#operation'];
$length = $operation === 'add' ? -4 : -3;
$element_parents = array_slice($triggering_element['#parents'], 0, $length);
if (!$element_parents) {
$length = $operation == 'add' ? -4 : -3;
$element_parents = array_slice($triggering_element['#array_parents'], 0, $length);
}
$element_state = self::getElementState($element_parents, $form_state);
$index = $triggering_element['#' . $operation_on . '_index'] ?? NULL;
if ($operation_on === 'row') {
$element_state = self::tableRowOperation($element_state, $form_state, $operation, $index);
}
else {
$element_state = self::tableColumnOperation($element_state, $form_state, $operation, $index, $element_parents);
}
self::setElementState($element_parents, $form_state, $element_state);
$form_state
->setRebuild();
}
public static function importCsvToTableSubmit(array $form, FormStateInterface $form_state) {
$triggering_element = $form_state
->getTriggeringElement();
$element_parents = array_slice($triggering_element['#parents'], 0, -2);
$id_prefix = implode('-', $element_parents);
$files = \Drupal::request()->files
->get('files');
$file_upload = $files[$id_prefix];
if (!ini_get("auto_detect_line_endings")) {
ini_set("auto_detect_line_endings", '1');
}
$handle = $file_upload ? fopen($file_upload
->getPathname(), 'r') : NULL;
if ($handle) {
$encoding = 'UTF-8';
if (function_exists('mb_detect_encoding')) {
$file_contents = file_get_contents($file_upload
->getPathname());
$encodings = [
'UTF-8',
'ISO-8859-1',
'WINDOWS-1251',
];
$encodings_list = implode(',', $encodings);
$encoding = mb_detect_encoding($file_contents, $encodings_list);
}
$rows_count = 0;
$element_state = [];
$separator = $triggering_element['#csv_separator'];
while ($row = fgetcsv($handle, 0, $separator)) {
foreach ($row as $column_value) {
$element_state['data_collector_table'][$rows_count][] = [
'data' => self::convertEncoding($column_value, $encoding),
];
}
$rows_count++;
}
fclose($handle);
\Drupal::messenger()
->addMessage(t('Successfully imported @file', [
'@file' => $file_upload
->getClientOriginalName(),
]));
self::setElementState($element_parents, $form_state, $element_state);
$input = $form_state
->getUserInput();
NestedArray::setValue($input, $element_parents, $element_state);
$form_state
->setUserInput($input);
}
else {
\Drupal::messenger()
->addError(t('There was a problem importing the provided file data.'));
}
$form_state
->setRebuild();
}
public static function getElementState(array $parents, FormStateInterface $form_state) {
$parents = array_merge([
'element_state',
'#parents',
], $parents);
return NestedArray::getValue($form_state
->getStorage(), $parents);
}
public static function setElementState(array $parents, FormStateInterface $form_state, array $element_state) {
$parents = array_merge([
'element_state',
'#parents',
], $parents);
NestedArray::setValue($form_state
->getStorage(), $parents, $element_state);
}
private static function buildOperationButton($operation, $on, $id_prefix, $wrapper_id, $index = NULL, $attributes = [], $wrapper_atrributes = []) {
$name = $id_prefix . '_' . $operation . '_' . $on;
$submit = [];
if (!is_null($index)) {
$name .= '_' . $index;
$submit['#' . $on . '_index'] = $index;
}
if ($attributes) {
$submit['#attributes'] = $attributes;
}
if ($wrapper_atrributes) {
$submit['#wrapper_attributes'] = $wrapper_atrributes;
}
$submit += [
'#type' => 'submit',
'#name' => $name,
'#value' => t('@op @on', [
'@op' => ucfirst($operation),
'@on' => $on,
]),
'#limit_validation_errors' => [],
'#submit' => [
[
get_called_class(),
'tableOperationSubmit',
],
],
'#operation' => $operation,
'#operation_on' => $on,
'#ajax' => [
'callback' => [
get_called_class(),
'ajaxRefresh',
],
'wrapper' => $wrapper_id,
'effect' => 'fade',
],
];
return $submit;
}
private static function initializeEmptyTable(array $element, $identifier_value) {
$is_first_column = $identifier_value === self::FIRST_COLUMN;
$columns = $element['#initial_columns'];
$columns_arr = range(0, $columns - 1);
$rows = $element['#initial_rows'];
$rows_arr = range(0, $rows - 1);
$data = [];
$first_row_key = NULL;
foreach ($rows_arr as $i) {
$first_row_key = $first_row_key === NULL ? $i : $first_row_key;
$table_first_row = $i === $first_row_key;
$first_col_key = NULL;
foreach ($columns_arr as $j) {
$first_col_key = $first_col_key === NULL ? $j : $first_col_key;
$table_first_col = $j === $first_col_key;
$is_category_cell = $table_first_col && $table_first_row;
$data[$i][$j]['data'] = '';
if (!$is_category_cell && ($is_first_column && $i === $first_row_key || !$is_first_column && $j === $first_col_key)) {
$data[$i][$j]['color'] = ChartsDefaultColors::randomColor();
}
}
}
return $data;
}
private static function tableRowOperation(array $element_state, FormStateInterface $form_state, $op, $index = NULL) {
if ($op === 'delete') {
if (count($element_state['data_collector_table']) === 1) {
$row = $element_state['data_collector_table'][$index];
$element_state['data_collector_table'][$index][] = self::emptyRowColumns($row);
return $element_state;
}
unset($element_state['data_collector_table'][$index]);
}
else {
$first_row = current($element_state['data_collector_table']);
$element_state['data_collector_table'][] = self::emptyRowColumns($first_row);
}
return $element_state;
}
private static function tableColumnOperation(array $element_state, FormStateInterface $form_state, $op, $index = NULL, array $element_parents) {
if ($op === 'delete') {
foreach ($element_state['data_collector_table'] as $row_key => $columns) {
$row = $element_state['data_collector_table'][$row_key];
if (count(self::excludeWeightColumnFromRow($row)) === 1) {
$element_state['data_collector_table'][$row_key][$index]['data'] = '';
}
else {
array_splice($element_state['data_collector_table'][$row_key], $index, 1);
$user_input = $form_state
->getUserInput();
$values = NestedArray::getValue($form_state
->getUserInput(), $element_parents);
if (!empty($values['data_collector_table'][$row_key][$index])) {
array_splice($values['data_collector_table'][$row_key], $index, 1);
}
NestedArray::setValue($user_input, $element_parents, $values);
$form_state
->setUserInput($user_input);
}
}
}
else {
foreach ($element_state['data_collector_table'] as $row_key => $columns) {
$element_state['data_collector_table'][$row_key][]['data'] = '';
}
}
return $element_state;
}
private static function excludeWeightColumnFromRow(array $row) {
return array_filter(array_keys($row), function ($key) {
return is_int($key);
});
}
private static function emptyRowColumns(array $row) {
$columns = self::excludeWeightColumnFromRow($row);
$empty_row_columns = [];
foreach ($columns as $key => $column) {
$empty_row_columns[$key]['data'] = '';
}
return $empty_row_columns;
}
private static function convertEncoding($data, $encoding) {
if ($encoding == 'UTF-8') {
return $data;
}
if ($encoded_data = Unicode::convertToUtf8($data, $encoding)) {
return $encoded_data;
}
return $data;
}
public static function getCategoriesFromCollectedTable(array $data) {
$categories_identifier = $data['table_categories_identifier'];
$table = $data_table = $data['data_collector_table'];
$categories = [
'label' => '',
'data' => [],
];
$is_first_column = $categories_identifier === self::FIRST_COLUMN;
$first_row = current($table);
$category_col_key = key($first_row);
$categories['label'] = $first_row[$category_col_key];
$data = [];
if ($is_first_column) {
$col_cells = array_column($table, $category_col_key);
foreach ($col_cells as $cell) {
$data[] = is_array($cell) ? $cell['data'] : $cell;
}
}
else {
$col_cells = array_values($first_row);
foreach ($col_cells as $cell) {
$data[] = is_array($cell) ? $cell['data'] : $cell;
}
}
$categories['data'] = $data;
$categories_data = $categories['data'];
array_shift($categories_data);
$categories['data'] = $categories_data;
return $categories;
}
public static function getSeriesFromCollectedTable(array $data, $type) {
$table = $data['data_collector_table'];
$categories_identifier = $data['table_categories_identifier'];
$chart_type_plugin_manager = \Drupal::service('plugin.manager.charts_type');
$chart_type = $chart_type_plugin_manager
->getDefinition($type);
$is_single_axis = $chart_type['axis'] === ChartInterface::SINGLE_AXIS;
$is_first_column = $categories_identifier === self::FIRST_COLUMN;
$first_row = current($table);
$category_col_key = key($first_row);
if (!$is_first_column) {
array_shift($table);
}
$series = [];
$i = 0;
foreach ($table as $row_key => $row) {
if (!$is_first_column) {
$name_key = key($row);
$series[$i]['name'] = $row[$name_key]['data'];
$series[$i]['color'] = $row[$name_key]['color'];
unset($row[$name_key]);
foreach (array_values($row) as $column) {
if (is_numeric($column) || is_string($column)) {
$series[$i]['data'][] = self::castValueToNumeric($column);
}
elseif (is_array($column) && isset($column['data'])) {
$series[$i]['data'][] = self::castValueToNumeric($column['data']);
}
}
}
else {
$j = 0;
foreach ($row as $column_key => $column) {
if ($column_key === $category_col_key || !is_numeric($column_key)) {
continue;
}
elseif ($i === 0) {
$series[$j]['name'] = $column['data'] ?? $column;
$series[$j]['color'] = $column['color'] ?? ChartsDefaultColors::randomColor();
}
else {
$cell_value = is_array($column) && isset($column['data']) ? $column['data'] : $column;
$cell_value = self::castValueToNumeric($cell_value);
if ($is_single_axis) {
$series[$j]['data'][] = [
$row[$j]['data'],
$cell_value,
];
}
else {
$series[$j]['data'][] = $cell_value;
}
}
$j++;
}
}
$i++;
}
return $series;
}
private static function castValueToNumeric($value) {
if (is_numeric($value)) {
$value = is_int($value) ? (int) $value : (double) $value;
}
elseif ($value === '') {
$value = NULL;
}
else {
$value = 0;
}
return $value;
}
}