1<?php 2/** 3 * Matomo - free/libre analytics platform 4 * 5 * @link https://matomo.org 6 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later 7 * 8 */ 9namespace Piwik\Plugins\CoreVisualizations\Visualizations; 10 11use Piwik\API\Request; 12use Piwik\Common; 13use Piwik\DataTable; 14use Piwik\Metrics; 15use Piwik\Period\Factory; 16use Piwik\Plugin\ViewDataTable; 17use Piwik\Plugins\API\Filter\DataComparisonFilter; 18use Piwik\SettingsPiwik; 19use Piwik\Url; 20use Piwik\View; 21 22/** 23 * Reads the requested DataTable from the API and prepares data for the Sparklines view. It can display any amount 24 * of sparklines. Within a reporting page sparklines are shown in 2 columns, in a dashboard or when exported as a widget 25 * the sparklines are shown in one column. 26 * 27 * The sparklines view currently only supports requesting columns from the same API (the API method of the defining 28 * report) via {Sparklines\Config::addSparklineMetric($columns = array('nb_visits', 'nb_unique_visitors'))}. 29 * 30 * Example: 31 * $view->config->addSparklineMetric('nb_visits'); // if an array of metrics given, they will be displayed comma separated 32 * $view->config->addTranslation('nb_visits', 'Visits'); 33 * Results in: [sparkline image] X visits 34 * Data is fetched from the configured $view->requestConfig->apiMethodToRequestDataTable. 35 * 36 * In case you want to add any custom sparklines from any other API method you can call 37 * {@link Sparklines\Config::addSparkline()}. 38 * 39 * Example: 40 * $sparklineUrlParams = array('columns' => array('nb_visits)); 41 * $evolution = array('currentValue' => 5, 'pastValue' => 10, 'tooltip' => 'Foo bar'); 42 * $view->config->addSparkline($sparklineUrlParams, $value = 5, $description = 'Visits', $evolution); 43 * 44 * @property Sparklines\Config $config 45 */ 46class Sparklines extends ViewDataTable 47{ 48 const ID = 'sparklines'; 49 50 public static function getDefaultConfig() 51 { 52 return new Sparklines\Config(); 53 } 54 55 public function supportsComparison() 56 { 57 return true; 58 } 59 60 /** 61 * @see ViewDataTable::main() 62 * @return mixed 63 */ 64 public function render() 65 { 66 $view = new View('@CoreVisualizations/_dataTableViz_sparklines.twig'); 67 68 $columnsList = array(); 69 if ($this->config->hasSparklineMetrics()) { 70 foreach ($this->config->getSparklineMetrics() as $cols) { 71 $columns = $cols['columns']; 72 if (!is_array($columns)) { 73 $columns = array($columns); 74 } 75 76 $columnsList = array_merge($columns, $columnsList); 77 } 78 } 79 80 $view->allMetricsDocumentation = array_merge(Metrics::getDefaultMetricsDocumentation(), $this->config->metrics_documentation); 81 82 $this->requestConfig->request_parameters_to_modify['columns'] = $columnsList; 83 $this->requestConfig->request_parameters_to_modify['format_metrics'] = '1'; 84 85 $request = $this->getRequestArray(); 86 if ($this->isComparing() 87 && !empty($request['comparePeriods']) 88 && count($request['comparePeriods']) == 1 89 ) { 90 $this->requestConfig->request_parameters_to_modify['invert_compare_change_compute'] = 1; 91 } 92 93 if (!empty($this->requestConfig->apiMethodToRequestDataTable)) { 94 $this->fetchConfiguredSparklines(); 95 } 96 97 $view->sparklines = $this->config->getSortedSparklines(); 98 $view->isWidget = Common::getRequestVar('widget', 0, 'int'); 99 $view->titleAttributes = $this->config->title_attributes; 100 $view->footerMessage = $this->config->show_footer_message; 101 $view->areSparklinesLinkable = $this->config->areSparklinesLinkable(); 102 $view->isComparing = $this->isComparing(); 103 104 $view->title = ''; 105 if ($this->config->show_title) { 106 $view->title = $this->config->title; 107 } 108 109 return $view->render(); 110 } 111 112 private function fetchConfiguredSparklines() 113 { 114 $data = $this->loadDataTableFromAPI(); 115 116 $this->applyFilters($data); 117 118 if (!$this->config->hasSparklineMetrics()) { 119 foreach ($data->getColumns() as $column) { 120 $this->config->addSparklineMetric($column); 121 } 122 } 123 124 $firstRow = $data->getFirstRow(); 125 if ($firstRow) { 126 $comparisons = $firstRow->getComparisons(); 127 } else { 128 $comparisons = null; 129 } 130 131 $originalDate = Common::getRequestVar('date'); 132 $originalPeriod = Common::getRequestVar('period'); 133 134 if ($this->isComparing() && !empty($comparisons)) { 135 $comparisonRows = []; 136 foreach ($comparisons->getRows() as $comparisonRow) { 137 $segment = $comparisonRow->getMetadata('compareSegment'); 138 if ($segment === false) { 139 $segment = Request::getRawSegmentFromRequest() ?: ''; 140 } 141 142 $date = $comparisonRow->getMetadata('compareDate'); 143 $period = $comparisonRow->getMetadata('comparePeriod'); 144 145 $comparisonRows[$segment][$period][$date] = $comparisonRow; 146 } 147 } 148 149 foreach ($this->config->getSparklineMetrics() as $sparklineMetricIndex => $sparklineMetric) { 150 $column = $sparklineMetric['columns']; 151 $order = $sparklineMetric['order']; 152 $graphParams = $sparklineMetric['graphParams']; 153 154 if (!isset($order)) { 155 $order = 1000; 156 } 157 158 if ($column === 'label') { 159 continue; 160 } 161 162 if (empty($column)) { 163 $this->config->addPlaceholder($order); 164 continue; 165 } 166 167 $sparklineUrlParams = array( 168 'columns' => $column, 169 'module' => $this->requestConfig->getApiModuleToRequest(), 170 'action' => $this->requestConfig->getApiMethodToRequest() 171 ); 172 173 if ($this->isComparing() && !empty($comparisons)) { 174 $periodObj = Factory::build($originalPeriod, $originalDate); 175 176 $sparklineUrlParams['compareSegments'] = []; 177 178 $comparePeriods = $data->getMetadata('comparePeriods'); 179 $compareDates = $data->getMetadata('compareDates'); 180 181 $compareSegments = $data->getMetadata('compareSegments'); 182 foreach ($compareSegments as $segmentIndex => $segment) { 183 $metrics = []; 184 $seriesIndices = []; 185 186 foreach ($comparePeriods as $periodIndex => $period) { 187 $date = $compareDates[$periodIndex]; 188 189 $compareRow = $comparisonRows[$segment][$period][$date]; 190 $segmentPretty = $compareRow->getMetadata('compareSegmentPretty'); 191 $periodPretty = $compareRow->getMetadata('comparePeriodPretty'); 192 193 $columnToUse = $this->removeUniqueVisitorsIfNotEnabledForPeriod($column, $period); 194 195 list($compareValues, $compareDescriptions, $evolutions) = $this->getValuesAndDescriptions($compareRow, $columnToUse, '_change'); 196 197 foreach ($compareValues as $i => $value) { 198 $metricInfo = [ 199 'value' => $value, 200 'description' => $compareDescriptions[$i], 201 'group' => $periodPretty, 202 ]; 203 204 if (isset($evolutions[$i])) { 205 $metricInfo['evolution'] = $evolutions[$i]; 206 } 207 208 $metrics[] = $metricInfo; 209 } 210 211 $seriesIndices[] = DataComparisonFilter::getComparisonSeriesIndex($data, $periodIndex, $segmentIndex); 212 } 213 214 // only set the title (which is the segment) if comparing more than one segment 215 $title = count($compareSegments) > 1 ? $segmentPretty : null; 216 217 $params = array_merge($sparklineUrlParams, [ 218 'segment' => $segment, 219 'period' => $periodObj->getLabel(), 220 'date' => $periodObj->getRangeString(), 221 ]); 222 $this->config->addSparkline($params, $metrics, $desc = null, null, ($order * 100) + $segmentIndex, $title, $sparklineMetricIndex, $seriesIndices, $graphParams); 223 } 224 } else { 225 list($values, $descriptions) = $this->getValuesAndDescriptions($firstRow, $column); 226 227 $metrics = []; 228 foreach ($values as $i => $value) { 229 $newMetric = [ 230 'value' => $value, 231 'description' => $descriptions[$i], 232 ]; 233 234 $metrics[] = $newMetric; 235 } 236 237 $evolution = null; 238 239 $computeEvolution = $this->config->compute_evolution; 240 if ($computeEvolution) { 241 $evolution = $computeEvolution(array_combine($column, $values)); 242 $newMetric['evolution'] = $evolution; 243 } 244 245 $this->config->addSparkline($sparklineUrlParams, $metrics, $desc = null, $evolution, $order, $title = null, $group = $sparklineMetricIndex, $seriesIndices = null, $graphParams); 246 } 247 } 248 } 249 250 private function applyFilters(DataTable\DataTableInterface $table) 251 { 252 foreach ($this->config->getPriorityFilters() as $filter) { 253 $table->filter($filter[0], $filter[1]); 254 } 255 256 // queue other filters so they can be applied later if queued filters are disabled 257 foreach ($this->config->getPresentationFilters() as $filter) { 258 $table->queueFilter($filter[0], $filter[1]); 259 } 260 261 $table->applyQueuedFilters(); 262 } 263 264 private function getValuesAndDescriptions($firstRow, $columns, $evolutionColumnNameSuffix = null) 265 { 266 if (!is_array($columns)) { 267 $columns = array($columns); 268 } 269 270 $translations = $this->config->translations; 271 272 $values = array(); 273 $descriptions = array(); 274 $evolutions = []; 275 276 foreach ($columns as $col) { 277 $value = 0; 278 if ($firstRow) { 279 $value = $firstRow->getColumn($col); 280 } 281 282 if ($value === false) { 283 $value = 0; 284 } 285 286 if ($evolutionColumnNameSuffix !== null) { 287 $evolution = $firstRow->getColumn($col . $evolutionColumnNameSuffix); 288 if ($evolution !== false) { 289 $evolutions[] = ['percent' => ltrim($evolution, '+'), 'tooltip' => '']; 290 } 291 } 292 293 $values[] = $value; 294 $descriptions[] = isset($translations[$col]) ? $translations[$col] : $col; 295 } 296 297 return [$values, $descriptions, $evolutions]; 298 } 299 300 private function removeUniqueVisitorsIfNotEnabledForPeriod($columns, $period) 301 { 302 if (SettingsPiwik::isUniqueVisitorsEnabled($period)) { 303 return $columns; 304 } 305 306 return array_diff($columns, ['nb_users', 'nb_uniq_visitors']); 307 } 308} 309