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\JqplotDataGenerator; 10 11use Piwik\Archive\DataTableFactory; 12use Piwik\Common; 13use Piwik\DataTable; 14use Piwik\DataTable\DataTableInterface; 15use Piwik\DataTable\Row; 16use Piwik\Date; 17use Piwik\Metrics; 18use Piwik\Period; 19use Piwik\Period\Factory; 20use Piwik\Plugins\API\Filter\DataComparisonFilter; 21use Piwik\Plugins\CoreVisualizations\JqplotDataGenerator; 22use Piwik\Url; 23 24/** 25 * Generates JQPlot JSON data/config for evolution graphs. 26 */ 27class Evolution extends JqplotDataGenerator 28{ 29 protected function getUnitsForColumnsToDisplay() 30 { 31 $idSite = Common::getRequestVar('idSite', null, 'int'); 32 33 $units = []; 34 foreach ($this->properties['columns_to_display'] as $columnName) { 35 $derivedUnit = Metrics::getUnit($columnName, $idSite); 36 $units[$columnName] = empty($derivedUnit) ? false : $derivedUnit; 37 } 38 return $units; 39 } 40 41 /** 42 * @param DataTable|DataTable\Map $dataTable 43 * @param Chart $visualization 44 */ 45 protected function initChartObjectData($dataTable, $visualization) 46 { 47 // if the loaded datatable is a simple DataTable, it is most likely a plugin plotting some custom data 48 // we don't expect plugin developers to return a well defined Set 49 50 if ($dataTable instanceof DataTable) { 51 parent::initChartObjectData($dataTable, $visualization); 52 return; 53 } 54 55 $dataTables = $dataTable->getDataTables(); 56 57 // determine x labels based on both the displayed date range and the compared periods 58 /** @var Period[][] $xLabels */ 59 $xLabels = [ 60 [], // placeholder for first series 61 ]; 62 63 $this->addComparisonXLabels($xLabels, reset($dataTables)); 64 $this->addSelectedSeriesXLabels($xLabels, $dataTables); 65 66 $units = $this->getUnitsForColumnsToDisplay(); 67 68 // if rows to display are not specified, default to all rows (TODO: perhaps this should be done elsewhere?) 69 $rowsToDisplay = $this->properties['rows_to_display'] 70 ? : array_unique($dataTable->getColumn('label')) 71 ? : array(false) // make sure that a series is plotted even if there is no data 72 ; 73 74 $columnsToDisplay = array_values($this->properties['columns_to_display']); 75 76 list($seriesMetadata, $seriesUnits, $seriesLabels, $seriesToXAxis) = 77 $this->getSeriesMetadata($rowsToDisplay, $columnsToDisplay, $units, $dataTables); 78 79 // collect series data to show. each row-to-display/column-to-display permutation creates a series. 80 $allSeriesData = array(); 81 foreach ($rowsToDisplay as $rowLabel) { 82 foreach ($columnsToDisplay as $columnName) { 83 if (!$this->isComparing) { 84 $this->setNonComparisonSeriesData($allSeriesData, $rowLabel, $columnName, $dataTable); 85 } else { 86 $this->setComparisonSeriesData($allSeriesData, $seriesLabels, $rowLabel, $columnName, $dataTable); 87 } 88 } 89 } 90 91 $visualization->dataTable = $dataTable; 92 $visualization->properties = $this->properties; 93 94 $visualization->setAxisYValues($allSeriesData, $seriesMetadata); 95 $visualization->setAxisYUnits($seriesUnits); 96 97 $xLabelStrs = []; 98 $xAxisTicks = []; 99 foreach ($xLabels as $index => $seriesXLabels) { 100 $xLabelStrs[$index] = array_map(function (Period $p) { return $p->getLocalizedLongString(); }, $seriesXLabels); 101 $xAxisTicks[$index] = array_map(function (Period $p) { return $p->getLocalizedShortString(); }, $seriesXLabels); 102 } 103 104 $visualization->setAxisXLabelsMultiple($xLabelStrs, $seriesToXAxis, $xAxisTicks); 105 106 if ($this->isLinkEnabled()) { 107 $idSite = Common::getRequestVar('idSite', null, 'int'); 108 $periodLabel = reset($dataTables)->getMetadata(DataTableFactory::TABLE_METADATA_PERIOD_INDEX)->getLabel(); 109 110 $axisXOnClick = array(); 111 foreach ($dataTable->getDataTables() as $metadataDataTable) { 112 $dateInUrl = $metadataDataTable->getMetadata(DataTableFactory::TABLE_METADATA_PERIOD_INDEX)->getDateStart(); 113 $parameters = array( 114 'idSite' => $idSite, 115 'period' => $periodLabel, 116 'date' => $dateInUrl->toString(), 117 'segment' => \Piwik\API\Request::getRawSegmentFromRequest() 118 ); 119 $link = Url::getQueryStringFromParameters($parameters); 120 $axisXOnClick[] = $link; 121 } 122 $visualization->setAxisXOnClick($axisXOnClick); 123 } 124 } 125 126 private function getSeriesData($rowLabel, $columnName, DataTable\Map $dataTable) 127 { 128 $seriesData = array(); 129 foreach ($dataTable->getDataTables() as $childTable) { 130 // get the row for this label (use the first if $rowLabel is false) 131 if ($rowLabel === false) { 132 $row = $childTable->getFirstRow(); 133 } else { 134 $row = $childTable->getRowFromLabel($rowLabel); 135 } 136 137 // get series data point. defaults to 0 if no row or no column value. 138 if ($row === false) { 139 $seriesData[] = 0; 140 } else { 141 $seriesData[] = $row->getColumn($columnName) ? : 0; 142 } 143 } 144 return $seriesData; 145 } 146 147 /** 148 * Derive the series label from the row label and the column name. 149 * If the row label is set, both the label and the column name are displayed. 150 * @param string $rowLabel 151 * @param string $columnName 152 * @return string 153 */ 154 private function getSeriesLabel($rowLabel, $columnName) 155 { 156 $metricLabel = @$this->properties['translations'][$columnName]; 157 158 if ($rowLabel !== false) { 159 // eg. "Yahoo! (Visits)" 160 $label = "$rowLabel ($metricLabel)"; 161 } else { 162 // eg. "Visits" 163 $label = $metricLabel; 164 } 165 166 return $label; 167 } 168 169 private function isLinkEnabled() 170 { 171 static $linkEnabled; 172 if (!isset($linkEnabled)) { 173 // 1) Custom Date Range always have link disabled, otherwise 174 // the graph data set is way too big and fails to display 175 // 2) disableLink parameter is set in the Widgetize "embed" code 176 $linkEnabled = !Common::getRequestVar('disableLink', 0, 'int') 177 && Common::getRequestVar('period', 'day') != 'range'; 178 } 179 return $linkEnabled; 180 } 181 182 /** 183 * Each period comparison shows data over different data points than the main series (eg, 2014-02-03,1014-02-06 compared w/ 2015-03-04,2015-03-15). 184 * Though we only display the selected period's x labels, we need to both have the labels for all these data points for tooltips and to stretch 185 * out the selected period x axis, in case it is shorter than one of the compared periods (as in the example above). 186 */ 187 private function addComparisonXLabels(array &$xLabels, DataTable $table) 188 { 189 $comparePeriods = $table->getMetadata('comparePeriods') ?: []; 190 $compareDates = $table->getMetadata('compareDates') ?: []; 191 192 // get rid of selected period 193 array_shift($comparePeriods); 194 array_shift($compareDates); 195 196 foreach (array_values($comparePeriods) as $index => $period) { 197 $date = $compareDates[$index]; 198 199 $range = Factory::build($period, $date); 200 foreach ($range->getSubperiods() as $subperiod) { 201 $xLabels[$index + 1][] = $subperiod; 202 } 203 } 204 } 205 206 /** 207 * @param array $xLabels 208 * @param DataTable[] $dataTables 209 * @throws \Exception 210 */ 211 protected function addSelectedSeriesXLabels(array &$xLabels, array $dataTables) 212 { 213 $xTicksCount = count($dataTables); 214 foreach ($xLabels as $labelSeries) { 215 $xTicksCount = max(count($labelSeries), $xTicksCount); 216 } 217 218 /** @var Date $startDate */ 219 $startDate = reset($dataTables)->getMetadata(DataTableFactory::TABLE_METADATA_PERIOD_INDEX)->getDateStart(); 220 $periodType = reset($dataTables)->getMetadata(DataTableFactory::TABLE_METADATA_PERIOD_INDEX)->getLabel(); 221 222 for ($i = 0; $i < $xTicksCount; ++$i) { 223 $period = Factory::build($periodType, $startDate->addPeriod($i, $periodType)); 224 $xLabels[0][] = $period; 225 } 226 } 227 228 private function setNonComparisonSeriesData(array &$allSeriesData, $rowLabel, $columnName, DataTable\Map $dataTable) 229 { 230 $seriesLabel = $this->getSeriesLabel($rowLabel, $columnName); 231 232 $seriesData = $this->getSeriesData($rowLabel, $columnName, $dataTable); 233 $allSeriesData[$seriesLabel] = $seriesData; 234 } 235 236 private function setComparisonSeriesData(array &$allSeriesData, array $seriesLabels, $rowLabel, $columnName, DataTable\Map $dataTable) 237 { 238 foreach ($dataTable->getDataTables() as $label => $childTable) { 239 // get the row for this label (use the first if $rowLabel is false) 240 if ($rowLabel === false) { 241 $row = $childTable->getFirstRow(); 242 } else { 243 $row = $childTable->getRowFromLabel($rowLabel); 244 } 245 246 if (empty($row) 247 || empty($row->getComparisons()) 248 ) { 249 foreach ($seriesLabels as $seriesIndex => $seriesLabelPrefix) { 250 $wholeSeriesLabel = $this->getComparisonSeriesLabelFromCompareSeries($seriesLabelPrefix, $columnName, $rowLabel); 251 $allSeriesData[$wholeSeriesLabel][] = 0; 252 } 253 254 continue; 255 } 256 257 /** @var DataTable $comparisonTable */ 258 $comparisonTable = $row->getComparisons(); 259 foreach ($comparisonTable->getRows() as $compareRow) { 260 $seriesLabel = $this->getComparisonSeriesLabel($compareRow, $columnName, $rowLabel); 261 $allSeriesData[$seriesLabel][] = $compareRow->getColumn($columnName); 262 } 263 264 $totalsRow = $comparisonTable->getTotalsRow(); 265 if ($totalsRow) { 266 $seriesLabel = $this->getComparisonSeriesLabel($totalsRow, $columnName, $rowLabel); 267 $allSeriesData[$seriesLabel][] = $totalsRow->getColumn($columnName); 268 } 269 } 270 } 271 272 private function getSeriesMetadata(array $rowsToDisplay, array $columnsToDisplay, array $units, array $dataTables) 273 { 274 $seriesMetadata = null; // maps series labels to any metadata of the series 275 $seriesUnits = array(); // maps series labels to unit labels 276 $seriesToXAxis = []; // maps series index to x-axis index (groups of metrics for a single comparison will use the same x-axis) 277 278 $table = reset($dataTables); 279 $seriesLabels = $table->getMetadata('comparisonSeries') ?: []; 280 foreach ($rowsToDisplay as $rowIndex => $rowLabel) { 281 foreach ($columnsToDisplay as $columnIndex => $columnName) { 282 if ($this->isComparing) { 283 foreach ($seriesLabels as $seriesIndex => $seriesLabel) { 284 $wholeSeriesLabel = $this->getComparisonSeriesLabelFromCompareSeries($seriesLabel, $columnName, $rowLabel); 285 286 $allSeriesData[$wholeSeriesLabel] = []; 287 288 $metricIndex = $rowIndex * count($columnsToDisplay) + $columnIndex; 289 $seriesMetadata[$wholeSeriesLabel] = [ 290 'metricIndex' => $metricIndex, 291 'seriesIndex' => $seriesIndex, 292 ]; 293 294 $seriesUnits[$wholeSeriesLabel] = $units[$columnName]; 295 296 list($periodIndex, $segmentIndex) = DataComparisonFilter::getIndividualComparisonRowIndices($table, $seriesIndex); 297 $seriesToXAxis[] = $periodIndex; 298 } 299 } else { 300 $seriesLabel = $this->getSeriesLabel($rowLabel, $columnName); 301 $seriesUnits[$seriesLabel] = $units[$columnName]; 302 } 303 } 304 } 305 306 return [$seriesMetadata, $seriesUnits, $seriesLabels, $seriesToXAxis]; 307 } 308} 309