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