dvf-2.x-dev/src/Plugin/Visualisation/Style/VisualisationStyleBase.php
src/Plugin/Visualisation/Style/VisualisationStyleBase.php
<?php
namespace Drupal\dvf\Plugin\Visualisation\Style;
use Drupal\Component\Utility\NestedArray;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Plugin\PluginBase;
use Drupal\dvf\ConfigurablePluginTrait;
use Drupal\dvf\DvfHelpers;
use Drupal\dvf\Plugin\VisualisationInterface;
use Drupal\dvf\Plugin\VisualisationStyleInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Component\Render\FormattableMarkup;
/**
* Provides a base class for VisualisationStyle plugins.
*/
abstract class VisualisationStyleBase extends PluginBase implements VisualisationStyleInterface, ContainerFactoryPluginInterface {
use ConfigurablePluginTrait;
/**
* The visualisation.
*
* @var \Drupal\dvf\Plugin\VisualisationInterface
*/
protected $visualisation;
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The logger.
*
* @var \Psr\Log\LoggerInterface
*/
protected $logger;
/**
* DVF Helpers.
*
* @var \Drupal\dvf\DvfHelpers
*/
protected $dvfHelpers;
/**
* The Messenger service.
*
* @var \Drupal\Core\Messenger\MessengerInterface
*/
protected $messenger;
/**
* Constructs a new VisualisationStyleBase.
*
* @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\dvf\Plugin\VisualisationInterface $visualisation
* The visualisation context in which the plugin will run.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Psr\Log\LoggerInterface $logger
* Instance of the logger object.
* @param \Drupal\dvf\DvfHelpers $dvf_helpers
* The DVF helpers.
* @param \Drupal\Core\Messenger\MessengerInterface $messenger
* The Messenger service.
*/
public function __construct(
array $configuration,
$plugin_id,
$plugin_definition,
VisualisationInterface $visualisation = NULL,
ModuleHandlerInterface $module_handler,
LoggerInterface $logger,
DvfHelpers $dvf_helpers,
MessengerInterface $messenger
) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->visualisation = $visualisation;
$this->moduleHandler = $module_handler;
$this->logger = $logger;
$this->dvfHelpers = $dvf_helpers;
$this->messenger = $messenger;
}
/**
* Creates an instance of the plugin.
*
* @param \Symfony\Component\DependencyInjection\ContainerInterface $container
* The container to pull out services used in the plugin.
* @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\dvf\Plugin\VisualisationInterface $visualisation
* The visualisation context in which the plugin will run.
*
* @return static
* Returns an instance of this plugin.
*/
public static function create(
ContainerInterface $container,
array $configuration,
$plugin_id,
$plugin_definition,
VisualisationInterface $visualisation = NULL
) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$visualisation,
$container->get('module_handler'),
$container->get('logger.channel.dvf'),
$container->get('dvf.helpers'),
$container->get('messenger')
);
}
/**
* {@inheritdoc}
*/
public function getConfiguration() {
return NestedArray::mergeDeep($this->defaultConfiguration(), $this->configuration);
}
/**
* {@inheritdoc}
*/
public function setConfiguration(array $configuration) {
$this->configuration = $configuration;
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [
'data' => [
'fields' => [],
'field_labels' => '',
'split_field' => '',
'cache_expiry' => '',
'column_overrides' => [],
'data_filters' => [],
],
];
}
/**
* {@inheritdoc}
*/
public function calculateDependencies() {
return [];
}
/**
* {@inheritdoc}
*/
public function settingsForm(array $form, FormStateInterface $form_state) {
$form['#after_build'][] = [get_called_class(), 'afterBuildSettingsForm'];
$form['#attached']['library'][] = 'dvf/dvfAdmin';
$form['data'] = [
'#type' => 'details',
'#title' => $this->t('Data settings'),
'#tree' => TRUE,
'#open' => TRUE,
];
$form['data']['fields'] = [
'#type' => 'select',
'#title' => $this->t('Fields'),
'#description' => $this->t('What fields to include in the visualisation. Select at least one field to display its data. A field is typically a column in a CSV. @help',
['@help' => $this->dvfHelpers->getHelpPageLink('keys')]),
'#options' => $this->getSourceFieldOptions(),
'#multiple' => TRUE,
'#size' => 5,
'#default_value' => $this->config('data', 'fields'),
'#required' => TRUE,
];
$form['data']['field_labels'] = [
'#type' => 'textarea',
'#title' => $this->t('Field label overrides'),
'#description' => $this->t('Optionally override one or more field labels. Add one original_label|new_label per line and separate with a pipe. @help',
['@help' => $this->dvfHelpers->getHelpPageLink('label-overrides')]),
'#rows' => 2,
'#default_value' => $this->config('data', 'field_labels'),
'#placeholder' => 'Old label|New label',
];
$form['data']['split_field'] = [
'#type' => 'select',
'#title' => $this->t('Split field'),
'#description' => $this->t('Optionally split into multiple visualisations based on the value of this field. A new visualisation will be made for each unique value in this field. @help',
['@help' => $this->dvfHelpers->getHelpPageLink('split')]),
'#options' => $this->getSourceFieldOptions(),
'#empty_option' => $this->t('- None -'),
'#empty_value' => '',
'#default_value' => $this->config('data', 'split_field'),
];
$form['data']['cache_expiry'] = [
'#type' => 'select',
'#title' => $this->t('Cache expiry'),
'#description' => $this->t('How long the results for this dataset will be cached.'),
'#options' => $this->getCacheOptions(),
'#default_value' => $this->config('data', 'cache_expiry'),
];
$column_override_examples = [
'type|line', 'color|#000000', 'legend|hide', 'style|dashed', 'weight|20', 'class|hide-points', 'label|New label',
];
$form['data']['column_overrides'] = [
'#prefix' => '<div id="column-overrides">',
'#suffix' => '</div>',
'#collapsible' => TRUE,
'#collapsed' => TRUE,
'#type' => 'details',
'#title' => $this->t('Column/Group overrides'),
'#description' => '<p>' . $this->t('Optionally override a style for a specific column, add one key|value per line and separate key value with a pipe. @help.<br />Examples: <strong>@examples</strong>.',
[
'@examples' => new FormattableMarkup(implode('</strong> or <strong>', $column_override_examples), []),
'@help' => $this->dvfHelpers->getHelpPageLink('column-overrides'),
]) . '</p>',
];
foreach ($this->getColumnOverrideValues() as $override) {
$form['data']['column_overrides'][$override] = [
'#type' => 'textarea',
'#rows' => 2,
'#title' => substr($override, 1),
'#default_value' => $this->config('data', 'column_overrides', $override),
];
}
$form['data']['data_filters'] = [
'#prefix' => '<div>',
'#suffix' => '</div>',
'#collapsible' => TRUE,
'#collapsed' => TRUE,
'#access' => ($this->getVisualisation()->getSourcePlugin()->getPluginId() === 'dvf_ckan_resource'),
'#type' => 'details',
'#title' => $this->t('CKAN data filters'),
'#description' => $this->t('Filters can be used to refine/reduce the records returned from the CKAN datasource. @help',
['@help' => $this->dvfHelpers->getHelpPageLink('data-filters')]),
];
$form['data']['data_filters']['q'] = [
'#type' => 'textfield',
'#title' => $this->t('Full text query'),
'#description' => $this->t('Optionally query entire dataset for any string value.'),
'#default_value' => $this->config('data', 'data_filters', 'q'),
];
$form['data']['data_filters']['filters'] = [
'#type' => 'textfield',
'#title' => $this->t('Filters'),
'#description' => $this->t('Filter on key/value dictionary. For example: {"code": "4000", "year": "2016"} or {"year": ["2014", "2015", "2015"]}. Case sensitive.'),
'#default_value' => $this->config('data', 'data_filters', 'filters'),
];
return $form;
}
/**
* Gets the options array for caching.
*
* @return array
* The options array.
*/
protected function getCacheOptions() {
return [
'_global_default' => 'Global default',
'0' => 'No cache',
'1800' => '30 minutes',
'3600' => '1 hour',
'21600' => '6 hours',
'86400' => '1 day',
'604800' => '1 week',
'2592000' => '1 month',
'15552000' => '6 months',
];
}
/**
* Settings form #after_build callback.
*
* @param array $element
* The form element.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*
* @return array
* The updated form element.
*/
public static function afterBuildSettingsForm(array $element, FormStateInterface $form_state) {
return $element;
}
/**
* Gets the list of source field options.
*
* @return array
* The source field options.
*/
protected function getSourceFieldOptions() {
$fields = $this->visualisation->getSourcePlugin()->getFields();
$options = array_map('\Drupal\Component\Utility\Html::escape', $fields);
return !empty($options) ? $options : [];
}
/**
* Gets the source field values.
*
* @param string $field_id
* The field ID.
* @param array $records
* The set of records that we should get the values from.
*
* @return array
* The source field values.
*/
protected function getSourceFieldValues($field_id, array $records = []) {
$values = [];
foreach ($records as $record) {
if (property_exists($record, $field_id)) {
$values[] = $record->{$field_id};
}
}
return $values;
}
/**
* Gets the source records.
*
* @return array
* An array of source records.
*/
public function getSourceRecords() {
$records = [];
foreach ($this->getVisualisation()->data() as $record) {
if ($this->splitField() && property_exists($record, $this->splitField())) {
$records[$record->{$this->splitField()}][] = $record;
}
else {
$records['all'][] = $record;
}
}
return $records;
}
/**
* Gets the fields.
*
* @return array
* The fields.
*/
protected function fields() {
return array_unique(array_filter($this->config('data', 'fields')));
}
/**
* Gets the field labels.
*
* @return array
* The field labels.
*/
protected function fieldLabels() {
$labels = array_intersect(array_values($this->getSourceFieldOptions()), $this->fields());
$labels = array_combine($labels, $labels);
$label_overrides = $this->dvfHelpers->configStringToArray($this->config('data', 'field_labels'));
foreach ($label_overrides as $key => $val) {
if (isset($labels[$key])) {
$labels[$key] = strval($val);
}
}
return $labels;
}
/**
* Gets the original field labels without any overrides applied.
*
* @return array
* The unique field labels.
*/
protected function fieldLabelsOriginal() {
$labels = $this->getSourceFieldOptions();
if (!empty($labels['_id'])) {
unset($labels['_id']);
}
$keys = array_keys($labels);
return array_map('strval', $keys);
}
/**
* Gets a field label.
*
* @param string $field_id
* The field ID.
*
* @return string
* The field label.
*/
protected function fieldLabel($field_id) {
$label = '';
$labels = $this->fieldLabels();
if (array_key_exists($field_id, $labels)) {
$label = $labels[$field_id];
}
return $label;
}
/**
* Gets the split field.
*
* @return string
* The split field.
*/
protected function splitField() {
return $this->config('data', 'split_field');
}
/**
* {@inheritdoc}
*/
public function getVisualisation() {
return $this->visualisation;
}
/**
* {@inheritdoc}
*/
public function getDatasetDownloadUri() {
try {
return $this->getVisualisation()->getSourcePlugin()->getDownloadUrl();
}
catch (\Exception $e) {
$this->logger->error($this->t('Unable to get download url for visualisation :message',
[':message' => $e->getMessage()]));
}
return NULL;
}
/**
* {@inheritdoc}
*/
public function isValidDownloadUri($uri) {
return (UrlHelper::isValid($uri) || filter_var($uri, FILTER_VALIDATE_URL)) ? $uri : FALSE;
}
/**
* Returns the column override values to make a form array.
*
* @return array
* The array of column override values. NOTE: Each value has a underscore
* prependend to ensure it is a string. Without this the field name gets
* converted to a int/float during form submission.
*/
protected function getColumnOverrideValues() {
if ($this->config('axis', 'x', 'x_axis_grouping') === 'values' && $this->config('axis', 'x', 'tick', 'values', 'field')) {
$x_tick_field = $this->config('axis', 'x', 'tick', 'values', 'field');
$columns = array_map(function ($e) use ($x_tick_field) {
return $e->{$x_tick_field};
}, $this->getVisualisation()->data());
}
else {
$columns = $this->fieldLabelsOriginal();
}
return array_map(function ($item) {
return '_' . $item;
}, $columns);
}
/**
* Gets the column overrides settings in a nicely formatted array.
*
* @return array
* An array of column override settings.
*/
protected function getColumnOverrides() {
$columns = array_map(function ($item) {
return substr($item, 1);
}, $this->getColumnOverrideValues());
$column_overrides = array_fill_keys($columns, []);
foreach ($this->config('data', 'column_overrides') as $field_name => $column_override) {
if (empty($column_override)) {
continue;
}
$real_field_name = substr($field_name, 1);
$column_overrides[$real_field_name] = $this->dvfHelpers->configStringToArray($column_override);
}
return $this->setArrayOrder($column_overrides);
}
/**
* Re-orders the keys as per provided order array.
*
* @param array $array_to_order
* An array keyed by the key (original) name, the value for each should be
* an array containing a weight key. The lower the weight the higher it
* appears in the list. If no weight found, default order is used.
* @param string $weight_key
* The key that contains the weight.
*
* @return array
* An ordered array.
*/
protected function setArrayOrder(array $array_to_order, $weight_key = 'weight') {
$i = 0;
// Set default weights if does not exist.
foreach ($array_to_order as $key => $value) {
$array_to_order[$key][$weight_key] = isset($value[$weight_key]) ? (int) $value[$weight_key] : $i;
$array_to_order[$key]['key'] = strval($key);
$i++;
}
// Sort by weight and return.
uasort($array_to_order, function ($a, $b) use ($weight_key) {
return $a[$weight_key] - $b[$weight_key];
});
return $array_to_order;
}
/**
* Get fields that will be displayed ordered correctly by weight.
*
* @return array
* Array of field values.
*/
public function fieldsSorted() {
return array_map('strval', array_keys($this->getColumnOverrides()));
}
/**
* Checks to see if columns are numeric.
*
* @param array $array
* The array of column values.
*
* @return bool
* True if numeric, false if not.
*/
public function columnsAreNumeric(array $array) {
$array = reset($array);
array_shift($array);
if (count($array) === count(array_filter($array, 'is_numeric'))) {
return TRUE;
}
return FALSE;
}
/**
* Return markup for a split field heading.
*
* @param string $label
* Label for the heading.
*
* @return array
* A heading tag for the label, if label is "all" (ungrouped), return empty.
*/
public function buildSplitHeading($label) {
if ('all' === $label) {
return [];
}
return [
'#type' => 'html_tag',
'#tag' => 'h3',
'#value' => htmlentities($label),
'#attributes' => ['class' => 'dvf-split-heading'],
];
}
}
