site-accueil-insa/matomo/plugins/CoreHome/DataTableRowAction/RowEvolution.php

434 lines
16 KiB
PHP

<?php
/**
* Matomo - free/libre analytics platform
*
* @link https://matomo.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
namespace Piwik\Plugins\CoreHome\DataTableRowAction;
use Exception;
use Piwik\API\DataTablePostProcessor;
use Piwik\API\Request;
use Piwik\Common;
use Piwik\Date;
use Piwik\Metrics;
use Piwik\NumberFormatter;
use Piwik\Period\Factory as PeriodFactory;
use Piwik\Piwik;
use Piwik\Plugins\API\Filter\DataComparisonFilter;
use Piwik\Plugins\CoreVisualizations\Visualizations\JqplotGraph\Evolution as EvolutionViz;
use Piwik\Url;
use Piwik\ViewDataTable\Factory;
use Piwik\ViewDataTable\Manager as ViewDataTableManager;
/**
* ROW EVOLUTION
* The class handles the popover that shows the evolution of a single row in a data table
*/
class RowEvolution
{
/** The current site id */
protected $idSite;
/** The api method to get the data. Format: Plugin.apiAction */
protected $apiMethod;
/** The label of the requested row */
protected $label;
/** The requested period */
protected $period;
/** The requested date */
protected $date;
/** The request segment */
protected $segment;
/** The metrics that are available for the requested report and period */
protected $availableMetrics;
/** The name of the dimension of the current report */
protected $dimension;
/**
* The data
* @var \Piwik\DataTable
*/
protected $dataTable;
/** The label of the current record */
protected $rowLabel;
/** The icon of the current record */
protected $rowIcon;
/** The type of graph that has been requested last */
protected $graphType;
/** The metrics for the graph that has been requested last */
protected $graphMetrics;
/** Whether or not to show all metrics in the evolution graph when to popover opens */
protected $initiallyShowAllMetrics = false;
/**
* The constructor
* Initialize some local variables from the request
* @param int $idSite
* @param Date $date ($this->date from controller)
* @param null|string $graphType
* @throws Exception
*/
public function __construct($idSite, $date, $graphType = 'graphEvolution')
{
$this->apiMethod = Common::getRequestVar('apiMethod', '', 'string');
if (empty($this->apiMethod)) {
throw new Exception("Parameter apiMethod not set.");
}
$this->label = DataTablePostProcessor::getLabelFromRequest($_GET);
if (!is_array($this->label)) {
throw new Exception("Expected label to be an array, got instead: " . $this->label);
}
$this->label = Common::unsanitizeInputValue($this->label[0]);
if ($this->label === '') {
throw new Exception("Parameter label not set.");
}
$this->period = Common::getRequestVar('period', '', 'string');
PeriodFactory::checkPeriodIsEnabled($this->period);
if ($this->period != 'range' && !($date instanceof Date)) {
throw new Exception("Expected date to be an instance of \\Piwik\\Date");
}
$this->idSite = $idSite;
$this->graphType = $graphType;
if ($this->period != 'range') {
// handle day, week, month and year: display last X periods
//handle cache if exist
$cache = ViewDataTableManager::getViewDataTableParameters(Piwik::getCurrentUserLogin(),
'CoreHome.getRowEvolutionGraph');
$lastDay = (isset($cache['evolution_' . $this->period . '_last_n']) ? $cache['evolution_' . $this->period . '_last_n'] : null);
$end = $date->toString();
list($this->date, $lastN) = EvolutionViz::getDateRangeAndLastN($this->period, $end, $lastDay);
}
$this->segment = \Piwik\API\Request::getRawSegmentFromRequest();
$this->loadEvolutionReport();
}
/**
* Render the popover
* @param \Piwik\Plugins\CoreHome\Controller $controller
* @param \Piwik\View (the popover_rowevolution template)
*/
public function renderPopover($controller, $view)
{
// render main evolution graph
$this->graphType = 'graphEvolution';
$this->graphMetrics = $this->availableMetrics;
$view->graph = $this->getRowEvolutionGraphFromController($controller);
// render metrics overview
$view->metrics = $this->getMetricsToggles();
// available metrics text
$metricsText = Piwik::translate('RowEvolution_AvailableMetrics');
$popoverTitle = '';
if ($this->rowLabel) {
$icon = $this->rowIcon ? '<img height="16px" src="' . $this->rowIcon . '" alt="">' : '';
$rowLabel = str_replace('/', '<wbr>/', str_replace('&', '<wbr>&', Common::fixLbrace($this->rowLabel) ));
$metricsText = sprintf(Piwik::translate('RowEvolution_MetricsFor'), $this->dimension . ': ' . $icon . ' ' . $rowLabel);
$popoverTitle = $icon . ' ' . Common::fixLbrace($this->rowLabel);
}
$view->availableMetricsText = $metricsText;
$view->popoverTitle = $popoverTitle;
return $view->render();
}
protected function loadEvolutionReport($column = false)
{
list($apiModule, $apiAction) = explode('.', $this->apiMethod);
// getQueryStringFromParameters expects sanitised query parameter values
$parameters = array(
'method' => 'API.getRowEvolution',
'label' => $this->label,
'apiModule' => $apiModule,
'apiAction' => $apiAction,
'idSite' => $this->idSite,
'period' => $this->period,
'date' => $this->date,
'format' => 'original',
'serialize' => '0',
);
if (!empty($this->segment)) {
$parameters['segment'] = $this->segment;
}
if ($column !== false) {
$parameters['column'] = $column;
}
$isComparing = DataComparisonFilter::isCompareParamsPresent();
if ($isComparing) {
$compareDates = Common::getRequestVar('compareDates', [], 'array');
$comparePeriods = Common::getRequestVar('comparePeriods', [], 'array');
$compareSegments = Common::getRequestVar('compareSegments', [], 'array');
$totalSeriesCount = (count($compareSegments) + 1) * (count($comparePeriods) + 1);
$unmodifiedSeriesLabels = [];
for ($i = 0; $i < $totalSeriesCount; ++$i) {
$unmodifiedSeriesLabels[] = DataComparisonFilter::getPrettyComparisonLabelFromSeriesIndex($i);
}
$parameters['compare'] = 1;
foreach ($comparePeriods as $index => $period) {
$date = $compareDates[$index];
if ($period == 'range') {
$comparePeriods[$index] = 'day';
} else {
list($newDate, $lastN) = EvolutionViz::getDateRangeAndLastN($period, $date);
$compareDates[$index] = $newDate;
}
}
$parameters['compareDates'] = $compareDates;
$parameters['comparePeriods'] = $comparePeriods;
}
$url = Url::getQueryStringFromParameters($parameters);
$request = new Request($url);
$report = $request->process();
// at this point the report data will reference the comparison series labels for the changed compare periods/dates. We don't
// want to show this to users because they will not recognize the changed periods, so we have to replace them.
if ($isComparing) {
$modifiedSeriesLabels = reset($report['reportData']->getDataTables())->getMetadata('comparisonSeries');
$seriesMap = array_combine($modifiedSeriesLabels, $unmodifiedSeriesLabels);
foreach ($report['metadata']['metrics'] as $key => $metricInfo) {
foreach ($seriesMap as $modified => $unmodified) {
$report['metadata']['metrics'][$key]['name'] = str_replace($modified, $unmodified, $report['metadata']['metrics'][$key]['name']);
}
}
}
$this->extractEvolutionReport($report);
}
protected function extractEvolutionReport($report)
{
$this->dataTable = $report['reportData'];
$this->rowLabel = $this->extractPrettyLabel($report);
$this->rowIcon = !empty($report['logo']) ? $report['logo'] : false;
$this->availableMetrics = $report['metadata']['metrics'];
$this->dimension = $report['metadata']['dimension'];
}
/**
* Generic method to get an evolution graph or a sparkline for the row evolution popover.
* Do as much as possible from outside the controller.
* @param string|bool $graphType
* @param array|bool $metrics
* @return Factory
*/
public function getRowEvolutionGraph($graphType = false, $metrics = false)
{
// set up the view data table
$view = Factory::build($graphType ? : $this->graphType, $this->apiMethod,
$controllerAction = 'CoreHome.getRowEvolutionGraph', $forceDefault = true);
$view->setDataTable($this->dataTable);
if (!empty($this->graphMetrics)) { // In row Evolution popover, this is empty
$view->config->columns_to_display = array_keys($metrics ? : $this->graphMetrics);
}
$view->requestConfig->request_parameters_to_modify['label'] = '';
$view->config->show_goals = false;
$view->config->show_search = false;
$view->config->show_all_views_icons = false;
$view->config->show_related_reports = false;
$view->config->show_series_picker = false;
$view->config->show_footer_message = false;
foreach ($this->availableMetrics as $metric => $metadata) {
$view->config->translations[$metric] = $metadata['name'];
}
$view->config->external_series_toggle = 'RowEvolutionSeriesToggle';
$view->config->external_series_toggle_show_all = $this->initiallyShowAllMetrics;
return $view;
}
/**
* Prepare metrics toggles with spark lines
* @return array
*/
protected function getMetricsToggles()
{
$i = 0;
$metrics = array();
foreach ($this->availableMetrics as $metric => $metricData) {
$unit = Metrics::getUnit($metric, $this->idSite);
$change = isset($metricData['change']) ? $metricData['change'] : false;
list($first, $last) = $this->getFirstAndLastDataPointsForMetric($metric);
$fractionDigits = max($this->getFractionDigits($first), $this->getFractionDigits($last));
$details = Piwik::translate('RowEvolution_MetricBetweenText', array(
NumberFormatter::getInstance()->format($first, $fractionDigits) . $unit,
NumberFormatter::getInstance()->format($last, $fractionDigits) . $unit,
));
if ($change !== false) {
$lowerIsBetter = Metrics::isLowerValueBetter($metric);
if (substr($change, 0, 1) == '+') {
$changeClass = $lowerIsBetter ? 'bad' : 'good';
$changeImage = $lowerIsBetter ? 'arrow_up_red' : 'arrow_up';
} else if (substr($change, 0, 1) == '-') {
$changeClass = $lowerIsBetter ? 'good' : 'bad';
$changeImage = $lowerIsBetter ? 'arrow_down_green' : 'arrow_down';
} else {
$changeClass = 'neutral';
$changeImage = false;
}
$change = '<span class="' . $changeClass . '">'
. ($changeImage ? '<img src="plugins/MultiSites/images/' . $changeImage . '.png" /> ' : '')
. $change . '</span>';
$details .= ', ' . Piwik::translate('RowEvolution_MetricChangeText', $change);
}
// set metric min/max text (used as tooltip for details)
$max = isset($metricData['max']) ? $metricData['max'] : 0;
$min = isset($metricData['min']) ? $metricData['min'] : 0;
$minmax = Piwik::translate('RowEvolution_MetricMinMax', array(
$metricData['name'],
NumberFormatter::getInstance()->formatNumber($min, $fractionDigits, $fractionDigits) . $unit,
NumberFormatter::getInstance()->formatNumber($max, $fractionDigits, $fractionDigits) . $unit,
));
$newMetric = array(
'label' => $metricData['name'],
'details' => $details,
'minmax' => $minmax,
'sparkline' => $this->getSparkline($metric),
);
// Multi Rows, each metric can be for a particular row and display an icon
if (!empty($metricData['logo'])) {
$newMetric['logo'] = $metricData['logo'];
}
// TODO: this check should be determined by metric metadata, not hardcoded here
if ($metric == 'nb_users'
&& $first == 0
&& $last == 0
) {
$newMetric['hide'] = true;
}
$metrics[] = $newMetric;
$i++;
}
return $metrics;
}
/** Get the img tag for a sparkline showing a single metric */
protected function getSparkline($metric)
{
// sparkline is always echoed, so we need to buffer the output
$view = $this->getRowEvolutionGraph($graphType = 'sparkline', $metrics = array($metric => $metric));
ob_start();
$view->render();
$spark = ob_get_contents();
ob_end_clean();
// undo header change by sparkline renderer
Common::sendHeader('Content-type: text/html');
// base64 encode the image and put it in an img tag
$spark = base64_encode($spark);
return '<img width="100" height="25" src="data:image/png;base64,' . $spark . '" />';
}
/** Use the available metrics for the metrics of the last requested graph. */
public function useAvailableMetrics()
{
$this->graphMetrics = $this->availableMetrics;
}
private function getFirstAndLastDataPointsForMetric($metric)
{
$first = 0;
$firstTable = $this->dataTable->getFirstRow();
if (!empty($firstTable)) {
$row = $firstTable->getFirstRow();
if (!empty($row)) {
$first = floatval($row->getColumn($metric));
}
}
$last = 0;
$lastTable = $this->dataTable->getLastRow();
if (!empty($lastTable)) {
$row = $lastTable->getFirstRow();
if (!empty($row)) {
$last = floatval($row->getColumn($metric));
}
}
return array($first, $last);
}
/**
* @param $report
* @return string
*/
protected function extractPrettyLabel($report)
{
// By default, use the specified label
$rowLabel = Common::sanitizeInputValue($report['label']);
// If the dataTable specifies a label_html, use this instead
/** @var $dataTableMap \Piwik\DataTable\Map */
$dataTableMap = $report['reportData'];
$labelPretty = $dataTableMap->getColumn('label_html');
$labelPretty = array_filter($labelPretty, 'strlen');
$labelPretty = current($labelPretty);
if (!empty($labelPretty)) {
return $labelPretty;
}
return $rowLabel;
}
private function getFractionDigits($value)
{
$value = (string) $value;
$fraction = substr(strrchr($value, "."), 1);
return strlen($fraction);
}
protected function getRowEvolutionGraphFromController(\Piwik\Plugins\CoreHome\Controller $controller)
{
return $controller->getRowEvolutionGraph($fetch = true, $rowEvolution = $this);
}
}