charts_echarts-1.0.0-alpha1/src/Plugin/chart/Library/Echarts.php
src/Plugin/chart/Library/Echarts.php
<?php
namespace Drupal\charts_echarts\Plugin\chart\Library;
use Drupal\charts\Plugin\chart\Library\ChartBase;
use Drupal\Component\Utility\Color;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Url;
/**
* Define a concrete class for a Chart.
*
* @Chart(
* id = "echarts",
* name = @Translation("ECharts"),
* types = {
* "area",
* "bar",
* "bubble",
* "column",
* "donut",
* "gauge",
* "line",
* "pie",
* "scatter",
* "spline",
* },
* )
*/
class Echarts extends ChartBase {
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$form = parent::buildConfigurationForm($form, $form_state);
$form['placeholder'] = [
'#title' => $this->t('Placeholder'),
'#type' => 'fieldset',
'#description' => $this->t(
'This is a placeholder for eChart.js-specific library options. If you would like to help build this out, please work from <a href="@issue_link">this issue</a>.', [
'@issue_link' => Url::fromUri('https://www.drupal.org/project/charts/issues/3046984')
->toString(),
]),
];
$xaxis_configuration = $this->configuration['xaxis'] ?? [];
$yaxis_configuration = $this->configuration['yaxis'] ?? [];
$form['xaxis'] = [
'#title' => $this->t('X-Axis Settings'),
'#type' => 'fieldset',
'#tree' => TRUE,
];
$form['xaxis']['autoskip'] = [
'#title' => $this->t('Enable autoskip'),
'#type' => 'checkbox',
'#default_value' => $xaxis_configuration['autoskip'] ?? 1,
];
$form['xaxis']['label_rotation'] = [
'#title' => $this->t('Label Rotation'),
'#type' => 'number',
'#default_value' => $xaxis_configuration['label_rotation'] ?? 0,
'#description' => $this->t('Rotate the X-axis labels by the specified degree.'),
];
$form['xaxis']['line_style'] = [
'#title' => $this->t('Line Style'),
'#type' => 'select',
'#options' => [
'solid' => $this->t('Solid'),
'dashed' => $this->t('Dashed'),
'dotted' => $this->t('Dotted'),
],
'#default_value' => $xaxis_configuration['line_style'] ?? 'solid',
];
$form['xaxis']['horizontal_axis_title_align'] = [
'#title' => $this->t('Align horizontal axis title'),
'#type' => 'select',
'#options' => [
'start' => $this->t('Start'),
'center' => $this->t('Center'),
'end' => $this->t('End'),
],
'#default_value' => $xaxis_configuration['horizontal_axis_title_align'] ?? '',
];
$form['yaxis'] = [
'#title' => $this->t('Y-Axis Settings'),
'#type' => 'fieldset',
'#tree' => TRUE,
];
$form['yaxis']['vertical_axis_title_align'] = [
'#title' => $this->t('Align vertical axis title'),
'#type' => 'select',
'#options' => [
'start' => $this->t('Start'),
'center' => $this->t('Center'),
'end' => $this->t('End'),
],
'#default_value' => $yaxis_configuration['vertical_axis_title_align'] ?? '',
];
return $form;
}
/**
* Build configurations.
*
* @param array $form
* The form element.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
parent::submitConfigurationForm($form, $form_state);
if (!$form_state->getErrors()) {
$values = $form_state->getValue($form['#parents']);
$this->configuration['xaxis'] = $values['xaxis'];
$this->configuration['yaxis'] = $values['yaxis'];
}
}
/**
* {@inheritdoc}
*/
public function preRender(array $element) {
$chart_definition = [];
if (!isset($element['#id'])) {
$element['#id'] = Html::getUniqueId('echarts-render');
}
$chart_definition = $this->populateDatasets($element, $chart_definition);
$chart_definition = $this->populateOptions($element, $chart_definition);
$element['#attached']['library'][] = 'charts_echarts/echarts';
$element['#attributes']['class'][] = 'charts-echarts';
$element['#chart_definition'] = $chart_definition;
return $element;
}
/**
* Populate options.
*
* @param array $element
* The element.
* @param array $chart_definition
* The chart definition.
*
* @return array
* Return the chart definition.
*/
private function populateOptions(array $element, array $chart_definition) {
$chart_type = $this->populateChartType($element);
$children = Element::children($element);
$y_axes = [];
foreach ($children as $child) {
$type = $element[$child]['#type'];
if ($type === 'chart_xaxis' && !in_array($chart_type, ['pie', 'doughnut'])) {
$categories = [];
if (!empty($element[$child]['#labels'])) {
$categories = array_map('strip_tags', $element[$child]['#labels']);
}
// Merge in axis raw options.
if (!empty($element[$child]['#raw_options'])) {
$categories = NestedArray::mergeDeepArray([
$element[$child]['#raw_options'],
$categories,
]);
}
$chart_definition['options']['xAxis']['data'] = $categories;
if ($chart_type !== 'radar') {
if (!empty($element[$child]['#title'])) {
$chart_definition['options']['xAxis']['name'] = $element[$child]['#title'];
}
}
else {
$radar = new \stdClass();
$indicators = [];
foreach ($categories as $category) {
$indicator = new \stdClass();
$indicator->name = $category;
$indicators[] = $indicator;
}
$radar->indicator = $indicators;
$chart_definition['options']['radar'] = $radar;
unset($chart_definition['options']['xAxis']);
}
if ($chart_type === 'scatter') {
// From the Handbook: However, the normal scene for the scatter
// chart is to have 2 continuous value axis (also called the
// cartesian coordinate system). The series type is different in that
// both x-axis and y-axis value are included in data, but not in
// xAxis and yAxis.
unset($chart_definition['options']['xAxis']['data']);
}
}
if ($type === 'chart_yaxis') {
if ($chart_type !== 'radar') {
if (!empty($element[$child]['#min'])) {
$y_axes[$child]['min'] = $element[$child]['#min'];
}
if (!empty($element[$child]['#max'])) {
$y_axes[$child]['max'] = $element[$child]['#max'];
}
if (!empty($element[$child]['#title'])) {
$y_axes[$child]['name'] = $element[$child]['#title'];
}
}
else {
unset($chart_definition['options']['yAxis']);
}
}
}
if (!in_array($chart_type, ['pie', 'doughnut'])) {
$chart_definition['options']['yAxis'] = array_values($y_axes);
}
$chart_definition['options']['title'] = $this->buildTitle($element);
$chart_definition['options']['tooltip'] = $this->buildTooltip($element);
$chart_definition['options']['toolbox'] = $this->buildToolbox($element);
$chart_definition['options']['legend'] = $this->buildLegend($element);
$chart_definition['options']['width'] = $element['#width'] ?? 800;
$chart_definition['options']['width_units'] = $element['#width_units'] ?? 'px';
$chart_definition['options']['height'] = $element['#height'] ?? 400;
$chart_definition['options']['height_units'] = $element['#height_units'] ?? 'px';
// Adjust grid layout to ensure space for both the legend and the toolbox.
$chart_definition['options']['grid'] = [
'top' => 100,
'right' => ($element['#legend_position'] === 'right') ? 100 : 50,
'bottom' => ($element['#legend_position'] === 'bottom') ? 100 : 50,
'left' => 100,
];
// Merge in chart raw options.
if (!empty($element['#raw_options'])) {
$chart_definition = NestedArray::mergeDeepArray([
$chart_definition,
$element['#raw_options'],
]);
}
if ($element['#chart_type'] === 'bar') {
$new_x_axis = $chart_definition['options']['yAxis'];
$new_y_axis = $chart_definition['options']['xAxis'];
$chart_definition['options']['yAxis'] = $new_y_axis;
$chart_definition['options']['xAxis'] = $new_x_axis;
}
return $chart_definition;
}
/**
* Populate yAxis array.
*
* @param array $element
* The element.
*
* @return array
* Return the chart y axes.
*/
private function getYaxisArray(array $element) {
$y_axes = [];
foreach (Element::children($element) as $key) {
if ($element[$key]['#type'] === 'chart_yaxis') {
if ($element[$key]['#opposite'] === TRUE) {
$y_axes[$key] = 1;
}
else {
$y_axes[$key] = 0;
}
}
}
return $y_axes;
}
/**
* Populate Dataset.
*
* @param array $element
* The element.
* @param array $chart_definition
* The chart definition.
*
* @return array
* Return the chart definition.
*/
private function populateDatasets(array $element, array $chart_definition) {
$chart_type = $this->populateChartType($element);
$datasets = [];
foreach (Element::children($element) as $key) {
if ($element[$key]['#type'] === 'chart_data') {
$series_data = [];
$dataset = new \stdClass();
// Populate the data.
foreach ($element[$key]['#data'] as $data_index => $data) {
if (isset($series_data[$data_index])) {
$series_data[$data_index][] = $data;
}
else {
/*
* This is here to account for differences between Views and
* the API. Will change if someone can find a better way.
*/
if ($chart_type === 'gauge' && !empty($data[1])) {
$data = ['value' => $data[1], 'name' => $data[0]];
}
if (in_array($chart_type, ['pie', 'doughnut'])) {
if (!empty($data[1])) {
$data = ['value' => $data[1], 'name' => $data[0]];
}
else {
// Handle the case where $data[1] is null.
$data = [
'value' => 0,
'name' => $data[0],
];
}
// Assign default colors from $element['#colors'] if available.
if (empty($chart_definition['options']['color'])) {
$chart_definition['options']['color'] = $element['#colors'] ?? [];
}
}
$series_data[$data_index] = $data;
}
}
if (!empty($element['#stacking']) && $element['#stacking'] == 1) {
$dataset->stack = 'total';
}
$dataset->name = $element[$key]['#title'];
$dataset->data = $series_data;
$series_type = isset($element[$key]['#chart_type']) ? $this->populateChartType($element[$key]) : $chart_type;
$dataset->type = $series_type;
if (!empty($element[$key]['#color'])) {
$dataset->color = $element[$key]['#color'];
}
$background_style = new \stdClass();
$background_style->color = $element[$key]['#color'];
$dataset->backgroundStyle = $background_style;
if (in_array($chart_type, ['pie', 'doughnut'])) {
$dataset->type = 'pie';
if ($chart_type === 'pie') {
$dataset->radius = '60%';
}
if ($chart_type === 'doughnut') {
$dataset->radius = ['40%', '70%'];
}
// Use x-axis labels as the labels for the slices.
if (!empty($element['x_axis']['#labels'])) {
foreach ($dataset->data as $index => &$data_item) {
if (!empty($element['x_axis']['#labels'][$index])) {
$data_item['name'] = strip_tags($element['x_axis']['#labels'][$index]);
}
}
}
// Assign different colors for each slice from $element['#colors'].
if (!empty($element['#colors'])) {
foreach ($dataset->data as $index => &$data_item) {
$data_item['itemStyle'] = [
'color' => $element['#colors'][$index % count($element['#colors'])],
];
}
}
}
if ($chart_type === 'gauge') {
$line_style = new \stdClass();
$line_style->width = 30;
$line_style->color = [
[0.3, '#67e0e3'],
[0.7, '#37a2da'],
[1, '#fd666d'],
];
$axis_line = new \stdClass();
$axis_line->lineStyle = $line_style;
$dataset->axisLine = $axis_line;
$axis_tick = new \stdClass();
$axis_tick->distance = -30;
$axis_tick->length = 8;
$axis_line_style = new \stdClass();
$axis_line_style->color = '#fff';
$axis_line_style->width = 2;
$axis_tick->lineStyle = $axis_line_style;
$dataset->axisTick = $axis_tick;
$split_line = new \stdClass();
$split_line_line_style = new \stdClass();
$split_line->distance = -30;
$split_line->length = 30;
$split_line_line_style->color = '#fff';
$split_line_line_style->width = 4;
$split_line->lineStyle = $split_line_line_style;
$dataset->splitLine = $split_line;
$axis_label = new \stdClass();
$axis_label->color = 'auto';
$axis_label->distance = 40;
$axis_label->fontSize = 14;
$dataset->axisLabel = $axis_label;
}
if ((!empty($element[$key]['#chart_type']) && $element[$key]['#chart_type'] === 'area') || (!empty($element['#chart_type']) && $element['#chart_type'] === 'area')) {
$dataset->type = 'line';
$background_style = new \stdClass();
$background_style->color = $this->getTranslucentColor($element[$key]['#color']);
$dataset->backgroundStyle = $background_style;
$dataset->areaStyle = new \stdClass();
}
if (!empty($element[$key]['#target_axis'])) {
$y_axes_array = $this->getYaxisArray($element);
$dataset->yAxisIndex = $y_axes_array[$element[$key]['#target_axis']];
}
if (!empty($element['#connect_nulls'])) {
$dataset->connectNulls = TRUE;
}
$datasets[] = $dataset;
}
// Merge in axis raw options.
if (!empty($element[$key]['#raw_options'])) {
$datasets = NestedArray::mergeDeepArray([
$datasets,
$element[$key]['#raw_options'],
]);
}
}
if ($chart_type === 'radar' && $datasets) {
$new_datasets = new \stdClass();
$combined_dataset = [];
foreach ($datasets as $dataset) {
$combined_dataset_item = new \stdClass();
$combined_dataset_item->name = $dataset->name;
$combined_dataset_item->value = $dataset->data;
$combined_dataset[] = $combined_dataset_item;
}
$new_datasets->data = $combined_dataset;
$new_datasets->type = 'radar';
$datasets = $new_datasets;
}
$chart_definition['options']['series'] = $datasets;
return $chart_definition;
}
/**
* Outputs a type that can be used by Chart.js.
*
* @param array $element
* The given element.
*
* @return string
* The generated type.
*/
protected function populateChartType(array $element) {
switch ($element['#chart_type']) {
case 'bar':
case 'column':
$type = 'bar';
break;
case 'area':
case 'spline':
$type = 'line';
break;
case 'donut':
$type = 'doughnut';
break;
case 'gauge':
$type = 'gauge';
break;
default:
$type = $element['#chart_type'];
break;
}
if (isset($element['#polar']) && $element['#polar'] == 1) {
$type = 'radar';
}
return $type;
}
/**
* Builds legend based on element properties.
*
* @param array $element
* The element.
*
* @return array
* The legend array.
*/
protected function buildLegend(array $element) {
$chart_type = $this->populateChartType($element);
$legend = [];
// Configure the legend display.
$legend['show'] = (bool) $element['#legend'];
if ($element['#legend_position'] === 'right') {
$legend['right'] = 0;
$legend['top'] = 'center';
$legend['orient'] = 'vertical';
}
elseif ($element['#legend_position'] === 'bottom') {
$legend['bottom'] = 0;
$legend['left'] = 'center';
$legend['orient'] = 'horizontal';
}
else {
$legend[$element['#legend_position']] = 0;
$legend['orient'] = 'horizontal';
}
$legend['type'] = 'scroll';
// Configure legend position.
if (!empty($element['#legend_position'])) {
if (!empty($element['#legend_font_weight'])) {
$legend['textStyle']['fontWeight'] = $element['#legend_font_weight'];
}
if (!empty($element['#legend_font_style'])) {
$legend['textStyle']['fontStyle'] = $element['#legend_font_style'];
}
if (!empty($element['#legend_font_size'])) {
$legend['textStyle']['fontSize'] = $element['#legend_font_size'];
}
}
return $legend;
}
/**
* Builds tooltip based on element properties.
*
* @param array $element
* The element.
*
* @return array
* The tooltip array.
*/
protected function buildTooltip(array $element) {
$chart_type = $this->populateChartType($element);
// Configure the tooltip display.
$tooltip = [];
$tooltip['show'] = (bool) $element['#tooltips'];
if (in_array($chart_type, ['pie', 'doughnut'])) {
$tooltip['trigger'] = 'item';
}
else {
$tooltip['trigger'] = 'axis';
$tooltip['showContent'] = TRUE;
$tooltip['alwaysShowContent'] = TRUE;
$tooltip['axisPointer'] = [
'label' => [
'show' => FALSE,
],
];
}
$tooltip['triggerOn'] = 'mousemove';
$tooltip['textStyle']['fontSize'] = '10';
return $tooltip;
}
/**
* Builds toolbox based on element properties.
*
* @param array $element
* The element.
*
* @return array
* The toolbox array.
*/
protected function buildToolbox(array $element) {
$toolbox = [];
// Configure the toolbox display.
$toolbox['show'] = TRUE;
$toolbox['feature'] = [
'mark' => [
'show' => TRUE,
],
'dataZoom' => [
'yAxisIndex' => 'none',
],
'magicType' => [
'show' => TRUE,
'type' => ['line', 'bar'],
'title' => [
'line' => 'Line',
'bar' => 'Bar',
],
],
'restore' => [
'show' => TRUE,
'title' => 'Reset',
],
'saveAsImage' => [
'show' => TRUE,
'title' => 'Save',
],
];
return $toolbox;
}
/**
* Builds title based on element properties.
*
* @param array $element
* The element.
*
* @return array
* The title array.
*/
protected function buildTitle(array $element) {
$title = [];
if (!empty($element['#title'])) {
$title = [
'show' => TRUE,
'text' => $element['#title'],
];
if (!empty($element['#subtitle'])) {
$title['subtext'] = $element['#subtitle'];
}
if (!empty($element['#title_position'])) {
if (in_array($element['#title_position'], ['in', 'out'])) {
$title['top'] = 'auto';
}
else {
$title[$element['#title_position']] = 'auto';
}
}
if (!empty($element['#title_color'])) {
$title['textStyle']['color'] = $element['#title_color'];
}
if (!empty($element['#title_font_weight'])) {
$title['textStyle']['fontWeight'] = $element['#title_font_weight'];
}
if (!empty($element['#title_font_style'])) {
$title['textStyle']['fontStyle'] = $element['#title_font_style'];
}
if (!empty($element['#title_font_size'])) {
$title['textStyle']['fontSize'] = $element['#title_font_size'];
}
}
else {
// Ensure title is an array even if empty.
$title = [
'show' => FALSE,
'text' => '',
];
}
return $title;
}
/**
* Get translucent color.
*
* @param string $color
* The color.
*
* @return string
* The color.
*/
protected function getTranslucentColor($color) {
if (!$color) {
return '';
}
$rgb = Color::hexToRgb($color);
return 'rgba(' . implode(",", $rgb) . ',' . 0.5 . ')';
}
}
