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 ? '' : ''; $rowLabel = str_replace('/', '/', str_replace('&', '&', 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 = '' . ($changeImage ? ' ' : '') . $change . ''; $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 ''; } /** 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); } }