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