added simple math and charts modules

This commit is contained in:
2024-12-10 23:53:23 +01:00
parent 62a5f3c97c
commit 688ff42752
196 changed files with 20285 additions and 8 deletions

View File

@@ -0,0 +1,35 @@
<?php
namespace Drupal\charts\Annotation;
use Drupal\Component\Annotation\Plugin;
/**
* Defines a Chart annotation object.
*
* @Annotation
*/
class Chart extends Plugin {
/**
* The plugin ID.
*
* @var string
*/
public $id;
/**
* The plugin name.
*
* @var string
*/
public $name;
/**
* An array of chart types the chart library supports.
*
* @var array
*/
public $types = [];
}

View File

@@ -0,0 +1,34 @@
<?php
namespace Drupal\charts;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\DefaultPluginManager;
/**
* Chart Manager.
*
* Provides the Chart plugin manager and manages discovery and instantiation of
* chart plugins.
*/
class ChartManager extends DefaultPluginManager {
/**
* Constructor for ChartManager objects.
*
* @param \Traversable $namespaces
* An object that implements \Traversable which contains the root paths
* keyed by the corresponding namespace to look for plugin implementations.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
* Cache backend instance to use.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler to invoke the alter hook with.
*/
public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
parent::__construct('Plugin/chart/Library', $namespaces, $module_handler, 'Drupal\charts\Plugin\chart\Library\ChartInterface', 'Drupal\charts\Annotation\Chart');
$this->alterInfo('charts_chart_library');
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Drupal\charts;
/**
* Defines the interface for the chart views field data type.
*/
interface ChartViewsFieldInterface {
/**
* Get the chart field data type.
*
* @return string
* The chart field data type.
*/
public function getChartFieldDataType(): string;
}

View File

@@ -0,0 +1,20 @@
<?php
namespace Drupal\charts;
/**
* Contains helper method to help with the generation of random color.
*/
trait ColorHelperTrait {
/**
* Provide a random color.
*
* @return string
* A random color.
*/
public static function randomColor(): string {
return sprintf('#%06X', mt_rand(0, 0xFFFFFF));
}
}

View File

@@ -0,0 +1,346 @@
<?php
namespace Drupal\charts;
use Drupal\Component\Serialization\Yaml;
use Drupal\Component\Utility\Color;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleExtensionList;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Manage the updates and upgrades of settings.
*/
class ConfigUpdater implements ContainerInjectionInterface {
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The module extension list.
*
* @var \Drupal\Core\Extension\ModuleExtensionList
*/
protected $moduleExtensionList;
/**
* ConfigEntityUpdater constructor.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\Core\Extension\ModuleExtensionList $extension_list_module
* The module extension list.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, ModuleHandlerInterface $module_handler, ModuleExtensionList $extension_list_module) {
$this->entityTypeManager = $entity_type_manager;
$this->moduleHandler = $module_handler;
$this->moduleExtensionList = $extension_list_module;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_type.manager'),
$container->get('module_handler'),
$container->get('extension.list.module')
);
}
/**
* Transforms legacy settings to newer setting architecture.
*
* @param array $old_settings
* The old settings.
* @param string $for
* The configuration for which the transformation is being done.
* Regular config or view.
*
* @return array
* The new format settings.
*/
public function transformVersion3SettingsToNew(array &$old_settings, string $for = 'config') {
$new_settings = [];
$new_settings['fields']['stacking'] = !empty($old_settings['grouping']);
$old_config_keys = $this->getLegacySettingsMappingKeys();
foreach ($old_settings as $setting_id => $setting_value) {
if (empty($old_config_keys[$setting_id])) {
continue;
}
$setting_key_map = $old_config_keys[$setting_id];
$value = $this->transformBoolStringValueToBool($setting_value);
// When a block setting belongs to the chart blocks we save it in a
// new setting.
if (substr($setting_key_map, 0, 7) === 'display') {
// Stripping the 'display_' in front of the mapping key.
$setting_key_map = substr($setting_key_map, 8, strlen($setting_key_map));
if (substr($setting_key_map, 0, 10) === 'dimensions') {
// Stripping dimensions_.
$setting_key_map = substr($setting_key_map, 11, strlen($setting_key_map));
$new_settings['display']['dimensions'][$setting_key_map] = $value;
}
elseif (substr($setting_key_map, 0, 5) === 'gauge') {
// Stripping gauge_.
$setting_key_map = substr($setting_key_map, 6, strlen($setting_key_map));
$new_settings['display']['gauge'][$setting_key_map] = $value;
}
else {
$new_settings['display'][$setting_key_map] = $value;
}
}
elseif (substr($setting_key_map, 0, 5) === 'xaxis') {
// Stripping xaxis_.
$setting_key_map = substr($setting_key_map, 6, strlen($setting_key_map));
$new_settings['xaxis'][$setting_key_map] = $value;
}
elseif (substr($setting_key_map, 0, 5) === 'yaxis') {
// Stripping yaxis_.
$setting_key_map = substr($setting_key_map, 6, strlen($setting_key_map));
if (substr($setting_key_map, 0, 9) === 'secondary') {
// Stripping gauge_.
$setting_key_map = substr($setting_key_map, 10, strlen($setting_key_map));
$new_settings['yaxis']['secondary'][$setting_key_map] = $value;
}
else {
$new_settings['yaxis'][$setting_key_map] = $value;
}
}
elseif (substr($setting_key_map, 0, 6) === 'fields') {
// Stripping fields_.
$setting_key_map = substr($setting_key_map, 7, strlen($setting_key_map));
if ($setting_key_map === 'data_providers' && is_array($value)) {
$data_providers = $new_settings['fields']['data_providers'] ?? [];
if ($setting_id === 'data_fields' || $setting_id == 'field_colors') {
$new_settings['fields']['data_providers'] = $this->transformLegacyFieldsDataProvidersToNew($data_providers, $value);
}
}
else {
$new_settings['fields'][$setting_key_map] = $value;
}
}
elseif ($setting_key_map === 'grouping' && $new_settings['fields']['stacking']) {
$new_settings[$setting_key_map] = [];
}
else {
// We make sure that we handle the color unneeded array.
$new_settings[$setting_key_map] = $setting_key_map !== 'color' ? $value : $value[0];
}
// Then we remove it from the main old settings tree.
unset($old_settings[$setting_id]);
}
// Allow other modules to alter the new settings.
$this->moduleHandler->alter('charts_version3_to_new_settings_structure', $new_settings, $for, $this);
return $new_settings;
}
/**
* Initialize the current default settings.
*/
public function initializedCurrentDefaultSettings() {
$path = $this->moduleExtensionList->getPath('charts');
$default_install_settings_file = $path . '/config/install/charts.settings.yml';
$default_install_settings = Yaml::decode(file_get_contents($default_install_settings_file));
$new_settings = &$default_install_settings['charts_default_settings'];
// Allow other modules to alter the new settings.
$for = 'config';
$this->moduleHandler->alter('charts_version3_to_new_settings_structure', $new_settings, $for, $this);
return $default_install_settings;
}
/**
* Updates settings from version 3 of views.
*/
public function updateExistingViewsVersion3ToNewSettings() {
$view_storage = $this->entityTypeManager->getStorage('view');
$view_ids = $view_storage->getQuery()
->accessCheck(FALSE)
->condition('display.*.display_options.style.type', 'chart', '=')
->execute();
if (!$view_ids) {
return 'Views: No views had a display set to a charts style.';
}
$updated_views = [];
foreach ($view_ids as $view_id) {
/** @var \Drupal\views\ViewEntityInterface $view */
if (!($view = $view_storage->load($view_id))) {
continue;
}
$changed = FALSE;
$displays = $view->get('display');
foreach ($displays as &$display) {
$style = &$display['display_options']['style'];
if ($style['type'] !== 'chart' || !isset($style['options']['field_colors']) || !isset($style['options']['fields']['table'])) {
continue;
}
$changed = TRUE;
// Removing this because it was set in version 3 but was not used for
// anything.
unset($style['options']['fields']);
$options = &$style['options'];
$options = $this->transformVersion3SettingsToNew($options, 'view');
$chart_settings_elements = [
'library',
'type',
'fields',
'display',
'xaxis',
'yaxis',
];
foreach ($options as $option_key => $option) {
if (in_array($option_key, $chart_settings_elements)) {
$options['chart_settings'][$option_key] = $option;
unset($options[$option_key]);
}
}
}
if ($changed) {
$view->set('display', $displays);
$view->save();
$updated_views[] = $view_id;
}
}
if ($updated_views) {
return sprintf('Views: The following views were updated: %s', implode(', ', $updated_views));
}
return sprintf('Views: The following views(%s) with at least one display of charts style were loaded but not updated!', implode(', ', array_values($view_ids)));
}
/**
* Transforms boolean string value to real boolean.
*
* @param mixed $value
* The value to be transformed.
*
* @return bool|mixed
* The boolean value or the original passed value.
*/
public function transformBoolStringValueToBool($value) {
if ($value === 'FALSE' || $value === 'false') {
return FALSE;
}
elseif ($value === 'TRUE' || $value === 'true') {
return TRUE;
}
return $value;
}
/**
* Transforms legacy fields data providers to new.
*
* @param array $data_providers
* Data providers.
* @param array $legacy_value
* Legacy value.
*
* @return mixed
* Data providers returned
*/
private function transformLegacyFieldsDataProvidersToNew(array $data_providers, array $legacy_value) {
$default_weight = 0;
foreach ($legacy_value as $field_id => $value) {
if (Color::validateHex($value)) {
$data_providers[$field_id]['color'] = $value;
}
else {
$data_providers[$field_id]['enabled'] = !empty($value);
}
$data_providers[$field_id]['weight'] = $default_weight;
$default_weight++;
}
return $data_providers;
}
/**
* Gets legacy settings mapping keys.
*
* @return array
* Legacy settings keys to newer ones mapping.
*/
private function getLegacySettingsMappingKeys() {
return [
'library' => 'library',
'chart_library' => 'library',
'type' => 'type',
'chart_type' => 'type',
'grouping' => 'grouping',
'title' => 'display_title',
'title_position' => 'display_title_position',
'data_labels' => 'display_data_labels',
'data_markers' => 'display_data_markers',
'legend' => 'display_legend',
'legend_position' => 'display_legend_position',
'background' => 'display_background',
'three_dimensional' => 'display_three_dimensional',
'polar' => 'display_polar',
'series' => 'series',
'data' => 'data',
'color' => 'color',
'data_series' => 'data_series',
'series_label' => 'series_label',
'categories' => 'categories',
'field_colors' => 'fields_data_providers',
'tooltips' => 'display_tooltips',
'tooltips_use_html' => 'display_tooltips_use_html',
'width' => 'display_dimensions_width',
'height' => 'display_dimensions_height',
'width_units' => 'display_dimensions_width_units',
'height_units' => 'display_dimensions_height_units',
'colors' => 'display_colors',
'xaxis_title' => 'xaxis_title',
'xaxis_labels_rotation' => 'xaxis_labels_rotation',
'yaxis_title' => 'yaxis_title',
'yaxis_min' => 'yaxis_min',
'yaxis_max' => 'yaxis_max',
'yaxis_prefix' => 'yaxis_prefix',
'yaxis_suffix' => 'yaxis_suffix',
'yaxis_decimal_count' => 'yaxis_decimal_count',
'yaxis_labels_rotation' => 'yaxis_labels_rotation',
'inherit_yaxis' => 'yaxis_inherit',
'secondary_yaxis_title' => 'yaxis_secondary_title',
'secondary_yaxis_min' => 'yaxis_secondary_min',
'secondary_yaxis_max' => 'yaxis_secondary_min',
'secondary_yaxis_prefix' => 'yaxis_secondary_prefix',
'secondary_yaxis_suffix' => 'yaxis_secondary_suffix',
'secondary_yaxis_decimal_count' => 'yaxis_secondary_decimal_count',
'secondary_yaxis_labels_rotation' => 'yaxis_secondary_labels_rotation',
'green_from' => 'display_gauge_green_from',
'green_to' => 'display_gauge_green_to',
'red_from' => 'display_gauge_red_from',
'red_to' => 'display_gauge_red_to',
'yellow_from' => 'display_gauge_yellow_from',
'yellow_to' => 'display_gauge_yellow_to',
'max' => 'display_gauge_max',
'min' => 'display_gauge_min',
'allow_advanced_rendering' => 'fields_allow_advanced_rendering',
'label_field' => 'fields_label',
'data_fields' => 'fields_data_providers',
];
}
}

View File

@@ -0,0 +1,87 @@
<?php
namespace Drupal\charts;
/**
* Provide method to calculate dependencies for charts config.
*/
trait DependenciesCalculatorTrait {
/**
* The chart library plugin manager.
*
* @var \Drupal\charts\ChartManager
*/
protected $chartPluginManager;
/**
* The chart type plugin library manager.
*
* @var \Drupal\charts\TypeManager
*/
protected $chartTypePluginManager;
/**
* Calculates the dependencies given the chart library and chart type.
*
* @param string $chart_library_id
* The chart library plugin id.
* @param string $chart_type_id
* The chart type plugin id.
*
* @return array
* The calculated dependencies.
*
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
public function calculateDependencies(string $chart_library_id, string $chart_type_id):array {
$calculated_dependencies = [];
$dependent_modules = [];
if ($chart_library_id) {
// Getting charting library provider and set it as one of the
// dependencies for the chart settings.
$plugin_definition = $this->chartPluginManager()->getDefinition($chart_library_id);
$dependent_modules = [$plugin_definition['provider']];
}
if ($chart_type_id) {
// Do the same thing for the chart type unless it was added by the main
// "charts" module.
$plugin_definition = $this->chartTypePluginManager()->getDefinition($chart_type_id);
$provider = $plugin_definition['provider'];
if ($provider !== 'charts' && !in_array($provider, $dependent_modules)) {
$dependent_modules[] = $provider;
}
}
if ($dependent_modules) {
$calculated_dependencies = ['module' => $dependent_modules];
}
return $calculated_dependencies;
}
/**
* Initialize the chart plugin manager if needed.
*
* @return \Drupal\charts\ChartManager
* The chart manager plugin.
*/
private function chartPluginManager(): ChartManager {
if (!isset($this->chartPluginManager)) {
$this->chartPluginManager = \Drupal::service('plugin.manager.charts');
}
return $this->chartPluginManager;
}
/**
* Initialize the chart type plugin manager if needed.
*
* @return \Drupal\charts\TypeManager
* The chart type manager plugin.
*/
private function chartTypePluginManager(): TypeManager {
if (!isset($this->chartTypePluginManager)) {
$this->chartTypePluginManager = \Drupal::service('plugin.manager.charts_type');
}
return $this->chartTypePluginManager;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,449 @@
<?php
namespace Drupal\charts\Element;
use Drupal\charts\ChartManager;
use Drupal\charts\Plugin\chart\Library\ChartBase;
use Drupal\charts\Plugin\chart\Library\ChartInterface;
use Drupal\charts\TypeManager;
use Drupal\Component\Serialization\Json;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Render\Element\RenderElementBase;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a chart render element.
*
* @RenderElement("chart")
*/
class Chart extends RenderElementBase implements ContainerFactoryPluginInterface {
use StringTranslationTrait;
/**
* The config factory service.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* The chart plugin manager.
*
* @var \Drupal\charts\ChartManager
*/
protected $chartsManager;
/**
* The chart type info service.
*
* @var \Drupal\charts\Plugin\chart\Type\TypeInterface
*/
protected $chartsTypeManager;
/**
* The module handler service.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* Constructs a Chart object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory service.
* @param \Drupal\charts\ChartManager $chart_manager
* The chart plugin manager.
* @param \Drupal\charts\TypeManager $type_manager
* The chart type manager.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler service.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, ConfigFactoryInterface $config_factory, ChartManager $chart_manager, TypeManager $type_manager, ModuleHandlerInterface $module_handler) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->configFactory = $config_factory;
$this->chartsManager = $chart_manager;
$this->chartsTypeManager = $type_manager;
$this->moduleHandler = $module_handler;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('config.factory'),
$container->get('plugin.manager.charts'),
$container->get('plugin.manager.charts_type'),
$container->get('module_handler')
);
}
/**
* {@inheritdoc}
*/
public function getInfo() {
return [
'#chart_type' => NULL,
'#chart_library' => NULL,
'#chart_id' => NULL,
'#title' => NULL,
'#title_color' => '#000',
'#title_font_weight' => 'normal',
'#title_font_style' => 'normal',
'#title_font_size' => 14,
'#title_position' => 'out',
'#subtitle' => NULL,
'#colors' => ChartBase::getDefaultColors(),
'#font' => 'Arial',
'#font_size' => 12,
'#gauge' => [],
'#background' => 'transparent',
'#stacking' => NULL,
'#color_changer' => FALSE,
'#pre_render' => [
[$this, 'preRender'],
],
'#tooltips' => TRUE,
'#tooltips_use_html' => FALSE,
'#data_labels' => FALSE,
'#data_markers' => FALSE,
'#legend' => TRUE,
'#legend_title' => '',
'#legend_title_font_weight' => 'bold',
'#legend_title_font_style' => 'normal',
'#legend_title_font_size' => '',
'#legend_position' => 'right',
'#legend_font_weight' => 'normal',
'#legend_font_style' => 'normal',
'#legend_font_size' => NULL,
'#width' => NULL,
'#height' => NULL,
'#attributes' => [],
'#chart_definition' => [],
'#raw_options' => [],
'#content_prefix' => [],
'#content_suffix' => [],
];
}
/**
* Main #pre_render callback to expand a chart element.
*
* @param array $element
* The element.
*
* @return array
* The chart element.
*
* @throws \Drupal\Component\Plugin\Exception\PluginException
*/
public function preRender(array $element) {
/** @var \Drupal\charts\Plugin\chart\Library\ChartInterface[] $definitions */
$definitions = $this->chartsManager->getDefinitions();
if (!$definitions) {
$element['#type'] = 'markup';
$element['#markup'] = $this->t('No charting library found. Enable a charting module such as Google Charts or Highcharts.');
return $element;
}
// Ensure there's an x and y axis to provide defaults.
$type_name = $element['#chart_type'];
$type = $this->chartsTypeManager->getDefinition($type_name);
if ($type && $type['axis'] === ChartInterface::DUAL_AXIS) {
$children_types = [];
foreach (Element::children($element) as $key) {
$children_types[] = $element[$key]['#type'];
}
if (!in_array('chart_xaxis', $children_types)) {
$element['xaxis'] = ['#type' => 'chart_xaxis'];
}
if (!in_array('chart_yaxis', $children_types)) {
$element['yaxis'] = ['#type' => 'chart_yaxis'];
}
}
self::castElementIntergerValues($element);
// Generic theme function assuming it will be suitable for most chart types.
$element['#theme'] = 'charts_chart';
// Allow the chart to be altered - @TODO use event dispatching if needed.
$alter_hooks = ['chart'];
$chart_id = $element['#chart_id'];
if ($chart_id) {
$alter_hooks[] = 'chart_' . $element['#chart_id'];
}
$this->moduleHandler->alter($alter_hooks, $element, $chart_id);
// Include the library-specific render callback via their plugin manager.
// Use the first charting library if the requested library is not available.
$library = $element['#chart_library'] ?? '';
$library = $this->getLibrary($library);
$element['#chart_library'] = $library;
$charts_settings = $this->configFactory->get('charts.settings');
$plugin_configuration = $charts_settings->get('charts_default_settings.library_config') ?? [];
/** @var \Drupal\charts\Plugin\chart\Library\ChartInterface $plugin */
$plugin = $this->chartsManager->createInstance($library, $plugin_configuration);
if (!$plugin->isSupportedChartType($type_name)) {
// Chart type not supported by the library.
throw new \LogicException(sprintf('The provided chart type "%s" is not supported by "%s" chart plugin library.', $type_name, $plugin->getChartName()));
}
$element = $plugin->preRender($element);
if (!empty($element['#chart_definition'])) {
$chart_definition = $element['#chart_definition'];
unset($element['#chart_definition']);
// Allow the chart definition to be altered - @TODO use event dispatching
// if needed.
$alter_hooks = ['chart_definition'];
if ($element['#chart_id']) {
$alter_hooks[] = 'chart_definition_' . $chart_id;
}
$this->moduleHandler->alter($alter_hooks, $chart_definition, $element, $chart_id);
// Set the element #chart_json property as a data-attribute.
$element['#attributes']['data-chart'] = Json::encode($chart_definition);
}
$element['#cache']['tags'][] = 'config:charts.settings';
return $element;
}
/**
* Casts recursively integer values.
*
* @param array $element
* The element.
*/
public static function castElementIntergerValues(array &$element) {
// Cast options to integers to avoid redundant library fixing problems.
$integer_options = [
// Chart options.
'#title_font_size',
'#font_size',
'#legend_title_font_size',
'#legend_font_size',
'#width',
'#height',
// Axis options.
'#title_font_size',
'#labels_font_size',
'#labels_rotation',
'#max',
'#min',
// Data options.
'#decimal_count',
];
foreach ($element as $property_name => $value) {
if (is_array($element[$property_name])) {
self::castElementIntergerValues($element[$property_name]);
}
elseif ($property_name && in_array($property_name, $integer_options)) {
$element[$property_name] = (is_null($element[$property_name]) || strlen($element[$property_name]) === 0) ? NULL : (int) $element[$property_name];
}
}
}
/**
* Trims out, recursively, empty options that aren't used.
*
* @param array $array
* The array to trim.
*/
public static function trimArray(array &$array) {
foreach ($array as $key => &$value) {
if (is_array($value)) {
self::trimArray($value);
}
elseif (is_null($value) || (is_array($value) && count($value) === 0)) {
unset($array[$key]);
}
}
}
/**
* Get the library.
*
* @param string $library
* The library.
*
* @return string
* The library.
*/
private function getLibrary($library) {
$definitions = $this->chartsManager->getDefinitions();
if (!$library || $library === 'site_default') {
$charts_settings = $this->configFactory->get('charts.settings');
$default_settings_library = $charts_settings->get('charts_default_settings.library');
$library = !empty($default_settings_library) ? $default_settings_library : key($definitions);
}
elseif (!isset($definitions[$library])) {
$library = key($definitions);
}
return $library;
}
/**
* Build the element.
*
* @param array $settings
* The settings.
* @param string $chart_id
* The chart id.
*
* @return array
* The element.
*/
public static function buildElement(array $settings, string $chart_id): array {
$type = $settings['type'];
$single_axis = in_array($type, ['pie', 'donut']);
$display_colors = $settings['display']['colors'] ?? [];
$element = [
'#type' => 'chart',
'#chart_type' => $type,
'#chart_library' => $settings['library'],
'#title' => $settings['display']['title'],
'#title_position' => $settings['display']['title_position'],
'#subtitle' => $settings['display']['subtitle'] ?? '',
'#tooltips' => $settings['display']['tooltips'] ?? [],
'#data_labels' => $settings['display']['data_labels'] ?? FALSE,
'#data_markers' => $settings['display']['data_markers'] ?? FALSE,
'#colors' => $display_colors,
'#background' => $settings['display']['background'] ?? 'transparent',
'#three_dimensional' => $settings['display']['three_dimensional'] ?? FALSE,
'#polar' => $settings['display']['polar'] ?? FALSE,
'#legend' => !empty($settings['display']['legend_position']),
'#legend_position' => $settings['display']['legend_position'] ?? '',
'#gauge' => $settings['display']['gauge'] ?? [],
'#stacking' => !empty($settings['display']['stacking']) ?? NULL,
'#width' => $settings['display']['dimensions']['width'],
'#height' => $settings['display']['dimensions']['height'],
'#width_units' => $settings['display']['dimensions']['width_units'],
'#height_units' => $settings['display']['dimensions']['height_units'],
'#color_changer' => $settings['display']['color_changer'] ?? FALSE,
];
if (empty($settings['series'])) {
return $element;
}
$table = $settings['series'];
// Extracting the categories.
$categories = ChartDataCollectorTable::getCategoriesFromCollectedTable($table, $type);
// Extracting the rest of the data.
$series_data = ChartDataCollectorTable::getSeriesFromCollectedTable($table, $type);
$element['xaxis'] = [
'#type' => 'chart_xaxis',
'#labels' => $single_axis ? '' : $categories['data'],
'#title' => $settings['xaxis']['title'] ?? FALSE,
'#labels_rotation' => $settings['xaxis']['labels_rotation'],
];
if (empty($series_data)) {
return $element;
}
$element['yaxis'] = [
'#type' => 'chart_yaxis',
'#title' => $settings['yaxis']['title'] ?? '',
'#labels_rotation' => $settings['yaxis']['labels_rotation'],
'#max' => $settings['yaxis']['max'],
'#min' => $settings['yaxis']['min'],
'#prefix' => $settings['yaxis']['prefix'],
'#suffix' => $settings['yaxis']['suffix'],
'#decimal_count' => $settings['yaxis']['decimal_count'],
];
// Create a secondary axis if needed.
$series_count = count($series_data);
if (!empty($settings['yaxis']['inherit']) && $series_count === 2) {
$element['secondary_yaxis'] = [
'#type' => 'chart_yaxis',
'#title' => $settings['yaxis']['secondary']['title'] ?? '',
'#labels_rotation' => $settings['yaxis']['secondary']['labels_rotation'],
'#max' => $settings['yaxis']['secondary']['max'],
'#min' => $settings['yaxis']['secondary']['min'],
'#prefix' => $settings['yaxis']['secondary']['prefix'],
'#suffix' => $settings['yaxis']['secondary']['suffix'],
'#decimal_count' => $settings['yaxis']['secondary']['decimal_count'],
'#opposite' => TRUE,
];
}
// Overriding element colors for pie and donut chart types when the
// settings display colors is empty.
$overrides_element_colors = !$display_colors && ($type === 'pie' || $type === 'donut');
$series_key = $chart_id . '__series';
if ($single_axis) {
$new_series = [];
$labels = [];
foreach ($series_data as $datum) {
$new_series[] = $datum['data'][0][1];
$labels[] = $datum['name'];
if ($overrides_element_colors) {
$element['#colors'][] = $datum['color'];
}
}
$element['xaxis']['#labels'] = $labels;
// @todo Address more than one series.
$element[$series_key] = [
'#type' => 'chart_data',
'#data' => $new_series,
'#title' => $series_data[0]['title'] ?? '',
];
}
else {
$series_counter = 0;
foreach ($series_data as $data_index => $data) {
$key = $series_key . '__' . $data_index;
$element[$key] = [
'#type' => 'chart_data',
'#data' => $data['data'],
'#title' => $data['name'],
];
if (!empty($data['color'])) {
$element[$key]['#color'] = $data['color'];
}
if (isset($element['yaxis'])) {
$element[$key]['#prefix'] = $settings['yaxis']['prefix'];
$element[$key]['#suffix'] = $settings['yaxis']['suffix'];
$element[$key]['#decimal_count'] = $settings['yaxis']['decimal_count'];
}
if (isset($element['secondary_yaxis']) && $series_counter === 1) {
$element[$key]['#target_axis'] = 'secondary_yaxis';
$element[$key]['#prefix'] = $settings['yaxis']['secondary']['prefix'];
$element[$key]['#suffix'] = $settings['yaxis']['secondary']['suffix'];
$element[$key]['#decimal_count'] = $settings['yaxis']['secondary']['decimal_count'];
}
$series_counter++;
}
}
return $element;
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace Drupal\charts\Element;
use Drupal\Core\Render\Element\RenderElementBase;
/**
* Provides a chart render element.
*/
abstract class ChartAxisBase extends RenderElementBase {
/**
* {@inheritdoc}
*/
public function getInfo(): array {
return [
// Options: linear, logarithmic, datetime, labels.
'#axis_type' => '',
'#title' => '',
'#title_color' => '#000',
// Options: normal, bold.
'#title_font_weight' => 'normal',
// Options: normal, italic.
'#title_font_style' => 'normal',
'#title_font_size' => 12,
// CSS value for font size, e.g. 1em or 12px.
'#labels' => NULL,
'#labels_color' => '#000',
// Options: normal, bold.
'#labels_font_weight' => 'normal',
// Options: normal, italic.
'#labels_font_style' => 'normal',
// CSS value for font size, e.g. 1em or 12px.
'#labels_font_size' => NULL,
// Integer rotation value, e.g. 30, -60 or 90.
'#labels_rotation' => NULL,
'#grid_line_color' => '#ccc',
'#base_line_color' => '#ccc',
'#minor_grid_line_color' => '#e0e0e0',
// Integer max value on this axis.
'#max' => NULL,
// Integer minimum value on this axis.
'#min' => NULL,
// Display axis on opposite normal side.
'#opposite' => FALSE,
];
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace Drupal\charts\Element;
use Drupal\Core\Render\Element\RenderElementBase;
/**
* Provides a chart data render element.
*
* @RenderElement("chart_data")
*/
class ChartData extends RenderElementBase {
/**
* {@inheritdoc}
*/
public function getInfo(): array {
return [
'#title' => NULL,
'#labels' => NULL,
'#data' => [],
'#color' => NULL,
'#show_in_legend' => TRUE,
// Show inline labels next to the data.
'#show_labels' => FALSE,
// If building multicharts. The chart type, e.g. pie.
'#chart_type' => NULL,
// Line chart only.
'#line_width' => 1,
// Line chart only. Size in pixels, e.g. 1, 5.
'#marker_radius' => 3,
// If using multiple axes, key for the matching y axis.
'#target_axis' => NULL,
// Formatting options.
// The number of digits after the decimal separator. e.g. 2.
'#decimal_count' => NULL,
// A custom date format, e.g. %Y-%m-%d.
'#date_format' => NULL,
'#prefix' => NULL,
'#suffix' => NULL,
];
}
}

View File

@@ -0,0 +1,909 @@
<?php
namespace Drupal\charts\Element;
use Drupal\charts\ColorHelperTrait;
use Drupal\charts\Plugin\chart\Library\ChartInterface;
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\FormElementBase;
/**
* Provides a chart data collector table form element.
*
* @FormElement("chart_data_collector_table")
*/
class ChartDataCollectorTable extends FormElementBase {
use ColorHelperTrait;
use ElementFormStateTrait;
const FIRST_COLUMN = 'first_column';
const FIRST_ROW = 'first_row';
/**
* {@inheritdoc}
*/
public function getInfo() {
$class = get_class($this);
return [
'#input' => TRUE,
// Either to enable csv import.
'#import_csv' => TRUE,
'#import_csv_separator' => ',',
// The initial number of rows to generate.
'#initial_rows' => 5,
// The initial number of columns to generate.
'#initial_columns' => 2,
// The optional element the table should be wrapped in.
'#table_wrapper' => '',
'#table_wrapper_attributes' => [],
'#table_attributes' => [],
// Allows to toggle on/off drupal tabledrag functionality.
'#table_drag' => TRUE,
'#default_colors' => [],
'#process' => [
[$class, 'processDataCollectorTable'],
],
'#element_validate' => [
[$class, 'validateDataCollectorTable'],
],
'#theme_wrappers' => ['container'],
];
}
/**
* Processes the element to render a table to collect a data for the chart.
*
* @param array $element
* The element.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
* @param array $complete_form
* The complete form.
*
* @return array
* The processed element.
*/
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);
// Getting columns and rows count.
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 {
// This is hack to make ajax call return the proper identifier.
$element_state['table_categories_identifier'] = $value['table_categories_identifier'];
}
// Enforce tree.
$element = [
'#tree' => TRUE,
'#prefix' => '<div id="' . $wrapper_id . '">',
'#suffix' => '</div>',
// Pass the id along to other methods.
'#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'] === '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']);
// Make the weight list always reflect the current number of values.
$max_weight = count($element_state['data_collector_table']);
$max_row = max(array_keys($element_state['data_collector_table']));
// The first column need to be for colors.
$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 ?? $i;
$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';
// Adding the row textfield cells.
foreach ($row as $j => $column) {
if ($j === 'weight') {
continue;
}
$first_col_key = $first_col_key ?? $j;
$table_first_col = $j === $first_col_key;
// To be used to skip color input on cell[0][0].
$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))) {
if (empty($column['color'])) {
$color_index = $is_first_column ? $j : $i;
$column['color'] = $element['#default_colors'][$color_index - 1] ?? self::randomColor();
}
$row_form[$j]['#wrapper_attributes'] = [
'class' => ['container-inline'],
];
$row_form[$j]['color'] = [
'#type' => 'textfield',
'#title' => t('Color'),
'#title_display' => 'invisible',
'#attributes' => [
'TYPE' => 'color',
'style' => 'min-width:50px;',
],
'#size' => 10,
'#maxlength' => 7,
'#default_value' => $column['color'],
];
}
}
// Adding weight if table drag enabled.
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],
],
];
// Used by SortArray::sortByWeightProperty to sort the rows.
if (isset($user_input['data_collector_table'][$i])) {
$input_weight = $user_input['data_collector_table'][$i]['weight'];
// Make sure the weight is not out of bounds due to removals.
if ($user_input['data_collector_table'][$i]['weight'] > $max_weight) {
$input_weight = $max_weight;
}
// Reflect the updated user input on the element.
$row_form['weight']['#value'] = $input_weight;
$row_form['#weight'] = $input_weight;
}
else {
$row_form['#weight'] = $default_weight;
}
}
// Row delete button.
$row_form['delete'] = self::buildOperationButton('delete', 'row', $id_prefix, $wrapper_id, $i, [], [
'class' => ['data-collector-table--row--delete'],
]);
}
$colspan = 1;
if ($table_drag) {
// Sort the values by weight. Ensures weight is preserved on ajax refresh.
uasort($table, [
'\Drupal\Component\Utility\SortArray',
'sortByWeightProperty',
]);
// Increasing colspan when weight column is added.
$colspan = 2;
}
// Building the column delete button.
$table['_delete_column_buttons'] = [
'#attributes' => ['class' => ['data-collector-table--column-deletes-row']],
];
// Using first row to get the count of columns.
$first_row = current($element_state['data_collector_table']);
// Using array filter to exclude weight key when grabbing the row columns.
$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;
}
}
// Empty Column under delete operation placeholder.
$table['_delete_column_buttons'][$max_column + 1] = [
'#markup' => '',
];
// Footer operations.
$table['_operations'] = [
'#attributes' => ['class' => ['data-collector-table--operations-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_separator'] = [
'#type' => 'textfield',
'#title' => t('CSV separator'),
'#default_value' => $element['#import_csv_separator'] ?? ',',
'#size' => 1,
'#required' => TRUE,
];
$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'] ?? ',',
];
}
$element['#attributes']['style'] = 'overflow: auto;';
return $element;
}
/**
* Validates the data collected.
*
* @param array $element
* The form element.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*/
public static function validateDataCollectorTable(array $element, FormStateInterface $form_state) {
$parents = $element['#parents'];
$value = $form_state->getValue($parents);
// Remove empty rows and unneeded keys.
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 identified.'));
}
}
/**
* Ajax callback.
*/
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);
}
/**
* Submit callback for table add and delete operations.
*/
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, $element_parents, $index);
}
self::setElementState($element_parents, $form_state, $element_state);
$form_state->setRebuild();
}
/**
* Submit callback for table csv import operations.
*/
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');
/** @var \Symfony\Component\HttpFoundation\File\UploadedFile $file_upload */
$file_upload = $files[$id_prefix];
$handle = $file_upload ? fopen($file_upload->getPathname(), 'r') : NULL;
if ($handle) {
// Checking the encoding of the CSV file to be UTF-8.
$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);
}
// Populate CSV values.
$rows_count = 0;
$element_state = [];
$user_inputs = $form_state->getUserInput();
$series = NestedArray::getValue($user_inputs, $element_parents);
$separator = $series['import']['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(),
]));
// Updating form state storage.
self::setElementState($element_parents, $form_state, $element_state);
// Making sure that the user input is updated as well.
$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();
}
/**
* Utility method to build a button render array for the various data table.
*
* Operation.
*/
private static function buildOperationButton($operation, $on, $id_prefix, $wrapper_id, $index = NULL, $attributes = [], $wrapper_attributes = []) {
$name = $id_prefix . '_' . $operation . '_' . $on;
$submit = [];
if (!is_null($index)) {
$name .= '_' . $index;
$submit['#' . $on . '_index'] = $index;
}
if ($attributes) {
$submit['#attributes'] = $attributes;
}
if ($wrapper_attributes) {
$submit['#wrapper_attributes'] = $wrapper_attributes;
}
$value = [];
$value['add']['row'] = t('Add row');
$value['add']['column'] = t('Add column');
$value['delete']['row'] = t('Delete row');
$value['delete']['column'] = t('Delete column');
$submit += [
'#type' => 'submit',
'#name' => $name,
'#value' => $value[$operation][$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;
}
/**
* Initializes an empty table.
*
* @param array $element
* The element.
* @param string $identifier_value
* The identifier value.
*
* @return array
* The element state storage.
*/
private static function initializeEmptyTable(array $element, string $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;
$counter_default_used_color_index = 0;
$max_default_colors = count($element['#default_colors']);
foreach ($rows_arr as $i) {
$first_row_key = $first_row_key ?? $i;
$table_first_row = $i === $first_row_key;
$first_col_key = NULL;
foreach ($columns_arr as $j) {
$first_col_key = $first_col_key ?? $j;
$table_first_col = $j === $first_col_key;
// Used to skip category cell.
$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))) {
if ($counter_default_used_color_index === $max_default_colors) {
$counter_default_used_color_index = 0;
}
$data[$i][$j]['color'] = $element['#default_colors'][$counter_default_used_color_index] ?? self::randomColor();
$counter_default_used_color_index++;
}
}
}
return $data;
}
/**
* Performs add or delete operation on the table row.
*
* @param array $element_state
* The element state storage.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
* @param string $op
* The operation.
* @param null|int $index
* The row index.
*
* @return array
* The updated element state storage.
*/
private static function tableRowOperation(array $element_state, FormStateInterface $form_state, $op, $index = NULL) {
if ($op === 'delete') {
// When only one row left we just empty it's columns.
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;
}
/**
* Performs add or delete operation on the table column.
*
* @param array $element_state
* The element state storage.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
* @param string $op
* The operation.
* @param array $element_parents
* The element parents.
* @param null|int $index
* The column index.
*
* @return array
* The updated element state storage.
*/
private static function tableColumnOperation(array $element_state, FormStateInterface $form_state, $op, array $element_parents, $index = NULL) {
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);
// Making sure that the user input is updated as well.
$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;
}
/**
* Excludes weight column from row.
*
* @param array $row
* The row.
*
* @return array
* The columns.
*/
private static function excludeWeightColumnFromRow(array $row) {
return array_filter(array_keys($row), function ($key) {
return is_int($key);
});
}
/**
* Empty row columns.
*
* @param array $row
* The row.
*
* @return array
* The empty row column.
*/
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;
}
/**
* Helper function to detect and convert strings not in UTF-8 to UTF-8.
*
* @param string $data
* The string which needs converting.
* @param string $encoding
* The encoding of the CSV file.
*
* @return string
* UTF encoded string.
*/
private static function convertEncoding($data, $encoding) {
// Converting UTF-8 to UTF-8 will not work.
if ($encoding == 'UTF-8') {
return $data;
}
// Try to convert the data to UTF-8.
if ($encoded_data = Unicode::convertToUtf8($data, $encoding)) {
return $encoded_data;
}
// Fallback on the input data.
return $data;
}
/**
* Gets the categories from the data collected by this element.
*
* @param array $data
* The data.
* @param string $type
* The chart type.
*
* @return array
* The category label and data.
*/
public static function getCategoriesFromCollectedTable(array $data, string $type) {
$categories_identifier = $data['table_categories_identifier'] ?? '';
$table = $data['data_collector_table'];
$categories = [];
$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) {
if (!in_array($type, ['pie', 'donut'])) {
// Extracting the categories data.
$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;
}
}
}
else {
$col_cells = array_values($first_row);
foreach ($col_cells as $cell) {
$data[] = is_array($cell) ? $cell['data'] : $cell;
}
}
$categories['data'] = $data;
// Removing the category label from categories.
$categories_data = $categories['data'];
array_shift($categories_data);
$categories['data'] = $categories_data;
return $categories;
}
/**
* Gets the series from the data collected by this element.
*
* @param array $data
* The data.
* @param string $type
* The type of chart.
*
* @return array
* The series.
*/
public static function getSeriesFromCollectedTable(array $data, string $type) {
$table = $data['data_collector_table'];
$categories_identifier = $data['table_categories_identifier'] ?? '';
/** @var \Drupal\charts\TypeManager $chart_type_plugin_manager */
$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);
// Skip the first row if it's considered as the holding categories data.
if (!$is_first_column) {
array_shift($table);
}
$series = [];
$i = 0;
foreach ($table as $row) {
if (!$is_first_column) {
$name_key = key($row);
$series[$i]['name'] = $row[$name_key]['data'] ?? [];
$series[$i]['color'] = $row[$name_key]['color'] ?? '';
// Removing the name from data array.
unset($row[$name_key]);
foreach ($row as $column) {
// Get all the data in this column and break out of this loop.
if ($is_single_axis) {
if (is_numeric($column) || is_string($column)) {
$series[$i]['data'][] = [
$series[$i]['name'],
self::castValueToNumeric($column),
];
}
elseif (is_array($column) && isset($column['data'])) {
$series[$i]['data'][] = [
$series[$i]['name'],
self::castValueToNumeric($column['data']),
];
}
}
else {
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']);
}
}
}
// Adding a couple types not currently supported but hopefully soon.
if (in_array($type, [
'scatter',
'bubble',
'candlestick',
'boxplot',
])) {
// Enclose the data value in an array.
$series[$i]['data'] = [$series[$i]['data']];
}
}
else {
$j = 0;
foreach ($row as $column_key => $column) {
// Skipping the category label and it's data.
if ($column_key === $category_col_key || !is_numeric($column_key)) {
continue;
}
elseif ($i === 0) {
// This is the first column which holds the data names and colors.
$series[$j]['name'] = $column['data'] ?? $column;
$series[$j]['color'] = $column['color'] ?? self::randomColor();
}
else {
// Get all the data in this column and break out of this loop.
$cell_value = is_array($column) && isset($column['data']) ? $column['data'] : $column;
$cell_value = self::castValueToNumeric($cell_value);
if ($is_single_axis) {
$series[$j]['data'][] = [$series[$j]['name'], $cell_value];
$series[$j]['title'][] = $row[0]['data'];
}
elseif (in_array($type, [
'scatter',
'bubble',
'candlestick',
'boxplot',
])) {
$series[$j]['data'][0][] = $cell_value;
}
else {
$series[$j]['data'][] = $cell_value;
}
}
$j++;
}
}
$i++;
}
return $series;
}
/**
* Casts string value to numeric.
*
* @param string $value
* The value.
*
* @return float|int
* The numeric value.
*/
private static function castValueToNumeric($value) {
if (is_numeric($value)) {
$value = is_int($value) ? (integer) $value : (float) $value;
}
elseif ($value === '') {
$value = NULL;
}
else {
$value = 0;
}
return $value;
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace Drupal\charts\Element;
use Drupal\Core\Render\Element\RenderElementBase;
/**
* Provides a chart data item render element.
*
* @RenderElement("chart_data_item")
*/
class ChartDataItem extends RenderElementBase {
/**
* {@inheritdoc}
*/
public function getInfo(): array {
return [
'#data' => NULL,
'#color' => NULL,
// Often used as content of the tooltip.
'#title' => NULL,
];
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace Drupal\charts\Element;
/**
* Provides a chart xaxis render element.
*
* @RenderElement("chart_xaxis")
*/
class ChartXaxis extends ChartAxisBase {
}

View File

@@ -0,0 +1,12 @@
<?php
namespace Drupal\charts\Element;
/**
* Provides a chart yaxis render element.
*
* @RenderElement("chart_yaxis")
*/
class ChartYaxis extends ChartAxisBase {
}

View File

@@ -0,0 +1,45 @@
<?php
namespace Drupal\charts\Element;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Form\FormStateInterface;
/**
* Contains useful form state related methods to use by element plugins.
*/
trait ElementFormStateTrait {
/**
* Gets the element state.
*
* @param array $parents
* The element parents.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*
* @return array|null
* The element state. Possibly NULL if the value is NULL or not all
* nested parent keys exist.
*/
public static function getElementState(array $parents, FormStateInterface $form_state): ?array {
$parents = array_merge(['element_state', '#parents'], $parents);
return NestedArray::getValue($form_state->getStorage(), $parents);
}
/**
* Sets the element state.
*
* @param array $parents
* The element parents.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
* @param array $element_state
* The element state.
*/
public static function setElementState(array $parents, FormStateInterface $form_state, array $element_state): void {
$parents = array_merge(['element_state', '#parents'], $parents);
NestedArray::setValue($form_state->getStorage(), $parents, $element_state);
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace Drupal\charts\Event;
/**
* Defined events for the charts module.
*
* Note that submodules might have their own events.
*/
final class ChartsEvents {
/**
* Name of the event fired when chart types definitions are being collected.
*
* @Event
*
* @see \Drupal\charts\Event\TypesInfoEvent
*/
const TYPE_INFO = 'charts.type_info';
}

View File

@@ -0,0 +1,82 @@
<?php
namespace Drupal\charts\Event;
use Symfony\Contracts\EventDispatcher\Event;
/**
* Provides getters and setters for the type.
*/
class TypesInfoEvent extends Event {
/**
* The chart types.
*
* @var array
*/
protected $types;
/**
* Constructs a new TypesInfoEvent object.
*
* @param array $types
* The chart type definitions.
*/
public function __construct(array $types) {
$this->types = $types;
}
/**
* Gets the condition definitions.
*
* @return array
* The condition definitions.
*/
public function getTypes(): array {
return $this->types;
}
/**
* Sets the condition definitions.
*
* @param array $types
* The condition definitions.
*
* @return $this
*/
public function setTypes(array $types) {
$this->types = $types;
return $this;
}
/**
* Gets a chart type.
*
* @param string $type
* The chart type name.
*
* @return array
* The chart type info.
*/
public function getType($type): array {
return $this->types[$type] ?? [];
}
/**
* Sets a chart type information.
*
* @param string $type
* The chart type name.
* @param array $info
* The chart type information settings.
*
* @return $this
*/
public function setType($type, array $info) {
if ($this->getType($type)) {
$this->types[$type] = $info;
}
return $this;
}
}

View File

@@ -0,0 +1,87 @@
<?php
namespace Drupal\charts\EventSubscriber;
use Drupal\charts\ChartManager;
use Drupal\charts\DependenciesCalculatorTrait;
use Drupal\charts\TypeManager;
use Drupal\Core\Config\ConfigEvents;
use Drupal\Core\Config\ConfigImporterEvent;
use Drupal\Core\Config\StorageTransformEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Ensure charts settings are calculated when configurations are imported.
*/
class ConfigImportSubscriber implements EventSubscriberInterface {
use DependenciesCalculatorTrait;
/**
* Constructs a ConfigImportSubscriber instance.
*
* @param \Drupal\charts\ChartManager $chart_manager
* The chart library plugin manager.
* @param \Drupal\charts\TypeManager $chart_type_manager
* The chart type plugin manager.
*/
public function __construct(ChartManager $chart_manager, TypeManager $chart_type_manager) {
$this->chartPluginManager = $chart_manager;
$this->chartTypePluginManager = $chart_type_manager;
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents(): array {
return [
ConfigEvents::STORAGE_TRANSFORM_IMPORT => ['onImportTransform'],
// There is no specific reason for choosing 50 beside it should be
// executed before \Drupal\Core\EventSubscriber::onConfigImporterImport()
// set at 40.
ConfigEvents::IMPORT => ['onConfigImporterImport', 50],
];
}
/**
* Ensure the config dependencies are calculated for charts settings.
*
* @param \Drupal\Core\Config\ConfigImporterEvent $event
* The event to process.
*
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
public function onConfigImporterImport(ConfigImporterEvent $event) {
$config_importer = $event->getConfigImporter();
$storage_comparer = $config_importer->getStorageComparer();
$source_storage = $storage_comparer->getSourceStorage();
$charts_config = $source_storage->read('charts.settings');
if ($settings = $charts_config['charts_default_settings'] ?? []) {
$target_storage = $storage_comparer->getTargetStorage();
$library = $settings['library'] ?? '';
$type = $settings['type'] ?? '';
$charts_config['dependencies'] = $this->calculateDependencies($library, $type);
$target_storage->write('charts.settings', $charts_config);
}
}
/**
* Acts when the storage is transformed for import.
*
* @param \Drupal\Core\Config\StorageTransformEvent $event
* The config storage transform event.
*
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
public function onImportTransform(StorageTransformEvent $event) {
$storage = $event->getStorage();
if ($charts_config = $storage->read('charts.settings')) {
$settings = $charts_config['charts_default_settings'];
$library = $settings['library'] ?? '';
$type = $settings['type'] ?? '';
$charts_config['dependencies'] = $this->calculateDependencies($library, $type);
$storage->write('charts.settings', $charts_config);
}
}
}

View File

@@ -0,0 +1,72 @@
<?php
namespace Drupal\charts\Form;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
/**
* Advanced tab on the Charts configuration form.
*/
class ChartsConfigAdvancedForm extends ConfigFormBase {
/**
* {@inheritdoc}
*/
protected function getEditableConfigNames() {
return ['charts.settings'];
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'charts_settings_advanced_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$config = $this->config('charts.settings');
$form['advanced'] = [
'#type' => 'container',
'#tree' => TRUE,
];
$form['advanced']['debug'] = [
'#type' => 'checkbox',
'#title' => $this->t('Enable Charts Debug'),
'#description' => $this->t("Show the JSON generated for the chart in a code block below the chart."),
'#default_value' => $config->get('advanced.debug'),
];
$form['advanced']['requirements'] = [
'#type' => 'details',
'#title' => $this->t('Requirement settings'),
'#description' => $this->t('The below requirements are checked by the <a href=":href">Status report</a>.', [':href' => Url::fromRoute('system.status')->toString()]),
'#open' => TRUE,
'#tree' => TRUE,
];
$form['advanced']['requirements']['cdn'] = [
'#type' => 'checkbox',
'#title' => $this->t('Use a CDN by default for external libraries'),
'#description' => $this->t('If checked, the module will use a CDN unless a local copy of the library is present. If unchecked, all warnings about missing libraries will be disabled.') . '<br/><br/>' . $this->t('Relying on a CDN (content delivery network) for external libraries can cause unexpected issues with Ajax and BigPipe support. For more information see: <a href=":href">Issue #1988968</a>', [':href' => 'https://www.drupal.org/project/drupal/issues/1988968']),
'#return_value' => TRUE,
'#default_value' => $config->get('advanced.requirements.cdn'),
];
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$advanced = $form_state->getValue('advanced');
$config = $this->config('charts.settings');
$config->set('advanced', $advanced)->save();
parent::submitForm($form, $form_state);
}
}

View File

@@ -0,0 +1,203 @@
<?php
namespace Drupal\charts\Form;
use Drupal\charts\ChartManager;
use Drupal\charts\DependenciesCalculatorTrait;
use Drupal\charts\TypeManager;
use Drupal\Component\Serialization\Yaml;
use Drupal\Core\Cache\CacheTagsInvalidatorInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Config\TypedConfigManagerInterface;
use Drupal\Core\Extension\ModuleExtensionList;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Charts Config Form.
*/
class ChartsConfigForm extends ConfigFormBase {
use DependenciesCalculatorTrait;
/**
* The cache tags invalidator.
*
* @var \Drupal\Core\Cache\CacheTagsInvalidatorInterface
*/
protected $cacheTagsInvalidator;
/**
* The chart library plugin manager.
*
* @var \Drupal\charts\ChartManager
*/
protected $chartPluginManager;
/**
* The chart type plugin library manager.
*
* @var \Drupal\charts\TypeManager
*/
protected $chartTypePluginManager;
/**
* The module extension service.
*
* @var \Drupal\Core\Extension\ModuleExtensionList
*/
protected $moduleExtensionList;
/**
* Constructs a new ChartsConfigForm.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* Config factory.
* @param \Drupal\Core\Config\TypedConfigManagerInterface $typedConfigManager
* Typed config manager.
* @param \Drupal\Core\Cache\CacheTagsInvalidatorInterface $cache_tags_invalidator
* Cache tag invalidator.
* @param \Drupal\charts\ChartManager|null $chart_plugin_manager
* The chart plugin manager.
* @param \Drupal\charts\TypeManager|null $chart_type_plugin_manager
* The chart type plugin manager.
* @param \Drupal\Core\Extension\ModuleExtensionList|null $module_extension_list
* The module extension list.
*/
public function __construct(ConfigFactoryInterface $config_factory, TypedConfigManagerInterface $typedConfigManager, CacheTagsInvalidatorInterface $cache_tags_invalidator, ChartManager $chart_plugin_manager = NULL, TypeManager $chart_type_plugin_manager = NULL, ModuleExtensionList $module_extension_list = NULL) {
parent::__construct($config_factory, $typedConfigManager);
$this->cacheTagsInvalidator = $cache_tags_invalidator;
// @todo Implement full if statement for optional parameters, and add
// deprecation warnings when they are not passed.
$this->chartPluginManager = $chart_plugin_manager ?: \Drupal::service('plugin.manager.charts');
$this->chartTypePluginManager = $chart_type_plugin_manager ?: \Drupal::service('plugin.manager.charts_type');
$this->moduleExtensionList = $module_extension_list ?: \Drupal::service('extension.list.module');
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('config.factory'),
$container->get('config.typed'),
$container->get('cache_tags.invalidator'),
$container->get('plugin.manager.charts'),
$container->get('plugin.manager.charts_type'),
$container->get('extension.list.module')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'charts_form_base';
}
/**
* {@inheritdoc}
*/
protected function getEditableConfigNames() {
return ['charts.settings'];
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$default_config = $this->config('charts.settings')->get('charts_default_settings') ?: [];
$form['help'] = [
'#type' => 'html_tag',
'#tag' => 'p',
'#value' => $this->t('The settings on this page are used to set
<strong>default</strong> settings. They do not affect existing charts.
To make a new chart, create a new view and select the display format of
"Chart." Or use a Charts Block and add your own data inside that block.
You can also attach a Chart field to your content (or other entity)
type and add your data within the Chart field.'),
];
$form['settings'] = [
'#type' => 'charts_settings',
'#used_in' => 'config_form',
'#required' => TRUE,
'#default_value' => $default_config,
];
$form['actions']['reset_to_default'] = [
'#type' => 'submit',
'#submit' => ['::submitReset'],
'#value' => $this->t('Reset to default configurations'),
'#weight' => 100,
'#access' => !empty($default_config['library']),
];
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
$settings = $form_state->getValue('settings');
if (empty($settings['library'])) {
$form_state->setError($form['settings'], $this->t('Please select a library to use by default or install a module implementing a chart library plugin.'));
}
parent::validateForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$settings = $form_state->getValue('settings');
// The settings form element is returning an unneeded 'defaults' value.
if (isset($settings['defaults'])) {
unset($settings['defaults']);
}
// Process the default colors to remove unneeded data.
foreach ($settings['display']['colors'] as $color_index => $color_item) {
$settings['display']['colors'][$color_index] = $color_item['color'];
}
// Save the main settings.
$config = $this->config('charts.settings');
$config->set('dependencies', $this->calculateDependencies($settings['library'], $settings['type']))
->set('charts_default_settings', $settings)
->save();
// Invalidate cache tags to refresh any view relying on this.
$this->cacheTagsInvalidator->invalidateTags($config->getCacheTags());
parent::submitForm($form, $form_state);
}
/**
* Reset submit callback.
*
* @param array $form
* The form structure.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*/
public function submitReset(array &$form, FormStateInterface $form_state) {
$path = $this->moduleExtensionList->getPath('charts');
$default_install_settings_file = $path . '/config/install/charts.settings.yml';
if (!file_exists($default_install_settings_file)) {
$this->messenger()->addWarning($this->t('We could not reset the configuration to default because the default settings file does not exist. Please re-download the charts module files.'));
return;
}
$config = $this->config('charts.settings');
$default_install_settings = Yaml::decode(file_get_contents($default_install_settings_file));
$config->set('charts_default_settings', $default_install_settings['charts_default_settings'])
->set('dependencies', $default_install_settings['dependencies'])
->save();
$this->messenger()->addStatus($this->t('The charts configuration were successfully reset to default.'));
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace Drupal\charts\Plugin\DataType;
use Drupal\Core\TypedData\TypedData;
/**
* Provides a data type wrapping for chart.
*
* @DataType(
* id = "chart_config",
* label = @Translation("Chart config"),
* description = @Translation("A chart configuration"),
* )
*/
class ChartConfigData extends TypedData {
/**
* Cached processed value.
*
* @var string
*/
protected $value;
}

View File

@@ -0,0 +1,89 @@
<?php
namespace Drupal\charts\Plugin\Field\FieldFormatter;
use Drupal\charts\Element\Chart;
use Drupal\Component\Utility\Html;
use Drupal\Core\Field\FieldItemInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\FormatterBase;
/**
* Plugin implementation of the "chart_config_default" formatter.
*
* @FieldFormatter(
* id = "chart_config_default",
* label = @Translation("Default"),
* field_types = {
* "chart_config",
* },
* )
*/
class ChartConfigItemDefaultFormatter extends FormatterBase {
/**
* {@inheritdoc}
*/
public function viewElements(FieldItemListInterface $items, $langcode) {
$elements = [];
$entity = $items->getEntity();
$entity_uuid = $entity->uuid();
$entity_type_id = $entity->getEntityTypeId();
$bundle = $entity->bundle();
$field_name = $items->getName();
$chart_id = $entity_type_id . '__' . $bundle;
foreach ($items as $delta => $item) {
$id = 'charts-item--' . $entity_uuid . '--' . $delta;
$elements[$delta] = $this->viewElement($item, $chart_id);
$elements[$delta]['#id'] = Html::getUniqueId($id);
$elements[$delta]['#chart_id'] = $chart_id;
$elements[$delta]['#entity'] = $entity;
$elements[$delta]['#field_name'] = $field_name;
}
return $elements;
}
/**
* Builds a renderable array for a single chart item.
*
* @param \Drupal\Core\Field\FieldItemInterface $item
* The chart field item.
* @param string $chart_id
* The chart id.
*
* @return array
* A renderable array.
*
* @throws \Drupal\Core\TypedData\Exception\MissingDataException
*/
protected function viewElement(FieldItemInterface $item, string $chart_id) {
$settings = $item->toArray()['config'];
if ($this->hasData($settings['series']['data_collector_table'])) {
return Chart::buildElement($settings, $chart_id);
}
return [];
}
/**
* Checks if the chart has data.
*
* @param array $data_collector_table
* The data collector table.
*
* @return bool
* TRUE if the chart has data, FALSE otherwise.
*/
protected function hasData(array $data_collector_table) {
foreach ($data_collector_table as $row) {
foreach ($row as $cell) {
if (!empty($cell['data'])) {
return TRUE;
}
}
}
return FALSE;
}
}

View File

@@ -0,0 +1,94 @@
<?php
namespace Drupal\charts\Plugin\Field\FieldType;
use Drupal\Core\Field\FieldItemBase;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\TypedData\DataDefinition;
/**
* Plugin implementation of the 'chart_config' field type.
*
* @FieldType(
* id = "chart_config",
* label = @Translation("Chart"),
* description = @Translation("An entity field containing data for a chart item"),
* default_widget = "chart_config_default",
* default_formatter = "chart_config_default"
* )
*/
class ChartConfigItem extends FieldItemBase {
/**
* {@inheritdoc}
*/
public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
$properties['config'] = DataDefinition::create('chart_config')
->setLabel(new TranslatableMarkup('Chart configuration'))
->setRequired(TRUE);
$properties['library'] = DataDefinition::create('string')
->setLabel(t('Chart library'));
$properties['type'] = DataDefinition::create('string')
->setLabel(t('Chart type'));
return $properties;
}
/**
* {@inheritdoc}
*/
public static function mainPropertyName() {
return 'config';
}
/**
* {@inheritdoc}
*/
public static function schema(FieldStorageDefinitionInterface $field_definition) {
return [
'columns' => [
'config' => [
'type' => 'blob',
'size' => 'big',
'not null' => TRUE,
'serialize' => TRUE,
],
'library' => [
'description' => 'The chart library.',
'type' => 'varchar_ascii',
'length' => 255,
'not null' => TRUE,
],
'type' => [
'description' => 'The chart type.',
'type' => 'varchar_ascii',
'length' => 255,
'not null' => TRUE,
],
],
];
}
/**
* {@inheritdoc}
*/
public function isEmpty() {
return empty($this->config);
}
/**
* {@inheritdoc}
*/
public function setValue($values, $notify = TRUE) {
if (is_array($values['config'])) {
$values += [
'library' => $values['config']['library'] ?? NULL,
'type' => $values['config']['type'] ?? NULL,
];
}
parent::setValue($values, $notify);
}
}

View File

@@ -0,0 +1,151 @@
<?php
namespace Drupal\charts\Plugin\Field\FieldWidget;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\WidgetBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Plugin implementation of the 'chart_config_default' widget.
*
* @FieldWidget(
* id = "chart_config_default",
* label = @Translation("Chart"),
* field_types = {
* "chart_config",
* },
* )
*/
class ChartConfigItemDefaultWidget extends WidgetBase implements ContainerFactoryPluginInterface {
/**
* The config factory service.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* Constructs a ChartItemDefaultWidget instance.
*
* @param string $plugin_id
* The plugin_id for the widget.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
* The definition of the field to which the widget is associated.
* @param array $settings
* The widget settings.
* @param array $third_party_settings
* Any third party settings.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory service.
*/
public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, ConfigFactoryInterface $config_factory) {
parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings);
$this->configFactory = $config_factory;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$plugin_id,
$plugin_definition,
$configuration['field_definition'],
$configuration['settings'],
$configuration['third_party_settings'],
$container->get('config.factory')
);
}
/**
* {@inheritdoc}
*/
public static function defaultSettings() {
return [
'change_default_library' => TRUE,
] + parent::defaultSettings();
}
/**
* {@inheritdoc}
*/
public function settingsForm(array $form, FormStateInterface $form_state) {
$elements = parent::settingsForm($form, $form_state);
$elements['change_default_library'] = [
'#type' => 'checkbox',
'#title' => $this->t('Allow users to change the default charting library'),
'#description' => $this->t('The default charting library can be updated at <a href="/admin/config/content/charts">the chart settings</a> page.'),
'#default_value' => $this->getSetting('change_default_library'),
];
return $elements;
}
/**
* {@inheritdoc}
*/
public function settingsSummary() {
$summary = [];
$change_default_library = $this->getSetting('change_default_library');
if (empty($change_default_library)) {
$summary[] = $this->t('User is not allowed to change/set the default charting library');
}
else {
$summary[] = $this->t('User is allowed to change/set the charting library');
}
return $summary;
}
/**
* {@inheritdoc}
*/
public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
$item = $items[$delta];
$value = !is_null($item->toArray()['config']) ? $item->toArray()['config'] : [];
$change_default_library = $this->getSetting('change_default_library');
$library = '';
// Build default settings.
$charts_settings = $this->configFactory->get('charts.settings');
$charts_default_settings = $charts_settings->get('charts_default_settings') ?? [];
$value = NestedArray::mergeDeep($charts_default_settings, $value) ?? [];
// Specify the library.
if (empty($change_default_library) && !empty($charts_default_settings['library'])) {
$library = $charts_default_settings['library'];
}
$element += [
'#type' => 'details',
'#open' => TRUE,
];
$element['config'] = [
'#type' => 'charts_settings',
'#used_in' => 'basic_form',
'#required' => $element['#required'],
'#series' => TRUE,
'#default_value' => $value,
'#library' => $library,
];
// Make the element none required for all at the default value widget.
if ($this->isDefaultValueWidget($form_state)) {
$element['config']['#required'] = FALSE;
}
return $element;
}
}

View File

@@ -0,0 +1,152 @@
<?php
namespace Drupal\charts\Plugin\chart\Library;
use Drupal\Component\Plugin\PluginBase;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
/**
* Base class Chart plugins.
*/
abstract class ChartBase extends PluginBase implements ChartInterface {
use StringTranslationTrait;
/**
* {@inheritdoc}
*/
public function getChartName(): string {
return $this->pluginDefinition['name'];
}
/**
* {@inheritdoc}
*/
public function getSupportedChartTypes(): array {
$types = $this->pluginDefinition['types'];
$chart_plugin_id = $this->getPluginId();
// @todo Add dependency injection for the next major version.
\Drupal::moduleHandler()->alter('charts_plugin_supported_chart_types', $types, $chart_plugin_id);
return $types;
}
/**
* {@inheritdoc}
*/
public function isSupportedChartType(string $chart_type_id): bool {
$supported_chart_types = $this->getSupportedChartTypes();
return !$supported_chart_types || in_array($chart_type_id, $supported_chart_types);
}
/**
* {@inheritdoc}
*/
public function getConfiguration(): array {
return $this->configuration;
}
/**
* {@inheritdoc}
*/
public function setConfiguration(array $configuration): void {
$this->configuration = NestedArray::mergeDeep($this->defaultConfiguration(), $configuration);
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [];
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
return $form;
}
/**
* {@inheritdoc}
*/
public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
}
/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
}
/**
* Gets defaults settings.
*
* @return array
* The defaults settings.
*/
public static function getDefaultSettings(): array {
return [
'type' => 'line',
'library' => NULL,
'grouping' => FALSE,
'fields' => [
'label' => NULL,
'data_providers' => NULL,
],
'display' => [
'title' => '',
'title_position' => 'out',
'data_labels' => FALSE,
'data_markers' => TRUE,
'legend' => TRUE,
'legend_position' => 'right',
'background' => '',
'three_dimensional' => FALSE,
'polar' => FALSE,
'tooltips' => TRUE,
'tooltips_use_html' => FALSE,
'dimensions' => [
'width' => NULL,
'width_units' => '%',
'height' => NULL,
'height_units' => 'px',
],
'gauge' => [
'green_to' => 100,
'green_from' => 85,
'yellow_to' => 85,
'yellow_from' => 50,
'red_to' => 50,
'red_from' => 0,
'max' => 100,
'min' => 0,
],
'colors' => self::getDefaultColors(),
],
];
}
/**
* Gets the default hex colors.
*
* @return array
* The hex colors.
*/
public static function getDefaultColors(): array {
return [
'#2f7ed8',
'#0d233a',
'#8bbc21',
'#910000',
'#1aadce',
'#492970',
'#f28f43',
'#77a1e5',
'#c42525',
'#a6c96a',
];
}
}

View File

@@ -0,0 +1,69 @@
<?php
namespace Drupal\charts\Plugin\chart\Library;
use Drupal\Component\Plugin\ConfigurableInterface;
use Drupal\Component\Plugin\PluginInspectionInterface;
use Drupal\Core\Plugin\PluginFormInterface;
/**
* Defines an interface for Chart plugins.
*/
interface ChartInterface extends PluginInspectionInterface, PluginFormInterface, ConfigurableInterface {
/**
* Used to define a single axis.
*
* Constant used in chartsTypeInfo() to declare chart types with a
* single axis. For example a pie chart only has a single dimension.
*/
const SINGLE_AXIS = 'y_only';
/**
* Used to define a dual axis.
*
* Constant used in chartsTypeInfo() to declare chart types with a dual
* axes. Most charts use this type of data, meaning multiple categories each
* have multiple values. This type of data is usually represented as a table.
*/
const DUAL_AXIS = 'xy';
/**
* Pre render.
*
* @param array $element
* The element.
*
* @return array
* The chart element.
*/
public function preRender(array $element);
/**
* Return the name of the chart.
*
* @return string
* Returns the name as a string.
*/
public function getChartName();
/**
* Gets the supported chart types.
*
* @return array
* The supported chart types.
*/
public function getSupportedChartTypes();
/**
* Checks if a chart type is supported.
*
* @param string $chart_type_id
* The chart type ID.
*
* @return bool
* TRUE if the chart type is supported, FALSE otherwise.
*/
public function isSupportedChartType(string $chart_type_id);
}

View File

@@ -0,0 +1,47 @@
<?php
namespace Drupal\charts\Plugin\chart\Type;
use Drupal\Core\Plugin\PluginBase;
/**
* Chart type class plugins.
*/
class Type extends PluginBase implements TypeInterface {
/**
* {@inheritdoc}
*/
public function getId() {
return $this->pluginDefinition['id'];
}
/**
* {@inheritdoc}
*/
public function getLabel() {
return $this->pluginDefinition['label'];
}
/**
* {@inheritdoc}
*/
public function getAxis() {
return $this->pluginDefinition['axis'];
}
/**
* {@inheritdoc}
*/
public function isAxisInverted() {
return $this->pluginDefinition['axis_inverted'] == TRUE;
}
/**
* {@inheritdoc}
*/
public function supportStacking() {
return $this->pluginDefinition['stacking'] == TRUE;
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace Drupal\charts\Plugin\chart\Type;
/**
* Defines an interface for Chart type plugins.
*/
interface TypeInterface {
/**
* Gets the chart type ID.
*
* @return string
* The chart type ID.
*/
public function getId();
/**
* Gets the chart type label.
*
* @return string
* The chart type label.
*/
public function getLabel();
/**
* Gets the chart type axis.
*
* @return string
* The chart type axis.
*/
public function getAxis();
/**
* Gets whether the chart type axis is inverted.
*
* @return bool
* TRUE if the chart type axis is inverted, FALSE otherwise.
*/
public function isAxisInverted();
/**
* Gets whether the chart type axis supports stacking.
*
* @return bool
* TRUE if the chart type axis supports stacking, FALSE otherwise.
*/
public function supportStacking();
}

View File

@@ -0,0 +1,153 @@
<?php
namespace Drupal\charts\Plugin\views\display;
use Drupal\Core\Form\FormStateInterface;
use Drupal\views\Plugin\views\display\Attachment;
use Drupal\views\ViewExecutable;
/**
* Display plugin to attach multiple chart configurations to the same chart.
*
* @ingroup views_display_plugins
*
* @ViewsDisplay(
* id = "chart_extension",
* title = @Translation("Chart attachment"),
* help = @Translation("Display that produces a chart."),
* theme = "views_view",
* contextual_links_locations = {""}
* )
*/
class ChartsPluginDisplayChart extends Attachment {
/**
* {@inheritdoc}
*/
protected function defineOptions() {
$options = parent::defineOptions();
$options['style_plugin']['default'] = 'chart';
$options['inherit_yaxis'] = ['default' => '1'];
// Set the default style plugin to 'chart'.
$options['style']['contains']['type']['default'] = 'chart';
$options['defaults']['default']['style'] = FALSE;
return $options;
}
/**
* {@inheritdoc}
*/
public function execute() {
return $this->view->render($this->display['id']);
}
/**
* {@inheritdoc}
*/
public function optionsSummary(&$categories, &$options) {
parent::optionsSummary($categories, $options);
$categories['attachment'] = [
'title' => $this->t('Chart settings'),
'column' => 'second',
'build' => ['#weight' => -10],
];
$displays = array_filter($this->getOption('displays'));
if (count($displays) > 1) {
$attach_to = $this->t('Multiple displays');
}
elseif (count($displays) == 1) {
$display = array_shift($displays);
if ($display = $this->view->storage->getDisplay($display)) {
$attach_to = $display['display_title'];
}
}
if (!isset($attach_to)) {
$attach_to = $this->t('Not defined');
}
$options['displays'] = [
'category' => 'attachment',
'title' => $this->t('Parent display'),
'value' => $attach_to,
];
$options['inherit_yaxis'] = [
'category' => 'attachment',
'title' => $this->t('Axis settings'),
'value' => $this->getOption('inherit_yaxis') ? $this->t('Use primary Y-axis') : $this->t('Create secondary axis'),
];
unset($options['attachment_position']);
}
/**
* {@inheritdoc}
*/
public function buildOptionsForm(&$form, FormStateInterface $form_state) {
parent::buildOptionsForm($form, $form_state);
switch ($form_state->get('section')) {
case 'displays':
$form['#title'] .= $this->t('Parent display');
break;
case 'inherit_yaxis':
$form['#title'] .= $this->t('Axis settings');
$form['inherit_yaxis'] = [
'#title' => $this->t('Y-Axis settings'),
'#type' => 'radios',
'#options' => [
1 => $this->t('Inherit primary of parent display'),
0 => $this->t('Create a secondary axis'),
],
'#default_value' => $this->getOption('inherit_yaxis'),
'#description' => $this->t('In most charts, the x- and y-axis from the parent display are both shared with each attached child chart. However, if this chart is going to use a different unit of measurement, a secondary axis may be added on the opposite side of the normal y-axis. Only create a secondary y-axis on the first chart attachment. You can rearrange displays if needed.'),
];
break;
}
}
/**
* {@inheritdoc}
*/
public function submitOptionsForm(&$form, FormStateInterface $form_state) {
// It is very important to call the parent function here:
parent::submitOptionsForm($form, $form_state);
$section = $form_state->get('section');
switch ($section) {
case 'displays':
$form_state->setValue($section, array_filter($form_state->getValue($section)));
break;
// @todo set isDefaulted to false by default.
case 'inherit_arguments':
case 'inherit_exposed_filters':
case 'inherit_pager':
case 'inherit_yaxis':
$this->setOption($section, $form_state->getValue($section));
break;
}
}
/**
* {@inheritdoc}
*/
public function attachTo(ViewExecutable $view, $display_id, array &$build) {
$displays = $this->getOption('displays');
if (empty($displays[$display_id])) {
return;
}
if (!$this->access()) {
return;
}
}
}

View File

@@ -0,0 +1,218 @@
<?php
namespace Drupal\charts\Plugin\views\field;
use Drupal\charts\ChartViewsFieldInterface;
use Drupal\Component\Serialization\Json;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\views\Plugin\views\field\FieldPluginBase;
use Drupal\views\ResultRow;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* @file
* Defines Drupal\charts\Plugin\views\field\BubbleField.
*/
/**
* Field handler to provide x, y, and z values for a bubble chart.
*
* @ingroup views_field_handlers
* @ViewsField("field_charts_fields_bubble")
*/
class BubbleField extends FieldPluginBase implements ContainerFactoryPluginInterface, ChartViewsFieldInterface {
/**
* The messenger service.
*
* @var \Drupal\Core\Messenger\MessengerInterface
*/
protected $messenger;
/**
* Constructs a \Drupal\views\Plugin\Block\ViewsBlockBase object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Messenger\MessengerInterface $messenger
* The messenger.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, MessengerInterface $messenger) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->messenger = $messenger;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('messenger')
);
}
/**
* Sets the initial field data at zero.
*/
public function query() {
}
/**
* {@inheritdoc}
*/
protected function defineOptions() {
$options = parent::defineOptions();
$this->field_alias = 'bubble_field';
$options['fieldset_one']['default'] = NULL;
$options['fieldset_two']['default'] = NULL;
$options['fieldset_three']['default'] = NULL;
$options['fieldset_one']['x_axis'] = ['default' => NULL];
$options['fieldset_two']['y_axis'] = ['default' => NULL];
$options['fieldset_three']['z_axis'] = ['default' => NULL];
return $options;
}
/**
* {@inheritdoc}
*/
public function buildOptionsForm(&$form, FormStateInterface $form_state) {
parent::buildOptionsForm($form, $form_state);
$fieldList = $this->displayHandler->getFieldLabels();
unset($fieldList['field_charts_fields_bubble']);
$form['fieldset_one'] = [
'#type' => 'fieldset',
'#title' => $this->t('Select the field representing the X axis.'),
'#collapsible' => FALSE,
'#collapsed' => FALSE,
'#weight' => -10,
'#required' => TRUE,
];
$form['fieldset_one']['x_axis'] = [
'#type' => 'radios',
'#title' => $this->t('X Axis Field'),
'#options' => $fieldList,
'#default_value' => $this->options['fieldset_one']['x_axis'],
'#weight' => -10,
];
$form['fieldset_two'] = [
'#type' => 'fieldset',
'#collapsible' => FALSE,
'#collapsed' => FALSE,
'#title' => $this->t('Select the field representing the Y axis.'),
'#weight' => -9,
'#required' => TRUE,
];
$form['fieldset_two']['y_axis'] = [
'#type' => 'radios',
'#title' => $this->t('Y Axis Field'),
'#options' => $fieldList,
'#default_value' => $this->options['fieldset_two']['y_axis'],
'#weight' => -9,
];
$form['fieldset_three'] = [
'#type' => 'fieldset',
'#collapsible' => FALSE,
'#collapsed' => FALSE,
'#title' => $this->t('Select the field representing the Z axis.'),
'#weight' => -9,
'#required' => TRUE,
];
$form['fieldset_three']['z_axis'] = [
'#type' => 'radios',
'#title' => $this->t('Z Axis Field'),
'#options' => $fieldList,
'#default_value' => $this->options['fieldset_three']['z_axis'],
'#weight' => -9,
];
return $form;
}
/**
* Get the value of a simple math field.
*
* @param \Drupal\views\ResultRow $values
* Row results.
* @param string $fieldset
* The items fieldset.
* @param string $axis
* Whether we are fetching field one's value.
*
* @return mixed
* The field value.
*
* @throws \Exception
*/
protected function getFieldValue(ResultRow $values, $fieldset, $axis) {
$field = $this->options[$fieldset][$axis];
$data = NULL;
// Fetch the data from the database alias.
if (isset($this->view->field[$field])) {
if ($field == 'bubble_field') {
$data = $this->view->field['field_charts_fields_bubble']->getValue($values);
}
else {
$data = $this->view->field[$field]->getValue($values);
}
}
if (!isset($data)) {
// There's no value. Default to 0.
$data = 0;
}
// Ensure the input is numeric.
if (!empty($data) && !is_numeric($data)) {
$this->messenger->addError($this->t('Check the formatting of your
Bubble Field inputs: one or both of them are not numeric.'));
}
return $data;
}
/**
* {@inheritdoc}
*
* @throws \Exception
*/
public function getValue(ResultRow $values, $field = NULL) {
parent::getValue($values, $field);
$xAxisFieldValue = $this->getFieldValue($values, 'fieldset_one', 'x_axis');
$yAxisFieldValue = $this->getFieldValue($values, 'fieldset_two', 'y_axis');
$zAxisFieldValue = $this->getFieldValue($values, 'fieldset_three', 'z_axis');
return Json::encode([
Json::decode($xAxisFieldValue),
Json::decode($yAxisFieldValue),
Json::decode($zAxisFieldValue),
]);
}
/**
* Set the data type for the chart field to be an array.
*
* @return string
* The data type.
*/
public function getChartFieldDataType(): string {
return 'array';
}
}

View File

@@ -0,0 +1,153 @@
<?php
namespace Drupal\charts\Plugin\views\field;
use Drupal\charts\Element\BaseSettings;
use Drupal\charts\Plugin\views\style\ChartsPluginStyleChart;
use Drupal\Core\Form\FormStateInterface;
use Drupal\views\Plugin\views\field\FieldPluginBase;
/**
* Provides a Views handler that exposes a Chart Type field.
*
* @ingroup views_field_handlers
*
* @ViewsField("field_exposed_chart_type")
*/
class ExposedChartType extends FieldPluginBase {
/**
* {@inheritdoc}
*/
public function canExpose() {
return TRUE;
}
/**
* {@inheritdoc}
*/
public function isExposed() {
return TRUE;
}
/**
* {@inheritdoc}
*/
public function buildExposedForm(&$form, FormStateInterface $form_state) {
$label = $this->options['label'] ? $this->options['label'] : 'Chart Type';
$selected_options = $this->options['chart_types'];
$style_plugin = $this->view->style_plugin;
$settings = $style_plugin->options['chart_settings'] ?? [];
$all_types = BaseSettings::getChartTypes($settings['library']);
$options = array_filter($all_types, function ($key) use ($selected_options) {
return in_array($key, $selected_options, TRUE);
}, ARRAY_FILTER_USE_KEY);
$chart_plugin_selected_type = $settings['type'] ?? '';
if ($chart_plugin_selected_type) {
// Move the selected.
if (isset($options[$chart_plugin_selected_type])) {
$options = [
$chart_plugin_selected_type => $options[$chart_plugin_selected_type],
] + $options;
}
else {
$options = [
$chart_plugin_selected_type => $all_types[$chart_plugin_selected_type],
] + $options;
}
}
$form['ct'] = [
'#title' => $this->t('@value', ['@value' => $label]),
'#type' => $this->options['exposed_select_type'],
'#options' => $options,
'#weight' => -20,
];
if ($this->options['exposed_select_type'] == 'radios') {
$form['ct']['#attributes']['class'] = [
'chart-type-radios',
'container-inline',
];
}
$form['ect'] = [
'#type' => 'hidden',
'#default_value' => 1,
];
}
/**
* {@inheritdoc}
*/
protected function defineOptions() {
$options = parent::defineOptions();
$options['chart_types'] = ['default' => []];
$options['exposed_select_type'] = ['default' => 'checkboxes'];
$options['expose'] = ['default' => ['identifier' => 'ct']];
return $options;
}
/**
* {@inheritdoc}
*/
public function buildOptionsForm(&$form, FormStateInterface $form_state) {
parent::buildOptionsForm($form, $form_state);
$style_plugin = $this->view->style_plugin;
$settings = $style_plugin->options['chart_settings'] ?? [];
$form['chart_types'] = [
'#type' => 'checkboxes',
'#title' => $this->t('Chart Type Options'),
'#description' => $this->t('Pick the chart type options to be exposed. You may need to disable your Views cache.'),
'#options' => BaseSettings::getChartTypes($settings['library']),
'#default_value' => $this->options['chart_types'],
];
if (!empty($settings['type'])) {
$form['chart_types'][$settings['type']] = [
'#default_value' => $settings['type'],
'#disabled' => TRUE,
];
}
$form['exposed_select_type'] = [
'#type' => 'radios',
'#title' => $this->t('Exposed Selection Type'),
'#description' => $this->t('Choose your options widget.'),
'#options' => [
'radios' => $this->t('Radios'),
'select' => $this->t('Single select'),
],
'#default_value' => $this->options['exposed_select_type'],
];
}
/**
* {@inheritdoc}
*/
public function validateOptionsForm(&$form, FormStateInterface $form_state) {
parent::validateOptionsForm($form, $form_state);
$style_plugin = $this->view->style_plugin;
if (!($style_plugin instanceof ChartsPluginStyleChart)) {
$form_state->setError($form['chart_types'], $this->t('You can only use this field type when the selected views style is chart!'));
}
}
/**
* {@inheritdoc}
*/
public function query() {
// This is not a real field and it does not affect the query. But Views
// won't render if the query() method is not present. This doesn't do
// anything, but it has to be here. This function is a void so it doesn't
// return anything.
}
}

View File

@@ -0,0 +1,201 @@
<?php
namespace Drupal\charts\Plugin\views\field;
use Drupal\charts\ChartViewsFieldInterface;
use Drupal\Component\Serialization\Json;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\views\Plugin\views\field\FieldPluginBase;
use Drupal\views\ResultRow;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* @file
* Defines Drupal\charts\Plugin\views\field\ScatterField.
*/
/**
* Field handler to provide x and y values for a scatter plot.
*
* @ingroup views_field_handlers
* @ViewsField("field_charts_fields_scatter")
*/
class ScatterField extends FieldPluginBase implements ContainerFactoryPluginInterface, ChartViewsFieldInterface {
/**
* The messenger service.
*
* @var \Drupal\Core\Messenger\MessengerInterface
*/
protected $messenger;
/**
* Constructs a \Drupal\views\Plugin\Block\ViewsBlockBase object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Messenger\MessengerInterface $messenger
* The messenger.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, MessengerInterface $messenger) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->messenger = $messenger;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('messenger')
);
}
/**
* Sets the initial field data at zero.
*/
public function query() {
}
/**
* {@inheritdoc}
*/
protected function defineOptions() {
$options = parent::defineOptions();
$this->field_alias = 'scatter_field';
$options['fieldset_one']['default'] = NULL;
$options['fieldset_two']['default'] = NULL;
$options['fieldset_one']['x_axis'] = ['default' => NULL];
$options['fieldset_two']['y_axis'] = ['default' => NULL];
return $options;
}
/**
* {@inheritdoc}
*/
public function buildOptionsForm(&$form, FormStateInterface $form_state) {
parent::buildOptionsForm($form, $form_state);
$fieldList = $this->displayHandler->getFieldLabels();
unset($fieldList['field_charts_fields_scatter']);
$form['fieldset_one'] = [
'#type' => 'fieldset',
'#title' => $this->t('Select the field representing the X axis.'),
'#collapsible' => FALSE,
'#collapsed' => FALSE,
'#weight' => -10,
'#required' => TRUE,
];
$form['fieldset_one']['x_axis'] = [
'#type' => 'radios',
'#title' => $this->t('X Axis Field'),
'#options' => $fieldList,
'#default_value' => $this->options['fieldset_one']['x_axis'],
'#weight' => -10,
];
$form['fieldset_two'] = [
'#type' => 'fieldset',
'#collapsible' => FALSE,
'#collapsed' => FALSE,
'#title' => $this->t('Select the field representing the Y axis.'),
'#weight' => -9,
'#required' => TRUE,
];
$form['fieldset_two']['y_axis'] = [
'#type' => 'radios',
'#title' => $this->t('Y Axis Field'),
'#options' => $fieldList,
'#default_value' => $this->options['fieldset_two']['y_axis'],
'#weight' => -9,
];
return $form;
}
/**
* Get the value of a simple math field.
*
* @param \Drupal\views\ResultRow $values
* Row results.
* @param bool $xAxis
* Whether we are fetching field one's value.
*
* @return mixed
* The field value.
*
* @throws \Exception
*/
protected function getFieldValue(ResultRow $values, $xAxis) {
if (!empty($xAxis)) {
$field = $this->options['fieldset_one']['x_axis'];
}
else {
$field = $this->options['fieldset_two']['y_axis'];
}
$data = NULL;
// Fetch the data from the database alias.
if (isset($this->view->field[$field])) {
if ($field == 'scatter_field') {
$data = $this->view->field['field_charts_fields_scatter']->getValue($values);
}
else {
$data = $this->view->field[$field]->getValue($values);
}
}
if (!isset($data)) {
// There's no value. Default to 0.
$data = 0;
}
// Ensure the input is numeric.
if (!empty($data) && !is_numeric($data)) {
$this->messenger->addError($this->t('Check the formatting of your
Scatter Field inputs: one or both of them are not numeric.'));
}
return $data;
}
/**
* {@inheritdoc}
*
* @throws \Exception
*/
public function getValue(ResultRow $values, $field = NULL) {
parent::getValue($values, $field);
$xAxisFieldValue = $this->getFieldValue($values, TRUE);
$yAxisFieldValue = $this->getFieldValue($values, FALSE);
return Json::encode([
Json::decode($xAxisFieldValue),
Json::decode($yAxisFieldValue),
]);
}
/**
* Set the data type for the chart field to be an array.
*
* @return string
* The data type.
*/
public function getChartFieldDataType(): string {
return 'array';
}
}

View File

@@ -0,0 +1,929 @@
<?php
namespace Drupal\charts\Plugin\views\style;
use Drupal\charts\ChartManager;
use Drupal\charts\ChartViewsFieldInterface;
use Drupal\charts\Element\BaseSettings;
use Drupal\charts\Plugin\chart\Library\ChartInterface;
use Drupal\charts\TypeManager;
use Drupal\Component\Serialization\Json;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\Xss;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\views\Plugin\views\field\EntityField;
use Drupal\views\Plugin\views\style\StylePluginBase;
use Drupal\views\ResultRow;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Style plugin to render view as a chart.
*
* @ingroup views_style_plugins
*
* @ViewsStyle(
* id = "chart",
* title = @Translation("Chart"),
* help = @Translation("Render a chart of your data."),
* theme = "views_view_charts",
* display_types = { "normal" }
* )
*/
class ChartsPluginStyleChart extends StylePluginBase implements ContainerFactoryPluginInterface {
/**
* {@inheritdoc}
*/
protected $usesFields = TRUE;
/**
* {@inheritdoc}
*/
protected $usesRowPlugin = TRUE;
/**
* The config factory service.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected ConfigFactoryInterface $configFactory;
/**
* The chart manager service.
*
* @var \Drupal\charts\ChartManager
*/
protected ChartManager $chartManager;
/**
* The chart type manager.
*
* @var \Drupal\charts\TypeManager
*/
protected TypeManager $chartTypeManager;
/**
* The label field key.
*
* @var string
*/
protected string $labelFieldKey;
/**
* The current route match.
*
* @var \Drupal\Core\Routing\RouteMatchInterface
*/
protected RouteMatchInterface $routeMatch;
/**
* Constructs a ChartsPluginStyleChart object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory service.
* @param \Drupal\charts\ChartManager $chart_manager
* The chart manager service.
* @param \Drupal\charts\TypeManager $chart_type_manager
* The chart type manager.
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The current route match.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, ConfigFactoryInterface $config_factory, ChartManager $chart_manager, TypeManager $chart_type_manager, RouteMatchInterface $route_match) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->configFactory = $config_factory;
$this->chartManager = $chart_manager;
$this->chartTypeManager = $chart_type_manager;
$this->routeMatch = $route_match;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('config.factory'),
$container->get('plugin.manager.charts'),
$container->get('plugin.manager.charts_type'),
$container->get('current_route_match')
);
}
/**
* {@inheritdoc}
*/
protected function defineOptions() {
$options = parent::defineOptions();
$charts_settings = $this->configFactory->get('charts.settings');
$charts_default_settings = $charts_settings->get('charts_default_settings') ?? [];
$options['chart_settings'] = [
'default' => $charts_default_settings,
];
$options['chart_settings']['fields']['allow_advanced_rendering'] = FALSE;
$options['chart_settings']['library'] = '';
// @todo ensure that chart extensions inherit defaults from parent
// Remove the default setting for chart type so it can be inherited if this
// is a chart extension type.
$style_plugin = $this->view->style_plugin ?? NULL;
$style_plugin_id = $style_plugin ? $style_plugin->getPluginId() : '';
if ($style_plugin_id === 'chart_extension') {
$options['chart_settings']['default']['type'] = NULL;
}
$options['path'] = ['default' => 'charts'];
return $options;
}
/**
* {@inheritdoc}
*/
public function buildOptionsForm(&$form, FormStateInterface $form_state) {
parent::buildOptionsForm($form, $form_state);
$handlers = $this->displayHandler->getHandlers('field');
if (empty($handlers)) {
$form['error_markup'] = ['#markup' => '<div class="error messages">' . $this->t('You need at least one field before you can configure your chart settings') . '</div>'];
return;
}
$settings_wrapper = 'views-charts-plugin-style-chart-options-settings-wrapper';
// Limit grouping options (we only support one grouping field).
if (isset($form['grouping'][0])) {
$form['grouping'][0]['field']['#title'] = $this->t('Grouping field');
$form['grouping'][0]['field']['#description'] = $this->t('If grouping by a particular field, that field will be used to determine stacking of the chart. Generally this will be the same field as what you select for the "Label field" below. If you do not have more than one "Provides data" field below, there will be nothing to stack. If you want to have another series displayed, use a "Chart attachment" display, and set it to attach to this display.');
$form['grouping'][0]['field']['#attributes']['class'][] = 'charts-grouping-field';
// Grouping by rendered version has no effect in charts. Hide the options.
$form['grouping'][0]['rendered']['#access'] = FALSE;
$form['grouping'][0]['rendered_strip']['#access'] = FALSE;
// Add ajax related to grouping to allow taxonomy colors selection when
// the field is an entity reference.
$form['grouping'][0]['field']['#ajax'] = [
'wrapper' => $settings_wrapper,
'callback' => [static::class, 'groupingChartSettingsAjaxCallback'],
];
}
if (isset($form['grouping'][1])) {
$form['grouping'][1]['#access'] = FALSE;
}
// Merge in the global chart settings form.
$field_options = $this->displayHandler->getFieldLabels();
$form_state->set('default_options', $this->options);
$form['chart_settings'] = [
'#prefix' => '<div id="' . $settings_wrapper . '">',
'#type' => 'charts_settings',
'#used_in' => 'view_form',
'#required' => TRUE,
'#field_options' => $field_options,
'#default_value' => $this->options['chart_settings'],
'#suffix' => '</div>',
'#view_charts_style_plugin' => $this,
];
}
/**
* {@inheritdoc}
*/
public function validateOptionsForm(&$form, FormStateInterface $form_state) {
parent::validateOptionsForm($form, $form_state);
if (!$form_state->hasValue(['style_options', 'chart_settings'])) {
return;
}
$chart_settings = $form_state->getValue(['style_options', 'chart_settings']);
$selected_library_id = $chart_settings['library'] ?? '';
if (empty($chart_settings['library'])) {
$form_state->setError($form['chart_settings'], $this->t('Please select a valid charting library or <a href="/admin/modules">install</a> at least one module that implements a chart library plugin.'));
return;
}
if (($selected_library_id === 'site_default' && !BaseSettings::getConfiguredSiteDefaultLibraryId()) || empty($chart_settings['type'])) {
$destination = '/admin/structure/views/view/' . $this->view->storage->id();
if ($this->view->current_display) {
$destination .= '/' . $this->view->current_display;
}
$form_state->setError($form['chart_settings'], $this->t('The site default charting library has not been set yet, or it does not support chart type options. Please ensure that you have correctly <a href="@url">configured the chart module</a>.', [
'@url' => '/admin/config/content/charts?destination=' . $destination,
]));
}
}
/**
* {@inheritdoc}
*/
public function validate() {
$errors = parent::validate();
$chart_settings = $this->options['chart_settings'];
$selected_library_id = $chart_settings['library'] ?? '';
if (!$selected_library_id) {
$errors[] = $this->t('Please select a valid charting library.');
return $errors;
}
if ($selected_library_id === 'site_default' && !BaseSettings::getConfiguredSiteDefaultLibraryId()) {
$destination = '/admin/structure/views/view/' . $this->view->storage->id();
if ($this->view->current_display) {
$destination .= '/' . $this->view->current_display;
}
$errors[] = $this->t('The site default charting library has not been set yet, or it does not support chart type options. Please ensure that you have correctly <a href="@url">configured the chart module</a>.', [
'@url' => '/admin/config/content/charts?destination=' . $destination,
]);
return $errors;
}
$selected_data_fields = !empty($chart_settings['fields']['data_providers']) && is_array($chart_settings['fields']['data_providers']) ?
$this->getSelectedDataFields($chart_settings['fields']['data_providers']) : NULL;
// Avoid calling validation before arriving at the view edit page.
if ($this->routeMatch->getRouteName() != 'views_ui.add' && empty($selected_data_fields)) {
$errors[] = $this->t('At least one data field must be selected in the chart configuration before this chart may be shown');
}
return $errors;
}
/**
* {@inheritdoc}
*/
public function render() {
$field_handlers = $this->view->getHandlers('field');
$chart_settings = $this->options['chart_settings'];
$chart_fields = $chart_settings['fields'];
$is_grouped = isset($this->options['grouping'][0]['field']);
// Calculate the labels field alias.
$label_field_key = $this->getLabelFieldKey();
// Assemble the fields to be used to provide data access.
$field_keys = array_keys($this->getSelectedDataFields($chart_fields['data_providers']));
$data_fields = array_filter($field_handlers, function ($field_handler) use ($field_keys, $label_field_key) {
if (isset($field_handler['exclude']) && ($field_handler['exclude'] == FALSE || $field_handler['exclude'] == 0)) {
$field_id = $field_handler['id'];
}
else {
$field_id = '';
}
// Do not allow the label field to be used as a data field.
return $field_id !== $label_field_key && in_array($field_id, $field_keys);
});
$title = !empty($chart_settings['display']['title']) ? $chart_settings['display']['title'] : '';
$subtitle = !empty($chart_settings['display']['subtitle']) ? $chart_settings['display']['subtitle'] : '';
if (!empty($title) || !empty($subtitle)) {
$tokens = [];
$global_tokens = [];
foreach ($field_handlers as $field_id => $field_handler) {
// This needs to run or else the values are empty.
$this->getField(0, $field_id);
// If the row index is not set, set it to 0.
if (!isset($this->view->row_index)) {
$this->view->row_index = 0;
}
$render_tokens = $this->view->field[$field_id]->getRenderTokens([]) ?? [];
$global_tokens = array_merge($render_tokens, $global_tokens);
}
foreach ($global_tokens as $key => $value) {
$tokens[$key] = Xss::filterAdmin($this->tokenizeValue($key, 0));
}
if (!empty($tokens)) {
// Allow argument tokens in the title.
$title = $this->viewsTokenReplace($title, $tokens);
// Allow argument tokens in the subtitle.
$subtitle = $this->viewsTokenReplace($subtitle, $tokens);
}
}
// To be used with the exposed chart type field.
if ($this->view->storage->get('exposed_chart_type')) {
$chart_settings['type'] = $this->view->storage->get('exposed_chart_type');
}
$chart_id = $this->view->id() . '_' . $this->view->current_display;
$chart = [
'#type' => 'chart',
'#chart_type' => $chart_settings['type'],
'#chart_library' => $chart_settings['library'],
'#chart_id' => $chart_id,
'#id' => Html::getUniqueId('chart_' . $chart_id),
'#stacking' => $chart_settings['fields']['stacking'] ?? '0',
'#polar' => $chart_settings['display']['polar'],
'#three_dimensional' => $chart_settings['display']['three_dimensional'],
'#gauge' => $chart_settings['display']['gauge'],
'#title' => $title,
'#title_position' => $chart_settings['display']['title_position'],
'#subtitle' => $subtitle,
'#tooltips' => $chart_settings['display']['tooltips'],
'#data_labels' => $chart_settings['display']['data_labels'],
'#data_markers' => $chart_settings['display']['data_markers'],
// Colors only used if a grouped view or using a type such as a pie chart.
'#colors' => $chart_settings['display']['colors'] ?? [],
'#background' => $chart_settings['display']['background'] ?? 'transparent',
'#legend' => !empty($chart_settings['display']['legend_position']),
'#legend_position' => $chart_settings['display']['legend_position'] ?? '',
'#width' => $chart_settings['display']['dimensions']['width'],
'#height' => $chart_settings['display']['dimensions']['height'],
'#width_units' => $chart_settings['display']['dimensions']['width_units'],
'#height_units' => $chart_settings['display']['dimensions']['height_units'],
'#color_changer' => $chart_settings['display']['color_changer'] ?? FALSE,
'#attributes' => ['data-drupal-selector-chart' => Html::getId($chart_id)],
// Pass info about the actual view results to allow further processing.
'#view' => $this->view,
];
$chart_type = $this->chartTypeManager->getDefinition($chart_settings['type']);
if ($chart_type['axis'] === ChartInterface::SINGLE_AXIS) {
$data_field_key = key($data_fields);
$data_field = $data_fields[$data_field_key];
$data = [];
$this->renderFields($this->view->result);
$renders = $this->rendered_fields;
if (!$label_field_key && count($data_fields) > 1) {
foreach ($data_fields as $field_id => $row) {
$data_row = [];
if (!empty($row['label'])) {
$data_row['name'] = strip_tags($row['label'], ENT_QUOTES);
}
else {
$data_row['name'] = strip_tags($field_id, ENT_QUOTES);
}
if (!empty($chart_fields['data_providers'][$field_id]['color'])) {
$data_row['color'] = $chart_fields['data_providers'][$field_id]['color'];
}
$data_row['y'] = $this->processNumberValueFromField(0, $field_id);
$data[] = $data_row;
}
}
else {
foreach ($renders as $row_number => $row) {
$data_row = [];
if ($label_field_key) {
// Labels need to be decoded; the charting library will re-encode.
$data_row[] = trim(strip_tags($this->getField($row_number, $label_field_key), ENT_QUOTES));
}
$data_row[] = $this->processNumberValueFromField($row_number, $data_field_key);
$data[] = $data_row;
}
}
// @todo create a textfield for chart legend title.
// if ($chart_fields['label']) {
// $chart['#legend_title'] = $chart_fields['label'];
// }
$chart[$this->view->current_display . '_series'] = [
'#type' => 'chart_data',
'#data' => $data,
'#title' => $data_field['label'],
'#color' => isset($chart_fields['data_providers'][$data_field_key]) ? $chart_fields['data_providers'][$data_field_key]['color'] : '',
'#grouping_colors' => $this->extractGroupingColorsForSingleAxisChartType($data),
];
}
else {
$chart['xaxis'] = [
'#type' => 'chart_xaxis',
'#title' => $chart_settings['xaxis']['title'] ?? '',
'#labels_rotation' => $chart_settings['xaxis']['labels_rotation'],
];
$chart['yaxis'] = [
'#type' => 'chart_yaxis',
'#title' => $chart_settings['yaxis']['title'] ?? '',
'#labels_rotation' => $chart_settings['yaxis']['labels_rotation'],
'#max' => $chart_settings['yaxis']['max'],
'#min' => $chart_settings['yaxis']['min'],
];
$view_records = $this->view->result;
$sets = $this->renderGrouping($view_records, $this->options['grouping'], TRUE);
if ($is_grouped) {
$this->groupedChartElementBuild($chart, $sets, $data_fields);
}
else {
$series_index = 0;
foreach ($sets as $series_label => $data_set) {
foreach ($data_fields as $field_key => $field_handler) {
$element_key = $this->view->current_display . '__' . $field_key . '_' . $series_index;
$chart[$element_key] = [
'#type' => 'chart_data',
'#data' => [],
// If using a grouping field, inherit from the chart level colors.
'#color' => ($series_label === '' && isset($chart_fields['data_providers'][$field_key])) ? $chart_fields['data_providers'][$field_key]['color'] : '',
'#title' => $series_label ? strip_tags($series_label) : $field_handler['label'],
'#prefix' => $chart_settings['yaxis']['prefix'] ?? NULL,
'#suffix' => $chart_settings['yaxis']['suffix'] ?? NULL,
'#decimal_count' => $chart_settings['yaxis']['decimal_count'] ?? '',
];
}
// Grouped results come back indexed by their original result number
// from before the grouping, so we need to keep our own row number
// when looping through the rows.
foreach ($data_set['rows'] as $result_number => $row) {
$xaxis_label = trim(strip_tags((string) $this->getField($result_number, $label_field_key)));
if ($label_field_key) {
$xaxis_labels = $chart['xaxis']['#labels'] ?? [];
if (!in_array($xaxis_label, $xaxis_labels)) {
$chart['xaxis']['#labels'][] = $xaxis_label;
}
}
foreach ($data_fields as $field_key => $field_handler) {
$element_key = $this->view->current_display . '__' . $field_key . '_' . $series_index;
$value = $this->processNumberValueFromField($result_number, $field_key);
$chart[$element_key]['#data'][] = $value;
$chart[$element_key]['#mapped_data'][$xaxis_label] = $value;
if (strpos($field_handler['id'], 'field_charts_fields_scatter') === 0 || strpos($field_handler['id'], 'field_charts_fields_bubble') === 0) {
$chart['xaxis']['#labels'] = [];
}
}
}
// Incrementing series index.
$series_index++;
}
}
}
// Check if this display has any children charts that should be applied
// on top of it.
$children_displays = $this->getChildrenChartDisplays();
// Contains the different subviews of the attachments.
$attachments = [];
foreach ($children_displays as $child_display) {
// If the user doesn't have access to the child display, skip.
if (!$this->view->access($child_display)) {
continue;
}
// Generate the subchart by executing the child display. We load a fresh
// view here to avoid collisions in shifting the current display while in
// a display.
$subview = $this->view->createDuplicate();
$subview->setDisplay($child_display);
$child_display_handler = $this->view->displayHandlers->get($child_display);
$child_display_settings = $subview->display_handler->options['style']['options']['chart_settings'];
// Copy the settings for our axes over to the child view.
foreach ($chart_settings as $option_name => $option_value) {
if ($child_display_handler->options['inherit_yaxis'] === '1') {
$child_display_settings[$option_name] = $option_value;
}
}
// Set the arguments on the subview if it is configured to inherit;
// arguments.
if (!empty($child_display_handler->display['display_options']['inherit_arguments']) && $child_display_handler->display['display_options']['inherit_arguments'] == '1') {
$subview->setArguments($this->view->args);
}
// Execute the subview and get the result.
$subview->preExecute();
$subview->execute();
// If there's no results, don't attach the subview.
if (empty($subview->result)) {
continue;
}
$subchart = $subview->style_plugin->render();
// Add attachment views to attachments array.
array_push($attachments, $subview);
// Create a secondary axis if needed.
if ($child_display_handler->options['inherit_yaxis'] !== '1' && isset($subchart['yaxis'])) {
$chart['secondary_yaxis'] = $subchart['yaxis'];
$chart['secondary_yaxis']['#opposite'] = TRUE;
}
// Merge in the child chart data.
foreach (Element::children($subchart) as $key) {
if ($subchart[$key]['#type'] === 'chart_data') {
// This ensures that chart attachment data are placed correctly,
// but it doesn't allow for chart attachment data to have x-axis
// labels not already present in the parent chart.
if (!empty($chart['xaxis']['#labels'])) {
$processed_data = $this->alignSubchartData($chart['xaxis']['#labels'], $subchart[$key]['#mapped_data'], $subchart[$key]['#data']);
$subchart[$key]['#data'] = $processed_data;
}
$chart[$key] = $subchart[$key];
$chart[$key]['#chart_attachment_id'] = $child_display_handler->display['id'];
// If the subchart is a different type than the parent chart, set
// the #chart_type property on the individual chart data elements.
if ($subchart['#chart_type'] !== $chart['#chart_type']) {
$chart[$key]['#chart_type'] = $subchart['#chart_type'];
}
if ($child_display_handler->options['inherit_yaxis'] !== '1') {
$chart[$key]['#target_axis'] = 'secondary_yaxis';
}
}
}
}
// Print the chart.
return $chart;
}
/**
* {@inheritdoc}
*/
public function renderGrouping($records, $groupings = [], $group_rendered = NULL) {
if (empty($this->options['grouping'])) {
return parent::renderGrouping($records, $groupings, $group_rendered);
}
$xaxis_labels = [];
// Get the entire sets with grouping.
$sets = [];
// For the chart plugin the grouping level is always going to be at index
// 0 since only one grouping is allowed.
$grouping_level = 0;
$grouping_field_info = $groupings[$grouping_level];
$grouping_field = $grouping_field_info['field'];
$xaxis_label_field_key = $this->getLabelFieldKey();
$chart_settings = $this->options['chart_settings'];
$color_selection_method = $chart_settings['fields']['entity_grouping']['color_selection_method'] ?? '';
foreach ($records as $index => $row) {
$set = &$sets;
// Extract xaxis labels.
if (isset($this->view->field[$xaxis_label_field_key])) {
$xaxis_label = $this->getField($index, $xaxis_label_field_key);
$xaxis_label = trim(strip_tags(htmlspecialchars_decode($xaxis_label)));
if (!in_array($xaxis_label, $xaxis_labels, TRUE)) {
$xaxis_labels[] = $xaxis_label;
}
$row->xaxis_label_index = array_flip($xaxis_labels)[$xaxis_label];
}
$grouping = '';
$group_content = '';
// Extract grouping content/label.
if (isset($this->view->field[$grouping_field])) {
$group_content = $this->getField($index, $grouping_field);
$group_content = $grouping = trim(strip_tags(htmlspecialchars_decode($group_content)));
}
// Create the group if it does not exist yet.
if (empty($set[$grouping])) {
$grouping_entity_field = $this->view->field[$grouping_field];
$group_field_name = $grouping_entity_field ? ($grouping_entity_field->definition['field_name'] ?? '') : '';
if ($color_selection_method && $group_field_name && $grouping_entity_field instanceof EntityField && $row instanceof ResultRow) {
switch ($color_selection_method) {
case 'by_entities_on_entity_reference':
$set[$grouping]['color'] = $this->extractGroupedSelectedColorByEntity($grouping_entity_field, $row, $group_field_name);
break;
case 'by_field_on_referenced_entity':
$set[$grouping]['color'] = $this->extractGroupedSelectedColorOnReferencedEntityField($grouping_entity_field, $row, $group_field_name);
break;
}
}
$set[$grouping]['group'] = $group_content;
$set[$grouping]['level'] = $grouping_level;
$set[$grouping]['rows'] = [];
}
// Move the set reference into the set of the group we just determined.
$set = &$set[$grouping]['rows'];
// Add the row to the hierarchically positioned set we just determined.
$set[$index] = $row;
}
// Adding a workaround to pass the xaxis labels of grouping to the calling
// function.
$sets['_charts_xaxis_labels'] = $xaxis_labels;
return $sets;
}
/**
* Utility function to check if this chart has children displays.
*
* @return array
* Children Chart Display.
*/
public function getChildrenChartDisplays() {
$children_displays = $this->displayHandler->getAttachedDisplays();
foreach ($children_displays as $key => $child) {
$display_handler = $this->view->displayHandlers->get($child);
// Unset disabled & non chart attachments.
if ((!$display_handler->isEnabled()) || (strstr($child, 'chart_extension') == !TRUE)) {
unset($children_displays[$key]);
}
}
return array_values($children_displays);
}
/**
* {@inheritdoc}
*/
public function calculateDependencies() {
$dependencies = [];
if (!empty($this->options['chart_settings']['library'])) {
$plugin_definition = $this->chartManager->getDefinition($this->options['chart_settings']['library']);
$dependencies['module'] = [$plugin_definition['provider']];
}
return $dependencies;
}
/**
* Processes number value based on field.
*
* @param int $number
* The number.
* @param string $field
* The field.
*
* @return \Drupal\Component\Render\MarkupInterface|float|null
* The value.
*/
public function processNumberValueFromField($number, $field) {
if (is_array($this->getField($number, $field))) {
$value = $this->getField($number, $field)->__toString();
}
else {
$value = $this->getField($number, $field);
}
$value = trim(strip_tags($value));
// Get the field plugin class to determine if a Charts-specific field
// is being used.
$field_plugin = $this->view->field[$field];
if ($field_plugin instanceof ChartViewsFieldInterface && $field_plugin->getChartFieldDataType() === 'array') {
return Json::decode($value);
}
// Convert empty strings to NULL.
if ($value === '' || is_null($value)) {
$value = NULL;
}
else {
// Strip thousands placeholders if present, then cast to float.
$value = (float) str_replace([',', ' '], '', $value);
}
return $value;
}
/**
* Utility method to filter out unselected fields from data providers fields.
*
* @param array $data_providers
* The data providers.
*
* @return array
* The fields.
*/
protected function getSelectedDataFields(array $data_providers) {
return array_filter($data_providers, function ($value) {
return !empty($value['enabled']);
});
}
/**
* Returns the key of the Label Field.
*
* @return string
* The Label Field key.
*/
private function getLabelFieldKey() {
if (!isset($this->labelFieldKey)) {
$field_handlers = $this->view->getHandlers('field');
$chart_settings = $this->options['chart_settings'];
$chart_fields = $chart_settings['fields'];
$label_field = $field_handlers[$chart_fields['label']] ?? '';
$this->labelFieldKey = $label_field ? $chart_fields['label'] : '';
}
return $this->labelFieldKey;
}
/**
* Helper method to build the chart data elements of a grouped chart.
*
* @param array $chart
* The main chart element.
* @param array $sets
* The grouped record set.
* @param array $data_fields
* The selected data fields on the chart style options.
*/
private function groupedChartElementBuild(array &$chart, array $sets, array $data_fields) {
$original_xaxis = $chart['xaxis'];
$xaxis_labels = [];
$label_field_key = $this->getLabelFieldKey();
if (!empty($sets['_charts_xaxis_labels'])) {
$chart['xaxis']['#labels'] = $sets['_charts_xaxis_labels'];
unset($sets['_charts_xaxis_labels']);
// Flipping the labels here to get access to their index based on value.
$xaxis_labels = array_flip($chart['xaxis']['#labels']);
}
$series_index = 0;
$element_key_prefix = $this->view->current_display . '__' . $label_field_key;
$chart_settings = $this->options['chart_settings'];
foreach ($sets as $set_label => $data_set) {
$name = strtolower(Html::cleanCssIdentifier('set-label-' . $set_label));
// Remove the added prefix after processing.
$name = substr($name, strlen('set-label-'));
$element_key = $element_key_prefix . '__' . $name;
$chart[$element_key] = [
'#type' => 'chart_data',
'#data' => $xaxis_labels ? array_fill(0, count($xaxis_labels), NULL) : [],
// If using a grouping field, inherit from the chart level colors.
'#title' => $set_label,
'#prefix' => $chart_settings['yaxis']['prefix'] ?? NULL,
'#suffix' => $chart_settings['yaxis']['suffix'] ?? NULL,
'#decimal_count' => $chart_settings['yaxis']['decimal_count'] ?? '',
];
if (!empty($data_set['color'])) {
$chart[$element_key]['#color'] = $data_set['color'];
}
foreach ($data_set['rows'] as $result_number => $row) {
$set_id = $row->xaxis_label_index ?? $series_index;
foreach ($data_fields as $field_key => $field_handler) {
// Don't allow the grouping field to provide data.
if ($field_key === $this->options['grouping'][0]['field']) {
continue;
}
$value = $this->processNumberValueFromField($result_number, $field_key);
if (strpos($field_handler['id'], 'field_charts_fields_scatter') === 0 || strpos($field_handler['id'], 'field_charts_fields_bubble') === 0) {
$chart[$element_key]['#data'] = [];
$chart[$element_key]['#data'][] = $value;
$chart['xaxis'] = $original_xaxis;
}
else {
$chart[$element_key]['#data'][$set_id] = $value;
$chart[$element_key]['#mapped_data'][$set_id] = $value;
}
}
}
// Incrementing series index.
$series_index++;
}
}
/**
* Grouping chart settings ajax callback.
*
* @param array $form
* The form.
* @param \Drupal\core\form\FormStateInterface $form_state
* The form state.
*
* @return array
* The render array of the chart settings.
*/
public static function groupingChartSettingsAjaxCallback(array $form, FormStateInterface $form_state) {
return $form['options']['style_options']['chart_settings'];
}
/**
* Returns the selected color.
*
* @param \Drupal\views\Plugin\views\field\EntityField $view_entity_field
* The view entity field.
* @param \Drupal\views\ResultRow $row
* The result row.
* @param string $group_field_name
* The grouping field name.
*
* @return string
* The color.
*/
private function extractGroupedSelectedColorByEntity(EntityField $view_entity_field, ResultRow $row, string $group_field_name) {
$chart_settings = $this->options['chart_settings'];
$colors_settings = $chart_settings['fields']['entity_grouping']['selected_method']['colors'] ?? [];
/** @var \Drupal\Core\Entity\ContentEntityInterface $host_entity */
$host_entity = $view_entity_field->getEntity($row);
/** @var \Drupal\Core\Entity\ContentEntityInterface $referenced_entity */
$referenced_entity = $host_entity->get($group_field_name)->entity;
if (!$referenced_entity || !$colors_settings) {
return '';
}
$entity_type = $referenced_entity->getEntityType();
$has_uuid_key = $entity_type->hasKey('uuid');
$color_id_key = $has_uuid_key ? $referenced_entity->get('uuid')->value : $referenced_entity->id();
return $colors_settings[$color_id_key]['color'] ?? '';
}
/**
* Returns the color from the referenced entity field.
*
* @param \Drupal\views\Plugin\views\field\EntityField $view_entity_field
* The view entity field.
* @param \Drupal\views\ResultRow $row
* The result row.
* @param string $group_field_name
* The grouping field name.
*
* @return string
* The color.
*/
private function extractGroupedSelectedColorOnReferencedEntityField(EntityField $view_entity_field, ResultRow $row, string $group_field_name) {
$chart_settings = $this->options['chart_settings'];
$color_field_name = $chart_settings['fields']['entity_grouping']['selected_method']['color_field_name'] ?? '';
if (!$color_field_name) {
return '';
}
/** @var \Drupal\Core\Entity\ContentEntityInterface $host_entity */
$host_entity = $view_entity_field->getEntity($row);
/** @var \Drupal\Core\Entity\ContentEntityInterface $referenced_entity */
$referenced_entity = $host_entity->get($group_field_name)->entity;
$field_item_list = $referenced_entity ? $referenced_entity->get($color_field_name) : NULL;
if (!$field_item_list || $field_item_list->isEmpty()) {
return '';
}
/** @var \Drupal\color_field\Plugin\Field\FieldType\ColorFieldType $color_field */
$color_field = $field_item_list->first();
return $color_field->getFieldDefinition()->getType() === 'color_field_type' ? $color_field->color : '';
}
/**
* Ensures chart attachments are placed correctly on chart.
*
* @param array $parent_labels
* The parent labels.
* @param array $child_mapped_data
* The mapped data of the child display.
* @param array $data
* The data.
*
* @return array
* $processed_data
*/
private function alignSubchartData(array $parent_labels, array $child_mapped_data, array $data) {
$child_labels = array_keys($child_mapped_data);
if ($parent_labels === $child_labels) {
return $data;
}
$processed_data = [];
foreach ($parent_labels as $parent_label) {
$processed_data[] = $child_mapped_data[$parent_label] ?? NULL;
}
return $processed_data;
}
/**
* Returns the grouping colors for single axis chart type.
*
* @param array $data
* The data.
*
* @return array
* The grouping colors.
*/
private function extractGroupingColorsForSingleAxisChartType(array $data): array {
if (empty($this->options['grouping'][0]['field'])) {
return [];
}
$grouping_colors = [];
$grouping_field = $this->options['grouping'][0]['field'];
$chart_settings = $this->options['chart_settings'];
$color_selection_method = $chart_settings['fields']['entity_grouping']['color_selection_method'] ?? '';
$grouping_entity_field = $this->view->field[$grouping_field];
$group_field_name = $grouping_entity_field ? ($grouping_entity_field->definition['field_name'] ?? '') : '';
foreach ($data as $index => $set) {
$row = $this->view->result[$index];
if ($color_selection_method && $group_field_name && $grouping_entity_field instanceof EntityField && $row instanceof ResultRow) {
switch ($color_selection_method) {
case 'by_entities_on_entity_reference':
$grouping_colors[$index][$set[0]] = $this->extractGroupedSelectedColorByEntity($grouping_entity_field, $row, $group_field_name);
break;
case 'by_field_on_referenced_entity':
$grouping_colors[$index][$set[0]] = $this->extractGroupedSelectedColorOnReferencedEntityField($grouping_entity_field, $row, $group_field_name);
break;
}
}
}
return $grouping_colors;
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace Drupal\charts;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Extension\ModuleUninstallValidatorInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
/**
* Prevents uninstalling of a module providing default chart library plugin.
*/
class PluginsUninstallValidator implements ModuleUninstallValidatorInterface {
use StringTranslationTrait;
/**
* The config factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* Constructs a new ChartsPluginsUninstallValidator.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory.
*/
public function __construct(ConfigFactoryInterface $config_factory) {
$this->configFactory = $config_factory;
}
/**
* {@inheritdoc}
*/
public function validate($module) {
$reasons = [];
$config = $this->configFactory->get('charts.settings');
$dependent_modules = $config->get('dependencies.module') ?? [];
if (in_array($module, $dependent_modules)) {
$reasons[] = $this->t('Provides a chart library or chart type plugin configured as the default option for chart. Please update <a href="/admin/config/content/charts?destination=/admin/modules/uninstall">the configuration</a> or reset them before uninstalling it.');
}
return $reasons;
}
}

View File

@@ -0,0 +1,88 @@
<?php
// phpcs:ignoreFile
/**
* This file was generated via php core/scripts/generate-proxy-class.php 'Drupal\charts\PluginsUninstallValidator' "web/modules/custom/charts/src".
*/
namespace Drupal\charts\ProxyClass {
/**
* Provides a proxy class for \Drupal\charts\PluginsUninstallValidator.
*
* @see \Drupal\Component\ProxyBuilder
*/
class PluginsUninstallValidator implements \Drupal\Core\Extension\ModuleUninstallValidatorInterface
{
use \Drupal\Core\DependencyInjection\DependencySerializationTrait;
/**
* The id of the original proxied service.
*
* @var string
*/
protected $drupalProxyOriginalServiceId;
/**
* The real proxied service, after it was lazy loaded.
*
* @var \Drupal\charts\PluginsUninstallValidator
*/
protected $service;
/**
* The service container.
*
* @var \Symfony\Component\DependencyInjection\ContainerInterface
*/
protected $container;
/**
* Constructs a ProxyClass Drupal proxy object.
*
* @param \Symfony\Component\DependencyInjection\ContainerInterface $container
* The container.
* @param string $drupal_proxy_original_service_id
* The service ID of the original service.
*/
public function __construct(\Symfony\Component\DependencyInjection\ContainerInterface $container, $drupal_proxy_original_service_id)
{
$this->container = $container;
$this->drupalProxyOriginalServiceId = $drupal_proxy_original_service_id;
}
/**
* Lazy loads the real service from the container.
*
* @return object
* Returns the constructed real service.
*/
protected function lazyLoadItself()
{
if (!isset($this->service)) {
$this->service = $this->container->get($this->drupalProxyOriginalServiceId);
}
return $this->service;
}
/**
* {@inheritdoc}
*/
public function validate($module)
{
return $this->lazyLoadItself()->validate($module);
}
/**
* {@inheritdoc}
*/
public function setStringTranslation(\Drupal\Core\StringTranslation\TranslationInterface $translation)
{
return $this->lazyLoadItself()->setStringTranslation($translation);
}
}
}

View File

@@ -0,0 +1,104 @@
<?php
namespace Drupal\charts;
use Drupal\charts\Event\ChartsEvents;
use Drupal\charts\Event\TypesInfoEvent;
use Drupal\charts\Plugin\chart\Library\ChartInterface;
use Drupal\charts\Plugin\chart\Type\Type;
use Drupal\Component\Plugin\Exception\PluginException;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\DefaultPluginManager;
use Drupal\Core\Plugin\Discovery\ContainerDerivativeDiscoveryDecorator;
use Drupal\Core\Plugin\Discovery\YamlDiscovery;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
/**
* Manages discovery and instantiation of charts_type_info plugins.
*
* @see \Drupal\charts\Plugin\chart\Type\TypeInterface
*
* @see plugin_api
*/
class TypeManager extends DefaultPluginManager {
/**
* Default values for each plugin.
*
* @var array
*/
protected $defaults = [
'id' => '',
'label' => '',
'axis' => ChartInterface::DUAL_AXIS,
'axis_inverted' => FALSE,
'stacking' => FALSE,
'class' => Type::class,
];
/**
* The event dispatcher.
*
* @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
*/
protected $eventDispatcher;
/**
* Constructs a new TypeManager object.
*
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
* The cache backend.
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
* Even dispatcher.
*/
public function __construct(ModuleHandlerInterface $module_handler, CacheBackendInterface $cache_backend, EventDispatcherInterface $event_dispatcher) {
$this->moduleHandler = $module_handler;
$this->eventDispatcher = $event_dispatcher;
$this->setCacheBackend($cache_backend, 'charts_type', ['charts_type']);
$this->alterInfo('charts_type_info');
}
/**
* {@inheritdoc}
*/
protected function getDiscovery() {
if (!isset($this->discovery)) {
$this->discovery = new YamlDiscovery('charts_types', $this->moduleHandler->getModuleDirectories());
$this->discovery->addTranslatableProperty('label', 'label_context');
$this->discovery = new ContainerDerivativeDiscoveryDecorator($this->discovery);
}
return $this->discovery;
}
/**
* {@inheritdoc}
*/
public function processDefinition(&$definition, $plugin_id) {
parent::processDefinition($definition, $plugin_id);
$definition['id'] = $plugin_id;
foreach (['label'] as $required_property) {
if (empty($definition[$required_property])) {
throw new PluginException(sprintf('The charts type %s must define the %s property.', $plugin_id, $required_property));
}
}
}
/**
* {@inheritdoc}
*/
public function getDefinitions() {
$definitions = parent::getDefinitions();
// Allow other modules to alter the definition list.
$event = new TypesInfoEvent($definitions);
$this->eventDispatcher->dispatch($event, ChartsEvents::TYPE_INFO);
$definitions = $event->getTypes();
return $definitions;
}
}

View File

@@ -0,0 +1,151 @@
<?php
namespace Drupal\charts\Util;
use Drupal\views\ViewExecutable;
/**
* Utilities class containing various helper methods.
*/
class Util {
/**
* Views Data.
*
* @param \Drupal\views\ViewExecutable|null $view
* View.
* @param array $labelValues
* Label Values.
* @param string $labelField
* Label Field.
* @param array $color
* Colors.
* @param string|null $attachmentChartTypeOption
* Attachment Chart Type Option.
*
* @return array
* Data.
*/
public static function viewsData(ViewExecutable $view = NULL, array $labelValues = [], $labelField = '', array $color = [], $attachmentChartTypeOption = NULL) {
$data = [];
$style_options = $view->getStyle()->options['chart_settings'];
foreach ($view->result as $row_number => $row) {
$view->row_index = $row->index;
$numberFields = 0;
$rowData = [];
foreach ($labelValues as $fieldId => $rowDataValue) {
if ($style_options['fields']['allow_advanced_rendering'] == 1 || isset($view->field[$labelField]->options['type']) && $view->field[$labelField]->options['type'] === 'timestamp') {
$renderedLabelField = $view->field[$labelField]->advancedRender($row);
}
else {
$renderedLabelField = $view->field[$labelField]->getValue($row);
}
$renderedLabelField = strip_tags($renderedLabelField);
$rowData[$numberFields] = [
'value' => $style_options['fields']['allow_advanced_rendering'] ? $view->field[$fieldId]->advancedRender($row) : $view->field[$fieldId]->getValue($row),
'label_field' => $renderedLabelField,
'label' => $view->field[$fieldId]->label(),
'color' => $color[$fieldId]['color'],
'type' => $attachmentChartTypeOption,
];
$numberFields++;
}
$data[$row_number] = $rowData;
}
return $data;
}
/**
* Removes unselected fields.
*
* @param array $valueField
* Value Field.
*
* @return array
* Field Values.
*/
public static function removeUnselectedFields(array $valueField = []) {
$fieldValues = [];
foreach ($valueField as $key => $value) {
if (!empty($value)) {
$fieldValues[$key] = $value;
}
}
return $fieldValues;
}
/**
* Remove hidden fields.
*
* @param \Drupal\views\ViewExecutable $view
* The view.
* @param array $fieldValues
* Field values.
*
* @return array
* Visible views.
*/
public static function removeHiddenFields(ViewExecutable $view, array $fieldValues) {
$fields = $view->display_handler->getOption('fields');
$visibleFields = array_filter($fields, function ($field) {
return !empty($field['exclude']);
});
$visibleFields = array_diff_key($fieldValues, $visibleFields);
return $visibleFields;
}
/**
* Creates chart data to be used later by visualization frameworks.
*
* @param array $data
* Data.
*
* @return array
* Chart Data.
*/
public static function createChartableData(array $data = []) {
$chartData = [];
$categories = [];
$seriesData = [];
for ($i = 0; $i < count($data[0]); $i++) {
$seriesRowData = [
'name' => '',
'color' => '',
'type' => '',
'data' => [],
];
for ($j = 0; $j < count($data); $j++) {
$categories[$j] = $data[$j][$i]['label_field'];
$seriesRowData['name'] = $data[$j][$i]['label'];
$seriesRowData['type'] = $data[$j][$i]['type'];
$seriesRowData['color'] = $data[$j][$i]['color'];
array_push($seriesRowData['data'], (json_decode(($data[$j][$i]['value']))));
}
array_push($seriesData, $seriesRowData);
}
$chartData[0] = $categories;
$chartData[1] = $seriesData;
return $chartData;
}
/**
* Checks for missing libraries necessary for data visualization.
*
* @param string $libraryPath
* Library Path.
*/
public static function checkMissingLibrary($libraryPath = '') {
if (!file_exists(DRUPAL_ROOT . DIRECTORY_SEPARATOR . $libraryPath)) {
\Drupal::service('messenger')
->addMessage(t('Charting libraries might not be installed at the location @libraryPath.', [
'@libraryPath' => $libraryPath,
]), 'error');
}
}
}