| /** | |
| * Matomo - free/libre analytics platform | |
| * | |
| * @link https://matomo.org | |
| * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later | |
| */ | |
| namespace Piwik\Plugins\CoreVisualizations\Visualizations\Sparklines; | |
| use Piwik\Common; | |
| use Piwik\DataTable\Filter\CalculateEvolutionFilter; | |
| use Piwik\Metrics; | |
| use Piwik\NoAccessException; | |
| use Piwik\Period; | |
| use Piwik\Period\Range; | |
| use Piwik\Piwik; | |
| use Piwik\Site; | |
| use Piwik\Url; | |
| /** | |
| * DataTable Visualization that derives from Sparklines. | |
| */ | |
| class Config extends \Piwik\ViewDataTable\Config | |
| { | |
| /** | |
| * Holds metrics / column names that will be used to fetch data from the configured $requestConfig API. | |
| * Default value: array | |
| */ | |
| private $sparkline_metrics = array(); | |
| /** | |
| * Holds the actual sparkline entries based on fetched data that will be used in the template. | |
| * @var array | |
| */ | |
| private $sparklines = array(); | |
| /** | |
| * If false, will not link them with any evolution graph | |
| * @var bool | |
| */ | |
| private $evolutionGraphLinkable = true; | |
| /** | |
| * Adds possibility to set html attributes on the sparklines title / headline. | |
| * @var array | |
| */ | |
| public $title_attributes = array(); | |
| /** | |
| * Defines custom parameters that will be appended to the sparkline image urls | |
| */ | |
| public $custom_parameters = []; | |
| /** | |
| * If supplied, this function is used to compute the evolution percent displayed next to non-comparison sparkline views. | |
| * | |
| * The function is passed two parameters: | |
| * - an array mapping column names with column values ['column' => 123] | |
| * - an array of \Piwik\Plugin\Metrics objects available for the report - useful for formatting values | |
| * | |
| * compute_evolution(array, array) | |
| * | |
| * @var callable | |
| */ | |
| public $compute_evolution = null; | |
| public function __construct() | |
| { | |
| parent::__construct(); | |
| $this->translations = Metrics::getDefaultMetricTranslations(); | |
| } | |
| /** | |
| * @ignore | |
| * @return array | |
| */ | |
| public function getSparklineMetrics() | |
| { | |
| return $this->sparkline_metrics; | |
| } | |
| /** | |
| * @ignore | |
| * @return bool | |
| */ | |
| public function hasSparklineMetrics() | |
| { | |
| return !empty($this->sparkline_metrics); | |
| } | |
| /** | |
| * Removes an existing sparkline entry. Especially useful in dataTable filters in case sparklines should be not | |
| * displayed depending on the fetched data. | |
| * | |
| * Example: | |
| * $config->addSparklineMetric('nb_users'); | |
| * $config->filters[] = function ($dataTable) use ($config) { | |
| * if ($dataTable->getFirstRow()->getColumn('nb_users') == 0) { | |
| * // do not show a sparkline if there are no recorded users | |
| * $config->removeSparklineMetric('nb_users'); | |
| * } | |
| * } | |
| * | |
| * @param array|string $metricNames The name of the metrics in the same format they were used when added via | |
| * {@link addSparklineMetric} | |
| */ | |
| public function removeSparklineMetric($metricNames) | |
| { | |
| foreach ($this->sparkline_metrics as $index => $metric) { | |
| if ($metric['columns'] === $metricNames) { | |
| array_splice($this->sparkline_metrics, $index, 1); | |
| break; | |
| } | |
| } | |
| } | |
| /** | |
| * Replaces an existing sparkline entry with different columns. Especially useful in dataTable filters in case | |
| * sparklines should be not displayed depending on the fetched data. | |
| * | |
| * Example: | |
| * $config->addSparklineMetric('nb_users'); | |
| * $config->filters[] = function ($dataTable) use ($config) { | |
| * if ($dataTable->getFirstRow()->getColumn('nb_users') == 0) { | |
| * // instead of showing the sparklines for users, show a placeholder if there are no recorded users | |
| * $config->replaceSparklineMetric(array('nb_users'), ''); | |
| * } | |
| * } | |
| * | |
| * @param array|string $metricNames The name of the metrics in the same format they were used when added via | |
| * {@link addSparklineMetric} | |
| * @param array|string $replacementColumns The removed columns will be replaced with these columns | |
| */ | |
| public function replaceSparklineMetric($metricNames, $replacementColumns) | |
| { | |
| foreach ($this->sparkline_metrics as $index => $metric) { | |
| if ($metric['columns'] === $metricNames) { | |
| $this->sparkline_metrics[$index]['columns'] = $replacementColumns; | |
| } | |
| } | |
| } | |
| /** | |
| * Adds a new sparkline. | |
| * | |
| * It will show a sparkline image, the value of the resolved metric name and a descrption. Optionally, multiple | |
| * values can be shown after a sparkline image by passing multiple metric names | |
| * (eg array('nb_visits', 'nb_actions')). The data will be requested from the configured api method see | |
| * {@link \Piwik\ViewDataTable\RequestConfig::$apiMethodToRequestDataTable}. | |
| * | |
| * Example: | |
| * $config->addSparklineMetric('nb_visits'); | |
| * $config->addTranslation('nb_visits', 'Visits'); | |
| * Results in: [sparkline image] X visits | |
| * | |
| * Example: | |
| * $config->addSparklineMetric(array('nb_visits', 'nb_actions')); | |
| * $config->addTranslations(array('nb_visits' => 'Visits', 'nb_actions' => 'Actions')); | |
| * Results in: [sparkline image] X visits, Y actions | |
| * | |
| * @param string|array $metricName Either one metric name (eg 'nb_visits') or an array of metric names | |
| * @param int|null $order Defines the order. The lower the order the earlier the sparkline will be displayed. | |
| * By default the sparkline will be appended to the end. | |
| * @param array $graphParams The params to use when changing an associated evolution graph. By default this is determined | |
| * from the sparkline URL, but sometimes the sparkline API method may not match the evolution graph API method. | |
| */ | |
| public function addSparklineMetric($metricName, $order = null, $graphParams = null) | |
| { | |
| $this->sparkline_metrics[] = array( | |
| 'columns' => $metricName, | |
| 'order' => $order, | |
| 'graphParams' => $graphParams, | |
| ); | |
| } | |
| /** | |
| * Adds a placeholder. In this case nothing will be shown, neither a sparkline nor any description. This can be | |
| * useful if you want to have some kind of separator. Eg if you want to have a sparkline on the left side but | |
| * not sparkline on the right side. | |
| * | |
| * @param int|null $order Defines the order. The lower the order the earlier the sparkline will be displayed. | |
| * By default the sparkline will be appended to the end. | |
| */ | |
| public function addPlaceholder($order = null) | |
| { | |
| $this->sparklines[] = array( | |
| 'url' => '', | |
| 'metrics' => array(), | |
| 'order' => $this->getSparklineOrder($order), | |
| // adding this group ensures the sparkline will be placed between individual sparklines, and not in their own group together | |
| 'group' => 'placeholder' . count($this->sparklines), | |
| ); | |
| } | |
| /** | |
| * Add a new sparkline to be displayed to the view. | |
| * | |
| * Each sparkline can consist of one or multiple metrics. One metric consists of a value and a description. By | |
| * default the value is shown first, then the description. The description can optionally contain a '%s' in case | |
| * the value shall be displayed within the description. If multiple metrics are given, they will be separated by | |
| * a comma. | |
| * | |
| * @param array $requestParamsForSparkline You need to at least set a module / action eg | |
| * array('columns' => array('nb_visit'), 'module' => '', 'action' => '') | |
| * @param int|float|string|array $metricInfos Either the metric value or an array of values. | |
| * @param string|array $description Either one description or an array of descriptions. If an array, both | |
| * $value and $description need the same amount of array entries. | |
| * $description[0] should be the description for $value[0]. | |
| * $description should be already translated. If $value should appear | |
| * somewhere within the text a `%s` can be used in the translation. | |
| * @param array|null $evolution Optional array containing at least the array keys 'currentValue' and | |
| * 'pastValue' which are needed to calculate the correct percentage. | |
| * An optional 'tooltip' can be set as well. Eg | |
| * array('currentValue' => 10, 'pastValue' => 20, | |
| * 'tooltip' => '10 visits in 2015-07-26 compared to 20 visits in 2015-07-25') | |
| * @param int $order Defines the order. The lower the order the earlier the sparkline will be | |
| * displayed. By default the sparkline will be appended to the end. | |
| * @param string $title The title of this specific sparkline. It is displayed on the left above the sparkline image. | |
| * @param string $group The ID of the group for this sparkline. | |
| * @param int $seriesIndices The indexes of each series displayed in the sparkline. This determines what color is used for each series. Mainly used for comparison. | |
| * @param array $graphParams The params to use when changing an associated evolution graph. By default this is determined | |
| * from the sparkline URL, but sometimes the sparkline API method may not match the evolution graph API method. | |
| * @throws \Exception In case an evolution parameter is set but has wrong data structure | |
| */ | |
| public function addSparkline($requestParamsForSparkline, $metricInfos, $description, $evolution = null, $order = null, $title = null, $group = '', $seriesIndices = null, $graphParams = null) | |
| { | |
| $metrics = array(); | |
| if ($description === null && is_array($metricInfos)) { | |
| $metrics = $metricInfos; | |
| } else { | |
| $value = $metricInfos; | |
| if (is_array($value)) { | |
| $values = $value; | |
| } else { | |
| $values = array($value); | |
| } | |
| if (!is_array($description)) { | |
| $description = array($description); | |
| } | |
| if (count($values) === count($description)) { | |
| foreach ($values as $index => $value) { | |
| $metrics[] = array( | |
| 'value' => $value, | |
| 'description' => $description[$index], | |
| ); | |
| } | |
| } else { | |
| $msg = 'The number of values and descriptions need to be the same to add a sparkline. '; | |
| $msg .= 'Values: ' . implode(', ', $values) . ' Descriptions: ' . implode(', ', $description); | |
| throw new \Exception($msg); | |
| } | |
| } | |
| if ( | |
| !empty($requestParamsForSparkline['columns']) | |
| && is_array($requestParamsForSparkline['columns']) | |
| && count($requestParamsForSparkline['columns']) === count($metrics) | |
| ) { | |
| $columns = array_values($requestParamsForSparkline['columns']); | |
| } elseif ( | |
| !empty($requestParamsForSparkline['columns']) | |
| && is_string($requestParamsForSparkline['columns']) | |
| && count($metrics) === 1 | |
| ) { | |
| $columns = array($requestParamsForSparkline['columns']); | |
| } else { | |
| $columns = array(); | |
| } | |
| foreach ($metrics as $index => $metricInfo) { | |
| $metrics[$index]['column'] = isset($columns[$index]) ? $columns[$index] : ''; | |
| } | |
| if (empty($metrics)) { | |
| return; | |
| } | |
| $groupedMetrics = []; | |
| foreach ($metrics as $metricInfo) { | |
| $metricGroup = isset($metricInfo['group']) ? $metricInfo['group'] : ''; | |
| $groupedMetrics[$metricGroup][] = $metricInfo; | |
| } | |
| $tooltip = $this->generateSparklineTooltip($requestParamsForSparkline); | |
| $sparkline = array( | |
| 'url' => $this->getUrlSparkline($requestParamsForSparkline), | |
| 'tooltip' => $tooltip, | |
| 'metrics' => $groupedMetrics, | |
| 'order' => $this->getSparklineOrder($order), | |
| 'title' => $title, | |
| 'group' => $group, | |
| 'seriesIndices' => $seriesIndices, | |
| 'graphParams' => $graphParams, | |
| ); | |
| if (!empty($evolution)) { | |
| if ( | |
| !is_array($evolution) || | |
| !array_key_exists('currentValue', $evolution) || | |
| !array_key_exists('pastValue', $evolution) | |
| ) { | |
| throw new \Exception('In order to show an evolution in the sparklines view a currentValue and pastValue array key needs to be present'); | |
| } | |
| $evolutionPercent = CalculateEvolutionFilter::calculate($evolution['currentValue'], $evolution['pastValue'], $precision = 1); | |
| $sparkline['evolution'] = array( | |
| 'percent' => $evolutionPercent, | |
| 'isLowerValueBetter' => !empty($evolution['isLowerValueBetter']) ? $evolution['isLowerValueBetter'] : false, | |
| 'tooltip' => !empty($evolution['tooltip']) ? $evolution['tooltip'] : null, | |
| 'trend' => $evolution['currentValue'] - $evolution['pastValue'], | |
| ); | |
| } | |
| $this->sparklines[] = $sparkline; | |
| } | |
| public function generateSparklineTooltip($params) | |
| { | |
| $tooltip = ''; | |
| if (!empty($params['period'])) { | |
| $periodTranslated = Piwik::translate('Intl_Period' . ucfirst($params['period'])); | |
| $tooltip = Piwik::translate('General_SparklineTooltipUsedPeriod', $periodTranslated); | |
| if (!empty($params['date'])) { | |
| $period = Period\Factory::build('day', $params['date']); | |
| $tooltip .= ' ' . Piwik::translate('General_Period') . ': ' . $period->getLocalizedShortString() . '.'; | |
| if (!empty($params['compareDates'])) { | |
| foreach ($params['compareDates'] as $index => $comparisonDate) { | |
| $comparePeriod = Period\Factory::build('day', $comparisonDate); | |
| $tooltip .= ' ' . Piwik::translate('General_Period') . ' ' . ($index + 2) . ': ' . $comparePeriod->getLocalizedShortString() . '.'; | |
| } | |
| } | |
| } | |
| } | |
| return $tooltip; | |
| } | |
| /** | |
| * If there are sparklines and evolution graphs on one page, we try to connect them so that when you click on a | |
| * sparkline, the evolution graph will update and show the evolution for that sparkline metric. In some cases | |
| * we might falsely connect sparklines with an evolution graph that don't belong together. In this case you can | |
| * mark all sparklines as "not linkable". This will prevent the sparklines being linked with an evolution graph. | |
| */ | |
| public function setNotLinkableWithAnyEvolutionGraph() | |
| { | |
| $this->evolutionGraphLinkable = false; | |
| } | |
| /** | |
| * Detect whether sparklines are linkable with an evolution graph. {@link setNotLinkableWithAnyEvolutionGraph()} | |
| */ | |
| public function areSparklinesLinkable() | |
| { | |
| return $this->evolutionGraphLinkable; | |
| } | |
| /** | |
| * @return array | |
| * @ignore | |
| */ | |
| public function getSortedSparklines() | |
| { | |
| usort($this->sparklines, function ($a, $b) { | |
| if ($a['order'] == $b['order']) { | |
| return 0; | |
| } | |
| return ($a['order'] < $b['order']) ? -1 : 1; | |
| }); | |
| $sparklines = []; | |
| foreach ($this->sparklines as $sparkline) { | |
| $group = $sparkline['group']; | |
| $sparklines[$group][] = $sparkline; | |
| } | |
| return $sparklines; | |
| } | |
| private function getSparklineOrder($order) | |
| { | |
| if (!isset($order)) { | |
| // make sure to append to the end if nothing set (in the order they are added) | |
| $order = 999 + count($this->sparklines); | |
| } | |
| return (int) $order; | |
| } | |
| /** | |
| * Returns a URL to a sparkline image for a report served by the current plugin. | |
| * | |
| * The result of this URL should be used with the [sparkline()](/api-reference/Piwik/View#twig) twig function. | |
| * | |
| * The current site ID and period will be used. | |
| * | |
| * @param array $customParameters The array of query parameter name/value pairs that | |
| * should be set in result URL. | |
| * @return string The generated URL. | |
| */ | |
| private function getUrlSparkline($customParameters = array()) | |
| { | |
| $customParameters['viewDataTable'] = 'sparkline'; | |
| $params = $this->getGraphParamsModified($customParameters); | |
| foreach ($params as $key => $value) { | |
| if (is_array($value) && in_array($key, ['compareDates', 'comparePeriods'])) { | |
| foreach ($value as $index => $inner) { | |
| $value[$index] = rawurlencode($inner); | |
| } | |
| $params[$key] = $value; | |
| } elseif (is_array($value)) { | |
| // convert array values to comma separated | |
| $params[$key] = rawurlencode(implode(',', $value)); | |
| } | |
| } | |
| $url = Url::getCurrentQueryStringWithParametersModified($params); | |
| return $url; | |
| } | |
| /** | |
| * Returns the array of new processed parameters once the parameters are applied. | |
| * For example: if you set range=last30 and date=2008-03-10, | |
| * the date element of the returned array will be "2008-02-10,2008-03-10" | |
| * | |
| * Parameters you can set: | |
| * - range: last30, previous10, etc. | |
| * - date: YYYY-MM-DD, today, yesterday | |
| * - period: day, week, month, year | |
| * | |
| * @param array $paramsToSet array( 'date' => 'last50', 'viewDataTable' =>'sparkline' ) | |
| * @throws \Piwik\NoAccessException | |
| * @return array | |
| */ | |
| public function getGraphParamsModified($paramsToSet = array()) | |
| { | |
| if (!isset($paramsToSet['period'])) { | |
| $period = Common::getRequestVar('period'); | |
| } else { | |
| $period = $paramsToSet['period']; | |
| } | |
| if ($period == 'range') { | |
| return $paramsToSet; | |
| } | |
| if (!isset($paramsToSet['range'])) { | |
| $range = 'last30'; | |
| } else { | |
| $range = $paramsToSet['range']; | |
| } | |
| if (!isset($paramsToSet['idSite'])) { | |
| $idSite = Common::getRequestVar('idSite'); | |
| } else { | |
| $idSite = $paramsToSet['idSite']; | |
| } | |
| if (!isset($paramsToSet['date'])) { | |
| $endDate = Common::getRequestVar('date', 'yesterday', 'string'); | |
| } else { | |
| $endDate = $paramsToSet['date']; | |
| } | |
| $site = new Site($idSite); | |
| if (is_null($site)) { | |
| throw new NoAccessException("Website not initialized, check that you are logged in and/or using the correct token_auth."); | |
| } | |
| if ( | |
| !isset($paramsToSet['date']) | |
| || !Range::isMultiplePeriod($paramsToSet['date'], $period) | |
| ) { | |
| $paramsToSet['date'] = Range::getRelativeToEndDate($period, $range, $endDate, $site); | |
| $paramsToSet['period'] = $period; | |
| } | |
| return $paramsToSet; | |
| } | |
| } | |