diff options
Diffstat (limited to 'plugins')
173 files changed, 6276 insertions, 1181 deletions
diff --git a/plugins/API/API.php b/plugins/API/API.php index cfdbe92985..8d21ab11a8 100644 --- a/plugins/API/API.php +++ b/plugins/API/API.php @@ -476,7 +476,7 @@ class API extends \Piwik\Plugin\API * @param bool|int $idDimension * @return array */ - public function getRowEvolution($idSite, $period, $date, $apiModule, $apiAction, $label = false, $segment = false, $column = false, $language = false, $idGoal = false, $legendAppendMetric = true, $labelUseAbsoluteUrl = true, $idDimension = false) + public function getRowEvolution($idSite, $period, $date, $apiModule, $apiAction, $label = false, $segment = false, $column = false, $language = false, $idGoal = false, $legendAppendMetric = true, $labelUseAbsoluteUrl = true, $idDimension = false, $labelSeries = false) { // check if site exists $idSite = (int) $idSite; @@ -504,7 +504,7 @@ class API extends \Piwik\Plugin\API $rowEvolution = new RowEvolution(); return $rowEvolution->getRowEvolution($idSite, $period, $date, $apiModule, $apiAction, $label, $segment, $column, - $language, $apiParameters, $legendAppendMetric, $labelUseAbsoluteUrl); + $language, $apiParameters, $legendAppendMetric, $labelUseAbsoluteUrl, $labelSeries); } /** @@ -526,6 +526,10 @@ class API extends \Piwik\Plugin\API foreach ($urls as $url) { $params = Request::getRequestArrayFromString($url . '&format=php&serialize=0'); + if (!empty($params['method']) && $params['method'] === 'API.getBulkRequest') { + continue; + } + if (isset($params['urls']) && $params['urls'] == $urls) { // by default 'urls' is added to $params as Request::getRequestArrayFromString adds all $_GET/$_POST // default parameters @@ -620,6 +624,37 @@ class API extends \Piwik\Plugin\API return $values; } + /** + * Returns category/subcategory pairs as "CategoryId.SubcategoryId" for whom comparison features should + * be disabled. + * + * @return string[] + */ + public function getPagesComparisonsDisabledFor() + { + $pages = []; + + /** + * If your plugin has pages where you'd like comparison features to be disabled, you can add them + * via this event. Add the pages as "CategoryId.SubcategoryId". + * + * **Example** + * + * ``` + * public function getPagesComparisonsDisabledFor(&$pages) + * { + * $pages[] = "General_Visitors.MyPlugin_MySubcategory"; + * $pages[] = "MyPlugin.myControllerAction"; // if your plugin defines a whole page you want comparison disabled for + * } + * ``` + * + * @param string[] &$pages + */ + Piwik::postEvent('API.getPagesComparisonsDisabledFor', [&$pages]); + + return $pages; + } + private function findSegment($segmentName, $idSite) { $segmentsMetadata = $this->getSegmentsMetadata($idSite, $_hideImplementationData = false); diff --git a/plugins/API/Filter/DataComparisonFilter.php b/plugins/API/Filter/DataComparisonFilter.php new file mode 100644 index 0000000000..78872f3b41 --- /dev/null +++ b/plugins/API/Filter/DataComparisonFilter.php @@ -0,0 +1,590 @@ +<?php +/** + * Piwik - free/libre analytics platform + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\API\Filter; + +use Piwik\API\Request; +use Piwik\Common; +use Piwik\Config; +use Piwik\DataTable; +use Piwik\DataTable\DataTableInterface; +use Piwik\DataTable\Simple; +use Piwik\Http\BadRequestException; +use Piwik\Metrics; +use Piwik\Period; +use Piwik\Period\Factory; +use Piwik\Piwik; +use Piwik\Plugin\Report; +use Piwik\Plugins\API\Filter\DataComparisonFilter\ComparisonRowGenerator; +use Piwik\Segment; +use Piwik\Segment\SegmentExpression; +use Piwik\Site; + +/** + * Handles the API portion of the data comparison feature. + * + * If the `compareSegments`/`comparePeriods`/`compareDates` parameters are supplied this class will fetch + * the data to compare with and store this data next to each row in the root report. + * + * Additionally, `..._change` columns will be added that show the percentage change for a column. This is only + * done when comparing periods, since segments are subsets of visits, so it doesn't make sense to consider + * differences between them as "changes". + * + * ### Comparing multiple periods + * + * It is possible to compare multiple periods with other multiple periods. For example, it + * is possible to compare period=day, date=2018-02-03,2018-02-13 with period=day, date=2018-03-01,2018-03-15. + * When done, this filter will compare the first period in the first set w/ the first period in the second set, + * etc. So in the previous example, 2018-02-03 will be compared with 2018-03-01, 2018-02-04 with 2018-03-02, etc. + * + * ### Metadata + * + * This filter adds the following metadata to DataTables: + * + * - 'compareSegments': The list of segments being compared. The first entry will always be the value of the `segment` query param. + * - 'comparePeriods': The list of labels of periods being compared. The first entry will always be the value of the + * `period` query param. + * - 'compareDates': The list of dates being compared. The first entry will always be the value of the `date` query param. + * - 'comparisonSeries': Prettified labels for every comparison series in order. + * + * This filter adds the following metadata to rows in the comparison DataTables: + * + * - 'compareSegment': The segment of the data for the comparison row. + * - 'compareSegmentPretty': The prettified label for the segment. + * - 'comparePeriod': The period label for the data in the comparison row. This does not have to match a value in the + * `comparePeriods` query parameter if comparing multiple periods. + * - 'compareDate': The date for the period in the data in the comparison row. This does not have to match a value in the + * `compareDates` query parameter if comparing multiple periods. + * - 'comparePeriodPretty': The prettified label for the period. + * - 'compareSeriesPretty': Prettified label for the comparison data represented by the row. This will match an entry + * in the DataTable's `comparisonSeries` metadata. + */ +class DataComparisonFilter +{ + /** + * @var array + */ + private $request; + + /** + * @var int + */ + private $segmentCompareLimit; + + /** + * @var int + */ + private $periodCompareLimit; + + /** + * @var string + */ + private $segmentName; + + /** + * @var string[] + */ + private $compareSegments; + + /** + * @var string[] + */ + private $compareDates; + + /** + * @var string[] + */ + private $comparePeriods; + + /** + * @var int[] + */ + private $compareSegmentIndices; + + /** + * @var int[] + */ + private $comparePeriodIndices; + + /** + * @var bool + */ + private $isRequestMultiplePeriod; + + /** + * @var ComparisonRowGenerator + */ + private $comparisonRowGenerator; + + /** + * @var array + */ + private $columnMappings; + + public function __construct($request, Report $report = null) + { + $this->request = $request; + + $generalConfig = Config::getInstance()->General; + $this->segmentCompareLimit = (int) $generalConfig['data_comparison_segment_limit']; + $this->checkComparisonLimit($this->segmentCompareLimit, 'data_comparison_segment_limit'); + + $this->periodCompareLimit = (int) $generalConfig['data_comparison_period_limit']; + $this->checkComparisonLimit($this->periodCompareLimit, 'data_comparison_period_limit'); + + $this->segmentName = $this->getSegmentNameFromReport($report); + + $this->compareSegments = self::getCompareSegments(); + if (count($this->compareSegments) > $this->segmentCompareLimit + 1) { + throw new BadRequestException(Piwik::translate('General_MaximumNumberOfSegmentsComparedIs', [$this->segmentCompareLimit])); + } + + $this->compareDates = self::getCompareDates($request); + $this->comparePeriods = self::getComparePeriods($request); + + if (count($this->compareDates) !== count($this->comparePeriods)) { + throw new BadRequestException(Piwik::translate('General_CompareDatesParamMustMatchComparePeriods', ['compareDates', 'comparePeriods'])); + } + + if (count($this->compareDates) > $this->periodCompareLimit + 1) { + throw new BadRequestException(Piwik::translate('General_MaximumNumberOfPeriodsComparedIs', [$this->periodCompareLimit])); + } + + if (count($this->compareSegments) == 1 + && count($this->comparePeriods) == 1 + ) { + return; + } + + $this->checkMultiplePeriodCompare(); + + // map segments/periods to their indexes in the query parameter arrays for comparisonIdSubtable matching + $this->compareSegmentIndices = array_flip($this->compareSegments); + foreach ($this->comparePeriods as $index => $period) { + $date = $this->compareDates[$index]; + $this->comparePeriodIndices[$period][$date] = $index; + } + + $this->columnMappings = $this->getColumnMappings(); + $this->comparisonRowGenerator = new ComparisonRowGenerator($this->segmentName, $this->isRequestMultiplePeriod(), $this->columnMappings); + } + + public static function isCompareParamsPresent($request = null) + { + return !empty(Common::getRequestVar('compareSegments', [], $type = 'array', $request)) + || !empty(Common::getRequestVar('comparePeriods', [], $type = 'array', $request)) + || !empty(Common::getRequestVar('compareDates', [], $type = 'array', $request)); + } + + /** + * @param DataTable\DataTableInterface $table + */ + public function compare(DataTable\DataTableInterface $table) + { + if (empty($this->compareSegments) + && empty($this->comparePeriods) + ) { + return; + } + + $method = Common::getRequestVar('method', $default = null, $type = 'string', $this->request); + if ($method == 'Live') { + throw new \Exception("Data comparison is not enabled for the Live API."); + } + + // optimization, if empty, single table, don't need to make extra queries + if ($table->getRowsCount() == 0) { + return; + } + + $comparisonSeries = []; + + // fetch data first + $reportsToCompare = self::getReportsToCompare($this->compareSegments, $this->comparePeriods, $this->compareDates); + foreach ($reportsToCompare as $index => $modifiedParams) { + $compareMetadata = $this->getMetadataFromModifiedParams($modifiedParams); + $comparisonSeries[] = $compareMetadata['compareSeriesPretty']; + + $compareTable = $this->requestReport($method, $modifiedParams); + $this->comparisonRowGenerator->compareTables($compareMetadata, $table, $compareTable); + } + + // calculate changes (including processed metric changes) + // NOTE: it doesn't make to sense to calculate these values for segments, since segments are subsets of all visits, where periods are + // time periods (so things can change from one to another). + if (count($this->comparePeriods) > 1) { + $this->compareChangePercents($table); + } + + // format comparison table metrics + $this->formatComparisonTables($table); + + // add comparison parameters as metadata + $table->filter(function (DataTable $singleTable) use ($comparisonSeries) { + if (isset($this->compareSegments)) { + $singleTable->setMetadata('compareSegments', $this->compareSegments); + } + + if (isset($this->comparePeriods)) { + $singleTable->setMetadata('comparePeriods', $this->comparePeriods); + } + + if (isset($this->compareDates)) { + $singleTable->setMetadata('compareDates', $this->compareDates); + } + + $singleTable->setMetadata('comparisonSeries', $comparisonSeries); + }); + } + + public static function getReportsToCompare($compareSegments, $comparePeriods, $compareDates) + { + $permutations = []; + + // NOTE: the order of these loops determines the order of the rows in the comparison table. ie, + // if we loop over dates then segments, then we'll see comparison rows change segments before changing + // periods. this is because this loop determines in what order we fetch report data. + foreach ($compareDates as $index => $date) { + foreach ($compareSegments as $segment) { + $period = $comparePeriods[$index]; + + $params = []; + $params['segment'] = $segment; + + if (!empty($period) + && !empty($date) + ) { + $params['date'] = $date; + $params['period'] = $period; + } + + $permutations[] = $params; + } + } + + return $permutations; + } + + /** + * @param $paramsToModify + * @return DataTable + */ + private function requestReport($method, $paramsToModify) + { + $params = array_merge( + [ + 'filter_limit' => -1, + 'filter_offset' => 0, + 'filter_sort_column' => '', + 'filter_truncate' => -1, + 'compare' => 0, + 'totals' => 1, + 'disable_queued_filters' => 1, + 'format_metrics' => 0, + 'label' => '', + 'flat' => Common::getRequestVar('flat', 0, 'int', $this->request), + ], + $paramsToModify + ); + + $params['keep_totals_row'] = Common::getRequestVar('keep_totals_row', 0, 'int', $this->request); + $params['keep_totals_row_label'] = Common::getRequestVar('keep_totals_row_label', '', 'string', $this->request); + + if (!isset($params['idSite'])) { + $params['idSite'] = Common::getRequestVar('idSite', null, 'string', $this->request); + } + if (!isset($params['period'])) { + $params['period'] = Common::getRequestVar('period', null, 'string', $this->request); + } + if (!isset($params['date'])) { + $params['date'] = Common::getRequestVar('date', null, 'string', $this->request); + } + + $idSubtable = Common::getRequestVar('idSubtable', 0, 'int', $this->request); + if ($idSubtable > 0) { + $comparisonIdSubtables = Common::getRequestVar('comparisonIdSubtables', $default = false, 'json', $this->request); + if (empty($comparisonIdSubtables)) { + throw new \Exception("Comparing segments/periods with subtables only works when the comparison idSubtables are supplied as well."); + } + + $segmentIndex = empty($paramsToModify['segment']) ? 0 : $this->compareSegmentIndices[$paramsToModify['segment']]; + $periodIndex = empty($paramsToModify['period']) ? 0 : $this->comparePeriodIndices[$paramsToModify['period']][$paramsToModify['date']]; + $seriesIndex = self::getComparisonSeriesIndex(null, $periodIndex, $segmentIndex, count($this->compareSegments)); + + if (!isset($comparisonIdSubtables[$seriesIndex])) { + throw new \Exception("Invalid comparisonIdSubtables parameter: no idSubtable found for segment $segmentIndex and period $periodIndex"); + } + + $comparisonIdSubtable = $comparisonIdSubtables[$seriesIndex]; + if ($comparisonIdSubtable === -1) { // no subtable in comparison row + $table = new DataTable(); + $table->setMetadata('site', new Site($params['idSite'])); + $table->setMetadata('period', Period\Factory::build($params['period'], $params['date'])); + return $table; + } + + $params['idSubtable'] = $comparisonIdSubtable; + } + + return Request::processRequest($method, $params); + } + + private function formatComparisonTables(DataTableInterface $tableOrMap) + { + $tableOrMap->filter(function (DataTable $table) { + $rows = $table->getRows(); + + $totalRow = $table->getTotalsRow(); + if ($totalRow) { + $rows[] = $totalRow; + } + + foreach ($rows as $row) { + /** @var DataTable $comparisonTable */ + $comparisonTable = $row->getComparisons(); + if (!empty($comparisonTable)) { // sanity check + $columnMappings = $this->columnMappings; + $comparisonTable->filter(DataTable\Filter\ReplaceColumnNames::class, [$columnMappings]); + } + + $subtable = $row->getSubtable(); + if ($subtable) { + $this->formatComparisonTables($subtable); + } + } + }); + } + + private function checkComparisonLimit($n, $configName) + { + if ($n <= 1) { + throw new \Exception("The [General] $configName INI config option must be greater than 1."); + } + } + + private function getMetadataFromModifiedParams($modifiedParams) + { + $metadata = []; + + $period = isset($modifiedParams['period']) ? $modifiedParams['period'] : reset($this->comparePeriods); + $date = isset($modifiedParams['date']) ? $modifiedParams['date'] : reset($this->compareDates); + $segment = isset($modifiedParams['segment']) ? $modifiedParams['segment'] : reset($this->compareSegments); + + $metadata['compareSegment'] = $segment; + + $segmentObj = new Segment($segment, []); + $metadata['compareSegmentPretty'] = $segmentObj->getStoredSegmentName(false); + + $metadata['comparePeriod'] = $period; + $metadata['compareDate'] = $date; + + $prettyPeriod = Factory::build($period, $date)->getLocalizedLongString(); + $metadata['comparePeriodPretty'] = ucfirst($prettyPeriod); + + $metadata['compareSeriesPretty'] = self::getComparisonSeriesLabelSuffixFromParts( + $metadata['comparePeriodPretty'], $metadata['compareSegmentPretty']); + + return $metadata; + } + + private static function getComparisonSeriesLabelSuffixFromParts($periodPretty, $segmentPretty) + { + $comparisonLabels = [ + $periodPretty, + $segmentPretty, + ]; + $comparisonLabels = array_filter($comparisonLabels); + + return '(' . implode(') (', $comparisonLabels) . ')'; + } + + private function getSegmentNameFromReport(Report $report = null) + { + if (empty($report)) { + return null; + } + + $dimension = $report->getDimension(); + if (empty($dimension)) { + return null; + } + + $segments = $dimension->getSegments(); + if (empty($segments)) { + return null; + } + + /** @var \Piwik\Plugin\Segment $segment */ + $segment = reset($segments); + $segmentName = $segment->getSegment(); + return $segmentName; + } + + private function checkMultiplePeriodCompare() + { + if ($this->isRequestMultiplePeriod()) { + foreach ($this->comparePeriods as $index => $period) { + if (!Period::isMultiplePeriod($this->compareDates[$index], $period)) { + throw new \Exception("Cannot compare: original request is multiple period and cannot be compared with single periods."); + } + } + } else { + foreach ($this->comparePeriods as $index => $period) { + if (Period::isMultiplePeriod($this->compareDates[$index], $period)) { + throw new \Exception("Cannot compare: original request is single period and cannot be compared with multiple periods."); + } + } + } + } + + private function isRequestMultiplePeriod() + { + if ($this->isRequestMultiplePeriod === null) { + $period = Common::getRequestVar('period', $default = null, 'string', $this->request); + $date = Common::getRequestVar('date', $default = null, 'string', $this->request); + + $this->isRequestMultiplePeriod = Period::isMultiplePeriod($date, $period); + } + return $this->isRequestMultiplePeriod; + } + + private function compareChangePercents(DataTableInterface $result) + { + $segmentCount = count($this->compareSegments); + + $result->filter(function (DataTable $table) use ($segmentCount) { + $rows = $table->getRows(); + + $totalRow = $table->getTotalsRow(); + if ($totalRow) { + $rows[] = $totalRow; + } + + foreach ($rows as $row) { + $comparisons = $row->getComparisons(); + if (empty($comparisons)) { + continue; + } + + /** @var DataTable\Row[] $rows */ + $rows = array_values($comparisons->getRows()); + foreach ($rows as $index => $compareRow) { + if ($index < $segmentCount) { + continue; // do not calculate for first period + } + + list($periodIndex, $segmentIndex) = self::getIndividualComparisonRowIndices($table, $index, $segmentCount); + + $otherPeriodRowIndex = $segmentIndex; + $otherPeriodRow = $comparisons[$otherPeriodRowIndex]; + + foreach ($compareRow->getColumns() as $name => $value) { + $valueToCompare = $otherPeriodRow ? $otherPeriodRow->getColumn($name) : 0; + $valueToCompare = $valueToCompare ?: 0; + + $change = DataTable\Filter\CalculateEvolutionFilter::calculate($value, $valueToCompare, $precision = 1, $appendPercent = false); + + if ($change >= 0) { + $change = '+' . $change; + } + $change .= '%'; + + $compareRow->addColumn($name . '_change', $change); + } + } + } + }); + } + + /** + * Returns the period and segment indices for a given comparison index. + * + * @param DataTable|null $table + * @param $comparisonRowIndex + * @param null $segmentCount + * @return array + */ + public static function getIndividualComparisonRowIndices($table, $comparisonRowIndex, $segmentCount = null) + { + $segmentCount = $segmentCount ?: count($table->getMetadata('compareSegments')); + $segmentIndex = $comparisonRowIndex % $segmentCount; + $periodIndex = floor($comparisonRowIndex / $segmentCount); + return [$periodIndex, $segmentIndex]; + } + + /** + * Returns the series index for a comparison based on the period and segment indices. + * + * @param DataTable|null $table + * @param int $periodIndex + * @param int $segmentIndex + * @param int|null $segmentCount + * @return int + */ + public static function getComparisonSeriesIndex($table, $periodIndex, $segmentIndex, $segmentCount = null) + { + $segmentCount = $segmentCount ?: count($table->getMetadata('compareSegments')); + return $periodIndex * $segmentCount + $segmentIndex; + } + + private static function getCompareSegments($request = null) + { + $segments = Common::getRequestVar('compareSegments', $default = [], $type = 'array', $request); + array_unshift($segments, Common::getRequestVar('segment', '', 'string', $request)); + $segments = Common::unsanitizeInputValues($segments); + return $segments; + } + + private static function getComparePeriods($request = null) + { + $periods = Common::getRequestVar('comparePeriods', $default = [], $type = 'array', $request); + array_unshift($periods, Common::getRequestVar('period', '', 'string', $request)); + return array_values($periods); + } + + private static function getCompareDates($request = null) + { + $dates = Common::getRequestVar('compareDates', $default = [], $type = 'array', $request); + array_unshift($dates, Common::getRequestVar('date', '', 'string', $request)); + return array_values($dates); + } + + /** + * Returns the pretty series label for a specific comparison based on the currently set comparison query parameters. + * + * @param int $labelSeriesIndex The index of the comparison. Comparison series order is determined by {@see self::getReportsToCompare()}. + */ + public static function getPrettyComparisonLabelFromSeriesIndex($labelSeriesIndex) + { + $compareSegments = self::getCompareSegments(); + $comparePeriods = self::getComparePeriods(); + $compareDates = self::getCompareDates(); + + list($periodIndex, $segmentIndex) = self::getIndividualComparisonRowIndices(null, $labelSeriesIndex, count($compareSegments)); + + $segmentObj = new Segment($compareSegments[$segmentIndex], []); + $prettySegment = $segmentObj->getStoredSegmentName(false); + + $prettyPeriod = Factory::build($comparePeriods[$periodIndex], $compareDates[$periodIndex])->getLocalizedLongString(); + $prettyPeriod = ucfirst($prettyPeriod); + + return self::getComparisonSeriesLabelSuffixFromParts($prettyPeriod, $prettySegment); + } + + private function getColumnMappings() + { + $allMappings = Metrics::getMappingFromIdToName(); + + $mappings = []; + foreach ($allMappings as $index => $name) { + $mappings[$index] = $name; + $mappings[$index . '_change'] = $name . '_change'; + } + return $mappings; + } +}
\ No newline at end of file diff --git a/plugins/API/Filter/DataComparisonFilter/ComparisonRowGenerator.php b/plugins/API/Filter/DataComparisonFilter/ComparisonRowGenerator.php new file mode 100644 index 0000000000..eaed08c13b --- /dev/null +++ b/plugins/API/Filter/DataComparisonFilter/ComparisonRowGenerator.php @@ -0,0 +1,239 @@ +<?php +/** + * Piwik - free/libre analytics platform + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\API\Filter\DataComparisonFilter; + +use Piwik\DataTable; +use Piwik\DataTable\DataTableInterface; +use Piwik\DataTable\Simple; +use Piwik\Metrics; +use Piwik\Period; +use Piwik\Segment; +use Piwik\Segment\SegmentExpression; + +class ComparisonRowGenerator +{ + /** + * @var bool + */ + private $isRequestMultiplePeriod; + + /** + * @var string + */ + private $segmentNameForReport; + + /** + * @var array + */ + private $columnMappings; + + public function __construct($segmentNameForReport, $isRequestMultiplePeriod, $columnMappings) + { + $this->segmentNameForReport = $segmentNameForReport; + $this->isRequestMultiplePeriod = $isRequestMultiplePeriod; + $this->columnMappings = $columnMappings; + } + + public function compareTables($compareMetadata, DataTableInterface $tables, DataTableInterface $compareTables = null) + { + if ($tables instanceof DataTable) { + $this->compareTable($compareMetadata, $tables, $compareTables, $compareTables); + } else if ($tables instanceof DataTable\Map) { + $childTablesArray = array_values($tables->getDataTables()); + $compareTablesArray = isset($compareTables) ? array_values($compareTables->getDataTables()) : []; + + $isDatePeriod = $tables->getKeyName() == 'date'; + + foreach ($childTablesArray as $index => $childTable) { + $compareChildTable = isset($compareTablesArray[$index]) ? $compareTablesArray[$index] : null; + $this->compareTables($compareMetadata, $childTable, $compareChildTable); + } + + // in case one of the compared periods has more periods than the main one, we want to fill the result with empty datatables + // so the comparison data is still present. this allows us to see that data in an evolution report. + if ($isDatePeriod) { + $lastTable = end($childTablesArray); + + /** @var Period $lastPeriod */ + $lastPeriod = $lastTable->getMetadata('period'); + $periodType = $lastPeriod->getLabel(); + + for ($i = count($childTablesArray); $i < count($compareTablesArray); ++$i) { + $periodChangeCount = $i - count($childTablesArray) + 1; + $newPeriod = Period\Factory::build($periodType, $lastPeriod->getDateStart()->addPeriod($periodChangeCount, $periodType)); + + // create an empty table for the main request + $newTable = new DataTable(); + $newTable->setAllTableMetadata($lastTable->getAllTableMetadata()); + $newTable->setMetadata('period', $newPeriod); + + if ($newPeriod->getLabel() === 'week' || $newPeriod->getLabel() === 'range') { + $periodLabel = $newPeriod->getRangeString(); + } else { + $periodLabel = $newPeriod->getPrettyString(); + } + + $tables->addTable($newTable, $periodLabel); + + // compare with the empty table + $compareTable = $compareTablesArray[$i]; + $this->compareTables($compareMetadata, $newTable, $compareTable); + } + } + } else { + throw new \Exception("Unexpected DataTable type: " . get_class($tables)); + } + } + + private function compareTable($compareMetadata, DataTable $table, DataTable $rootCompareTable = null, DataTable $compareTable = null) + { + // if there are no rows in the table because the metrics are 0, add one so we can still set comparison values + if ($table->getRowsCount() == 0) { + $table->addRow(new DataTable\Row()); + } + + foreach ($table->getRows() as $row) { + $label = $row->getColumn('label'); + + $compareRow = null; + if ($compareTable instanceof Simple) { + $compareRow = $compareTable->getFirstRow() ?: null; + } else if ($compareTable instanceof DataTable) { + $compareRow = $compareTable->getRowFromLabel($label) ?: null; + } + + $this->compareRow($table, $compareMetadata, $row, $compareRow, $rootCompareTable); + } + + $totalsRow = $table->getTotalsRow(); + if (!empty($totalsRow)) { + $compareRow = $compareTable ? $compareTable->getTotalsRow() : null; + $this->compareRow($table, $compareMetadata, $totalsRow, $compareRow, $rootCompareTable); + } + + if ($compareTable) { + $totals = $compareTable->getMetadata('totals'); + if (!empty($totals)) { + $totals = $this->replaceIndexesInTotals($totals); + $comparisonTotalsEntry = array_merge($compareMetadata, [ + 'totals' => $totals, + ]); + + $allTotalsTables = $table->getMetadata('comparisonTotals'); + $allTotalsTables[] = $comparisonTotalsEntry; + $table->setMetadata('comparisonTotals', $allTotalsTables); + } + } + } + + private function compareRow(DataTable $table, $compareMetadata, DataTable\Row $row, DataTable\Row $compareRow = null, DataTable $rootTable = null) + { + $comparisonDataTable = $row->getComparisons(); + if (empty($comparisonDataTable)) { + $comparisonDataTable = new DataTable(); + $comparisonDataTable->setMetadata(DataTable::EXTRA_PROCESSED_METRICS_METADATA_NAME, + $table->getMetadata(DataTable::EXTRA_PROCESSED_METRICS_METADATA_NAME)); + $row->setComparisons($comparisonDataTable); + } + + $this->addIndividualChildPrettifiedMetadata($compareMetadata, $rootTable); + + $columns = []; + if ($compareRow) { + foreach ($compareRow as $name => $value) { + if (!is_numeric($value) + || $name == 'label' + ) { + continue; + } + + $columns[$name] = $value; + } + } else { + foreach ($row as $name => $value) { + if (!is_numeric($value) + || $name == 'label' + ) { + continue; + } + + $columns[$name] = 0; + } + } + + $newRow = new DataTable\Row([ + DataTable\Row::COLUMNS => $columns, + DataTable\Row::METADATA => $compareMetadata, + ]); + + // set subtable + $newRow->setMetadata('idsubdatatable', -1); + if ($compareRow) { + $subtableId = $compareRow->getMetadata('idsubdatatable_in_db') ?: $compareRow->getIdSubDataTable(); + if ($subtableId) { + $newRow->setMetadata('idsubdatatable', $subtableId); + } + } + + // add segment metadatas + if ($row->getMetadata('segment')) { + $newSegment = $row->getMetadata('segment'); + if ($newRow->getMetadata('compareSegment')) { + $newSegment = Segment::combine($newRow->getMetadata('compareSegment'), SegmentExpression::AND_DELIMITER, $newSegment); + } + $newRow->setMetadata('segment', $newSegment); + } else if ($this->segmentNameForReport + && $row->getMetadata('segmentValue') !== false + ) { + $segmentValue = $row->getMetadata('segmentValue'); + $newRow->setMetadata('segment', sprintf('%s==%s', $this->segmentNameForReport, urlencode($segmentValue))); + } + + $comparisonDataTable->addRow($newRow); + + // recurse on subtable if there + $subtable = $row->getSubtable(); + if ($subtable + && $compareRow + ) { + $this->compareTable($compareMetadata, $subtable, $rootTable, $compareRow->getSubtable()); + } + } + + private function addIndividualChildPrettifiedMetadata(array &$metadata, DataTable $parentTable = null) + { + if ($parentTable + && $this->isRequestMultiplePeriod + ) { + /** @var Period $period */ + $period = $parentTable->getMetadata('period'); + if (empty($period)) { + return; + } + + $prettyPeriod = $period->getLocalizedLongString(); + $metadata['comparePeriodPretty'] = ucfirst($prettyPeriod); + + $metadata['comparePeriod'] = $period->getLabel(); + $metadata['compareDate'] = $period->getDateStart()->toString(); + } + } + + private function replaceIndexesInTotals($totals) + { + foreach ($totals as $index => $value) { + if (isset($this->columnMappings[$index])) { + $name = $this->columnMappings[$index]; + $totals[$name] = $totals[$index]; + unset($totals[$index]); + } + } + return $totals; + } +}
\ No newline at end of file diff --git a/plugins/API/ProcessedReport.php b/plugins/API/ProcessedReport.php index 79e01da316..6e963ec416 100644 --- a/plugins/API/ProcessedReport.php +++ b/plugins/API/ProcessedReport.php @@ -600,15 +600,17 @@ class ProcessedReport * - extract row metadata to a separate Simple $rowsMetadata * * @param int $idSite enables monetary value formatting based on site currency - * @param Simple $simpleDataTable + * @param DataTable $simpleDataTable * @param array $metadataColumns * @param boolean $hasDimension * @param bool $returnRawMetrics If set to true, the original metrics will be returned * @param bool|null $formatMetrics * @return array DataTable $enhancedDataTable filtered metrics with human readable format & Simple $rowsMetadata */ - private function handleSimpleDataTable($idSite, $simpleDataTable, $metadataColumns, $hasDimension, $returnRawMetrics = false, $formatMetrics = null) + private function handleSimpleDataTable($idSite, $simpleDataTable, $metadataColumns, $hasDimension, $returnRawMetrics = false, $formatMetrics = null, $keepMetadata = false) { + $comparisonColumns = $this->getComparisonColumns($metadataColumns); + // new DataTable to store metadata $rowsMetadata = new DataTable(); @@ -634,7 +636,11 @@ class ProcessedReport } } - $enhancedRow = new Row(); + $c = []; + if ($keepMetadata) { + $c[Row::METADATA] = $row->getMetadata(); + } + $enhancedRow = new Row($c); $enhancedDataTable->addRow($enhancedRow); foreach ($rowMetrics as $columnName => $columnValue) { @@ -669,11 +675,23 @@ class ProcessedReport } } + /** @var DataTable $comparisons */ + $comparisons = $row->getComparisons(); + + if (!empty($comparisons) + && $comparisons->getRowsCount() > 0 + ) { + list($newComparisons, $ignore) = $this->handleSimpleDataTable($idSite, $comparisons, $comparisonColumns, true, $returnRawMetrics, $formatMetrics, $keepMetadata = true); + $enhancedRow->setComparisons($newComparisons); + } + // If report has a dimension, extract metadata into a distinct DataTable if ($hasDimension) { $rowMetadata = $row->getMetadata(); $idSubDataTable = $row->getIdSubDataTable(); + unset($rowMetadata[Row::COMPARISONS_METADATA_NAME]); + // always add a metadata row - even if empty, so the number of rows and metadata are equal and can be matched directly $metadataRow = new Row(); $rowsMetadata->addRow($metadataRow); @@ -830,6 +848,10 @@ class ProcessedReport return $value; } + if (strpos($columnName, '_change') !== false) { // comparison change columns are formatted by DataComparisonFilter + return $value == '0' ? '+0%' : $value; + } + // Display time in human readable if (strpos($columnName, 'time_generation') !== false) { return $formatter->getPrettyTimeFromSeconds($value, true); @@ -853,4 +875,13 @@ class ProcessedReport return $value; } + + private function getComparisonColumns(array $metadataColumns) + { + $result = $metadataColumns; + foreach ($metadataColumns as $columnName => $columnTranslation) { + $result[$columnName . '_change'] = Piwik::translate('General_ChangeInX', lcfirst($columnName)); + } + return $result; + } } diff --git a/plugins/API/RowEvolution.php b/plugins/API/RowEvolution.php index 9025eb3436..24e695c88b 100644 --- a/plugins/API/RowEvolution.php +++ b/plugins/API/RowEvolution.php @@ -19,6 +19,7 @@ use Piwik\DataTable\Filter\SafeDecodeLabel; use Piwik\DataTable\Row; use Piwik\Period; use Piwik\Piwik; +use Piwik\Plugins\API\Filter\DataComparisonFilter; use Piwik\Site; use Piwik\Url; @@ -36,7 +37,7 @@ class RowEvolution 'getPageUrl' ); - public function getRowEvolution($idSite, $period, $date, $apiModule, $apiAction, $label = false, $segment = false, $column = false, $language = false, $apiParameters = array(), $legendAppendMetric = true, $labelUseAbsoluteUrl = true) + public function getRowEvolution($idSite, $period, $date, $apiModule, $apiAction, $label = false, $segment = false, $column = false, $language = false, $apiParameters = array(), $legendAppendMetric = true, $labelUseAbsoluteUrl = true, $labelSeries = '') { // validation of requested $period & $date if ($period == 'range') { @@ -49,7 +50,7 @@ class RowEvolution } $label = DataTablePostProcessor::unsanitizeLabelParameter($label); - $labels = Piwik::getArrayFromApiParameter($label); + $labels = Piwik::getArrayFromApiParameter($label, $onlyUnique = empty($labelSeries)); $metadata = $this->getRowEvolutionMetaData($idSite, $period, $date, $apiModule, $apiAction, $language, $apiParameters); @@ -72,7 +73,8 @@ class RowEvolution $labels, $column, $legendAppendMetric, - $labelUseAbsoluteUrl + $labelUseAbsoluteUrl, + $labelSeries ); } else { $data = $this->getSingleRowEvolution( @@ -420,8 +422,13 @@ class RowEvolution /** Get row evolution for a multiple labels */ private function getMultiRowEvolution(DataTable\Map $dataTable, $metadata, $apiModule, $apiAction, $labels, $column, $legendAppendMetric = true, - $labelUseAbsoluteUrl = true) + $labelUseAbsoluteUrl = true, + $labelSeries = '') { + $labelSeries = explode(',', $labelSeries); + $labelSeries = array_filter($labelSeries, 'strlen'); + $labelSeries = array_map('intval', $labelSeries); + if (!isset($metadata['metrics'][$column])) { // invalid column => use the first one that's available $metrics = array_keys($metadata['metrics']); @@ -455,6 +462,11 @@ class RowEvolution $cleanLabel = $this->cleanOriginalLabel($label); $actualLabels[$labelIdx] = $cleanLabel; } + + if (isset($labelSeries[$labelIdx])) { + $labelSeriesIndex = $labelSeries[$labelIdx]; + $actualLabels[$labelIdx] .= ' ' . DataComparisonFilter::getPrettyComparisonLabelFromSeriesIndex($labelSeriesIndex); + } } // convert rows to be array($column.'_'.$labelIdx => $value) as opposed to diff --git a/plugins/API/tests/Integration/Filter/DataComparisonFilter/ComparisonRowGeneratorTest.php b/plugins/API/tests/Integration/Filter/DataComparisonFilter/ComparisonRowGeneratorTest.php new file mode 100644 index 0000000000..1f9f3f17ec --- /dev/null +++ b/plugins/API/tests/Integration/Filter/DataComparisonFilter/ComparisonRowGeneratorTest.php @@ -0,0 +1,613 @@ +<?php +/** + * Piwik - free/libre analytics platform + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +namespace Piwik\Plugins\API\tests\Integration\Filter\DataComparisonFilter; + +use Piwik\DataTable; +use Piwik\Period\Factory; +use Piwik\Plugins\API\Filter\DataComparisonFilter\ComparisonRowGenerator; +use Piwik\Plugins\SegmentEditor\API; +use Piwik\Tests\Framework\TestCase\IntegrationTestCase; + +class ComparisonRowGeneratorTest extends IntegrationTestCase +{ + const TEST_SEGMENT = 'browserCode==ff'; + const OTHER_SEGMENT = 'operatingSystemCode=WIN'; + + protected static function beforeTableDataCached() + { + parent::beforeTableDataCached(); + + API::getInstance()->add('test segment', self::TEST_SEGMENT); + } + + public function test_compareTables_shouldCompareTwoDataTablesCorrectly() + { + $table1 = $this->makeTable([ + ['label' => 'row1', 'nb_visits' => 5, 'nb_actions' => 10], + ['label' => 'row2', 'nb_visits' => 10, 'nb_actions' => 25], + ['label' => 'row3', 'nb_visits' => 20], + ['label' => 'row4', 'nb_actions' => 30], + ]); + + $table2 = $this->makeTable([ + ['label' => 'row1', 'nb_visits' => 10, 'nb_actions' => 5], + ['label' => 'row3', 'somethingelse' => 25], + ]); + + $compareMetadata = [ + 'compareSegment' => self::TEST_SEGMENT, + 'comparePeriod' => 'day', + 'compareDate' => '2012-03-04', + ]; + + $comparisonRowGenerator = new ComparisonRowGenerator('reportSegment', false, []); + $comparisonRowGenerator->compareTables($compareMetadata, $table1, $table2); + + $xmlContent = $this->toXml($table1); + + $expectedXml = <<<END +<?xml version="1.0" encoding="utf-8" ?> +<result> + <row> + <label>row1</label> + <nb_visits>5</nb_visits> + <nb_actions>10</nb_actions> + <comparisons> + <row> + <nb_visits>10</nb_visits> + <nb_actions>5</nb_actions> + <compareSegment>browserCode==ff</compareSegment> + <comparePeriod>day</comparePeriod> + <compareDate>2012-03-04</compareDate> + <idsubdatatable>-1</idsubdatatable> + </row> + </comparisons> + </row> + <row> + <label>row2</label> + <nb_visits>10</nb_visits> + <nb_actions>25</nb_actions> + <comparisons> + <row> + <nb_visits>0</nb_visits> + <nb_actions>0</nb_actions> + <compareSegment>browserCode==ff</compareSegment> + <comparePeriod>day</comparePeriod> + <compareDate>2012-03-04</compareDate> + <idsubdatatable>-1</idsubdatatable> + </row> + </comparisons> + </row> + <row> + <label>row3</label> + <nb_visits>20</nb_visits> + <comparisons> + <row> + <somethingelse>25</somethingelse> + <compareSegment>browserCode==ff</compareSegment> + <comparePeriod>day</comparePeriod> + <compareDate>2012-03-04</compareDate> + <idsubdatatable>-1</idsubdatatable> + </row> + </comparisons> + </row> + <row> + <label>row4</label> + <nb_actions>30</nb_actions> + <comparisons> + <row> + <nb_actions>0</nb_actions> + <compareSegment>browserCode==ff</compareSegment> + <comparePeriod>day</comparePeriod> + <compareDate>2012-03-04</compareDate> + <idsubdatatable>-1</idsubdatatable> + </row> + </comparisons> + </row> +</result> +END; + $this->assertEquals($expectedXml, $xmlContent); + } + + public function test_compareTables_shouldUseFirstTableRowsForComparisons() + { + $table1 = $this->makeTable([ + ['label' => 'row1', 'nb_visits' => 10, 'nb_actions' => 5], + ['label' => 'row3', 'somethingelse' => 25], + ]); + + $table2 = $this->makeTable([ + ['label' => 'row1', 'nb_visits' => 5, 'nb_actions' => 10], + ['label' => 'row2', 'nb_visits' => 10, 'nb_actions' => 25], + ['label' => 'row3', 'nb_visits' => 20], + ['label' => 'row4', 'nb_actions' => 30], + ]); + + $compareMetadata = [ + 'compareSegment' => self::TEST_SEGMENT, + 'comparePeriod' => 'day', + 'compareDate' => '2012-03-04', + ]; + + $comparisonRowGenerator = new ComparisonRowGenerator('reportSegment', false, []); + $comparisonRowGenerator->compareTables($compareMetadata, $table1, $table2); + + $xmlContent = $this->toXml($table1); + + $expectedXml = <<<END +<?xml version="1.0" encoding="utf-8" ?> +<result> + <row> + <label>row1</label> + <nb_visits>10</nb_visits> + <nb_actions>5</nb_actions> + <comparisons> + <row> + <nb_visits>5</nb_visits> + <nb_actions>10</nb_actions> + <compareSegment>browserCode==ff</compareSegment> + <comparePeriod>day</comparePeriod> + <compareDate>2012-03-04</compareDate> + <idsubdatatable>-1</idsubdatatable> + </row> + </comparisons> + </row> + <row> + <label>row3</label> + <somethingelse>25</somethingelse> + <comparisons> + <row> + <nb_visits>20</nb_visits> + <compareSegment>browserCode==ff</compareSegment> + <comparePeriod>day</comparePeriod> + <compareDate>2012-03-04</compareDate> + <idsubdatatable>-1</idsubdatatable> + </row> + </comparisons> + </row> +</result> +END; + $this->assertEquals($expectedXml, $xmlContent); + } + + public function test_compareTables_shouldCompareTwoDataTableMapsCorrectly() + { + $tableSet1 = $this->makeTableMap([ + '2012-01-01' => [ + ['label' => 'row1', 'nb_visits' => 10, 'nb_actions' => 15], + ['label' => 'row2', 'nb_visits' => 15, 'nb_actions' => 15], + ['label' => 'row3', 'nb_visits' => 20, 'nb_actions' => 10], + ], + '2012-02-01' => [ + ['label' => 'row2', 'nb_visits' => 25, 'nb_actions' => 25], + ], + '2012-03-01' => [ + ['label' => 'row3', 'nb_visits' => 20, 'nb_actions' => 10], + ['label' => 'row4', 'nb_visits' => 40, 'nb_actions' => 50], + ], + ]); + + $tableSet2 = $this->makeTableMap([ + '2012-01-01' => [ + ['label' => 'row1', 'nb_visits' => 10, 'nb_actions' => 15], + ], + '2012-02-01' => [ + ['label' => 'row2', 'nb_visits' => 15, 'nb_actions' => 15], + ['label' => 'row3', 'nb_visits' => 20, 'nb_actions' => 10], + ], + '2012-03-01' => [ + ['label' => 'row2', 'nb_visits' => 25, 'nb_actions' => 25], + ['label' => 'row3', 'nb_visits' => 20, 'nb_actions' => 10], + ['label' => 'row4', 'nb_visits' => 40, 'nb_actions' => 50], + ], + ]); + + $compareMetadata = [ + 'compareSegment' => self::OTHER_SEGMENT, + 'comparePeriod' => 'month', + 'compareDate' => '2012-01-01,2012-03-01', + ]; + + $comparisonRowGenerator = new ComparisonRowGenerator('reportSegment', false, []); + $comparisonRowGenerator->compareTables($compareMetadata, $tableSet1, $tableSet2); + + $xmlContent = $this->toXml($tableSet1); + + $expectedXml = <<<END +<?xml version="1.0" encoding="utf-8" ?> +<results> + <result date="2012-01"> + <row> + <label>row1</label> + <nb_visits>10</nb_visits> + <nb_actions>15</nb_actions> + <comparisons> + <row> + <nb_visits>10</nb_visits> + <nb_actions>15</nb_actions> + <compareSegment>operatingSystemCode=WIN</compareSegment> + <comparePeriod>month</comparePeriod> + <compareDate>2012-01-01,2012-03-01</compareDate> + <idsubdatatable>-1</idsubdatatable> + </row> + </comparisons> + </row> + <row> + <label>row2</label> + <nb_visits>15</nb_visits> + <nb_actions>15</nb_actions> + <comparisons> + <row> + <nb_visits>0</nb_visits> + <nb_actions>0</nb_actions> + <compareSegment>operatingSystemCode=WIN</compareSegment> + <comparePeriod>month</comparePeriod> + <compareDate>2012-01-01,2012-03-01</compareDate> + <idsubdatatable>-1</idsubdatatable> + </row> + </comparisons> + </row> + <row> + <label>row3</label> + <nb_visits>20</nb_visits> + <nb_actions>10</nb_actions> + <comparisons> + <row> + <nb_visits>0</nb_visits> + <nb_actions>0</nb_actions> + <compareSegment>operatingSystemCode=WIN</compareSegment> + <comparePeriod>month</comparePeriod> + <compareDate>2012-01-01,2012-03-01</compareDate> + <idsubdatatable>-1</idsubdatatable> + </row> + </comparisons> + </row> + </result> + <result date="2012-02"> + <row> + <label>row2</label> + <nb_visits>25</nb_visits> + <nb_actions>25</nb_actions> + <comparisons> + <row> + <nb_visits>15</nb_visits> + <nb_actions>15</nb_actions> + <compareSegment>operatingSystemCode=WIN</compareSegment> + <comparePeriod>month</comparePeriod> + <compareDate>2012-01-01,2012-03-01</compareDate> + <idsubdatatable>-1</idsubdatatable> + </row> + </comparisons> + </row> + </result> + <result date="2012-03"> + <row> + <label>row3</label> + <nb_visits>20</nb_visits> + <nb_actions>10</nb_actions> + <comparisons> + <row> + <nb_visits>20</nb_visits> + <nb_actions>10</nb_actions> + <compareSegment>operatingSystemCode=WIN</compareSegment> + <comparePeriod>month</comparePeriod> + <compareDate>2012-01-01,2012-03-01</compareDate> + <idsubdatatable>-1</idsubdatatable> + </row> + </comparisons> + </row> + <row> + <label>row4</label> + <nb_visits>40</nb_visits> + <nb_actions>50</nb_actions> + <comparisons> + <row> + <nb_visits>40</nb_visits> + <nb_actions>50</nb_actions> + <compareSegment>operatingSystemCode=WIN</compareSegment> + <comparePeriod>month</comparePeriod> + <compareDate>2012-01-01,2012-03-01</compareDate> + <idsubdatatable>-1</idsubdatatable> + </row> + </comparisons> + </row> + </result> +</results> +END; + $this->assertEquals($expectedXml, $xmlContent); + } + + public function test_compareTables_shouldCompareTwoDataTaleMapsOfDifferentLengthsCorrectly_whenFirstIsLonger() + { + $tableSet1 = $this->makeTableMap([ + '2012-01-01' => [ + ['label' => 'row1', 'nb_visits' => 10, 'nb_actions' => 15], + ['label' => 'row2', 'nb_visits' => 15, 'nb_actions' => 15], + ['label' => 'row3', 'nb_visits' => 20, 'nb_actions' => 10], + ], + '2012-02-01' => [ + ['label' => 'row2', 'nb_visits' => 25, 'nb_actions' => 25], + ], + '2012-03-01' => [ + ['label' => 'row3', 'nb_visits' => 20, 'nb_actions' => 10], + ['label' => 'row4', 'nb_visits' => 40, 'nb_actions' => 50], + ], + ]); + + $tableSet2 = $this->makeTableMap([ + '2012-01-01' => [ + ['label' => 'row1', 'nb_visits' => 10, 'nb_actions' => 15], + ], + '2012-02-01' => [ + // empty + ], + ]); + + $compareMetadata = [ + 'compareSegment' => self::OTHER_SEGMENT, + 'comparePeriod' => 'month', + 'compareDate' => '2012-01-01,2012-03-01', + ]; + + $comparisonRowGenerator = new ComparisonRowGenerator('reportSegment', false, []); + $comparisonRowGenerator->compareTables($compareMetadata, $tableSet1, $tableSet2); + + $xmlContent = $this->toXml($tableSet1); + + $expectedXml = <<<END +<?xml version="1.0" encoding="utf-8" ?> +<results> + <result date="2012-01"> + <row> + <label>row1</label> + <nb_visits>10</nb_visits> + <nb_actions>15</nb_actions> + <comparisons> + <row> + <nb_visits>10</nb_visits> + <nb_actions>15</nb_actions> + <compareSegment>operatingSystemCode=WIN</compareSegment> + <comparePeriod>month</comparePeriod> + <compareDate>2012-01-01,2012-03-01</compareDate> + <idsubdatatable>-1</idsubdatatable> + </row> + </comparisons> + </row> + <row> + <label>row2</label> + <nb_visits>15</nb_visits> + <nb_actions>15</nb_actions> + <comparisons> + <row> + <nb_visits>0</nb_visits> + <nb_actions>0</nb_actions> + <compareSegment>operatingSystemCode=WIN</compareSegment> + <comparePeriod>month</comparePeriod> + <compareDate>2012-01-01,2012-03-01</compareDate> + <idsubdatatable>-1</idsubdatatable> + </row> + </comparisons> + </row> + <row> + <label>row3</label> + <nb_visits>20</nb_visits> + <nb_actions>10</nb_actions> + <comparisons> + <row> + <nb_visits>0</nb_visits> + <nb_actions>0</nb_actions> + <compareSegment>operatingSystemCode=WIN</compareSegment> + <comparePeriod>month</comparePeriod> + <compareDate>2012-01-01,2012-03-01</compareDate> + <idsubdatatable>-1</idsubdatatable> + </row> + </comparisons> + </row> + </result> + <result date="2012-02"> + <row> + <label>row2</label> + <nb_visits>25</nb_visits> + <nb_actions>25</nb_actions> + <comparisons> + <row> + <nb_visits>0</nb_visits> + <nb_actions>0</nb_actions> + <compareSegment>operatingSystemCode=WIN</compareSegment> + <comparePeriod>month</comparePeriod> + <compareDate>2012-01-01,2012-03-01</compareDate> + <idsubdatatable>-1</idsubdatatable> + </row> + </comparisons> + </row> + </result> + <result date="2012-03"> + <row> + <label>row3</label> + <nb_visits>20</nb_visits> + <nb_actions>10</nb_actions> + <comparisons> + <row> + <nb_visits>0</nb_visits> + <nb_actions>0</nb_actions> + <compareSegment>operatingSystemCode=WIN</compareSegment> + <comparePeriod>month</comparePeriod> + <compareDate>2012-01-01,2012-03-01</compareDate> + <idsubdatatable>-1</idsubdatatable> + </row> + </comparisons> + </row> + <row> + <label>row4</label> + <nb_visits>40</nb_visits> + <nb_actions>50</nb_actions> + <comparisons> + <row> + <nb_visits>0</nb_visits> + <nb_actions>0</nb_actions> + <compareSegment>operatingSystemCode=WIN</compareSegment> + <comparePeriod>month</comparePeriod> + <compareDate>2012-01-01,2012-03-01</compareDate> + <idsubdatatable>-1</idsubdatatable> + </row> + </comparisons> + </row> + </result> +</results> +END; + $this->assertEquals($expectedXml, $xmlContent); + } + + public function test_compareTables_shouldCompareTwoDataTaleMapsOfDifferentLengthsCorrectly_whenFirstIsShorter() + { + $tableSet1 = $this->makeTableMap([ + '2012-01-01' => [ + ['label' => 'row1', 'nb_visits' => 10, 'nb_actions' => 15], + ['label' => 'row2', 'nb_visits' => 15, 'nb_actions' => 15], + ['label' => 'row3', 'nb_visits' => 20, 'nb_actions' => 10], + ], + '2012-02-01' => [ + // empty + ], + ]); + + $tableSet2 = $this->makeTableMap([ + '2012-01-01' => [ + ['label' => 'row1', 'nb_visits' => 10, 'nb_actions' => 15], + ], + '2012-02-01' => [ + ['label' => 'row2', 'nb_visits' => 15, 'nb_actions' => 15], + ['label' => 'row3', 'nb_visits' => 20, 'nb_actions' => 10], + ], + '2012-03-01' => [ + ['label' => 'row2', 'nb_visits' => 25, 'nb_actions' => 25], + ['label' => 'row3', 'nb_visits' => 20, 'nb_actions' => 10], + ['label' => 'row4', 'nb_visits' => 40, 'nb_actions' => 50], + ], + ]); + + $compareMetadata = [ + 'compareSegment' => self::OTHER_SEGMENT, + 'comparePeriod' => 'month', + 'compareDate' => '2012-01-01,2012-03-01', + ]; + + $comparisonRowGenerator = new ComparisonRowGenerator('reportSegment', false, []); + $comparisonRowGenerator->compareTables($compareMetadata, $tableSet1, $tableSet2); + + $xmlContent = $this->toXml($tableSet1); + + $expectedXml = <<<END +<?xml version="1.0" encoding="utf-8" ?> +<results> + <result date="2012-01"> + <row> + <label>row1</label> + <nb_visits>10</nb_visits> + <nb_actions>15</nb_actions> + <comparisons> + <row> + <nb_visits>10</nb_visits> + <nb_actions>15</nb_actions> + <compareSegment>operatingSystemCode=WIN</compareSegment> + <comparePeriod>month</comparePeriod> + <compareDate>2012-01-01,2012-03-01</compareDate> + <idsubdatatable>-1</idsubdatatable> + </row> + </comparisons> + </row> + <row> + <label>row2</label> + <nb_visits>15</nb_visits> + <nb_actions>15</nb_actions> + <comparisons> + <row> + <nb_visits>0</nb_visits> + <nb_actions>0</nb_actions> + <compareSegment>operatingSystemCode=WIN</compareSegment> + <comparePeriod>month</comparePeriod> + <compareDate>2012-01-01,2012-03-01</compareDate> + <idsubdatatable>-1</idsubdatatable> + </row> + </comparisons> + </row> + <row> + <label>row3</label> + <nb_visits>20</nb_visits> + <nb_actions>10</nb_actions> + <comparisons> + <row> + <nb_visits>0</nb_visits> + <nb_actions>0</nb_actions> + <compareSegment>operatingSystemCode=WIN</compareSegment> + <comparePeriod>month</comparePeriod> + <compareDate>2012-01-01,2012-03-01</compareDate> + <idsubdatatable>-1</idsubdatatable> + </row> + </comparisons> + </row> + </result> + <result date="2012-02"> + <row> + <comparisons> + <row> + <compareSegment>operatingSystemCode=WIN</compareSegment> + <comparePeriod>month</comparePeriod> + <compareDate>2012-01-01,2012-03-01</compareDate> + <idsubdatatable>-1</idsubdatatable> + </row> + </comparisons> + </row> + </result> + <result date="2012-03"> + <row> + <comparisons> + <row> + <compareSegment>operatingSystemCode=WIN</compareSegment> + <comparePeriod>month</comparePeriod> + <compareDate>2012-01-01,2012-03-01</compareDate> + <idsubdatatable>-1</idsubdatatable> + </row> + </comparisons> + </row> + </result> +</results> +END; + $this->assertEquals($expectedXml, $xmlContent); + } + + private function makeTable(array $rows) + { + $table = new DataTable(); + $table->addRowsFromSimpleArray($rows); + return $table; + } + + private function makeTableMap(array $tableRows) + { + $result = new DataTable\Map(); + $result->setKeyName('date'); + foreach ($tableRows as $label => $rows) { + $period = Factory::build('month', $label); + + $table = $this->makeTable($rows); + $table->setMetadata('period', $period); + + $result->addTable($table, $period->getPrettyString()); + } + return $result; + } + + private function toXml(DataTable\DataTableInterface $table) + { + $renderer = new DataTable\Renderer\Xml(); + $renderer->setTable($table); + return $renderer->render(); + } +}
\ No newline at end of file diff --git a/plugins/API/tests/Unit/XmlRendererTest.php b/plugins/API/tests/Unit/XmlRendererTest.php index 833d303aac..a026f7778a 100644 --- a/plugins/API/tests/Unit/XmlRendererTest.php +++ b/plugins/API/tests/Unit/XmlRendererTest.php @@ -28,6 +28,11 @@ class XmlRendererTest extends \PHPUnit_Framework_TestCase DataTable\Manager::getInstance()->deleteAll(); } + public function tearDown() + { + DataTable\Manager::getInstance()->deleteAll(); + } + public function test_renderSuccess_shouldIncludeMessage() { $response = $this->builder->renderSuccess('ok'); diff --git a/plugins/Actions/DataTable/Filter/Actions.php b/plugins/Actions/DataTable/Filter/Actions.php index c45716a98b..1295cae57e 100644 --- a/plugins/Actions/DataTable/Filter/Actions.php +++ b/plugins/Actions/DataTable/Filter/Actions.php @@ -54,29 +54,31 @@ class Actions extends BaseFilter } foreach ($dataTable->getRows() as $row) { - $url = $row->getMetadata('url'); - $pageTitlePath = $row->getMetadata('page_title_path'); - $folderUrlStart = $row->getMetadata('folder_url_start'); - $label = $row->getColumn('label'); - if ($url) { - $row->setMetadata('segmentValue', urldecode($url)); - } else if ($folderUrlStart) { - $row->setMetadata('segment', 'pageUrl=^' . urlencode(urlencode($folderUrlStart))); - } else if ($pageTitlePath) { - if ($row->getIdSubDataTable()) { - $row->setMetadata('segment', 'pageTitle=^' . urlencode(urlencode(trim(urldecode($pageTitlePath))))); - } else { - $row->setMetadata('segmentValue', trim(urldecode($pageTitlePath))); - } - } else if ($isPageTitleType && !in_array($label, [DataTable::LABEL_SUMMARY_ROW])) { - // for older data w/o page_title_path metadata - if ($row->getIdSubDataTable()) { - $row->setMetadata('segment', 'pageTitle=^' . urlencode(urlencode(trim(urldecode($label))))); - } else { - $row->setMetadata('segmentValue', trim(urldecode($label))); + if (!$row->isSummaryRow()) { + $url = $row->getMetadata('url'); + $pageTitlePath = $row->getMetadata('page_title_path'); + $folderUrlStart = $row->getMetadata('folder_url_start'); + $label = $row->getColumn('label'); + if ($url) { + $row->setMetadata('segmentValue', urldecode($url)); + } else if ($folderUrlStart) { + $row->setMetadata('segment', 'pageUrl=^' . urlencode(urlencode($folderUrlStart))); + } else if ($pageTitlePath) { + if ($row->getIdSubDataTable()) { + $row->setMetadata('segment', 'pageTitle=^' . urlencode(urlencode(trim(urldecode($pageTitlePath))))); + } else { + $row->setMetadata('segmentValue', trim(urldecode($pageTitlePath))); + } + } else if ($isPageTitleType && !in_array($label, [DataTable::LABEL_SUMMARY_ROW])) { + // for older data w/o page_title_path metadata + if ($row->getIdSubDataTable()) { + $row->setMetadata('segment', 'pageTitle=^' . urlencode(urlencode(trim(urldecode($label))))); + } else { + $row->setMetadata('segmentValue', trim(urldecode($label))); + } + } else if ($this->actionType == Action::TYPE_PAGE_URL && $urlPrefix) { // folder for older data w/ no folder URL metadata + $row->setMetadata('segment', 'pageUrl=^' . urlencode(urlencode($urlPrefix . '/' . $label))); } - } else if ($this->actionType == Action::TYPE_PAGE_URL && $urlPrefix) { // folder for older data w/ no folder URL metadata - $row->setMetadata('segment', 'pageUrl=^' . urlencode(urlencode($urlPrefix . '/' . $label))); } // remove the default action name 'index' in the end of flattened urls and prepend $actionDelimiter diff --git a/plugins/Actions/javascripts/actionsDataTable.js b/plugins/Actions/javascripts/actionsDataTable.js index eb5a14ed01..f8de151d36 100644 --- a/plugins/Actions/javascripts/actionsDataTable.js +++ b/plugins/Actions/javascripts/actionsDataTable.js @@ -137,7 +137,8 @@ var imagePlusMinusHeight = 12; $('td:first-child', rowsWithSubtables) .each(function () { - $(this).prepend('<img width="' + imagePlusMinusWidth + '" height="' + imagePlusMinusHeight + '" class="plusMinus" src="" />'); + $('<img width="' + imagePlusMinusWidth + '" height="' + imagePlusMinusHeight + '" class="plusMinus" src="" />').insertBefore($(this).children('.label')); + if (self.param.filter_pattern_recursive) { setImageMinus(this); } @@ -171,10 +172,7 @@ }, addOddAndEvenClasses: function(domElem) { - // Add some styles on the cells - // label (first column of a data row) or not - $("tr:not(.hidden) td:first-child", domElem).addClass('label'); - $("tr:not(.hidden) td", domElem).slice(1).addClass('column'); + // empty }, handleRowActions: function (domElem, rows) { @@ -190,7 +188,12 @@ var divIdToReplaceWithSubTable = 'subDataTable_' + idSubTable; - var NextStyle = $(domElem).next().attr('class'); + var $insertAfter = $(domElem).nextUntil(':not(.comparePeriod):not(.comparisonRow)').last(); + if (!$insertAfter.length) { + $insertAfter = $(domElem); + } + + var NextStyle = $insertAfter.next().attr('class'); var CurrentStyle = $(domElem).attr('class'); var currentRowLevel = getLevelFromClass(CurrentStyle); @@ -205,7 +208,7 @@ self.disabledRowDom = $(domElem); var numberOfColumns = $(domElem).children().length; - $(domElem).after('\ + $insertAfter.after('\ <tr id="' + divIdToReplaceWithSubTable + '" class="cellSubDataTable">\ <td colspan="' + numberOfColumns + '">\ <span class="loadingPiwik" style="display:inline"><img src="plugins/Morpheus/images/loading-blue.gif" /> Loading...</span>\ @@ -225,10 +228,13 @@ self.param.idSubtable = idSubTable; self.param.action = self.props.subtable_controller_action; + var extraParams = {}; + extraParams.comparisonIdSubtables = self.getComparisonIdSubtables($(domElem)); + self.reloadAjaxDataTable(false, function (resp) { self.actionsSubDataTableLoaded(resp, idSubTable); self.repositionRowActions($(domElem)); - }); + }, extraParams); self.param.action = savedActionVariable; self.restoreAllFilters(filtersToRestore); diff --git a/plugins/Actions/javascripts/rowactions.js b/plugins/Actions/javascripts/rowactions.js index 87b77868e4..422036aecc 100644 --- a/plugins/Actions/javascripts/rowactions.js +++ b/plugins/Actions/javascripts/rowactions.js @@ -20,6 +20,8 @@ $(function () { function getLinkForTransitionAndOverlayPopover(tr) { + tr = getRealRowIfComparisonRow(tr); + var link = tr.find('> td:first > a').attr('href'); link = $('<textarea>').html(link).val(); // remove html entities return link; @@ -31,11 +33,29 @@ $(function () { return isPageUrlReport(dataTableParams); }, isAvailableOnRow: function (dataTableParams, tr) { + tr = getRealRowIfComparisonRow(tr); return isPageUrlReport(dataTableParams) && tr.find('> td:first span.label').parent().is('a') }, - trigger: function (tr, e, subTableLabel) { + trigger: function (tr, e, subTableLabel, originalRow) { + var overrideParams = $.extend({}, $(originalRow || tr).data('param-override')); + if (typeof overrideParams !== 'object') { + overrideParams = {}; + } + + tr = getRealRowIfComparisonRow(tr); + var link = getLinkForTransitionAndOverlayPopover(tr); - this.openPopover('url:' + link); + var popoverUrl = 'url:' + link; + + Object.keys(overrideParams).forEach(function (paramName) { + if (!overrideParams[paramName]) { + return; + } + + popoverUrl += ':' + encodeURIComponent(paramName) + ':' + encodeURIComponent(overrideParams[paramName]); + }); + + this.openPopover(popoverUrl); } }); @@ -66,4 +86,11 @@ $(function () { }); } + function getRealRowIfComparisonRow(tr) { + if (tr.is('.comparisonRow')) { + var prevUntil = tr.prevUntil('.parentComparisonRow').prev(); + return prevUntil.length ? prevUntil : tr.prev(); + } + return tr; + } });
\ No newline at end of file diff --git a/plugins/Actions/tests/UI/ActionsDataTable_spec.js b/plugins/Actions/tests/UI/ActionsDataTable_spec.js index 9c9f855544..bc4386281d 100644 --- a/plugins/Actions/tests/UI/ActionsDataTable_spec.js +++ b/plugins/Actions/tests/UI/ActionsDataTable_spec.js @@ -105,6 +105,7 @@ describe("ActionsDataTable", function () { it("should show the search when clicking on the search icon", async function() { await page.click('.dataTableAction.searchAction'); + await page.mouse.move(-10, -10); await page.waitFor(500); expect(await page.screenshot({ fullPage: true })).to.matchImage('search_visible'); }); diff --git a/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_auto_expand.png b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_auto_expand.png index 4f1e0fc4ae..3ff4a2ccd8 100644 --- a/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_auto_expand.png +++ b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_auto_expand.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6bbb83ed6c93fff400bc059649b000d8a8434d1bee5a03c9d60b06f08351ca44 -size 373126 +oid sha256:377222cc01db62a192d1d01524aaa651a25c73fff29a5a051b878a8dc56f3cf4 +size 376343 diff --git a/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_column_sorted.png b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_column_sorted.png index 823d788aa2..ae4a0ad6d1 100644 --- a/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_column_sorted.png +++ b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_column_sorted.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:19c00677ca3e15f163b7e59886d1d1349717918848dc105a94f8f49c51d593a9 -size 349049 +oid sha256:e4f9b43bd75105de4023baf9345cbe47f138e69df30ff4c745c7d058eba1b229 +size 352520 diff --git a/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_exclude_low_population.png b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_exclude_low_population.png index dcaabb4970..11303bd0fc 100644 --- a/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_exclude_low_population.png +++ b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_exclude_low_population.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:198000cebc6a53a3b9310fbf33a441fe70190dd86a573c28694f3e08656df483 -size 79732 +oid sha256:a28f6ab4021b92051d0d5e1d440d1afdcd198a7a1aa6064555a8795653d266a9 +size 79681 diff --git a/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_flattened.png b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_flattened.png index 87a4c94bad..d59e2e3c67 100644 --- a/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_flattened.png +++ b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_flattened.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:58f11c3535ffa459122e849ebd5c9c5d2d5207754f7a6c0897de88ccfea64356 -size 467791 +oid sha256:2cf9ac000f4f6abbab9ad5139a9b0cd01463aa32d68ace9ec072e4fe7df62e8e +size 471736 diff --git a/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_initial.png b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_initial.png index 91357e2854..dfd04fc0e8 100644 --- a/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_initial.png +++ b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_initial.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:24a3a1a0cfe8f8b3a1258c4a09523f9204301d3458d94d822cb6b7a293615fcf -size 348489 +oid sha256:9d529f2e6ef47420eff3159924c5de969b93ef4447ad45f27114f0c0203596b5 +size 351793 diff --git a/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_pageview_percentages.png b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_pageview_percentages.png index 23278dc015..036bcb0d3e 100644 --- a/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_pageview_percentages.png +++ b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_pageview_percentages.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:092f50f1d9b32b57ee047e3c6b8235007ac36e35f5f4a0f2e10bb875bfb44110 -size 67302 +oid sha256:4fc514392680028f608f8c4e391a9655181106813b72bdf38f7db06afe286fc2 +size 67063 diff --git a/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_search.png b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_search.png index 2bfd563211..188ff83755 100644 --- a/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_search.png +++ b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_search.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eb076300fe4f89f96ed01e07d0a8af5637cff40d7de4c39255c2a31e493c4f12 -size 81492 +oid sha256:901b3ff98ab7314af98ce0f30f25d39b12faf00f576842c6c2834df46b0e0b96 +size 81557 diff --git a/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_search_closed.png b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_search_closed.png index 50c2df9696..687fc77ed5 100644 --- a/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_search_closed.png +++ b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_search_closed.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:730c0bd5c1e622c7fddedec312fbe74daecb21df021a15dc5e9dcabfcf717404 -size 59905 +oid sha256:c54831cd594cdb6d24e8772ac4757c1cda55d72cc52fcf5d0c66ace038033ac0 +size 59687 diff --git a/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_search_visible.png b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_search_visible.png index 5e77179a22..c7012ffb9a 100644 --- a/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_search_visible.png +++ b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_search_visible.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:75aed5872a02e45b9ece02d145819cf1c7434070a6dfbb3c9829f803fcd06322 -size 59910 +oid sha256:99e2cb16b5dc7475bb4dafe527d6f67c41d5bd087d2e4e90e7f9ccc147e5b95f +size 65813 diff --git a/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_segmented_visitor_log_hover.png b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_segmented_visitor_log_hover.png index 492c6f3b4f..1efa16a865 100644 --- a/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_segmented_visitor_log_hover.png +++ b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_segmented_visitor_log_hover.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c56217752d9be8b87d46287f62e21c1c87a94433124232c6db9e88a8e85ee532 -size 66616 +oid sha256:a1862e40dcf575461be061dc79be6b9e59f09e3af189460919ca1e7f5b22161f +size 71556 diff --git a/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_subtables_loaded.png b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_subtables_loaded.png index fc23b4e3d4..d0414cc612 100644 --- a/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_subtables_loaded.png +++ b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_subtables_loaded.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e734af53ed981781db46ee91fba9e0039c4d3c743b0dcd3c09d6dad3843ff9ea -size 371468 +oid sha256:b006bd8903a40cda94780f77e59632871ad9c6264e77ad3291e31560bc829a0c +size 419585 diff --git a/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_unflattened.png b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_unflattened.png index 50c2df9696..687fc77ed5 100644 --- a/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_unflattened.png +++ b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_unflattened.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:730c0bd5c1e622c7fddedec312fbe74daecb21df021a15dc5e9dcabfcf717404 -size 59905 +oid sha256:c54831cd594cdb6d24e8772ac4757c1cda55d72cc52fcf5d0c66ace038033ac0 +size 59687 diff --git a/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_unique_pageview_percentages.png b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_unique_pageview_percentages.png index 6aeb2fab8b..036bcb0d3e 100644 --- a/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_unique_pageview_percentages.png +++ b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_unique_pageview_percentages.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:752bb51c459aba94b8143e9d34c003a75c02dd87f5fb6c613b94ee6e716ecaed -size 66900 +oid sha256:4fc514392680028f608f8c4e391a9655181106813b72bdf38f7db06afe286fc2 +size 67063 diff --git a/plugins/Bandwidth b/plugins/Bandwidth -Subproject aafb2d9d8af8dc53b76ccf30b2ad7ed3af84ef2 +Subproject f36b18235c0dd1e33b2a917b9ee90faa609b75e diff --git a/plugins/CoreAdminHome/tests/UI/expected-screenshots/TrackingFailures_manage_with_failures_delete_all_confirmed.png b/plugins/CoreAdminHome/tests/UI/expected-screenshots/TrackingFailures_manage_with_failures_delete_all_confirmed.png index 18b1d26dd7..aa4611320b 100644 --- a/plugins/CoreAdminHome/tests/UI/expected-screenshots/TrackingFailures_manage_with_failures_delete_all_confirmed.png +++ b/plugins/CoreAdminHome/tests/UI/expected-screenshots/TrackingFailures_manage_with_failures_delete_all_confirmed.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:97ff2802880e3f34cd07dabdb9f32e326f1e6f9cb83e35760525ef82d97dcb02 -size 24159 +oid sha256:7705f452376551b6bfa30d1eebbc47a6fe4e156a2b72d2a5f99697356516f754 +size 24706 diff --git a/plugins/CoreHome/CoreHome.php b/plugins/CoreHome/CoreHome.php index fbcb7537b2..e55706314d 100644 --- a/plugins/CoreHome/CoreHome.php +++ b/plugins/CoreHome/CoreHome.php @@ -106,7 +106,6 @@ class CoreHome extends \Piwik\Plugin $stylesheets[] = "plugins/CoreHome/stylesheets/dataTable.less"; $stylesheets[] = "plugins/CoreHome/stylesheets/cloud.less"; $stylesheets[] = "plugins/CoreHome/stylesheets/jquery.ui.autocomplete.css"; - $stylesheets[] = "plugins/CoreHome/stylesheets/jqplotColors.less"; $stylesheets[] = "plugins/CoreHome/stylesheets/sparklineColors.less"; $stylesheets[] = "plugins/CoreHome/stylesheets/promo.less"; $stylesheets[] = "plugins/CoreHome/stylesheets/color_manager.css"; @@ -130,6 +129,7 @@ class CoreHome extends \Piwik\Plugin $stylesheets[] = "plugins/CoreHome/angularjs/dropdown-menu/dropdown-menu.directive.less"; $stylesheets[] = "plugins/CoreHome/angularjs/sparkline/sparkline.component.less"; $stylesheets[] = "plugins/CoreHome/angularjs/field-array/field-array.directive.less"; + $stylesheets[] = "plugins/CoreHome/angularjs/comparisons/comparisons.component.less"; } public function getJsFiles(&$jsFiles) @@ -279,6 +279,9 @@ class CoreHome extends \Piwik\Plugin $jsFiles[] = "plugins/CoreHome/angularjs/field-array/field-array.directive.js"; $jsFiles[] = "plugins/CoreHome/angularjs/field-array/field-array.controller.js"; + $jsFiles[] = "plugins/CoreHome/angularjs/comparisons/comparisons.service.js"; + $jsFiles[] = "plugins/CoreHome/angularjs/comparisons/comparisons.component.js"; + // we have to load these CoreAdminHome files here. If we loaded them in CoreAdminHome, // there would be JS errors as CoreAdminHome is loaded first. Meaning it is loaded before // any angular JS file is loaded etc. @@ -462,5 +465,10 @@ class CoreHome extends \Piwik\Plugin $translationKeys[] = 'CoreHome_PageDownShortcutDescription'; $translationKeys[] = 'CoreHome_MacPageUp'; $translationKeys[] = 'CoreHome_MacPageDown'; + $translationKeys[] = 'General_XComparedToY'; + $translationKeys[] = 'General_ComparisonCardTooltip1'; + $translationKeys[] = 'General_ComparisonCardTooltip2'; + $translationKeys[] = 'General_Comparisons'; + $translationKeys[] = 'General_ClickToRemoveComp'; } } diff --git a/plugins/CoreHome/DataTableRowAction/MultiRowEvolution.php b/plugins/CoreHome/DataTableRowAction/MultiRowEvolution.php index f708515ebf..38fd48fa24 100644 --- a/plugins/CoreHome/DataTableRowAction/MultiRowEvolution.php +++ b/plugins/CoreHome/DataTableRowAction/MultiRowEvolution.php @@ -9,6 +9,7 @@ namespace Piwik\Plugins\CoreHome\DataTableRowAction; use Piwik\Common; +use Piwik\Context; use Piwik\Piwik; /** @@ -68,4 +69,28 @@ class MultiRowEvolution extends RowEvolution return parent::renderPopover($controller, $view); } + + protected function getRowEvolutionGraphFromController(\Piwik\Plugins\CoreHome\Controller $controller) + { + // the row evolution graphs should not compare serieses + return Context::executeWithQueryParameters(['compareSegments' => [], 'comparePeriods' => [], 'compareDates' => []], function () use ($controller) { + return parent::getRowEvolutionGraphFromController($controller); + }); + } + + public function getRowEvolutionGraph($graphType = false, $metrics = false) + { + // the row evolution graphs should not compare serieses + return Context::executeWithQueryParameters(['compareSegments' => [], 'comparePeriods' => [], 'compareDates' => []], function () use ($graphType, $metrics) { + return parent::getRowEvolutionGraph($graphType, $metrics); + }); + } + + protected function getSparkline($metric) + { + // the row evolution graphs should not compare serieses + return Context::executeWithQueryParameters(['compareSegments' => [], 'comparePeriods' => [], 'compareDates' => []], function () use ($metric) { + return parent::getSparkline($metric); + }); + } } diff --git a/plugins/CoreHome/DataTableRowAction/RowEvolution.php b/plugins/CoreHome/DataTableRowAction/RowEvolution.php index 68b490dee5..1da36555dc 100644 --- a/plugins/CoreHome/DataTableRowAction/RowEvolution.php +++ b/plugins/CoreHome/DataTableRowAction/RowEvolution.php @@ -17,6 +17,7 @@ use Piwik\Metrics; use Piwik\NumberFormatter; use Piwik\Period\Factory as PeriodFactory; use Piwik\Piwik; +use Piwik\Plugins\API\Filter\DataComparisonFilter; use Piwik\Plugins\CoreVisualizations\Visualizations\JqplotGraph\Evolution as EvolutionViz; use Piwik\Url; use Piwik\ViewDataTable\Factory; @@ -126,7 +127,7 @@ class RowEvolution // render main evolution graph $this->graphType = 'graphEvolution'; $this->graphMetrics = $this->availableMetrics; - $view->graph = $controller->getRowEvolutionGraph($fetch = true, $rowEvolution = $this); + $view->graph = $this->getRowEvolutionGraphFromController($controller); // render metrics overview $view->metrics = $this->getMetricsToggles(); @@ -160,8 +161,9 @@ class RowEvolution 'period' => $this->period, 'date' => $this->date, 'format' => 'original', - 'serialize' => '0' + 'serialize' => '0', ); + if (!empty($this->segment)) { $parameters['segment'] = $this->segment; } @@ -170,11 +172,54 @@ class RowEvolution $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); } @@ -374,4 +419,9 @@ class RowEvolution $fraction = substr(strrchr($value, "."), 1); return strlen($fraction); } + + protected function getRowEvolutionGraphFromController(\Piwik\Plugins\CoreHome\Controller $controller) + { + return $controller->getRowEvolutionGraph($fetch = true, $rowEvolution = $this); + } } diff --git a/plugins/CoreHome/angularjs/common/services/periods.js b/plugins/CoreHome/angularjs/common/services/periods.js index 3bc9861e79..6f2deadf73 100644 --- a/plugins/CoreHome/angularjs/common/services/periods.js +++ b/plugins/CoreHome/angularjs/common/services/periods.js @@ -59,7 +59,7 @@ DayPeriod.prototype = { getPrettyString: function () { - return $.datepicker.formatDate('yy-mm-dd', this.dateInPeriod); + return format(this.dateInPeriod); }, getDateRange: function () { @@ -83,8 +83,8 @@ WeekPeriod.prototype = { getPrettyString: function () { var weekDates = this.getDateRange(this.dateInPeriod); - var startWeek = $.datepicker.formatDate('yy-mm-dd', weekDates[0]); - var endWeek = $.datepicker.formatDate('yy-mm-dd', weekDates[1]); + var startWeek = format(weekDates[0]); + var endWeek = format(weekDates[1]); return _pk_translate('General_DateRangeFromTo', [startWeek, endWeek]); }, @@ -252,8 +252,8 @@ RangePeriod.prototype = { getPrettyString: function () { - var start = $.datepicker.formatDate('yy-mm-dd', this.startDate); - var end = $.datepicker.formatDate('yy-mm-dd', this.endDate); + var start = format(this.startDate); + var end = format(this.endDate); return _pk_translate('General_DateRangeFromTo', [start, end]); }, @@ -271,7 +271,9 @@ isRecognizedPeriod: isRecognizedPeriod, get: get, parse: parse, - parseDate: parseDate + parseDate: parseDate, + format: format, + RangePeriod: RangePeriod }; function getAllLabels() { @@ -310,6 +312,10 @@ }; } + function format(date) { + return $.datepicker.formatDate('yy-mm-dd', date); + } + function parseDate(strDate) { if (strDate instanceof Date) { return strDate; diff --git a/plugins/CoreHome/angularjs/common/services/piwik-api.js b/plugins/CoreHome/angularjs/common/services/piwik-api.js index 04160d5ee1..e2195f7ff1 100644 --- a/plugins/CoreHome/angularjs/common/services/piwik-api.js +++ b/plugins/CoreHome/angularjs/common/services/piwik-api.js @@ -11,9 +11,9 @@ var hasBlockedContent = false; (function () { angular.module('piwikApp.service').factory('piwikApi', piwikApiService); - piwikApiService.$inject = ['$http', '$q', '$rootScope', 'piwik', '$window']; + piwikApiService.$inject = ['$http', '$q', '$rootScope', 'piwik', '$window', 'piwikUrl']; - function piwikApiService ($http, $q, $rootScope, piwik, $window) { + function piwikApiService ($http, $q, $rootScope, piwik, $window, piwikUrl) { var url = 'index.php'; var format = 'json'; @@ -33,7 +33,15 @@ var hasBlockedContent = false; params = piwik.broadcast.getValuesFromUrl(params); } + var arrayParams = ['compareSegments', 'comparePeriods', 'compareDates']; + for (var key in params) { + if (arrayParams.indexOf(key) !== -1 + && !params[key] + ) { + continue; + } + getParams[key] = params[key]; } } @@ -95,9 +103,7 @@ var hasBlockedContent = false; if (!angular.isDefined(response) || response === null) { return $q.reject(null); - } else if (isErrorResponse(response)) { - createResponseErrorNotification(response, options); return $q.reject(response.message || null); @@ -132,9 +138,8 @@ var hasBlockedContent = false; var ajaxCall = { method: 'POST', - url: url, + url: url + '?' + $.param(mixinDefaultGetParams(getParams)), responseType: requestFormat, - params: mixinDefaultGetParams(getParams), data: $.param(getPostParams(postParams)), timeout: requestPromise, headers: headers @@ -199,15 +204,16 @@ var hasBlockedContent = false; * @private */ function mixinDefaultGetParams (getParamsToMixin) { - var segment = piwik.broadcast.getValueFromHash('segment', $window.location.href.split('#')[1]); - // we have to decode the value manually because broadcast will not decode anything itself. if we don't, // angular will encode it again before sending the value in an HTTP request. - segment = decodeURIComponent(segment); + var segment = piwikUrl.getSearchParam('segment'); + if (segment) { + segment = decodeURIComponent(segment); + } var defaultParams = { - idSite: piwik.idSite || piwik.broadcast.getValueFromUrl('idSite'), - period: piwik.period || piwik.broadcast.getValueFromUrl('period'), + idSite: piwik.idSite || piwikUrl.getSearchParam('idSite'), + period: piwik.period || piwikUrl.getSearchParam('period'), segment: segment }; diff --git a/plugins/CoreHome/angularjs/common/services/piwik-api.spec.js b/plugins/CoreHome/angularjs/common/services/piwik-api.spec.js index 1faf387ea3..e33fa329e2 100644 --- a/plugins/CoreHome/angularjs/common/services/piwik-api.spec.js +++ b/plugins/CoreHome/angularjs/common/services/piwik-api.spec.js @@ -55,7 +55,7 @@ piwikApi.fetch({ method: "SomePlugin.action" }).then(function (response) { - expect(response).to.equal("Request url: index.php?date=&format=JSON2&idSite=1&method=SomePlugin.action&module=API&period=day"); + expect(response).to.equal("Request url: index.php?method=SomePlugin.action&module=API&format=JSON2&idSite=1&period=day&date="); done(); }).catch(function (ex) { @@ -115,7 +115,7 @@ piwikApi.fetch({ method: "SomePlugin.action" }).then(function (response) { - expect(response).to.equal("Request url: index.php?date=&format=JSON2&idSite=1&method=SomePlugin.action&module=API&period=day"); + expect(response).to.equal("Request url: index.php?method=SomePlugin.action&module=API&format=JSON2&idSite=1&period=day&date="); request1Done = true; @@ -127,7 +127,7 @@ piwikApi.fetch({ method: "SomeOtherPlugin.action" }).then(function (response) { - expect(response).to.equal("Request url: index.php?date=&format=JSON2&idSite=1&method=SomeOtherPlugin.action&module=API&period=day"); + expect(response).to.equal("Request url: index.php?method=SomeOtherPlugin.action&module=API&format=JSON2&idSite=1&period=day&date="); request2Done = true; @@ -160,7 +160,7 @@ piwikApi.fetch({ method: "SomeOtherPlugin.action" }).then(function (response) { - expect(response).to.equal("Request url: index.php?date=&format=JSON2&idSite=1&method=SomeOtherPlugin.action&module=API&period=day"); + expect(response).to.equal("Request url: index.php?method=SomeOtherPlugin.action&module=API&format=JSON2&idSite=1&period=day&date="); request2Done = true; @@ -216,9 +216,9 @@ method: "SomeOtherPlugin.action" } ]).then(function (response) { - var restOfExpected = "index.php?date=&format=JSON2&idSite=1&method=API.getBulkRequest&" + - "module=API&period=day - urls%5B%5D=%3Fmethod%3DSomePlugin.action%26param%3D" + - "value&urls%5B%5D=%3Fmethod%3DSomeOtherPlugin.action&token_auth=100bf5eeeed1468f3f9d93750044d3dd"; + var restOfExpected = "index.php?method=API.getBulkRequest&module=API&format=JSON2&idSite=1&period=day&date= - " + + "urls%5B%5D=%3Fmethod%3DSomePlugin.action%26param%3Dvalue&urls%5B%5D=%3Fmethod%3DSomeOtherPlugin.action" + + "&token_auth=100bf5eeeed1468f3f9d93750044d3dd"; expect(response.length).to.equal(2); expect(response[0]).to.equal("Response #1: " + restOfExpected); diff --git a/plugins/CoreHome/angularjs/common/services/piwik-url.js b/plugins/CoreHome/angularjs/common/services/piwik-url.js index b7e4e52f14..dc45b7e390 100644 --- a/plugins/CoreHome/angularjs/common/services/piwik-url.js +++ b/plugins/CoreHome/angularjs/common/services/piwik-url.js @@ -7,12 +7,12 @@ (function () { angular.module('piwikApp.service').service('piwikUrl', piwikUrl); - piwikUrl.$inject = ['$location', 'piwik']; + piwikUrl.$inject = ['$location', 'piwik', '$window']; /** * Similar to angulars $location but works around some limitation. Use it if you need to access search params */ - function piwikUrl($location, piwik) { + function piwikUrl($location, piwik, $window) { var model = { getSearchParam: getSearchParam @@ -22,42 +22,11 @@ function getSearchParam(paramName) { - if (paramName === 'segment') { - var hash = window.location.href.split('#'); - if (hash && hash[1]) { - return piwik.broadcast.getValueFromHash(paramName, hash[1]); - } - - return broadcast.getValueFromUrl(paramName); - } - - // available in global scope - var search = $location.search(); - - if (!search[paramName]) { - // see https://github.com/angular/angular.js/issues/7239 (issue is resolved but problem still exists) - var paramUrlValue = piwik.broadcast.getValueFromUrl(paramName); - if (paramUrlValue !== false - && paramUrlValue !== '' - && paramUrlValue !== null - && paramUrlValue !== undefined - && paramName !== 'token_auth') { - search[paramName] = paramUrlValue; - } else { - return paramUrlValue; - } - } - - if (search[paramName]) { - var value = search[paramName]; - - if (angular.isArray(search[paramName])) { - // use last one. Eg when having period=day&period=year angular would otherwise return ['day', 'year'] - return value[value.length - 1]; - } - - return value; + var hash = $window.location.href.split('#'); + if (hash && hash[1] && (new RegExp(paramName + '(\\[]|=)')).test(decodeURIComponent(hash[1]))) { + return broadcast.getValueFromHash(paramName, $window.location.href); } + return broadcast.getValueFromUrl(paramName, $window.location.search); } } })(); diff --git a/plugins/CoreHome/angularjs/common/services/piwik.js b/plugins/CoreHome/angularjs/common/services/piwik.js index 3f6c2682bd..57bc0b580a 100644 --- a/plugins/CoreHome/angularjs/common/services/piwik.js +++ b/plugins/CoreHome/angularjs/common/services/piwik.js @@ -38,8 +38,8 @@ piwik.period = period; var dateRange = piwikPeriods.parse(period, date).getDateRange(); - piwik.startDateString = $.datepicker.formatDate('yy-mm-dd', dateRange[0]); - piwik.endDateString = $.datepicker.formatDate('yy-mm-dd', dateRange[1]); + piwik.startDateString = piwikPeriods.format(dateRange[0]); + piwik.endDateString = piwikPeriods.format(dateRange[1]); updateDateInTitle(date, period); diff --git a/plugins/CoreHome/angularjs/comparisons/comparisons.component.html b/plugins/CoreHome/angularjs/comparisons/comparisons.component.html new file mode 100644 index 0000000000..f9fe8a3b2d --- /dev/null +++ b/plugins/CoreHome/angularjs/comparisons/comparisons.component.html @@ -0,0 +1,28 @@ +<div ng-if="$ctrl.comparisonsService.isComparing()"> + <h3>{{ 'General_Comparisons'|translate }}</h3> + + <div class="comparison card" ng-repeat="comparison in $ctrl.comparisonsService.getSegmentComparisons() track by $index"> + <div class="comparison-type">{{ 'General_Segment'|translate }}</div> + + <div class="title" title="{{ comparison.title }}"> + {{ comparison.title }} + </div> + + <div class="comparison-period" + ng-repeat="periodComparison in $ctrl.comparisonsService.getPeriodComparisons() track by $index" + title="{{ $ctrl.getComparisonTooltip(comparison, periodComparison) }}" + > + <span class="comparison-dot" ng-style="{'background-color': $ctrl.comparisonsService.getSeriesColor(comparison, periodComparison)}"></span> + <span class="comparison-period-label">{{ periodComparison.title }} ({{ $ctrl.getComparisonPeriodType(periodComparison) }})</span> + </div> + + <a class="remove-button" ng-click="$ctrl.comparisonsService.removeSegmentComparison($index)" ng-if="$ctrl.comparisonsService.getSegmentComparisons().length > 1"> + <span class="icon icon-close" title="{{ 'General_ClickToRemoveComp'|translate }}"></span> + </a> + </div> + + <div class="loadingPiwik" style="display:none;"> + <img src="plugins/Morpheus/images/loading-blue.gif" alt="{{ 'General_LoadingData'|translate }}" /> + {{ 'General_LoadingData'|translate }} + </div> +</div>
\ No newline at end of file diff --git a/plugins/CoreHome/angularjs/comparisons/comparisons.component.js b/plugins/CoreHome/angularjs/comparisons/comparisons.component.js new file mode 100644 index 0000000000..d4bdbdee8a --- /dev/null +++ b/plugins/CoreHome/angularjs/comparisons/comparisons.component.js @@ -0,0 +1,149 @@ +/*! + * Piwik - free/libre analytics platform + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +(function () { + angular.module('piwikApp').component('piwikComparisons', { + templateUrl: 'plugins/CoreHome/angularjs/comparisons/comparisons.component.html?cb=' + piwik.cacheBuster, + bindings: { + // empty + }, + controller: ComparisonsController + }); + + ComparisonsController.$inject = ['piwikComparisonsService', '$rootScope', 'piwikApi', '$element', 'piwikUrl']; + + function ComparisonsController(comparisonsService, $rootScope, piwikApi, $element, piwikUrl) { + var vm = this; + var comparisonTooltips = null; + + vm.comparisonsService = comparisonsService; + vm.$onInit = $onInit; + vm.$onDestroy = $onDestroy; + vm.comparisonHasSegment = comparisonHasSegment; + vm.getComparisonPeriodType = getComparisonPeriodType; + vm.getComparisonTooltip = getComparisonTooltip; + + function $onInit() { + $rootScope.$on('piwikComparisonsChanged', onComparisonsChanged); + + onComparisonsChanged(); + + setUpTooltips(); + } + + function $onDestroy() { + try { + $element.tooltip('destroy'); + } catch (e) { + // ignore + } + } + + function setUpTooltips() { + $element.tooltip({ + track: true, + content: function() { + var title = $(this).attr('title'); + return piwikHelper.escape(title.replace(/\n/g, '<br />')); + }, + show: {delay: 200, duration: 200}, + hide: false + }); + } + + function comparisonHasSegment(comparison) { + return typeof comparison.params.segment !== 'undefined'; + } + + function getComparisonPeriodType(comparison) { + var period = comparison.params.period; + if (period === 'range') { + return _pk_translate('CoreHome_PeriodRange'); + } + var periodStr = _pk_translate('Intl_Period' + period.substring(0, 1).toUpperCase() + period.substring(1)); + return periodStr.substring(0, 1).toUpperCase() + periodStr.substring(1); + } + + function getComparisonTooltip(segmentComparison, periodComparison) { + if (!comparisonTooltips + || !Object.keys(comparisonTooltips).length + ) { + return undefined; + } + + return comparisonTooltips[periodComparison.index][segmentComparison.index]; + } + + function onComparisonsChanged() { + comparisonTooltips = null; + + if (!vm.comparisonsService.isComparing()) { + return; + } + + var periodComparisons = comparisonsService.getPeriodComparisons(); + var segmentComparisons = comparisonsService.getSegmentComparisons(); + piwikApi.fetch({ + method: 'API.getProcessedReport', + apiModule: 'VisitsSummary', + apiAction: 'get', + compare: '1', + compareSegments: piwikUrl.getSearchParam('compareSegments'), + comparePeriods: piwikUrl.getSearchParam('comparePeriods'), + compareDates: piwikUrl.getSearchParam('compareDates'), + format_metrics: '1', + }).then(function (report) { + comparisonTooltips = {}; + periodComparisons.forEach(function (periodComp) { + comparisonTooltips[periodComp.index] = {}; + + segmentComparisons.forEach(function (segmentComp) { + comparisonTooltips[periodComp.index][segmentComp.index] = generateComparisonTooltip(report, periodComp, segmentComp); + }); + }); + }); + } + + function generateComparisonTooltip(visitsSummary, periodComp, segmentComp) { + if (!visitsSummary.reportData.comparisons) { // sanity check + return ''; + } + + var firstRowIndex = comparisonsService.getComparisonSeriesIndex(periodComp.index, 0); + + var firstRow = visitsSummary.reportData.comparisons[firstRowIndex]; + + var comparisonRowIndex = comparisonsService.getComparisonSeriesIndex(periodComp.index, segmentComp.index); + var comparisonRow = visitsSummary.reportData.comparisons[comparisonRowIndex]; + + var firstPeriodRow = visitsSummary.reportData.comparisons[segmentComp.index]; + + var tooltip = '<div class="comparison-card-tooltip">'; + + var visitsPercent = ((comparisonRow.nb_visits / firstRow.nb_visits) * 100).toFixed(2) + '%'; + + tooltip += _pk_translate('General_ComparisonCardTooltip1', [ + "'" + comparisonRow.compareSegmentPretty + "'", + comparisonRow.comparePeriodPretty, + visitsPercent, + comparisonRow.nb_visits, + firstRow.nb_visits + ]); + if (periodComp.index > 0) { + tooltip += '<br/><br/>'; + tooltip += _pk_translate('General_ComparisonCardTooltip2', [ + comparisonRow.nb_visits_change, + firstPeriodRow.compareSegmentPretty, + firstPeriodRow.comparePeriodPretty + ]); + } + + tooltip += '</div>'; + return tooltip; + } + } +})(); diff --git a/plugins/CoreHome/angularjs/comparisons/comparisons.component.less b/plugins/CoreHome/angularjs/comparisons/comparisons.component.less new file mode 100644 index 0000000000..c2eecb3a22 --- /dev/null +++ b/plugins/CoreHome/angularjs/comparisons/comparisons.component.less @@ -0,0 +1,74 @@ +piwik-comparisons { + h3 { + margin-top: 0; + margin-bottom: 0; + } + + .comparison { + display: inline-block; + min-width: 164px; + margin-right: 16px; + padding: 14px; + background-color: white; + margin-bottom: 16px; + position: relative; + } + + .comparison-type { + font-weight: bold; + text-transform: uppercase; + font-size: .7em; + color: #999; + } + + .remove-button { + font-size: .6em; + color: #666; + position: absolute; + right: 12px; + top: 12px; + } + + .title { + font-size: 1.1rem; + margin-top: 4px; + margin-bottom: 12px; + + overflow-x: hidden; + max-width: 250px; + text-overflow: ellipsis; + white-space: nowrap; + } + + .comparison-period { + margin-top: 4px; + + > span { + display: inline-block; + } + + .comparison-dot { + width: 1em; + height: 1em; + display: inline-block; + border-radius: .5em; + } + + .comparison-period-label { + position: relative; + top: -3px; + margin-left: 2px; + display: inline-block; + font-size: .85rem; + font-style: italic; + } + } +} + +.comparison-card-tooltip { + p { + font-size: 1.1em; + line-height: 1.3em; + color: #fff; + } +}
\ No newline at end of file diff --git a/plugins/CoreHome/angularjs/comparisons/comparisons.service.js b/plugins/CoreHome/angularjs/comparisons/comparisons.service.js new file mode 100644 index 0000000000..800004a359 --- /dev/null +++ b/plugins/CoreHome/angularjs/comparisons/comparisons.service.js @@ -0,0 +1,353 @@ +/*! + * Piwik - free/libre analytics platform + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +(function () { + angular.module('piwikApp.service').factory('piwikComparisonsService', ComparisonFactory); + + ComparisonFactory.$inject = ['$location', '$rootScope', 'piwikPeriods', 'piwikApi', 'piwikUrl']; + + function ComparisonFactory($location, $rootScope, piwikPeriods, piwikApi, piwikUrl) { + var segmentComparisons = []; + var periodComparisons = []; + var comparisonsDisabledFor = []; + var isEnabled = null; + + var SERIES_COLOR_COUNT = 8; + var SERIES_SHADE_COUNT = 3; + + var colors = getAllSeriesColors(); + + $rootScope.$on('$locationChangeSuccess', updateComparisonsFromQueryParams); + $rootScope.$on('piwikSegmentationInited', updateComparisonsFromQueryParams); + + if (!piwikHelper.isAngularRenderingThePage()) { // if we're, eg, widgetized + updateComparisonsFromQueryParams(); + } + + loadComparisonsDisabledFor(); + + return { + getComparisons: getComparisons, + removeSegmentComparison: removeSegmentComparison, + addSegmentComparison: addSegmentComparison, + isComparisonEnabled: isComparisonEnabled, + getSegmentComparisons: getSegmentComparisons, + getPeriodComparisons: getPeriodComparisons, + getSeriesColor: getSeriesColor, + getAllComparisonSeries: getAllComparisonSeries, + isComparing: isComparing, + isComparingPeriods: isComparingPeriods, + getIndividualComparisonRowIndices: getIndividualComparisonRowIndices, + getComparisonSeriesIndex: getComparisonSeriesIndex, + getSeriesColorName: getSeriesColorName + }; + + function getComparisons() { + return getSegmentComparisons().concat(getPeriodComparisons()); + } + + function isComparing() { + return isComparisonEnabled() && (segmentComparisons.length > 1 || periodComparisons.length > 1); // first two are for selected segment/period + } + + function isComparingPeriods() { + return getPeriodComparisons().length > 1; // first is currently selected period + } + + function getSegmentComparisons() { + if (!isComparisonEnabled()) { + return []; + } + + return segmentComparisons; + } + + function getPeriodComparisons() { + if (!isComparisonEnabled()) { + return []; + } + + return periodComparisons; + } + + function getSeriesColor(segmentComparison, periodComparison, metricIndex) { + metricIndex = metricIndex || 0; + + var seriesIndex = getComparisonSeriesIndex(periodComparison.index, segmentComparison.index) % SERIES_COLOR_COUNT; + if (metricIndex === 0) { + return colors['series' + seriesIndex]; + } else { + var shadeIndex = metricIndex % SERIES_SHADE_COUNT; + return colors['series' + seriesIndex + '-shade' + shadeIndex]; + } + } + + function getSeriesColorName(seriesIndex, metricIndex) { + var colorName = 'series' + (seriesIndex % SERIES_COLOR_COUNT); + if (metricIndex > 0) { + colorName += '-shade' + (metricIndex % SERIES_SHADE_COUNT); + } + return colorName; + } + + function isComparisonEnabled() { + return isEnabled; + } + + function getIndividualComparisonRowIndices(seriesIndex) { + var segmentCount = getSegmentComparisons().length; + var segmentIndex = seriesIndex % segmentCount; + var periodIndex = Math.floor(seriesIndex / segmentCount); + + return { + segmentIndex: segmentIndex, + periodIndex: periodIndex, + }; + } + + function getComparisonSeriesIndex(periodIndex, segmentIndex) { + var segmentCount = getSegmentComparisons().length; + return periodIndex * segmentCount + segmentIndex; + } + + function getAllComparisonSeries() { + var seriesInfo = []; + + var seriesIndex = 0; + getPeriodComparisons().forEach(function (periodComp) { + getSegmentComparisons().forEach(function (segmentComp) { + seriesInfo.push({ + index: seriesIndex, + params: $.extend({}, segmentComp.params, periodComp.params), + color: colors['series' + seriesIndex], + }); + ++seriesIndex; + }); + }); + return seriesInfo; + } + + function removeSegmentComparison(index) { + if (!isComparisonEnabled()) { + throw new Error('Comparison disabled.'); + } + + var newComparisons = [].concat(segmentComparisons); + newComparisons.splice(index, 1); + + var extraParams = {}; + if (index === 0) { + extraParams.segment = newComparisons[0].params.segment; + } + + updateQueryParamsFromComparisons(newComparisons, periodComparisons, extraParams); + } + + function addSegmentComparison(params) { + if (!isComparisonEnabled()) { + throw new Error('Comparison disabled.'); + } + + var newComparisons = segmentComparisons.concat([{ params: params }]); + updateQueryParamsFromComparisons(newComparisons, periodComparisons); + } + + function updateQueryParamsFromComparisons(segmentComparisons, periodComparisons, extraParams) { + extraParams = extraParams || {}; + + // get unique segments/periods/dates from new Comparisons + var compareSegments = {}; + var comparePeriodDatePairs = {}; + + var firstSegment = false; + var firstPeriod = false; + + segmentComparisons.forEach(function (comparison) { + if (firstSegment) { + compareSegments[comparison.params.segment] = true; + } else { + firstSegment = true; + } + }); + + periodComparisons.forEach(function (comparison) { + if (firstPeriod) { + comparePeriodDatePairs[comparison.params.period + '|' + comparison.params.date] = true; + } else { + firstPeriod = true; + } + }); + + var comparePeriods = []; + var compareDates = []; + Object.keys(comparePeriodDatePairs).forEach(function (pair) { + var parts = pair.split('|'); + comparePeriods.push(parts[0]); + compareDates.push(parts[1]); + }); + + var compareParams = { + compareSegments: Object.keys(compareSegments), + comparePeriods: comparePeriods, + compareDates: compareDates, + }; + + // change the page w/ these new param values + if (piwik.helper.isAngularRenderingThePage()) { + var search = $location.search(); + var newSearch = $.extend({}, search, compareParams, extraParams); + + delete newSearch['compareSegments[]']; + delete newSearch['comparePeriods[]']; + delete newSearch['compareDates[]']; + + if (JSON.stringify(newSearch) !== JSON.stringify(search)) { + $location.search($.param(newSearch)); + } + + return; + } + + var paramsToRemove = []; + ['compareSegments', 'comparePeriods', 'compareDates'].forEach(function (name) { + if (!compareParams[name].length) { + paramsToRemove.push(name); + } + }); + + // angular is not rendering the page (ie, we are in the embedded dashboard) or we need to change the segment + var url = $.param($.extend({}, extraParams)); + var strHash = $.param($.extend({}, compareParams)); + broadcast.propagateNewPage(url, undefined, strHash, paramsToRemove); + } + + function updateComparisonsFromQueryParams() { + var title; + var availableSegments = []; + try { + availableSegments = $('.segmentEditorPanel').data('uiControlObject').impl.availableSegments || []; + } catch (e) { + // segment editor is not initialized yet + } + + var compareSegments = piwikUrl.getSearchParam('compareSegments') || []; + compareSegments = compareSegments instanceof Array ? compareSegments : [compareSegments]; + + var comparePeriods = piwikUrl.getSearchParam('comparePeriods') || []; + comparePeriods = comparePeriods instanceof Array ? comparePeriods : [comparePeriods]; + + var compareDates = piwikUrl.getSearchParam('compareDates') || []; + compareDates = compareDates instanceof Array ? compareDates : [compareDates]; + + // add base comparisons + compareSegments.unshift(piwikUrl.getSearchParam('segment')); + comparePeriods.unshift(piwikUrl.getSearchParam('period')); + compareDates.unshift(piwikUrl.getSearchParam('date')); + + var newSegmentComparisons = []; + compareSegments.forEach(function (segment, idx) { + var storedSegment = null; + + availableSegments.forEach(function (s) { + if (s.definition === segment + || s.definition === decodeURIComponent(segment) + || decodeURIComponent(s.definition) === segment + ) { + storedSegment = s; + } + }); + + var segmentTitle = storedSegment ? storedSegment.name : _pk_translate('General_Unknown'); + if (segment.trim() === '') { + segmentTitle = _pk_translate('SegmentEditor_DefaultAllVisits'); + } + + newSegmentComparisons.push({ + params: { + segment: segment + }, + title: piwikHelper.htmlDecode(segmentTitle), + index: idx + }); + }); + + var newPeriodComparisons = []; + for (var i = 0; i < Math.min(compareDates.length, comparePeriods.length); ++i) { + try { + title = piwikPeriods.parse(comparePeriods[i], compareDates[i]).getPrettyString(); + } catch (e) { + title = _pk_translate('General_Error'); + } + + newPeriodComparisons.push({ + params: { + date: compareDates[i], + period: comparePeriods[i] + }, + title: title, + index: i + }); + } + + checkEnabledForCurrentPage(); + setComparisons(newSegmentComparisons, newPeriodComparisons); + } + + function setComparisons(newSegmentComparisons, newPeriodComparisons) { + var oldSegmentComparisons = segmentComparisons; + var oldPeriodComparisons = periodComparisons; + + segmentComparisons = newSegmentComparisons; + Object.freeze(segmentComparisons); + + periodComparisons = newPeriodComparisons; + Object.freeze(periodComparisons); + + if (JSON.stringify(oldPeriodComparisons) !== JSON.stringify(periodComparisons) + || JSON.stringify(oldSegmentComparisons) !== JSON.stringify(segmentComparisons) + ) { + $rootScope.$emit('piwikComparisonsChanged'); + } + } + + function checkEnabledForCurrentPage() { + // category/subcategory is not included on top bar pages, so in that case we use module/action + var category = piwikUrl.getSearchParam('category') || piwikUrl.getSearchParam('module'); + var subcategory = piwikUrl.getSearchParam('subcategory') || piwikUrl.getSearchParam('action'); + + var id = category + "." + subcategory; + isEnabled = comparisonsDisabledFor.indexOf(id) === -1 && comparisonsDisabledFor.indexOf(category + ".*") === -1; + + $('html').toggleClass('comparisonsDisabled', !isEnabled); + } + + function loadComparisonsDisabledFor() { + piwikApi.fetch({ + module: 'API', + method: 'API.getPagesComparisonsDisabledFor', + }).then(function (result) { + comparisonsDisabledFor = result; + checkEnabledForCurrentPage(); + }); + } + + function getAllSeriesColors() { + var colorManager = piwik.ColorManager, + seriesColorNames = []; + + for (var i = 0; i < SERIES_COLOR_COUNT; ++i) { + seriesColorNames.push('series' + i); + for (var j = 0; j < SERIES_SHADE_COUNT; ++j) { + seriesColorNames.push('series' + i + '-shade' + j); + } + } + + return colorManager.getColors('comparison-series-color', seriesColorNames); + } + } + +})(); diff --git a/plugins/CoreHome/angularjs/comparisons/comparisons.service.spec.js b/plugins/CoreHome/angularjs/comparisons/comparisons.service.spec.js new file mode 100644 index 0000000000..66841115e6 --- /dev/null +++ b/plugins/CoreHome/angularjs/comparisons/comparisons.service.spec.js @@ -0,0 +1,499 @@ +/*! + * Piwik - free/libre analytics platform + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + */ + +(function () { + describe('piwikComparisonsService', function () { + var DISABLED_PAGES = [ + 'MyModule1.disabledPage', + 'MyModule2.disabledPage2', + 'MyModule3.*', + ]; + var piwikComparisonsService; + var $httpBackend; + var $rootScope; + var $location; + var oldInjectorFn; + var $window; + + beforeEach(module('piwikApp.service')); + beforeEach(module(function($provide) { + var loc = {}; + Object.defineProperty(loc, 'href', { + get: function () { + return $location.absUrl(); + }, + set: function (v) { + $location.url(v); + }, + }); + Object.defineProperty(loc, 'search', { + get: function () { + return '?' + $location.absUrl().split('?')[1]; + }, + }); + + $window = { + location: loc, + }; + $provide.value('$window', $window); + })); + beforeEach(inject(function($injector) { + oldInjectorFn = angular.element.prototype.injector; + angular.element.prototype.injector = function () { return $injector; }; + + window.piwik.ColorManager = { + getColors: function (ns, colors) { + var result = {}; + colors.forEach(function (name) { + result[name] = ns + '.' + name; + }); + return result; + } + }; + + piwik_translations = { + 'SegmentEditor_DefaultAllVisits': 'SegmentEditor_DefaultAllVisits', + 'Intl_PeriodDay': 'Intl_PeriodDay', + 'General_Unknown': 'General_Unknown', + 'General_DateRangeFromTo': 'General_DateRangeFromTo', + }; + + $rootScope = $injector.get('$rootScope'); + $location = $injector.get('$location'); + $httpBackend = $injector.get('$httpBackend'); + })); + beforeEach(inject(function($injector) { + $httpBackend.whenPOST(function (url) { + return /API\.getPagesComparisonsDisabledFor/.test(url); + }).respond(function () { + return [200, DISABLED_PAGES]; + }); + + piwikComparisonsService = $injector.get('piwikComparisonsService'); + + $httpBackend.flush(); + })); + afterEach(function () { + angular.element.prototype.injector = oldInjectorFn; + }); + afterEach (function () { + $httpBackend.verifyNoOutstandingExpectation (); + $httpBackend.verifyNoOutstandingRequest (); + }); + + describe('#getComparisons()', function () { + it('should return all comparisons in URL', function () { + $location.search('category=MyModule1&subcategory=enabledPage&date=2018-01-02&period=day&segment=abcdefg&compareDates[]=2018-03-04&comparePeriods[]=week&compareSegments[]=comparedsegment&compareSegments[]='); + $rootScope.$apply(); + + expect(piwikComparisonsService.getComparisons()).to.deep.equal([ + { + params: { + segment: 'abcdefg', + }, + title: 'General_Unknown', + index: 0, + }, + { + params: { + segment: 'comparedsegment', + }, + title: 'General_Unknown', + index: 1, + }, + { + params: { + segment: '', + }, + title: 'SegmentEditor_DefaultAllVisits', + index: 2, + }, + { + params: { + date: '2018-01-02', + period: 'day' + }, + title: '2018-01-02', + index: 0, + }, + { + params: { + date: '2018-03-04', + period: 'week' + }, + title: 'General_DateRangeFromTo', + index: 1, + }, + ]); + }); + + it('should return base params if there are no comparisons', function () { + $location.search('category=MyModule1&subcategory=enabledPage&date=2018-01-02&period=day&segment=abcdefg'); + $rootScope.$apply(); + + expect(piwikComparisonsService.getComparisons()).to.deep.equal([ + { + params: { + segment: 'abcdefg' + }, + title: 'General_Unknown', + index: 0, + }, + { + params: { + date: '2018-01-02', + period: 'day' + }, + title: '2018-01-02', + index: 0, + }, + ]); + }); + + it('should return nothing if comparison is not enabled for the page', function () { + $location.search('category=MyModule1&subcategory=disabledPage&date=2018-01-02&period=day&segment=abcdefg&compareDates[]=2018-03-04&comparePeriods[]=week&compareSegments[]=comparedsegment&compareSegments[]='); + $rootScope.$apply(); + + expect(piwikComparisonsService.getComparisons()).to.deep.equal([]); + }); + }); + + describe('#removeSegmentComparison()', function () { + it('should remove an existing segment comparison from the URL', function () { + $location.search('category=MyModule1&subcategory=enabledPage&date=2018-01-02&period=day&segment=abcdefg&compareDates[]=2018-03-04&comparePeriods[]=week&compareSegments[]=comparedsegment&compareSegments[]='); + $rootScope.$apply(); + + piwikComparisonsService.removeSegmentComparison(1); + + expect($location.url()).to.equal('?category=MyModule1&subcategory=enabledPage&date=2018-01-02&period=day&segment=abcdefg&compareDates%5B%5D=2018-03-04&comparePeriods%5B%5D=week&compareSegments%5B%5D=comparedsegment&compareSegments%5B%5D=&updated=1#%3Fdate=2012-01-01,2012-01-02&period=range&comparePeriods%255B%255D=week&compareDates%255B%255D=2018-03-04'); + }); + + it('should change the base comparison if the first segment is removed', function () { + $location.search('category=MyModule1&subcategory=enabledPage&date=2018-01-02&period=day&segment=abcdefg&compareDates[]=2018-03-04&comparePeriods[]=week&compareSegments[]=comparedsegment&compareSegments[]='); + $rootScope.$apply(); + + piwikComparisonsService.removeSegmentComparison(0); + + expect($location.url()).to.equal('?category=MyModule1&subcategory=enabledPage&date=2018-01-02&period=day&segment=comparedsegment&compareDates%5B%5D=2018-03-04&comparePeriods%5B%5D=week&compareSegments%5B%5D=comparedsegment&compareSegments%5B%5D=&updated=1#%3Fdate=2012-01-01,2012-01-02&period=range&segment=comparedsegment&comparePeriods%255B%255D=week&compareDates%255B%255D=2018-03-04'); + }); + }); + + describe('#addSegmentComparison()', function () { + it('should add a new segment comparison to the URL', function () { + $location.search('category=MyModule1&subcategory=enabledPage&date=2018-01-02&period=day&segment=&compareDates[]=2018-03-04&comparePeriods[]=week&compareSegments[]=comparedsegment'); + $rootScope.$apply(); + + piwikComparisonsService.addSegmentComparison({ + segment: 'newsegment', + }); + + expect(piwikComparisonsService.getComparisons()).to.deep.equal([ + {"params":{"segment":""},"title":"SegmentEditor_DefaultAllVisits","index":0}, + {"params":{"segment":"comparedsegment"},"title":"General_Unknown","index":1}, + {"params":{"date":"2018-01-02","period":"day"},"title":"2018-01-02","index":0}, + {"params":{"date":"2018-03-04","period":"week"},"title":"General_DateRangeFromTo","index":1}, + ]); + }); + + it('should add the all visits segment to the URL', function () { + $location.search('category=MyModule1&subcategory=enabledPage&date=2018-01-02&period=day&segment=abcdefg&compareDates[]=2018-03-04&comparePeriods[]=week&compareSegments[]=comparedsegment'); + $rootScope.$apply(); + + piwikComparisonsService.addSegmentComparison({ + segment: '', + }); + + expect(piwikComparisonsService.getComparisons()).to.deep.equal([ + {"params":{"segment":"abcdefg"},"title":"General_Unknown","index":0}, + {"params":{"segment":"comparedsegment"},"title":"General_Unknown","index":1}, + {"params":{"date":"2018-01-02","period":"day"},"title":"2018-01-02","index":0}, + {"params":{"date":"2018-03-04","period":"week"},"title":"General_DateRangeFromTo","index":1} + ]); + }); + + it('should add a new period comparison to the URL', function () { + $location.search('category=MyModule1&subcategory=enabledPage&date=2018-01-02&period=day&segment=&compareSegments[]=comparedsegment'); + $rootScope.$apply(); + + piwikComparisonsService.addSegmentComparison({ + period: 'month', + date: '2018-02-03', + }); + + expect(piwikComparisonsService.getComparisons()).to.deep.equal([ + {"params":{"segment":""},"title":"SegmentEditor_DefaultAllVisits","index":0}, + {"params":{"segment":"comparedsegment"},"title":"General_Unknown","index":1}, + {"params":{"date":"2018-01-02","period":"day"},"title":"2018-01-02","index":0}, + ]); + }); + + it('should add another period comparison to the URL if one is already there', function () { + $location.search('category=MyModule1&subcategory=enabledPage&date=2018-01-02&period=day&segment=&compareDates[]=2018-03-04&comparePeriods[]=week&compareSegments[]=comparedsegment'); + $rootScope.$apply(); + + piwikComparisonsService.addSegmentComparison({ + period: 'year', + date: '2018-02-03', + }); + + expect(piwikComparisonsService.getComparisons()).to.deep.equal([ + {"params":{"segment":""},"title":"SegmentEditor_DefaultAllVisits","index":0}, + {"params":{"segment":"comparedsegment"},"title":"General_Unknown","index":1}, + {"params":{"date":"2018-01-02","period":"day"},"title":"2018-01-02","index":0}, + {"params":{"date":"2018-03-04","period":"week"},"title":"General_DateRangeFromTo","index":1}, + ]); + }); + }); + + describe('#isComparisonEnabled()', function () { + it('should return true if comparison is enabled for the page', function () { + $location.search('category=MyModule1&subcategory=enabledPage&date=2018-01-02&period=day&segment=&compareDates[]=2018-03-04&comparePeriods[]=week&compareSegments[]=comparedsegment'); + $rootScope.$apply(); + + expect(piwikComparisonsService.isComparisonEnabled()).to.be.true; + }); + + it('should return false if comparison is disabled for the page', function () { + $location.search('category=MyModule2&subcategory=disabledPage2&date=2018-01-02&period=day&segment=&compareDates[]=2018-03-04&comparePeriods[]=week&compareSegments[]=comparedsegment'); + $rootScope.$apply(); + + expect(piwikComparisonsService.isComparisonEnabled()).to.be.false; + }); + + it('should return false if comparison is disabled for the entire category', function () { + $location.search('category=MyModule3&subcategory=enabledPage&date=2018-01-02&period=day&segment=&compareDates[]=2018-03-04&comparePeriods[]=week&compareSegments[]=comparedsegment'); + $rootScope.$apply(); + + expect(piwikComparisonsService.isComparisonEnabled()).to.be.false; + }); + }); + + describe('#getSegmentComparisons()', function () { + it('should return the segment comparisons only', function () { + $location.search('category=MyModule1&subcategory=enabledPage&date=2018-01-02&period=day&segment=abcdefg&compareDates[]=2018-03-04&comparePeriods[]=week&compareSegments[]=comparedsegment&compareSegments[]='); + $rootScope.$apply(); + + expect(piwikComparisonsService.getSegmentComparisons()).to.be.deep.equal([ + {"params":{"segment":"abcdefg"},"title":"General_Unknown","index":0}, + {"params":{"segment":"comparedsegment"},"title":"General_Unknown","index":1}, + {"params":{"segment":""},"title":"SegmentEditor_DefaultAllVisits","index":2} + ]); + }); + + it('should return nothing if comparison is not enabled', function () { + $location.search('category=MyModule1&subcategory=disabledPage&date=2018-01-02&period=day&segment=abcdefg&compareDates[]=2018-03-04&comparePeriods[]=week&compareSegments[]=comparedsegment&compareSegments[]='); + $rootScope.$apply(); + + expect(piwikComparisonsService.getSegmentComparisons()).to.be.deep.equal([]); + }); + }); + + describe('#getPeriodComparisons()', function () { + it('should return the period comparisons only', function () { + $location.search('category=MyModule1&subcategory=enabledPage&date=2018-01-02&period=day&segment=abcdefg&compareDates[]=2018-03-04&comparePeriods[]=week&compareSegments[]=comparedsegment&compareSegments[]='); + $rootScope.$apply(); + + expect(piwikComparisonsService.getPeriodComparisons()).to.be.deep.equal([ + { + params: { + date: '2018-01-02', + period: 'day', + }, + title: '2018-01-02', + index: 0, + }, + { + params: { + date: '2018-03-04', + period: 'week', + }, + title: 'General_DateRangeFromTo', + index: 1, + }, + ]); + }); + + it('should return nothing if comparison is not enabled', function () { + $location.search('category=MyModule1&subcategory=disabledPage&date=2018-01-02&period=day&segment=abcdefg&compareDates[]=2018-03-04&comparePeriods[]=week&compareSegments[]=comparedsegment&compareSegments[]='); + $rootScope.$apply(); + + expect(piwikComparisonsService.getPeriodComparisons()).to.be.deep.equal([]); + }); + }); + + describe('#getAllComparisonSeries()', function () { + it('should return all individual comparison serieses', function () { + $location.search('category=MyModule1&subcategory=enabledPage&date=2018-01-02&period=day&segment=abcdefg&compareDates[]=2018-03-04&comparePeriods[]=week&compareSegments[]=comparedsegment&compareSegments[]='); + $rootScope.$apply(); + + expect(piwikComparisonsService.getAllComparisonSeries()).to.be.deep.equal([ + { + index: 0, + params: { + segment: 'abcdefg', + date: '2018-01-02', + period: 'day', + }, + color: 'comparison-series-color.series0', + }, + { + "index":1, + "params": { + "segment":"comparedsegment", + "date":"2018-01-02", + "period":"day" + }, + color: 'comparison-series-color.series1', + }, + { + "index":2, + "params": { + "segment":"", + "date":"2018-01-02", + "period":"day" + }, + color: 'comparison-series-color.series2', + }, + { + "index":3, + "params": { + "segment":"abcdefg", + "date":"2018-03-04", + "period":"week" + }, + color: 'comparison-series-color.series3', + }, + { + "index":4, + "params": { + "segment":"comparedsegment", + "date":"2018-03-04", + "period":"week" + }, + color: 'comparison-series-color.series4', + }, + { + "index":5, + "params": { + "segment":"", + "date":"2018-03-04", + "period":"week" + }, + color: 'comparison-series-color.series5', + }, + ]); + }); + + it('should return nothing if comparison is not enabled', function () { + $location.search('category=MyModule1&subcategory=disabledPage&date=2018-01-02&period=day&segment=abcdefg&compareDates[]=2018-03-04&comparePeriods[]=week&compareSegments[]=comparedsegment&compareSegments[]='); + $rootScope.$apply(); + + expect(piwikComparisonsService.getAllComparisonSeries()).to.be.deep.equal([]); + }); + }); + + describe('#isComparing()', function () { + it('should return true if there are comparison parameters present', function () { + $location.search('category=MyModule1&subcategory=enabledPage&date=2018-01-02&period=day&segment=abcdefg&compareDates[]=2018-03-04&comparePeriods[]=week&compareSegments[]=comparedsegment&compareSegments[]='); + $rootScope.$apply(); + + expect(piwikComparisonsService.isComparing()).to.be.true; + }); + + it('should return true if there are segment comparisons but no period comparisons', function () { + $location.search('category=MyModule1&subcategory=enabledPage&date=2018-01-02&period=day&segment=abcdefg&compareSegments[]=comparedsegment&compareSegments[]='); + $rootScope.$apply(); + + expect(piwikComparisonsService.isComparing()).to.be.true; + }); + + it('should return true if there are period comparisons but no segment comparisons', function () { + $location.search('category=MyModule1&subcategory=enabledPage&date=2018-01-02&period=day&segment=abcdefg&compareDates[]=2018-03-04&comparePeriods[]=week'); + $rootScope.$apply(); + + expect(piwikComparisonsService.isComparing()).to.be.true; + }); + + it('should return false if there are no comparison parameters present', function () { + $location.search('category=MyModule1&subcategory=enabledPage&date=2018-01-02&period=day&segment=abcdefg'); + $rootScope.$apply(); + + expect(piwikComparisonsService.isComparing()).to.be.false; + + $location.search('category=MyModule1&subcategory=enabledPage&date=2018-01-02&period=day'); + $rootScope.$apply(); + + expect(piwikComparisonsService.isComparing()).to.be.false; + }); + + it('should return false if comparison is not enabled', function () { + $location.search('category=MyModule1&subcategory=disabledPage&date=2018-01-02&period=day&segment=abcdefg&compareDates[]=2018-03-04&comparePeriods[]=week&compareSegments[]=comparedsegment&compareSegments[]='); + $rootScope.$apply(); + + expect(piwikComparisonsService.isComparing()).to.be.false; + }); + }); + + describe('#isComparingPeriods()', function () { + it('should return true if there are periods being compared', function () { + $location.search('category=MyModule1&subcategory=enabledPage&date=2018-01-02&period=day&segment=abcdefg&compareDates[]=2018-03-04&comparePeriods[]=week&compareSegments[]=comparedsegment&compareSegments[]='); + $rootScope.$apply(); + + expect(piwikComparisonsService.isComparingPeriods()).to.be.true; + }); + + it('should return false if there are no periods being compared, just segments', function () { + $location.search('category=MyModule1&subcategory=enabledPage&date=2018-01-02&period=day&segment=abcdefg&compareSegments[]=comparedsegment&compareSegments[]='); + $rootScope.$apply(); + + expect(piwikComparisonsService.isComparingPeriods()).to.be.false; + }); + + it('should return false if there is nothing being compared', function () { + $location.search('category=MyModule1&subcategory=enabledPage&date=2018-01-02&period=day&segment=abcdefg'); + $rootScope.$apply(); + + expect(piwikComparisonsService.isComparingPeriods()).to.be.false; + }); + + it('should return false if comparing is not enabled', function () { + $location.search('category=MyModule1&subcategory=disabledPage&date=2018-01-02&period=day&segment=abcdefg&compareDates[]=2018-03-04&comparePeriods[]=week&compareSegments[]=comparedsegment&compareSegments[]='); + $rootScope.$apply(); + + expect(piwikComparisonsService.isComparingPeriods()).to.be.false; + }); + }); + + describe('#getIndividualComparisonRowIndices()', function () { + it('should calculate the segment/period index from the given series index', function () { + $location.search('category=MyModule1&subcategory=enabledPage&date=2018-01-02&period=day&segment=abcdefg&compareDates[]=2018-03-04&comparePeriods[]=week&compareSegments[]=comparedsegment&compareSegments[]='); + $rootScope.$apply(); + + expect(piwikComparisonsService.getIndividualComparisonRowIndices(3)).to.be.deep.equal({ + segmentIndex: 0, + periodIndex: 1, + }); + + expect(piwikComparisonsService.getIndividualComparisonRowIndices(0)).to.be.deep.equal({ + segmentIndex: 0, + periodIndex: 0, + }); + }); + }); + + describe('#getComparisonSeriesIndex()', function () { + it('should return the comparison series index from the given segment & period indices', function () { + $location.search('category=MyModule1&subcategory=enabledPage&date=2018-01-02&period=day&segment=abcdefg&compareDates[]=2018-03-04&comparePeriods[]=week&compareSegments[]=comparedsegment&compareSegments[]='); + $rootScope.$apply(); + + expect(piwikComparisonsService.getComparisonSeriesIndex(1, 1)).to.be.deep.equal(4); + + expect(piwikComparisonsService.getComparisonSeriesIndex(0, 1)).to.be.deep.equal(1); + }); + }); + }); +})();
\ No newline at end of file diff --git a/plugins/CoreHome/angularjs/date-range-picker/date-range-picker.component.js b/plugins/CoreHome/angularjs/date-range-picker/date-range-picker.component.js index c9c23bb3f2..95432ea6f7 100644 --- a/plugins/CoreHome/angularjs/date-range-picker/date-range-picker.component.js +++ b/plugins/CoreHome/angularjs/date-range-picker/date-range-picker.component.js @@ -32,9 +32,9 @@ controller: DateRangePickerController }); - DateRangePickerController.$inject = []; + DateRangePickerController.$inject = ['piwikPeriods']; - function DateRangePickerController() { + function DateRangePickerController(piwikPeriods) { var vm = this; vm.fromPickerSelectedDates = null; @@ -118,7 +118,7 @@ function setStartRangeDate(date) { vm.startDateInvalid = false; - vm.startDate = $.datepicker.formatDate('yy-mm-dd', date); + vm.startDate = piwikPeriods.format(date); vm.fromPickerSelectedDates = [date, date]; @@ -127,7 +127,7 @@ function setEndRangeDate(date) { vm.endDateInvalid = false; - vm.endDate = $.datepicker.formatDate('yy-mm-dd', date); + vm.endDate = piwikPeriods.format(date); vm.toPickerSelectedDates = [date, date]; diff --git a/plugins/CoreHome/angularjs/period-selector/period-selector.controller.js b/plugins/CoreHome/angularjs/period-selector/period-selector.controller.js index c48150e696..58ed8295e3 100644 --- a/plugins/CoreHome/angularjs/period-selector/period-selector.controller.js +++ b/plugins/CoreHome/angularjs/period-selector/period-selector.controller.js @@ -8,9 +8,9 @@ (function () { angular.module('piwikApp').controller('PeriodSelectorController', PeriodSelectorController); - PeriodSelectorController.$inject = ['piwik', '$location', 'piwikPeriods']; + PeriodSelectorController.$inject = ['piwik', '$location', 'piwikPeriods', 'piwikComparisonsService', '$rootScope', 'piwikUrl', '$element', '$timeout']; - function PeriodSelectorController(piwik, $location, piwikPeriods) { + function PeriodSelectorController(piwik, $location, piwikPeriods, piwikComparisonsService, $rootScope, piwikUrl, $element, $timeout) { var piwikMinDate = new Date(piwik.minDateYear, piwik.minDateMonth - 1, piwik.minDateDay), piwikMaxDate = new Date(piwik.maxDateYear, piwik.maxDateMonth - 1, piwik.maxDateDay); @@ -28,6 +28,11 @@ vm.isLoadingNewPage = false; + vm.isComparing = false; + vm.comparePeriodType = 'previousPeriod'; + vm.compareStartDate = ''; + vm.compareEndDate = ''; + vm.getCurrentlyViewingText = getCurrentlyViewingText; vm.changeViewedPeriod = changeViewedPeriod; vm.setPiwikPeriodAndDate = setPiwikPeriodAndDate; @@ -38,10 +43,28 @@ vm.onRangeChange = onRangeChange; vm.isApplyEnabled = isApplyEnabled; vm.$onInit = init; + vm.isComparisonEnabled = isComparisonEnabled; + + $rootScope.$on('$locationChangeSuccess', setIsComparing); function init() { vm.updateSelectedValuesFromHash(); + setIsComparing(); initTopControls(); // must be called when a top control changes width + + handleZIndexPositionRelativeCompareDropdownIssue(); + } + + function handleZIndexPositionRelativeCompareDropdownIssue() { + $element.on('focus', '#comparePeriodToDropdown .select-dropdown', function () { + $element.addClass('compare-dropdown-open'); + }).on('blur', '#comparePeriodToDropdown .select-dropdown', function () { + $element.removeClass('compare-dropdown-open'); + }); + } + + function setIsComparing() { + vm.isComparing = piwikComparisonsService.isComparingPeriods(); } function $onChanges(changesObj) { @@ -68,6 +91,29 @@ return false; } + if (vm.isComparing + && vm.comparePeriodType === 'custom' + && !isCompareRangeValid() + ) { + return false; + } + + return true; + } + + function isCompareRangeValid() { + try { + piwikPeriods.parseDate(vm.compareStartDate); + } catch (e) { + return false; + } + + try { + piwikPeriods.parseDate(vm.compareEndDate); + } catch (e) { + return false; + } + return true; } @@ -78,8 +124,8 @@ } function updateSelectedValuesFromHash() { - var strDate = getQueryParamValue('date'); - var strPeriod = getQueryParamValue('period'); + var strDate = piwikUrl.getSearchParam('date'); + var strPeriod = piwikUrl.getSearchParam('period'); vm.periodValue = strPeriod; vm.selectedPeriod = strPeriod; @@ -97,16 +143,6 @@ } } - function getQueryParamValue(name) { - // $location doesn't parse the URL before the hashbang, but it can hold the query param - // values, if the page doesn't have the hashbang. - var result = $location.search()[name]; - if (!result) { - result = broadcast.getValueFromUrl(name); - } - return result; - } - function getPeriodDisplayText(periodLabel) { return piwikPeriods.get(periodLabel).getDisplayText(); } @@ -142,6 +178,22 @@ function onApplyClicked() { if (vm.selectedPeriod === 'range') { + var dateString = getSelectedDateString(); + if (!dateString) { + return; + } + + vm.periodValue = 'range'; + + propagateNewUrlParams(dateString, 'range'); + return; + } + + setPiwikPeriodAndDate(vm.selectedPeriod, vm.dateValue); + } + + function getSelectedDateString() { + if (vm.selectedPeriod === 'range') { var dateFrom = vm.startRangeDate, dateTo = vm.endRangeDate, oDateFrom = piwikPeriods.parseDate(dateFrom), @@ -154,16 +206,13 @@ // TODO: use a notification instead? $('#alert').find('h2').text(_pk_translate('General_InvalidDateRange')); piwik.helper.modalConfirm('#alert', {}); - return; + return null; } - vm.periodValue = 'range'; - - propagateNewUrlParams(dateFrom + ',' + dateTo, 'range'); - return; + return dateFrom + ',' + dateTo; + } else { + return formatDate(vm.dateValue); } - - setPiwikPeriodAndDate(vm.selectedPeriod, vm.dateValue); } function setPiwikPeriodAndDate(period, date) { @@ -184,17 +233,77 @@ vm.endRangeDate = formatDate(dateRange[1] > piwikMaxDate ? piwikMaxDate : dateRange[1]); } + function getSelectedComparisonParams() { + var previousDate; + + if (!vm.isComparing) { + return {}; + } + + if (vm.comparePeriodType === 'custom') { + return { + comparePeriods: ['range'], + compareDates: [vm.compareStartDate + ',' + vm.compareEndDate], + }; + } else if (vm.comparePeriodType === 'previousPeriod') { + previousDate = getPreviousPeriodDateToSelectedPeriod(); + return { + comparePeriods: [vm.selectedPeriod], + compareDates: [previousDate], + }; + } else if (vm.comparePeriodType === 'previousYear') { + var dateStr = vm.selectedPeriod === 'range' ? (vm.startRangeDate + ',' + vm.endRangeDate) : vm.dateValue; + var currentDateRange = piwikPeriods.parse(vm.selectedPeriod, dateStr).getDateRange(); + currentDateRange[0].setFullYear(currentDateRange[0].getFullYear() - 1); + currentDateRange[1].setFullYear(currentDateRange[1].getFullYear() - 1); + + return { + comparePeriods: ['range'], + compareDates: [piwikPeriods.format(currentDateRange[0]) + ',' + piwikPeriods.format(currentDateRange[1])], + }; + } else { + console.warn("Unknown compare period type: " + vm.comparePeriodType); + return {}; + } + } + + function getPreviousPeriodDateToSelectedPeriod() { + if (vm.selectedPeriod === 'range') { + var currentStartRange = piwikPeriods.parseDate(vm.startRangeDate); + var currentEndRange = piwikPeriods.parseDate(vm.endRangeDate); + var newEndDate = piwikPeriods.RangePeriod.getLastNRange('day', 2, currentStartRange).startDate; + + var rangeSize = Math.floor((currentEndRange - currentStartRange) / 86400000); + var newRange = piwikPeriods.RangePeriod.getLastNRange('day', 1 + rangeSize, newEndDate); + + return piwikPeriods.format(newRange.startDate) + ',' + piwikPeriods.format(newRange.endDate); + } + + var newStartDate = piwikPeriods.RangePeriod.getLastNRange(vm.selectedPeriod, 2, vm.dateValue).startDate; + return piwikPeriods.format(newStartDate); + } + function propagateNewUrlParams(date, period) { + var compareParams = getSelectedComparisonParams(); + if (piwik.helper.isAngularRenderingThePage()) { vm.closePeriodSelector(); // defined in directive var $search = $location.search(); - if (date !== $search.date || period !== $search.period) { + var isCurrentlyComparing = piwikUrl.getSearchParam('compareSegments') || piwikUrl.getSearchParam('comparePeriods'); + if (date !== $search.date || period !== $search.period || vm.isComparing || isCurrentlyComparing) { // eg when using back button the date might be actually already changed in the URL and we do not // want to change the URL again $search.date = date; $search.period = period; - $location.search($search); + $search.compareSegments = piwikUrl.getSearchParam('compareSegments') || []; + $.extend($search, compareParams); + + delete $search['compareSegments[]']; + delete $search['comparePeriods[]']; + delete $search['compareDates[]']; + + $location.search($.param($search)); } return; @@ -204,7 +313,8 @@ // not in an angular context (eg, embedded dashboard), so must actually // change the URL - broadcast.propagateNewPage('date=' + date + '&period=' + period); + var url = $.param($.extend({ date: date, period: period }, compareParams)); + broadcast.propagateNewPage(url); } function isValidDate(d) { @@ -216,7 +326,11 @@ } function formatDate(date) { - return $.datepicker.formatDate('yy-mm-dd', date); + return piwikPeriods.format(date); + } + + function isComparisonEnabled() { + return piwikComparisonsService.isComparisonEnabled(); } } })(); diff --git a/plugins/CoreHome/angularjs/period-selector/period-selector.directive.html b/plugins/CoreHome/angularjs/period-selector/period-selector.directive.html index ac7312c71c..6641d6714d 100644 --- a/plugins/CoreHome/angularjs/period-selector/period-selector.directive.html +++ b/plugins/CoreHome/angularjs/period-selector/period-selector.directive.html @@ -62,25 +62,71 @@ </label> </p> </div> - <input - type="submit" - value="{{ 'General_Apply'|translate }}" - id="calendarApply" - class="btn" - ng-click="periodSelector.onApplyClicked()" - ng-disabled="!periodSelector.isApplyEnabled()" - /> - <div id="ajaxLoadingCalendar" ng-if="periodSelector.isLoadingNewPage"> - <div class="loadingPiwik"> - <img src="plugins/Morpheus/images/loading-blue.gif" alt="{{ 'General_LoadingData'|translate }}" />{{ 'General_LoadingData'|translate }} - </div> - <div class="loadingSegment"> - {{ 'SegmentEditor_LoadingSegmentedDataMayTakeSomeTime'|translate }} - </div> - </div> </div> </td> </tr> </table> + + <div class="compare-checkbox" ng-if="periodSelector.isComparisonEnabled()"> + <input id="comparePeriodTo" type="checkbox" ng-model="periodSelector.isComparing"/> + <label for="comparePeriodTo">Compare to:</label> + + <div + id="comparePeriodToDropdown" + piwik-field + name="comparePeriodToDropdown" + uicontrol="select" + options="[{key: 'custom', value: 'Custom'}, {key: 'previousPeriod', value: 'Previous period'}, {key: 'previousYear', value: 'Previous year'}]" + ng-model="periodSelector.comparePeriodType" + full-width="true" + disabled="!periodSelector.isComparing" + ></div> + </div> + + <div class="compare-date-range" ng-if="periodSelector.isComparing && periodSelector.comparePeriodType == 'custom'"> + <div> + <div + id="comparePeriodStartDate" + piwik-field + name="comparePeriodStartDate" + uicontrol="text" + ng-model="periodSelector.compareStartDate" + full-width="true" + title="Start Date" + placeholder="YYYY-MM-DD" + ></div> + + <span class="compare-dates-separator"></span> + + <div + id="comparePeriodEndDate" + piwik-field + name="comparePeriodEndDate" + uicontrol="text" + ng-model="periodSelector.compareEndDate" + full-width="true" + title="End Date" + placeholder="YYYY-MM-DD" + ></div> + </div> + </div> + + <div class="apply-button-container"> + <input + type="submit" + value="{{ 'General_Apply'|translate }}" + id="calendarApply" + class="btn" + ng-click="periodSelector.onApplyClicked()" + ng-disabled="!periodSelector.isApplyEnabled()" + /> + </div> + + <div id="ajaxLoadingCalendar" ng-if="periodSelector.isLoadingNewPage"> + <div piwik-activity-indicator loading="true"></div> + <div class="loadingSegment"> + {{ 'SegmentEditor_LoadingSegmentedDataMayTakeSomeTime'|translate }} + </div> + </div> </div> </div> diff --git a/plugins/CoreHome/angularjs/period-selector/period-selector.directive.less b/plugins/CoreHome/angularjs/period-selector/period-selector.directive.less index e44e0e0f2c..2cf453fd5b 100644 --- a/plugins/CoreHome/angularjs/period-selector/period-selector.directive.less +++ b/plugins/CoreHome/angularjs/period-selector/period-selector.directive.less @@ -1,10 +1,91 @@ [piwik-period-selector] { display: inline-block; + &.compare-dropdown-open { + label[for=comparePeriodEndDate] { + visibility: hidden; + } + + #comparePeriodEndDate .input-field { + position: static; // needed when dropdown is open, since dropdown content is position: absolute, and this is position: relative, which causes it to show up in the background of the dropdown + } + } + // overrides theme CSS that always hides .loadingPiwik. since we use an ng-if to make // sure it's only shown when we're actually loading a new page, it's not necessary to // forcefully hide it. .loadingPiwik { display: inline-block !important; } + + .compare-checkbox { + margin-bottom: 20px; + } + + .compare-date-range { + padding-top: 20px; + } + + #comparePeriodToDropdown { + height: 30px; + display: inline-block; + width: 60%; + transform: scale(.8); + margin-left: -5%; + margin-right: -5%; + + .form-group { + margin: 0; + } + + .input-field { + margin-top: 0; + } + + .select-dropdown { + margin-bottom: 0; + } + + .dropdown-content { + overflow-y: visible; + } + } + + div#comparePeriodStartDate, div#comparePeriodEndDate { + display: inline-block; + width: calc(~"50% - 20px"); + + .form-group { + margin: 0; + } + + .input-field { + margin-top: 0; + padding: 0; + } + + .select-dropdown { + margin-bottom: 0; + } + } + + .compare-dates-separator { + height: 1px; + margin-left: 8px; + margin-right: 8px; + background-color: @color-silver-l14; + width: 16px; + display: inline-block; + vertical-align: top; + margin-top: 30px; + } + + label[for=comparePeriodTo] { + transform: scale(.9); + } + + .apply-button-container { + text-align: right; + } + } diff --git a/plugins/CoreHome/angularjs/report-export/reportexport.directive.js b/plugins/CoreHome/angularjs/report-export/reportexport.directive.js index c197ce07eb..3599c4f6d8 100644 --- a/plugins/CoreHome/angularjs/report-export/reportexport.directive.js +++ b/plugins/CoreHome/angularjs/report-export/reportexport.directive.js @@ -94,6 +94,27 @@ exportUrlParams.period = period; exportUrlParams.date = param_date; + if (dataTable.param.compareDates + && dataTable.param.compareDates.length + ) { + exportUrlParams.compareDates = dataTable.param.compareDates; + exportUrlParams.compare = '1'; + } + + if (dataTable.param.comparePeriods + && dataTable.param.comparePeriods.length + ) { + exportUrlParams.comparePeriods = dataTable.param.comparePeriods; + exportUrlParams.compare = '1'; + } + + if (dataTable.param.compareSegments + && dataTable.param.compareSegments.length + ) { + exportUrlParams.compareSegments = dataTable.param.compareSegments; + exportUrlParams.compare = '1'; + } + if (typeof dataTable.param.filter_pattern != "undefined") { exportUrlParams.filter_pattern = dataTable.param.filter_pattern; } diff --git a/plugins/CoreHome/angularjs/reporting-menu/reportingmenu.controller.js b/plugins/CoreHome/angularjs/reporting-menu/reportingmenu.controller.js index d284034cd6..41623802bf 100644 --- a/plugins/CoreHome/angularjs/reporting-menu/reportingmenu.controller.js +++ b/plugins/CoreHome/angularjs/reporting-menu/reportingmenu.controller.js @@ -7,14 +7,18 @@ (function () { angular.module('piwikApp').controller('ReportingMenuController', ReportingMenuController); - ReportingMenuController.$inject = ['$scope', 'piwik', '$location', '$timeout', 'reportingMenuModel', '$rootScope']; + ReportingMenuController.$inject = ['$scope', 'piwik', '$location', '$timeout', 'reportingMenuModel', '$rootScope', 'piwikUrl']; - function ReportingMenuController($scope, piwik, $location, $timeout, menuModel, $rootScope) { + function ReportingMenuController($scope, piwik, $location, $timeout, menuModel, $rootScope, piwikUrl) { - var idSite = getUrlParam('idSite'); - var period = getUrlParam('period'); - var date = getUrlParam('date'); - var segment = getUrlParam('segment'); + var idSite = piwikUrl.getSearchParam('idSite'); + var period = piwikUrl.getSearchParam('period'); + var date = piwikUrl.getSearchParam('date'); + var segment = piwikUrl.getSearchParam('segment'); + + var comparePeriods = piwikUrl.getSearchParam('comparePeriods'); + var compareDates = piwikUrl.getSearchParam('compareDates'); + var compareSegments = piwikUrl.getSearchParam('compareSegments'); function markAllCategoriesAsInactive() { @@ -40,15 +44,6 @@ }); } - function getUrlParam(param) - { - var value = piwik.broadcast.getValueFromHash(param); - if (!value) { - value = piwik.broadcast.getValueFromUrl(param); - } - return value; - } - $scope.menuModel = menuModel; // highlight the currently hovered subcategory (and category) @@ -69,18 +64,29 @@ }; $scope.makeUrl = function (category, subcategory) { + var params = { + idSite: idSite, + period: period, + date: date, + segment: decodeURIComponent(segment), + category: category.id, + subcategory: subcategory.id, + }; + + if (compareDates) { + params.compareDates = compareDates; + } - var url = 'idSite=' + encodeURIComponent(idSite); - url += '&period=' + encodeURIComponent(period); - url += '&date=' + encodeURIComponent(date); - url += '&category=' + encodeURIComponent(category.id); - url += '&subcategory=' + encodeURIComponent(subcategory.id); + if (comparePeriods) { + params.comparePeriods = comparePeriods; + } - if (segment) { - url+= '&segment='+ segment; + if (compareSegments) { + params.compareSegments = compareSegments; } - return url; - } + + return $.param(params); + }; $scope.loadCategory = function (category) { if (category.active) { @@ -146,9 +152,13 @@ var category = $search.category; var subcategory = $search.subcategory; - period = getUrlParam('period'); - date = getUrlParam('date'); - segment = getUrlParam('segment'); + period = piwikUrl.getSearchParam('period'); + date = piwikUrl.getSearchParam('date'); + segment = piwikUrl.getSearchParam('segment'); + + comparePeriods = piwikUrl.getSearchParam('comparePeriods'); + compareDates = piwikUrl.getSearchParam('compareDates'); + compareSegments = piwikUrl.getSearchParam('compareSegments'); if (!category || !subcategory) { return; diff --git a/plugins/CoreHome/angularjs/reporting-page/reportingpage.controller.js b/plugins/CoreHome/angularjs/reporting-page/reportingpage.controller.js index dca4f6ae4f..54785b189d 100644 --- a/plugins/CoreHome/angularjs/reporting-page/reportingpage.controller.js +++ b/plugins/CoreHome/angularjs/reporting-page/reportingpage.controller.js @@ -7,9 +7,9 @@ (function () { angular.module('piwikApp').controller('ReportingPageController', ReportingPageController); - ReportingPageController.$inject = ['$scope', 'piwik', '$rootScope', '$location', 'reportingPageModel', 'reportingPagesModel', 'notifications']; + ReportingPageController.$inject = ['$scope', 'piwik', '$rootScope', '$location', 'reportingPageModel', 'reportingPagesModel', 'notifications', 'piwikUrl']; - function ReportingPageController($scope, piwik, $rootScope, $location, pageModel, pagesModel, notifications) { + function ReportingPageController($scope, piwik, $rootScope, $location, pageModel, pagesModel, notifications, piwikUrl) { pageModel.resetPage(); $scope.pageModel = pageModel; @@ -19,12 +19,19 @@ var currentDate = null; var currentSegment = null; + var currentCompareDates = null; + var currentComparePeriods = null; + var currentCompareSegments = null; + function renderInitialPage() { var $search = $location.search(); currentPeriod = $search.period; currentDate = $search.date; currentSegment = $search.segment; + currentCompareSegments = piwikUrl.getSearchParam('compareSegments'); + currentCompareDates = piwikUrl.getSearchParam('compareDates'); + currentComparePeriods = piwikUrl.getSearchParam('comparePeriods'); $scope.renderPage($search.category, $search.subcategory); } @@ -59,7 +66,6 @@ } pageModel.fetchPage(category, subcategory).then(function () { - if (!pageModel.page) { var page = pagesModel.findPageInCategory(category); if (page && page.subcategory) { @@ -73,7 +79,7 @@ $scope.hasNoPage = !pageModel.page; $scope.loading = false; }); - } + }; $scope.loading = true; // we only set loading on initial load @@ -89,11 +95,20 @@ var date = $search.date; var segment = $search.segment; + // $location does not handle array parameters properly + var compareSegments = piwikUrl.getSearchParam('compareSegments'); + var compareDates = piwikUrl.getSearchParam('compareDates'); + var comparePeriods = piwikUrl.getSearchParam('comparePeriods'); + if (category === currentCategory && subcategory === currentSubcategory && period === currentPeriod && date === currentDate - && segment === currentSegment) { + && segment === currentSegment + && JSON.stringify(compareDates) === JSON.stringify(currentCompareDates) + && JSON.stringify(comparePeriods) === JSON.stringify(currentComparePeriods) + && JSON.stringify(compareSegments) === JSON.stringify(currentCompareSegments) + ) { // this page is already loaded return; } @@ -101,6 +116,9 @@ currentPeriod = period; currentDate = date; currentSegment = segment; + currentCompareDates = compareDates; + currentComparePeriods = comparePeriods; + currentCompareSegments = compareSegments; $scope.renderPage(category, subcategory); }); diff --git a/plugins/CoreHome/angularjs/sparkline/sparkline.component.js b/plugins/CoreHome/angularjs/sparkline/sparkline.component.js index 42e3e4f699..6853164113 100644 --- a/plugins/CoreHome/angularjs/sparkline/sparkline.component.js +++ b/plugins/CoreHome/angularjs/sparkline/sparkline.component.js @@ -16,6 +16,7 @@ angular.module('piwikApp').component('piwikSparkline', { template: '<img />', bindings: { + seriesIndices: '<', params: '<' }, controller: SparklineController @@ -34,12 +35,23 @@ } function getSparklineUrl() { + var seriesIndices = vm.seriesIndices; + var sparklineColors = piwik.getSparklineColors(); + + if (seriesIndices) { + sparklineColors.lineColor = sparklineColors.lineColor.filter(function (c, index) { + return seriesIndices.indexOf(index) !== -1; + }); + } + + var colors = JSON.stringify(sparklineColors); + var defaultParams = { forceView: '1', viewDataTable: 'sparkline', widget: $element.closest('[widgetId]').length ? '1' : '0', showtitle: '1', - colors: JSON.stringify(piwik.getSparklineColors()), + colors: colors, random: Date.now(), date: getDefaultDate() }; @@ -67,8 +79,8 @@ dateRange[0] = piwikMinDate; } - var startDateStr = $.datepicker.formatDate('yy-mm-dd', dateRange[0]); - var endDateStr = $.datepicker.formatDate('yy-mm-dd', dateRange[1]); + var startDateStr = piwikPeriods.format(dateRange[0]); + var endDateStr = piwikPeriods.format(dateRange[1]); return startDateStr + ',' + endDateStr; } } diff --git a/plugins/CoreHome/angularjs/widget-bydimension-container/widget-bydimension-container.directive.less b/plugins/CoreHome/angularjs/widget-bydimension-container/widget-bydimension-container.directive.less index 698d2ca5ea..94b1a3d5d0 100644 --- a/plugins/CoreHome/angularjs/widget-bydimension-container/widget-bydimension-container.directive.less +++ b/plugins/CoreHome/angularjs/widget-bydimension-container/widget-bydimension-container.directive.less @@ -12,6 +12,10 @@ table.dataTable tr td.label { max-width: 380px; } + + table.dataTable { + width: auto; + } } } diff --git a/plugins/CoreHome/angularjs/widget-loader/widgetloader.directive.js b/plugins/CoreHome/angularjs/widget-loader/widgetloader.directive.js index bda6b5d3b9..d741ac22e8 100644 --- a/plugins/CoreHome/angularjs/widget-loader/widgetloader.directive.js +++ b/plugins/CoreHome/angularjs/widget-loader/widgetloader.directive.js @@ -19,9 +19,9 @@ (function () { angular.module('piwikApp').directive('piwikWidgetLoader', piwikWidgetLoader); - piwikWidgetLoader.$inject = ['piwik', 'piwikUrl', '$http', '$compile', '$q', '$location', 'notifications', '$rootScope', '$timeout']; + piwikWidgetLoader.$inject = ['piwik', 'piwikUrl', '$http', '$compile', '$q', '$location', 'notifications', '$rootScope', '$timeout', 'piwikComparisonsService']; - function piwikWidgetLoader(piwik, piwikUrl, $http, $compile, $q, $location, notifications, $rootScope, $timeout){ + function piwikWidgetLoader(piwik, piwikUrl, $http, $compile, $q, $location, notifications, $rootScope, $timeout, piwikComparisonsService){ return { restrict: 'A', transclude: true, @@ -71,6 +71,10 @@ var $urlParams = $location.search(); + delete $urlParams['comparePeriods[]']; + delete $urlParams['compareDates[]']; + delete $urlParams['compareSegments[]']; + if ($.isEmptyObject($urlParams) || !$urlParams || !$urlParams['idSite']) { // happens eg in exported widget etc when URL does not have #?... $urlParams = {idSite: 'idSite', period: 'period',date: 'date'}; @@ -93,6 +97,17 @@ } }); + if (piwikComparisonsService.isComparisonEnabled()) { + ['comparePeriods', 'compareDates', 'compareSegments'].forEach(function (paramName) { + var value = piwikUrl.getSearchParam(paramName); + if (value) { + var map = {}; + map[paramName] = value; + url += '&' + $.param(map); + } + }); + } + if (!parameters || !('showtitle' in parameters)) { url += '&showtitle=1'; } diff --git a/plugins/CoreHome/javascripts/broadcast.js b/plugins/CoreHome/javascripts/broadcast.js index 5e392b50dd..ef1b23ff28 100644 --- a/plugins/CoreHome/javascripts/broadcast.js +++ b/plugins/CoreHome/javascripts/broadcast.js @@ -242,9 +242,10 @@ var broadcast = { } if (disableHistory) { - var newLocation = window.location.href.split('#')[0] + '#?' + currentHashStr; + var $window = piwikHelper.getAngularDependency('$window'); + var newLocation = $window.location.href.split('#')[0] + '#?' + currentHashStr; // window.location.replace changes the current url without pushing it on the browser's history stack - window.location.replace(newLocation); + $window.location.replace(newLocation); } else { // Let history know about this new Hash and load it. @@ -306,20 +307,25 @@ var broadcast = { * @param {string} str url with parameters to be updated * @param {boolean} [showAjaxLoading] whether to show the ajax loading gif or not. * @param {string} strHash additional parameters that should be updated on the hash + * @param {array} paramsToRemove Optional parameters to remove from the URL. * @return {void} */ - propagateNewPage: function (str, showAjaxLoading, strHash) { + propagateNewPage: function (str, showAjaxLoading, strHash, paramsToRemove) { // abort all existing ajax requests globalAjaxQueue.abort(); + paramsToRemove = paramsToRemove || []; + if (typeof showAjaxLoading === 'undefined' || showAjaxLoading) { piwikHelper.showAjaxLoading(); } var params_vals = str.split("&"); + var $window = piwikHelper.getAngularDependency('$window'); + // available in global scope - var currentSearchStr = window.location.search; + var currentSearchStr = $window.location.search; var currentHashStr = broadcast.getHashFromUrl(); if (!currentSearchStr) { @@ -328,19 +334,41 @@ var broadcast = { var oldUrl = currentSearchStr + currentHashStr; - for (var i = 0; i < params_vals.length; i++) { + // remove all array query params that are currently set. if we don't do this the array parameters we add + // just get added to the existing parameters. + params_vals.forEach(function (param) { + if (/\[]=/.test(decodeURIComponent(param))) { + var paramName = decodeURIComponent(param).split('[]=')[0]; + removeParam(paramName); + } + }); + + // remove parameters if needed + paramsToRemove.forEach(function (paramName) { + removeParam(paramName); + }); - if(params_vals[i].length == 0) { - continue; // updating with empty string would destroy some values + // update/add parameters based on whether the parameter is an array param or not + params_vals.forEach(function (param) { + if(!param.length) { + return; // updating with empty string would destroy some values } - // update both the current search query and hash string - currentSearchStr = broadcast.updateParamValue(params_vals[i], currentSearchStr); + if (/\[]=/.test(decodeURIComponent(param))) { // array param value + currentSearchStr = broadcast.addArrayParamValue(param, currentSearchStr); + + if (currentHashStr.length !== 0) { + currentHashStr = broadcast.addArrayParamValue(param, currentHashStr); + } + } else { + // update both the current search query and hash string + currentSearchStr = broadcast.updateParamValue(param, currentSearchStr); - if (currentHashStr.length != 0) { - currentHashStr = broadcast.updateParamValue(params_vals[i], currentHashStr); + if (currentHashStr.length !== 0) { + currentHashStr = broadcast.updateParamValue(param, currentHashStr); + } } - } + }); var updatedUrl = new RegExp('&updated=([0-9]+)'); var updatedCounter = updatedUrl.exec(currentSearchStr); @@ -367,16 +395,22 @@ var broadcast = { if (event) { event.preventDefault(); } - }) + }); } if (oldUrl == newUrl) { - window.location.reload(); + $window.location.reload(); } else { this.forceReload = true; - window.location.href = newUrl; + $window.location.href = newUrl; } return false; + + function removeParam(paramName) { + var paramRegex = new RegExp(paramName + '(\\[]|%5B%5D)?=[^&?#]*&?', 'gi'); + currentSearchStr = currentSearchStr.replace(paramRegex, ''); + currentHashStr = currentHashStr.replace(paramRegex, ''); + } }, /************************************************* @@ -430,6 +464,21 @@ var broadcast = { }, /** + * Adds a query param value. Use it to add an array parameter value where you don't want to remove an existing value first. + * + * @param newParamValue + * @param urlStr + */ + addArrayParamValue: function (newParamValue, urlStr) { + if (urlStr.indexOf('?') === -1) { + urlStr += '?'; + } else { + urlStr += '&'; + } + return urlStr + newParamValue; + }, + + /** * Loads a popover by adding a 'popover' query parameter to the current URL and * indirectly executing the popover handler. * @@ -446,7 +495,6 @@ var broadcast = { * handler. */ propagateNewPopoverParameter: function (handlerName, value) { - var $location = angular.element(document).injector().get('$location'); var popover = ''; @@ -467,7 +515,11 @@ var broadcast = { } } - $location.search('popover', popover); + var $window = piwikHelper.getAngularDependency('$window'); + var urlStr = $window.location.hash; + urlStr = broadcast.updateParamValue('popover=' + encodeURIComponent(popover), urlStr); + urlStr = urlStr.replace(/^[#?]+/, ''); + $location.search(urlStr); setTimeout(function () { angular.element(document).injector().get('$rootScope').$apply(); @@ -726,11 +778,30 @@ var broadcast = { var startStr = url.indexOf(lookFor); if (startStr >= 0) { - var endStr = url.indexOf("&", startStr); - if (endStr == -1) { + return getSingleValue(startStr, url); + } else { + url = decodeURIComponent(url); + + // try looking for multi value param + lookFor = param + '[]='; + startStr = url.indexOf(lookFor); + if (startStr >= 0) { + var result = [getSingleValue(startStr)]; + while ((startStr = url.indexOf(lookFor, startStr + 1)) !== -1) { + result.push(getSingleValue(startStr)); + } + return result; + } else { + return ''; + } + } + + function getSingleValue(startPos) { + var endStr = url.indexOf("&", startPos); + if (endStr === -1) { endStr = url.length; } - var value = url.substring(startStr + param.length + 1, endStr); + var value = url.substring(startPos + lookFor.length, endStr); // we sanitize values to add a protection layer against XSS // &segment= value is not sanitized, since segments are designed to accept any user input @@ -738,8 +809,6 @@ var broadcast = { value = value.replace(/[^_%~\*\+\-\<\>!@\$\.()=,;0-9a-zA-Z]/gi, ''); } return value; - } else { - return ''; } }, @@ -763,7 +832,7 @@ var broadcast = { var urlParts = url.split('#'); searchString = urlParts[0]; } else { - searchString = location.search; + searchString = window.location.search; } return searchString; } diff --git a/plugins/CoreHome/javascripts/dataTable.js b/plugins/CoreHome/javascripts/dataTable.js index 71dd8e78b7..dd00113ed7 100644 --- a/plugins/CoreHome/javascripts/dataTable.js +++ b/plugins/CoreHome/javascripts/dataTable.js @@ -107,7 +107,6 @@ $.extend(DataTable.prototype, UIControl.prototype, { this.workingDivId = this._createDivId(); domElem.attr('id', this.workingDivId); - this.maxNumRowsToHandleEvents = 255; this.loadedSubDataTable = {}; this.isEmpty = $('.pk-emptyDataTable', domElem).length > 0; this.bindEventsAndApplyStyle(domElem); @@ -215,7 +214,7 @@ $.extend(DataTable.prototype, UIControl.prototype, { // The ajax request contains the function callback to trigger if the request is successful or failed // displayLoading = false When we don't want to display the Loading... DIV .loadingPiwik // for example when the script add a Loading... it self and doesn't want to display the generic Loading - reloadAjaxDataTable: function (displayLoading, callbackSuccess) { + reloadAjaxDataTable: function (displayLoading, callbackSuccess, extraParams) { var self = this; if (typeof displayLoading == "undefined") { @@ -252,7 +251,7 @@ $.extend(DataTable.prototype, UIControl.prototype, { var params = {}; for (var key in self.param) { - if (typeof self.param[key] != "undefined" && self.param[key] != '') { + if (typeof self.param[key] != "undefined" && self.param[key] !== null && self.param[key] !== '') { if (key == 'filter_column' || key == 'filter_column_recursive' ) { // search in (metadata) `combinedLabel` when dimensions are shown separately in flattened tables // needs to be overwritten for each request as switching a searched table might return no results @@ -270,6 +269,9 @@ $.extend(DataTable.prototype, UIControl.prototype, { } ajaxRequest.addParams(params, 'get'); + if (extraParams) { + ajaxRequest.addParams(extraParams, 'post'); + } ajaxRequest.withTokenInUrl(); ajaxRequest.setCallback( @@ -410,6 +412,14 @@ $.extend(DataTable.prototype, UIControl.prototype, { $domElem.width(''); parentDataTable.width(''); + var $table = $('table.dataTable', domElem); + if ($table.closest('.reportsByDimensionView').length) { + var requiredTableWidth = $table.width() - 40; // 40 is padding on card content + if (domElem.width() > requiredTableWidth) { + domElem.css('max-width', requiredTableWidth + 'px'); + } + } + var tableWidth = getTableWidth(domElem); if (tableWidth <= maxTableWidth) { @@ -443,7 +453,7 @@ $.extend(DataTable.prototype, UIControl.prototype, { { var labelWidth = minLabelWidth; - var columnsInFirstRow = $('tr:nth-child(1) td:not(.label)', domElem); + var columnsInFirstRow = $('tbody tr:not(.parentComparisonRow):not(.comparePeriod):eq(0) td:not(.label)', domElem); var widthOfAllColumns = 0; columnsInFirstRow.each(function (index, column) { @@ -465,7 +475,8 @@ $.extend(DataTable.prototype, UIControl.prototype, { if (labelWidth > maxLabelWidth && !self.isWidgetized() && innerWidth !== domElem.width() - && !self.isDashboard()) { + && !self.isDashboard() + ) { labelWidth = maxLabelWidth; // prevent for instance table in Actions-Pages is not too wide } @@ -482,7 +493,6 @@ $.extend(DataTable.prototype, UIControl.prototype, { } var minWidthBody = $('tbody tr:nth-child(1) td.label', domElem).css('minWidth'); - if (minWidthBody) { minWidthBody = parseInt(minWidthBody, 10); if (minWidthBody && minWidthBody > minWidth) { @@ -528,8 +538,6 @@ $.extend(DataTable.prototype, UIControl.prototype, { return labelWidth; } - setMaxTableWidthIfNeeded(domElem, 1200); - var isTableVisualization = this.jsViewDataTable && typeof this.jsViewDataTable === 'string' && typeof this.jsViewDataTable.indexOf === 'function' @@ -560,6 +568,8 @@ $.extend(DataTable.prototype, UIControl.prototype, { self.overflowContentIfNeeded(domElem); } + setMaxTableWidthIfNeeded(domElem, 1200); + if (!self.windowResizeTableAttached) { self.windowResizeTableAttached = true; @@ -1451,36 +1461,16 @@ $.extend(DataTable.prototype, UIControl.prototype, { //Apply some miscelleaneous style to the DataTable applyCosmetics: function (domElem) { - var self = this; - - // Add some styles on the cells even/odd - // label (first column of a data row) or not - $("th:first-child", domElem).addClass('label'); - $("td:first-child", domElem).addClass('label'); - - var metadata = this.getReportMetadata(); - - if (self.param.flat == "1" && self.param.show_dimensions == "1" && metadata.dimensions && Object.keys(metadata.dimensions).length > 1) { - for (var i = 1; i < Object.keys(metadata.dimensions).length; i++) { - $("th:nth-child("+(i+1)+")", domElem).addClass('label'); - $("td:nth-child("+(i+1)+")", domElem).addClass('label'); - } - } - - $("tr td", domElem).addClass('column'); + // empty }, handleColumnHighlighting: function (domElem) { - if (!this.canHandleRowEvents(domElem)) { - return; - } - var maxWidth = {}; var currentNthChild = null; var self = this; - // higlight all columns on hover - $('td', domElem).hover(function() { + // give all values consistent width + $('td', domElem).each(function () { var $this = $(this); if ($this.hasClass('label')) { return; @@ -1492,16 +1482,29 @@ $.extend(DataTable.prototype, UIControl.prototype, { if (!maxWidth[nthChild]) { maxWidth[nthChild] = 0; - rows.find("td:nth-child(" + (nthChild) + ").column .value").each(function (index, element) { + rows.find("td:nth-child(" + (nthChild) + ").column .value").add('> thead th:not(.label) .thDIV', table).each(function (index, element) { var width = $(element).width(); if (width > maxWidth[nthChild]) { maxWidth[nthChild] = width; } }); rows.find("td:nth-child(" + (nthChild) + ").column .value").each(function (index, element) { - $(element).css({width: maxWidth[nthChild], display: 'inline-block'}); + $(element).closest('td').css({width: maxWidth[nthChild]}); }); } + }); + + // higlight all columns on hover + $(domElem).on('mouseenter', 'td', function (e) { + e.stopPropagation(); + var $this = $(e.target); + if ($this.hasClass('label')) { + return; + } + + var table = $this.closest('table'); + var nthChild = $this.parent('tr').children().index($(e.target)) + 1; + var rows = $('> tbody > tr', table); if (currentNthChild === nthChild) { return; @@ -1511,8 +1514,10 @@ $.extend(DataTable.prototype, UIControl.prototype, { rows.children("td:nth-child(" + (nthChild) + ")").addClass('highlight'); self.repositionRowActions($this.parent('tr')); - }, function(event) { - var $this = $(this); + }); + + $(domElem).on('mouseleave', 'td', function(event) { + var $this = $(event.target); var table = $this.closest('table'); var $parentTr = $this.parent('tr'); var tr = $parentTr.children(); @@ -1531,6 +1536,21 @@ $.extend(DataTable.prototype, UIControl.prototype, { }); }, + getComparisonIdSubtables: function ($row) { + if ($row.is('.parentComparisonRow')) { + var comparisonRows = $row.nextUntil('.parentComparisonRow').filter('.comparisonRow'); + + var comparisonIdSubtables = {}; + comparisonRows.each(function () { + var comparisonSeriesIndex = +$(this).data('comparison-series'); + comparisonIdSubtables[comparisonSeriesIndex] = $(this).data('idsubtable'); + }); + + return JSON.stringify(comparisonIdSubtables); + } + return undefined; + }, + //behaviour for 'nested DataTable' (DataTable loaded on a click on a row) handleSubDataTable: function (domElem) { var self = this; @@ -1544,12 +1564,17 @@ $.extend(DataTable.prototype, UIControl.prototype, { // if the subDataTable is not already loaded if (typeof self.loadedSubDataTable[divIdToReplaceWithSubTable] == "undefined") { - var numberOfColumns = $(this).children().length; + var numberOfColumns = $(this).closest('table').find('thead tr').first().children().length; + + var $insertAfter = $(this).nextUntil(':not(.comparePeriod):not(.comparisonRow)').last(); + if (!$insertAfter.length) { + $insertAfter = $(this); + } // at the end of the query it will replace the ID matching the new HTML table #ID // we need to create this ID first - $(this).after( - '<tr>' + + var newRow = $insertAfter.after( + '<tr class="subDataTableContainer">' + '<td colspan="' + numberOfColumns + '" class="cellSubDataTable">' + '<div id="' + divIdToReplaceWithSubTable + '">' + '<span class="loadingPiwik" style="display:inline"><img src="plugins/Morpheus/images/loading-blue.gif" />' + _pk_translate('General_Loading') + '</span>' + @@ -1558,6 +1583,8 @@ $.extend(DataTable.prototype, UIControl.prototype, { '</tr>' ); + piwikHelper.lazyScrollTo(newRow); + var savedActionVariable = self.param.action; // reset all the filters from the Parent table @@ -1570,9 +1597,12 @@ $.extend(DataTable.prototype, UIControl.prototype, { delete self.param.totalRows; + var extraParams = {}; + extraParams.comparisonIdSubtables = self.getComparisonIdSubtables($(this)); + self.reloadAjaxDataTable(false, function(response) { self.dataTableLoaded(response, divIdToReplaceWithSubTable); - }); + }, extraParams); self.param.action = savedActionVariable; delete self.param.idSubtable; @@ -1580,14 +1610,15 @@ $.extend(DataTable.prototype, UIControl.prototype, { self.loadedSubDataTable[divIdToReplaceWithSubTable] = true; - $(this).next().toggle(); - // when "loading..." is displayed, hide actions // repositioning after loading is not easily possible $(this).find('div.dataTableRowActions').hide(); + } else { + var $toToggle = $(this).nextUntil('.subDataTableContainer').last(); + $toToggle = $toToggle.length ? $toToggle : $(this); + $toToggle.next().toggle(); } - $(this).next().toggle(); $(this).toggleClass('expanded'); self.repositionRowActions($(this)); } @@ -1644,10 +1675,6 @@ $.extend(DataTable.prototype, UIControl.prototype, { }); }, - canHandleRowEvents: function (domElem) { - return domElem.find('table > tbody > tr').length <= this.maxNumRowsToHandleEvents; - }, - handleRowActions: function (domElem) { this.doHandleRowActions(domElem.find('table > tbody > tr')); }, @@ -1663,6 +1690,15 @@ $.extend(DataTable.prototype, UIControl.prototype, { hide: false, tooltipClass: 'small' }); + domElem.find('span.ratio').tooltip({ + track: true, + content: function() { + var title = $(this).attr('title'); + return piwikHelper.escape(title.replace(/\n/g, '<br />')); + }, + show: {delay: 700, duration: 200}, + hide: false + }) }, handleRelatedReports: function (domElem) { @@ -1828,7 +1864,7 @@ $.extend(DataTable.prototype, UIControl.prototype, { listenEvent = 'click'; } - parent.on(listenEvent, 'tr', function () { + parent.on(listenEvent, 'tr:not(.subDataTableContainer)', function () { var tr = this; var $tr = $(tr); var td = $tr.find('td.label:last'); @@ -1840,7 +1876,11 @@ $.extend(DataTable.prototype, UIControl.prototype, { } // if there are row actions, make sure the first column is not too narrow - td.css('minWidth', '145px'); + td.css('minWidth', $tr.is('.comparisonRow') ? '117px' : '145px'); + + if ($(this).is('.parentComparisonRow,.comparePeriod').length) { + return; + } if (useTouchEvent && tr.actionsDom && tr.actionsDom.prop('rowActionsVisible')) { tr.actionsDom.prop('rowActionsVisible', false); diff --git a/plugins/CoreHome/javascripts/dataTable_rowactions.js b/plugins/CoreHome/javascripts/dataTable_rowactions.js index 9d8751293b..a7272ad236 100644 --- a/plugins/CoreHome/javascripts/dataTable_rowactions.js +++ b/plugins/CoreHome/javascripts/dataTable_rowactions.js @@ -96,8 +96,8 @@ DataTable_RowActions_Registry.register({ isAvailableOnReport: function (dataTableParams) { return ( typeof dataTableParams.disable_row_evolution == 'undefined' - || dataTableParams.disable_row_evolution == "0" - ); + || dataTableParams.disable_row_evolution == "0" + ); }, isAvailableOnRow: function (dataTableParams, tr) { @@ -144,7 +144,7 @@ DataTable_RowAction.prototype.initTr = function (tr) { // API actions. For the label filter to work, we need to use the parent action. // We use jQuery events to let subtables access their parents. tr.bind(self.trEventName, function (e, params) { - self.trigger($(this), params.originalEvent, params.label); + self.trigger($(this), params.originalEvent, params.label, params.originalRow); }); }; @@ -152,7 +152,7 @@ DataTable_RowAction.prototype.initTr = function (tr) { * This method is called from the click event and the tr event (see this.trEventName). * It derives the label and calls performAction. */ -DataTable_RowAction.prototype.trigger = function (tr, e, subTableLabel) { +DataTable_RowAction.prototype.trigger = function (tr, e, subTableLabel, originalRow) { var label = this.getLabelFromTr(tr); // if we have received the event from the sub table, add the label @@ -166,7 +166,8 @@ DataTable_RowAction.prototype.trigger = function (tr, e, subTableLabel) { if (subtable.is('.subDataTable')) { subtable.closest('tr').prev().trigger(this.trEventName, { label: label, - originalEvent: e + originalEvent: e, + originalRow: tr }); return; } @@ -186,20 +187,24 @@ DataTable_RowAction.prototype.trigger = function (tr, e, subTableLabel) { } ptr.trigger(this.trEventName, { label: label, - originalEvent: e + originalEvent: e, + originalRow: tr }); return; } } } - this.performAction(label, tr, e); + this.performAction(label, tr, e, originalRow); }; /** Get the label string from a tr dom element */ DataTable_RowAction.prototype.getLabelFromTr = function (tr) { - var rowMetadata = this.getRowMetadata(tr); + if (tr.data('label')) { + return tr.data('label'); + } + var rowMetadata = this.getRowMetadata(tr); if (rowMetadata.combinedLabel) { return '@' + rowMetadata.combinedLabel; } @@ -264,6 +269,7 @@ function DataTable_RowActions_RowEvolution(dataTable) { /** The rows to be compared in multi row evolution */ this.multiEvolutionRows = []; + this.multiEvolutionRowsSeries = []; } /** Static helper method to launch row evolution from anywhere */ @@ -274,20 +280,38 @@ DataTable_RowActions_RowEvolution.launch = function (apiMethod, label) { DataTable_RowActions_RowEvolution.prototype = new DataTable_RowAction; -DataTable_RowActions_RowEvolution.prototype.performAction = function (label, tr, e) { +DataTable_RowActions_RowEvolution.prototype.performAction = function (label, tr, e, originalRow) { if (e.shiftKey) { // only mark for multi row evolution if shift key is pressed - this.addMultiEvolutionRow(label); + this.addMultiEvolutionRow(label, $(originalRow || tr).data('comparison-series')); return; } - this.addMultiEvolutionRow(label); + this.addMultiEvolutionRow(label, $(originalRow || tr).data('comparison-series')); // check whether we have rows marked for multi row evolution - var extraParams = {}; + var extraParams = $.extend({}, $(originalRow || tr).data('param-override')); + if (typeof extraParams !== 'object') { + extraParams = {}; + } + if (this.multiEvolutionRows.length > 1) { extraParams.action = 'getMultiRowEvolutionPopover'; label = this.multiEvolutionRows.join(','); + + if (this.multiEvolutionRowsSeries.length > 1) { // when comparison is active + var piwikUrl = piwikHelper.getAngularDependency('piwikUrl'); + extraParams.compareDates = piwikUrl.getSearchParam('compareDates'); + extraParams.comparePeriods = piwikUrl.getSearchParam('comparePeriods'); + extraParams.compareSegments = piwikUrl.getSearchParam('compareSegments'); + extraParams.labelSeries = this.multiEvolutionRowsSeries.join(','); + + // remove override period/date/segment since we are sending compare params so we can have the whole set of comparison + // serieses for LabelFilter + delete extraParams.period; + delete extraParams.date; + delete extraParams.segment; + } } $.each(this.dataTable.param, function (index, value) { @@ -310,9 +334,27 @@ DataTable_RowActions_RowEvolution.prototype.performAction = function (label, tr, this.openPopover(apiMethod, extraParams, label); }; -DataTable_RowActions_RowEvolution.prototype.addMultiEvolutionRow = function (label) { - if ($.inArray(label, this.multiEvolutionRows) == -1) { +DataTable_RowActions_RowEvolution.prototype.addMultiEvolutionRow = function (label, seriesIndex) { + if (typeof seriesIndex !== 'undefined') { + var self = this; + + var found = false; + this.multiEvolutionRows.forEach(function (rowLabel, index) { + var rowSeriesIndex = self.multiEvolutionRowsSeries[index]; + if (label === rowLabel && seriesIndex === rowSeriesIndex) { + found = true; + return false; + } + }); + + if (!found) { + this.multiEvolutionRows.push(label); + this.multiEvolutionRowsSeries.push(seriesIndex); + } + } else if ($.inArray(label, this.multiEvolutionRows) === -1) { this.multiEvolutionRows.push(label); + + this.multiEvolutionRowsSeries = []; // for safety, make sure state is consistent } }; @@ -374,6 +416,7 @@ DataTable_RowActions_RowEvolution.prototype.showRowEvolution = function (apiMeth Piwik_Popover.onClose(function () { // reset rows marked for multi row evolution on close self.multiEvolutionRows = []; + self.multiEvolutionRowsSeries = []; }); if (self.dataTable !== null) { diff --git a/plugins/CoreHome/javascripts/sparkline.js b/plugins/CoreHome/javascripts/sparkline.js index 87ab401863..1a7f65622a 100644 --- a/plugins/CoreHome/javascripts/sparkline.js +++ b/plugins/CoreHome/javascripts/sparkline.js @@ -13,19 +13,36 @@ var sparklineDisplayHeight = 25; var sparklineDisplayWidth = 100; piwik.getSparklineColors = function () { - return piwik.ColorManager.getColors('sparkline-colors', sparklineColorNames); + var colors = piwik.ColorManager.getColors('sparkline-colors', sparklineColorNames); + + var comparisonService = piwikHelper.getAngularDependency('piwikComparisonsService'); + if (comparisonService.isComparing()) { + var comparisons = comparisonService.getAllComparisonSeries(); + colors.lineColor = comparisons.map(function (comp) { return comp.color; }); + } + + return colors; }; // initializes each sparkline so they use colors defined in CSS piwik.initSparklines = function() { - $('.sparkline > img').each(function () { + $('.sparkline img').each(function () { var $self = $(this); if ($self.attr('src')) { return; } - var colors = JSON.stringify(piwik.getSparklineColors()); + var seriesIndices = $self.closest('.sparkline').data('series-indices'); + var sparklineColors = piwik.getSparklineColors(); + + if (seriesIndices && sparklineColors.lineColor instanceof Array) { + sparklineColors.lineColor = sparklineColors.lineColor.filter(function (c, index) { + return seriesIndices.indexOf(index) !== -1; + }); + } + + var colors = JSON.stringify(sparklineColors); var appendToSparklineUrl = '&colors=' + encodeURIComponent(colors); // Append the token_auth to the URL if it was set (eg. embed dashboard) @@ -40,8 +57,6 @@ piwik.initSparklines = function() { }; window.initializeSparklines = function () { - var sparklineUrlParamsToIgnore = ['module', 'action', 'idSite', 'period', 'date', 'showtitle', 'viewDataTable', 'forceView', 'random']; - $('.dataTableVizEvolution[data-report]').each(function () { var graph = $(this); @@ -73,16 +88,15 @@ window.initializeSparklines = function () { $this.addClass('linked'); - var params = broadcast.getValuesFromUrl(sparklineUrl); - for (var i = 0; i != sparklineUrlParamsToIgnore.length; ++i) { - delete params[sparklineUrlParamsToIgnore[i]]; - } - for (var key in params) { - if (typeof params[key] == 'undefined') { - // this happens for example with an empty segment parameter - delete params[key]; - } else { - params[key] = decodeURIComponent(params[key]); + var params = $this.data('graph-params') || {}; + if (!Object.keys(params).length) { + var urlParams = broadcast.getValuesFromUrl(sparklineUrl); + + if (urlParams.columns) { + params.columns = decodeURIComponent(urlParams.columns); + } + if (urlParams.rows) { + params.rows = decodeURIComponent(urlParams.rows); } } diff --git a/plugins/CoreHome/javascripts/uiControl.js b/plugins/CoreHome/javascripts/uiControl.js index 29b8f08711..4ffa385123 100644 --- a/plugins/CoreHome/javascripts/uiControl.js +++ b/plugins/CoreHome/javascripts/uiControl.js @@ -11,6 +11,8 @@ var exports = require('piwik/UI'); + var ARRAY_PARAM_NAMES = ['compareDates', 'comparePeriods', 'compareSegments']; + /** * Base type for Piwik UI controls. Provides functionality that all controls need (such as * cleanup on destruction). @@ -30,7 +32,9 @@ var params = JSON.parse($element.attr('data-params') || '{}'); for (var key in params) { // convert values in params that are arrays to comma separated string lists - if (params[key] instanceof Array) { + if (params[key] instanceof Array + && ARRAY_PARAM_NAMES.indexOf(key) === -1 + ) { params[key] = params[key].join(','); } } diff --git a/plugins/CoreHome/stylesheets/dataTable.less b/plugins/CoreHome/stylesheets/dataTable.less index 06ad9623f9..2478032b75 100644 --- a/plugins/CoreHome/stylesheets/dataTable.less +++ b/plugins/CoreHome/stylesheets/dataTable.less @@ -4,4 +4,5 @@ @import "dataTable/_rowActions.less"; @import "dataTable/_subDataTable.less"; @import "dataTable/_tableConfiguration.less"; -@import "dataTable/_entityTable.less";
\ No newline at end of file +@import "dataTable/_entityTable.less"; +@import "dataTable/_comparisonsTable.less";
\ No newline at end of file diff --git a/plugins/CoreHome/stylesheets/dataTable/_comparisonsTable.less b/plugins/CoreHome/stylesheets/dataTable/_comparisonsTable.less new file mode 100644 index 0000000000..aa5e3f861b --- /dev/null +++ b/plugins/CoreHome/stylesheets/dataTable/_comparisonsTable.less @@ -0,0 +1,71 @@ +.theWidgetContent .card .card-content table.dataTable, table.dataTable { + tr.comparePeriod, tr.comparisonRow { + > td:first-child { + padding-left: 45px; + } + } + + tr.parentComparisonRow { + .dataTableRowActions { + display: none !important; + } + + > td:not(.label) > * { + visibility: hidden; + } + } + + tr.comparePeriod { + .dataTableRowActions { + display: none !important; + } + + td.label { + font-weight: bold; + color: @color-silver-l60; + } + } + + td.label .prefix-numeral { + display: inline-block; + width: 20px; + } + + .totalsRow,.summaryRow { + td.label .prefix-numeral { + visibility: hidden; + } + } + + img { + margin-left: 1px; + } +} + +tr.comparePeriod, tr.comparisonRow { + &.level0 td.label { + padding-left: 44px !important; + } + + &.level1 td.label { + padding-left: 68px !important; + } + + &.level2 td.label { + padding-left: 90px !important; + } +} + +tr.parentComparisonRow { + td.label img.plusMinus { + margin-right: 8px; + } + + &.level1 td.label { + padding-left: 44px !important; + } + + &.level2 td.label { + padding-left: 68px !important; + } +} diff --git a/plugins/CoreHome/stylesheets/dataTable/_dataTable.less b/plugins/CoreHome/stylesheets/dataTable/_dataTable.less index 2fbf404861..c0e280876d 100644 --- a/plugins/CoreHome/stylesheets/dataTable/_dataTable.less +++ b/plugins/CoreHome/stylesheets/dataTable/_dataTable.less @@ -49,6 +49,11 @@ table.dataTable img { margin-left: 0.5em; } +table.dataTable tr:not(.parentComparisonRow) img.link { + position: relative; + top: 5px; +} + table.subDataTable img.link { transform: translateY(-50%); } @@ -83,7 +88,7 @@ body>.widget table.dataTable { } // for dataTables in report pages (not for widgets in the dashboard) -.theWidgetContent .card .card-content table.dataTable { +.theWidgetContent .card .card-content table.dataTable, table.dataTable, table.subDataTable { th { background: @theme-color-background-base !important; @@ -163,11 +168,17 @@ table.dataTable span.label.highlighted { font-style: italic; } +.widget div.dataTable:not(.isComparing) table.dataTable tbody > tr > td:not(.cellSubDataTable):first-child { + padding-left: 12px; +} + /* the cell containing the subdatatable */ -table.dataTable .cellSubDataTable { - margin: 0; - border-left: 0; - padding: 6px 12px 6px; +table.dataTable tr td.cellSubDataTable { + &, .widget & { + margin: 0; + border-left: 0; + padding: 6px 20px 6px; + } } .cellSubDataTable > .dataTable { @@ -445,13 +456,18 @@ tr.level0 td.label { } .widget { - tr.level0 td.label { + div.dataTable:not(.isComparing) table.dataTable tr.level0 td.label, + div.dataTable:not(.isComparing) table.dataTable th { padding-left: 12px !important; } } tr.level1 td.label { padding-left: 2.5em !important; + + .widget & { + padding-left: 28px !important; + } } tr.level2 td.label { @@ -493,7 +509,7 @@ tr.level12 td.label { /* less right margins for the link image in the Pa*/ .dataTableActions table.dataTable img.link { margin-right: 0.5em; - margin-left: 0; + margin-left: 1px; margin-bottom: 4px; vertical-align: text-bottom; } @@ -505,7 +521,7 @@ table.dataTable td.label img { tr td.label img.plusMinus { margin-left: -3px; - margin-right: 3px; + margin-right: 6px; margin-top: 0; } @@ -615,9 +631,11 @@ div.dataTableScroller { overflow-x: scroll; } -.theWidgetContent .card .card-content div.dataTableScroller { - margin-left: -20px; - width: ~"calc(100% + 40px)"; +.theWidgetContent .card .card-content, table.dataTable { + div.dataTableScroller { + margin-left: -20px; + width: ~"calc(100% + 40px)"; + } } @media only screen and (min-width: 993px) { diff --git a/plugins/CoreHome/stylesheets/dataTable/_subDataTable.less b/plugins/CoreHome/stylesheets/dataTable/_subDataTable.less index 3dcfea86da..e7f2dcda37 100644 --- a/plugins/CoreHome/stylesheets/dataTable/_subDataTable.less +++ b/plugins/CoreHome/stylesheets/dataTable/_subDataTable.less @@ -31,14 +31,6 @@ table.subDataTable td.label, table.subDataTable td.column { color: #2D2A27; } -table.subDataTable td.label { - width: 80%; -} - -table.subDataTable td.label { - padding: 5px; -} - /* are the following two supposed to be together? */ .subDataTable.dataTableFeatures { padding-top: 0; diff --git a/plugins/CoreHome/stylesheets/jqplotColors.less b/plugins/CoreHome/stylesheets/jqplotColors.less deleted file mode 100644 index 6abb9a7424..0000000000 --- a/plugins/CoreHome/stylesheets/jqplotColors.less +++ /dev/null @@ -1,150 +0,0 @@ -// evolution graph colors -.evolution-graph-colors[data-name=grid-background] { - color: #fff; -} - -.evolution-graph-colors[data-name=grid-border] { - color: #f00; -} - -.evolution-graph-colors[data-name=series1] { - color: #5170AE; -} - -.evolution-graph-colors[data-name=series2] { - color: #F29007; -} - -.evolution-graph-colors[data-name=series3] { - color: #CC3399; -} - -.evolution-graph-colors[data-name=series4] { - color: #9933CC; -} - -.evolution-graph-colors[data-name=series5] { - color: #80a033; -} - -.evolution-graph-colors[data-name=series6] { - color: #246AD2; -} - -.evolution-graph-colors[data-name=series7] { - color: #FD16EA; -} - -.evolution-graph-colors[data-name=series8] { - color: #49C100; -} - -.evolution-graph-colors[data-name=ticks] { - color: #ccc; -} - -.evolution-graph-colors[data-name=single-metric-label] { - color: @theme-color-text-lighter; -} - -// bar graph colors -.bar-graph-colors[data-name=grid-background] { - color: #fff; -} - -.bar-graph-colors[data-name=grid-border] { - color: #f00; -} - -.bar-graph-colors[data-name=series1] { - color: #5170AE; -} - -.bar-graph-colors[data-name=series2] { - color: #F3A010; -} - -.bar-graph-colors[data-name=series3] { - color: #CC3399; -} - -.bar-graph-colors[data-name=series4] { - color: #9933CC; -} - -.bar-graph-colors[data-name=series5] { - color: #80a033; -} - -.bar-graph-colors[data-name=series6] { - color: #246AD2; -} - -.bar-graph-colors[data-name=series7] { - color: #FD16EA; -} - -.bar-graph-colors[data-name=series8] { - color: #49C100; -} - -.bar-graph-colors[data-name=ticks] { - color: #ccc; -} - -.bar-graph-colors[data-name=single-metric-label] { - color: @theme-color-text-lighter; -} - -// pie graph colors -.pie-graph-colors[data-name=grid-background] { - color: #fff; -} - -.pie-graph-colors[data-name=grid-border] { - color: #f00; -} - -.pie-graph-colors[data-name=series1] { - color: #59727F; -} - -.pie-graph-colors[data-name=series2] { - color: #7DAAC0; -} - -.pie-graph-colors[data-name=series3] { - color: #7F7259; -} - -.pie-graph-colors[data-name=series4] { - color: #C09E7D; -} - -.pie-graph-colors[data-name=series5] { - color: #9BB39B; -} - -.pie-graph-colors[data-name=series6] { - color: #B1D8B3; -} - -.pie-graph-colors[data-name=series7] { - color: #B39BA7; -} - -.pie-graph-colors[data-name=series8] { - color: #D8B1C5; -} - -.pie-graph-colors[data-name=series9] { - color: #A5A5A5; -} - -.pie-graph-colors[data-name=ticks] { - color: #ccc; -} - -.pie-graph-colors[data-name=single-metric-label] { - color: @theme-color-text-lighter; -} diff --git a/plugins/CoreHome/templates/_dataTable.twig b/plugins/CoreHome/templates/_dataTable.twig index 29535d8bb8..14d52f4d1b 100644 --- a/plugins/CoreHome/templates/_dataTable.twig +++ b/plugins/CoreHome/templates/_dataTable.twig @@ -29,7 +29,7 @@ {% set summaryRowId = constant('Piwik\\DataTable::ID_SUMMARY_ROW') %}{# ID_SUMMARY_ROW #} {% set isSubtable = javascriptVariablesToSet.idSubtable is defined and javascriptVariablesToSet.idSubtable != 0 %} -<div class="dataTable {{ visualizationCssClass }} {{ properties.datatable_css_class|default('') }} {% if isSubtable %}subDataTable{% endif %}" +<div class="dataTable {{ visualizationCssClass }} {{ properties.datatable_css_class|default('') }} {% if isSubtable %}subDataTable{% endif %} {% if isComparing|default(false) %}isComparing{% endif %}" data-table-type="{{ properties.datatable_js_type }}" data-report="{{ properties.report_id }}" data-report-metadata="{{ reportMetdadata|json_encode|e('html_attr') }}" diff --git a/plugins/CoreHome/templates/_dataTableCell.twig b/plugins/CoreHome/templates/_dataTableCell.twig index 7aca38defd..7c72b57385 100644 --- a/plugins/CoreHome/templates/_dataTableCell.twig +++ b/plugins/CoreHome/templates/_dataTableCell.twig @@ -12,31 +12,14 @@ {% endif %} {% set totals = dataTable.getMetadata('totals') %} -{% if column in properties.report_ratio_columns and column in totals|keys -%} - {% set labelColumn = columns_to_display|first %} - {% set reportTotal = totals[column] %} - {% if siteSummary is defined and siteSummary is not empty and siteSummary.getFirstRow %} - {% set siteTotal = siteSummary.getFirstRow.getColumn(column) %} - {% else %} - {% set siteTotal = 0 %} - {% endif %} - {% set rowPercentage = row.getColumn(column)|percentage(reportTotal, 1) %} - {% set metricTitle = translations[column]|default(column) %} - {% set reportLabel = row.getColumn(labelColumn)|truncate(40)|rawSafeDecoded %} - - {% set reportRatioTooltip = 'General_ReportRatioTooltip'|translate(reportLabel, rowPercentage|e('html_attr'), reportTotal|e('html_attr'), metricTitle|e('html_attr'), translations[labelColumn]|default(labelColumn)|e('html_attr')) %} - - {% if siteTotal and siteTotal > reportTotal %} - {% set totalPercentage = row.getColumn(column)|percentage(siteTotal, 1) %} - {% set totalRatioTooltip = 'General_TotalRatioTooltip'|translate(totalPercentage, siteTotal|number(2,0), metricTitle) %} - {% else %} - {% set totalRatioTooltip = '' %} - {% endif %} +{% set labelColumn = columns_to_display|first %} +{% set reportLabel = row.getColumn(labelColumn)|truncate(40)|rawSafeDecoded %} +{% include "@CoreVisualizations/_dataTableViz_htmlTable_ratio.twig" with { + 'label': reportLabel, + 'labelColumn': labelColumn, + 'translations': properties.translations +} %} - <span class="ratio" - title="{{ reportRatioTooltip|raw }} {{ totalRatioTooltip|e('html_attr') }}" - > {{ rowPercentage }}</span> -{%- endif %} {% set dimensions = dataTable.getMetadata('dimensions')|default([]) %} {% if column=='label' or column in dimensions %} {% import 'macros.twig' as piwik %} diff --git a/plugins/CoreHome/templates/_dataTableHead.twig b/plugins/CoreHome/templates/_dataTableHead.twig index 719f599752..d607f7ad04 100644 --- a/plugins/CoreHome/templates/_dataTableHead.twig +++ b/plugins/CoreHome/templates/_dataTableHead.twig @@ -1,7 +1,7 @@ <thead> <tr> {% for column in properties.columns_to_display %} - <th class="{% if properties.enable_sort %}sortable{% endif %} {% if loop.first %}first{% elseif loop.last %}last{% endif %}" id="{{ column }}"> + <th class="{% if properties.enable_sort %}sortable{% endif %} {% if loop.first %}first{% elseif loop.last %}last{% endif %} {% if column == 'label' %}label{% endif %}" id="{{ column }}"> {% if properties.metrics_documentation[column]|default is not empty %} <div class="columnDocumentation"> <div class="columnDocumentationTitle"> diff --git a/plugins/CoreHome/tests/UI/SingleMetricView_spec.js b/plugins/CoreHome/tests/UI/SingleMetricView_spec.js index ac10abbebd..ce90f07f80 100644 --- a/plugins/CoreHome/tests/UI/SingleMetricView_spec.js +++ b/plugins/CoreHome/tests/UI/SingleMetricView_spec.js @@ -21,7 +21,7 @@ describe('SingleMetricView', function () { await page.click('.dashboard-manager a.title'); await (await page.jQuery('.widgetpreview-categorylist>li:contains(Goals)')).hover(); // have to mouse move twice... otherwise Live! will just be highlighted - await (await page.jQuery('.widgetpreview-categorylist > li:contains(Generic)')).hover(); + await (await page.jQuery('.widgetpreview-categorylist > li:contains(KPI Metric)')).hover(); await (await page.jQuery('.widgetpreview-widgetlist li:contains(Metric)')).hover(); await (await page.jQuery('.widgetpreview-widgetlist li:contains(Metric)')).click(); diff --git a/plugins/CorePluginsAdmin/tests/UI/expected-screenshots/TagManagerTeaser_admin_page_disable.png b/plugins/CorePluginsAdmin/tests/UI/expected-screenshots/TagManagerTeaser_admin_page_disable.png index 5e966f788d..1576b7e720 100644 --- a/plugins/CorePluginsAdmin/tests/UI/expected-screenshots/TagManagerTeaser_admin_page_disable.png +++ b/plugins/CorePluginsAdmin/tests/UI/expected-screenshots/TagManagerTeaser_admin_page_disable.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:087dd036e017afe506218fbd8f9d2458c736ce75a988193cd0f105df265e732c -size 183739 +oid sha256:1be4ae68227c322e9ce3333f96ff963f47d90d7402e47f1fcdea556cdf0aabff +size 183608 diff --git a/plugins/CoreVisualizations/JqplotDataGenerator.php b/plugins/CoreVisualizations/JqplotDataGenerator.php index d725d22b04..e120764005 100644 --- a/plugins/CoreVisualizations/JqplotDataGenerator.php +++ b/plugins/CoreVisualizations/JqplotDataGenerator.php @@ -10,11 +10,16 @@ namespace Piwik\Plugins\CoreVisualizations; use Exception; +use Piwik\API\Request; use Piwik\Common; use Piwik\DataTable; +use Piwik\DataTable\Row; use Piwik\Metrics; +use Piwik\Period\Factory; use Piwik\Piwik; +use Piwik\Plugins\API\Filter\DataComparisonFilter; use Piwik\Plugins\CoreVisualizations\JqplotDataGenerator\Chart; +use Piwik\Plugins\CoreVisualizations\Visualizations\JqplotGraph; require_once PIWIK_INCLUDE_PATH . '/plugins/CoreVisualizations/JqplotDataGenerator/Evolution.php'; @@ -26,7 +31,7 @@ require_once PIWIK_INCLUDE_PATH . '/plugins/CoreVisualizations/JqplotDataGenerat class JqplotDataGenerator { /** - * View properties. @see Piwik\ViewDataTable for more info. + * View properties. @see \Piwik\ViewDataTable for more info. * * @var array */ @@ -34,6 +39,15 @@ class JqplotDataGenerator protected $graphType; + protected $isComparing; + + private $availableSegments; + + /** + * @var JqplotGraph + */ + protected $graph; + /** * Creates a new JqplotDataGenerator instance for a graph type and view properties. * @@ -42,14 +56,14 @@ class JqplotDataGenerator * @throws \Exception * @return JqplotDataGenerator */ - public static function factory($type, $properties) + public static function factory($type, $properties, JqplotGraph $graph) { switch ($type) { case 'evolution': - return new JqplotDataGenerator\Evolution($properties, $type); + return new JqplotDataGenerator\Evolution($properties, $type, $graph); case 'pie': case 'bar': - return new JqplotDataGenerator($properties, $type); + return new JqplotDataGenerator($properties, $type, $graph); default: throw new Exception("Unknown JqplotDataGenerator type '$type'."); } @@ -63,10 +77,14 @@ class JqplotDataGenerator * * @internal param \Piwik\Plugin\ViewDataTable $visualization */ - public function __construct($properties, $graphType) + public function __construct($properties, $graphType, JqplotGraph $graph) { $this->properties = $properties; $this->graphType = $graphType; + $this->isComparing = $graph->isComparing(); + $this->graph = $graph; + + $this->availableSegments = Request::processRequest('SegmentEditor.getAll', $override = [], $default = []); } /** @@ -80,12 +98,6 @@ class JqplotDataGenerator $visualization = new Chart(); if ($dataTable->getRowsCount() > 0) { - // if addTotalRow was called in GenerateGraphHTML, add a row containing totals of - // different metrics - if ($this->properties['add_total_row']) { - $dataTable->queueFilter('AddSummaryRow', Piwik::translate('General_Total')); - } - $dataTable->applyQueuedFilters(); $this->initChartObjectData($dataTable, $visualization); } @@ -101,47 +113,123 @@ class JqplotDataGenerator { $xLabels = $dataTable->getColumn('label'); - $columnNames = $this->properties['columns_to_display']; - if (($labelColumnIndex = array_search('label', $columnNames)) !== false) { - unset($columnNames[$labelColumnIndex]); + $columnsToDisplay = array_values($this->properties['columns_to_display']); + if (($labelColumnIndex = array_search('label', $columnsToDisplay)) !== false) { + unset($columnsToDisplay[$labelColumnIndex]); } - $columnNameToTranslation = $columnNameToValue = array(); - foreach ($columnNames as $columnName) { - $columnNameToTranslation[$columnName] = @$this->properties['translations'][$columnName]; - - $columnNameToValue[$columnName] = $dataTable->getColumn($columnName); + $seriesMetadata = null; + if ($this->isComparing) { + list($yLabels, $serieses, $seriesMetadata) = $this->getComparisonTableSerieses($dataTable, $columnsToDisplay); + } else { + list($yLabels, $serieses) = $this->getMainTableSerieses($dataTable, $columnsToDisplay); } $visualization->dataTable = $dataTable; $visualization->properties = $this->properties; $visualization->setAxisXLabels($xLabels); - $visualization->setAxisYValues($columnNameToValue); - $visualization->setAxisYLabels($columnNameToTranslation); + $visualization->setAxisYValues($serieses, $seriesMetadata); + $visualization->setAxisYLabels($yLabels); - $units = $this->getUnitsForColumnsToDisplay(); + $units = $this->getUnitsForSerieses($yLabels); $visualization->setAxisYUnits($units); } - protected function getUnitsForColumnsToDisplay() + private function getMainTableSerieses(DataTable $dataTable, $columnNames) + { + $columnNameToTranslation = []; + + foreach ($columnNames as $columnName) { + $columnNameToTranslation[$columnName] = @$this->properties['translations'][$columnName]; + } + + $columnNameToValue = array(); + foreach ($columnNames as $columnName) { + $columnNameToValue[$columnName] = $dataTable->getColumn($columnName); + } + + return [$columnNameToTranslation, $columnNameToValue]; + } + + private function getComparisonTableSerieses(DataTable $dataTable, $columnsToDisplay) + { + $seriesLabels = []; + $serieses = []; + $seriesMetadata = []; + + $seriesIndices = []; + + foreach ($dataTable->getRows() as $row) { + /** @var DataTable $comparisonTable */ + $comparisonTable = $row->getComparisons(); + if (empty($comparisonTable)) { + continue; + } + + foreach ($comparisonTable->getRows() as $index => $compareRow) { + foreach ($columnsToDisplay as $columnIndex => $columnName) { + $seriesId = $columnName . '|' . $index; + + if (!isset($seriesIndices[$seriesId])) { + $seriesIndices[$seriesId] = count($seriesIndices); + } + + $seriesLabel = $this->getComparisonSeriesLabel($compareRow, $columnName); + $seriesLabels[$seriesId] = $seriesLabel; + $serieses[$seriesId][] = $compareRow->getColumn($columnName); + + $seriesMetadata[$seriesId] = [ + 'seriesIndex' => $seriesIndices[$seriesId], + 'metricIndex' => $columnIndex, + ]; + } + } + } + + return [$seriesLabels, $serieses, $seriesMetadata]; + } + + protected function getComparisonSeriesLabel(Row $compareRow, $columnName, $rowLabel = false) + { + return $this->getComparisonSeriesLabelFromCompareSeries($compareRow->getMetadata('compareSeriesPretty'), $columnName, $rowLabel); + } + + protected function getComparisonSeriesLabelFromCompareSeries($compareSeriesPretty, $columnName, $rowLabel = false) + { + $columnTranslation = @$this->properties['translations'][$columnName]; + + if (empty($rowLabel)) { + $label = $columnTranslation; + } else { + $label = "$rowLabel ($columnTranslation)"; + } + + $label .= ' ' . $compareSeriesPretty; + return $label; + } + + protected function getUnitsForSerieses($yLabels) { // derive units from column names - $units = $this->deriveUnitsFromRequestedColumnNames(); + $units = $this->deriveUnitsFromRequestedColumnNames($yLabels); if (!empty($this->properties['y_axis_unit'])) { $units = array_fill(0, count($units), $this->properties['y_axis_unit']); } return $units; } - private function deriveUnitsFromRequestedColumnNames() + private function deriveUnitsFromRequestedColumnNames($yLabels) { $idSite = Common::getRequestVar('idSite', null, 'int'); $units = array(); - foreach ($this->properties['columns_to_display'] as $columnName) { + foreach ($yLabels as $seriesId => $ignore) { + $parts = explode('|', $seriesId, 2); + $columnName = $parts[0]; + $derivedUnit = Metrics::getUnit($columnName, $idSite); - $units[$columnName] = empty($derivedUnit) ? false : $derivedUnit; + $units[$seriesId] = empty($derivedUnit) ? false : $derivedUnit; } return $units; } diff --git a/plugins/CoreVisualizations/JqplotDataGenerator/Chart.php b/plugins/CoreVisualizations/JqplotDataGenerator/Chart.php index 44be4af356..2ac4b6f8d7 100644 --- a/plugins/CoreVisualizations/JqplotDataGenerator/Chart.php +++ b/plugins/CoreVisualizations/JqplotDataGenerator/Chart.php @@ -25,14 +25,16 @@ class Chart // temporary public $properties; - public function setAxisXLabels($xLabels) + public function setAxisXLabels($xLabels, $xTicks = null, $index = 0) { + $axisName = $this->getXAxis($index); + $xSteps = $this->properties['x_axis_step_size']; $showAllTicks = $this->properties['show_all_ticks']; - $this->axes['xaxis']['labels'] = array_values($xLabels); + $this->axes[$axisName]['labels'] = array_values($xLabels); - $ticks = array_values($xLabels); + $ticks = array_values($xTicks ?: $xLabels); if (!$showAllTicks) { // unset labels so there are $xSteps number of blank ticks between labels @@ -42,7 +44,7 @@ class Chart } } } - $this->axes['xaxis']['ticks'] = $ticks; + $this->axes[$axisName]['ticks'] = $ticks; } public function setAxisXOnClick(&$onClick) @@ -50,14 +52,20 @@ class Chart $this->axes['xaxis']['onclick'] = & $onClick; } - public function setAxisYValues(&$values) + public function setAxisYValues(&$values, $seriesMetadata = null) { foreach ($values as $label => &$data) { - $this->series[] = array( + $seriesInfo = array( 'label' => $label, - 'internalLabel' => $label + 'internalLabel' => $label, ); + if (isset($seriesMetadata[$label])) { + $seriesInfo = array_merge($seriesInfo, $seriesMetadata[$label]); + } + + $this->series[] = $seriesInfo; + array_walk($data, function (&$v) { $v = (float) Common::forceDotAsSeparatorForDecimalPoint($v); }); @@ -116,4 +124,28 @@ class Chart return $data; } + + public function setAxisXLabelsMultiple($xLabels, $seriesToXAxis, $ticks = null) + { + foreach ($xLabels as $index => $labels) { + $this->setAxisXLabels($labels, $ticks === null ? null : $ticks[$index], $index); + } + + foreach ($seriesToXAxis as $seriesIndex => $xAxisIndex) { + $axisName = $this->getXAxis($xAxisIndex); + + // don't actually set xaxis otherwise jqplot will show too many axes. however, we need the xaxis labels, so we add them + // to the jqplot config + $this->series[$seriesIndex]['_xaxis'] = $axisName; + } + } + + private function getXAxis($index) + { + $axisName = 'xaxis'; + if ($index != 0) { + $axisName = 'x' . ($index + 1) . 'axis'; + } + return $axisName; + } } diff --git a/plugins/CoreVisualizations/JqplotDataGenerator/Evolution.php b/plugins/CoreVisualizations/JqplotDataGenerator/Evolution.php index cae2577965..814903265b 100644 --- a/plugins/CoreVisualizations/JqplotDataGenerator/Evolution.php +++ b/plugins/CoreVisualizations/JqplotDataGenerator/Evolution.php @@ -11,6 +11,13 @@ namespace Piwik\Plugins\CoreVisualizations\JqplotDataGenerator; use Piwik\Archive\DataTableFactory; use Piwik\Common; use Piwik\DataTable; +use Piwik\DataTable\DataTableInterface; +use Piwik\DataTable\Row; +use Piwik\Date; +use Piwik\Metrics; +use Piwik\Period; +use Piwik\Period\Factory; +use Piwik\Plugins\API\Filter\DataComparisonFilter; use Piwik\Plugins\CoreVisualizations\JqplotDataGenerator; use Piwik\Url; @@ -19,6 +26,18 @@ use Piwik\Url; */ class Evolution extends JqplotDataGenerator { + protected function getUnitsForColumnsToDisplay() + { + $idSite = Common::getRequestVar('idSite', null, 'int'); + + $units = []; + foreach ($this->properties['columns_to_display'] as $columnName) { + $derivedUnit = Metrics::getUnit($columnName, $idSite); + $units[$columnName] = empty($derivedUnit) ? false : $derivedUnit; + } + return $units; + } + /** * @param DataTable|DataTable\Map $dataTable * @param Chart $visualization @@ -33,11 +52,16 @@ class Evolution extends JqplotDataGenerator return; } - // the X label is extracted from the 'period' object in the table's metadata - $xLabels = array(); - foreach ($dataTable->getDataTables() as $metadataDataTable) { - $xLabels[] = $metadataDataTable->getMetadata(DataTableFactory::TABLE_METADATA_PERIOD_INDEX)->getLocalizedShortString(); // eg. "Aug 2009" - } + $dataTables = $dataTable->getDataTables(); + + // determine x labels based on both the displayed date range and the compared periods + /** @var Period[][] $xLabels */ + $xLabels = [ + [], // placeholder for first series + ]; + + $this->addComparisonXLabels($xLabels, reset($dataTables)); + $this->addSelectedSeriesXLabels($xLabels, $dataTables); $units = $this->getUnitsForColumnsToDisplay(); @@ -47,27 +71,37 @@ class Evolution extends JqplotDataGenerator ? : array(false) // make sure that a series is plotted even if there is no data ; + $columnsToDisplay = array_values($this->properties['columns_to_display']); + + list($seriesMetadata, $seriesUnits, $seriesLabels, $seriesToXAxis) = + $this->getSeriesMetadata($rowsToDisplay, $columnsToDisplay, $units, $dataTables); + // collect series data to show. each row-to-display/column-to-display permutation creates a series. $allSeriesData = array(); - $seriesUnits = array(); foreach ($rowsToDisplay as $rowLabel) { - foreach ($this->properties['columns_to_display'] as $columnName) { - $seriesLabel = $this->getSeriesLabel($rowLabel, $columnName); - $seriesData = $this->getSeriesData($rowLabel, $columnName, $dataTable); - - $allSeriesData[$seriesLabel] = $seriesData; - $seriesUnits[$seriesLabel] = $units[$columnName]; + foreach ($columnsToDisplay as $columnName) { + if (!$this->isComparing) { + $this->setNonComparisonSeriesData($allSeriesData, $rowLabel, $columnName, $dataTable); + } else { + $this->setComparisonSeriesData($allSeriesData, $seriesLabels, $rowLabel, $columnName, $dataTable); + } } } $visualization->dataTable = $dataTable; $visualization->properties = $this->properties; - $visualization->setAxisXLabels($xLabels); - $visualization->setAxisYValues($allSeriesData); + $visualization->setAxisYValues($allSeriesData, $seriesMetadata); $visualization->setAxisYUnits($seriesUnits); - $dataTables = $dataTable->getDataTables(); + $xLabelStrs = []; + $xAxisTicks = []; + foreach ($xLabels as $index => $seriesXLabels) { + $xLabelStrs[$index] = array_map(function (Period $p) { return $p->getLocalizedLongString(); }, $seriesXLabels); + $xAxisTicks[$index] = array_map(function (Period $p) { return $p->getLocalizedShortString(); }, $seriesXLabels); + } + + $visualization->setAxisXLabelsMultiple($xLabelStrs, $seriesToXAxis, $xAxisTicks); if ($this->isLinkEnabled()) { $idSite = Common::getRequestVar('idSite', null, 'int'); @@ -144,4 +178,131 @@ class Evolution extends JqplotDataGenerator } return $linkEnabled; } + + /** + * 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). + * 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 + * out the selected period x axis, in case it is shorter than one of the compared periods (as in the example above). + */ + private function addComparisonXLabels(array &$xLabels, DataTable $table) + { + $comparePeriods = $table->getMetadata('comparePeriods') ?: []; + $compareDates = $table->getMetadata('compareDates') ?: []; + + // get rid of selected period + array_shift($comparePeriods); + array_shift($compareDates); + + foreach (array_values($comparePeriods) as $index => $period) { + $date = $compareDates[$index]; + + $range = Factory::build($period, $date); + foreach ($range->getSubperiods() as $subperiod) { + $xLabels[$index + 1][] = $subperiod; + } + } + } + + /** + * @param array $xLabels + * @param DataTable[] $dataTables + * @throws \Exception + */ + private function addSelectedSeriesXLabels(array &$xLabels, array $dataTables) + { + $xTicksCount = count($dataTables); + foreach ($xLabels as $labelSeries) { + $xTicksCount = max(count($labelSeries), $xTicksCount); + } + + /** @var Date $startDate */ + $startDate = reset($dataTables)->getMetadata(DataTableFactory::TABLE_METADATA_PERIOD_INDEX)->getDateStart(); + $periodType = reset($dataTables)->getMetadata(DataTableFactory::TABLE_METADATA_PERIOD_INDEX)->getLabel(); + + for ($i = 0; $i < $xTicksCount; ++$i) { + $period = Factory::build($periodType, $startDate->addPeriod($i, $periodType)); + $xLabels[0][] = $period; + } + } + + private function setNonComparisonSeriesData(array &$allSeriesData, $rowLabel, $columnName, DataTable\Map $dataTable) + { + $seriesLabel = $this->getSeriesLabel($rowLabel, $columnName); + + $seriesData = $this->getSeriesData($rowLabel, $columnName, $dataTable); + $allSeriesData[$seriesLabel] = $seriesData; + } + + private function setComparisonSeriesData(array &$allSeriesData, array $seriesLabels, $rowLabel, $columnName, DataTable\Map $dataTable) + { + foreach ($dataTable->getDataTables() as $label => $childTable) { + // get the row for this label (use the first if $rowLabel is false) + if ($rowLabel === false) { + $row = $childTable->getFirstRow(); + } else { + $row = $childTable->getRowFromLabel($rowLabel); + } + + if (empty($row) + || empty($row->getComparisons()) + ) { + foreach ($seriesLabels as $seriesIndex => $seriesLabelPrefix) { + $wholeSeriesLabel = $this->getComparisonSeriesLabelFromCompareSeries($seriesLabelPrefix, $columnName, $rowLabel); + $allSeriesData[$wholeSeriesLabel][] = 0; + } + + continue; + } + + /** @var DataTable $comparisonTable */ + $comparisonTable = $row->getComparisons(); + foreach ($comparisonTable->getRows() as $compareRow) { + $seriesLabel = $this->getComparisonSeriesLabel($compareRow, $columnName, $rowLabel); + $allSeriesData[$seriesLabel][] = $compareRow->getColumn($columnName); + } + + $totalsRow = $comparisonTable->getTotalsRow(); + if ($totalsRow) { + $seriesLabel = $this->getComparisonSeriesLabel($totalsRow, $columnName, $rowLabel); + $allSeriesData[$seriesLabel][] = $totalsRow->getColumn($columnName); + } + } + } + + private function getSeriesMetadata(array $rowsToDisplay, array $columnsToDisplay, array $units, array $dataTables) + { + $seriesMetadata = null; // maps series labels to any metadata of the series + $seriesUnits = array(); // maps series labels to unit labels + $seriesToXAxis = []; // maps series index to x-axis index (groups of metrics for a single comparison will use the same x-axis) + + $table = reset($dataTables); + $seriesLabels = $table->getMetadata('comparisonSeries') ?: []; + foreach ($rowsToDisplay as $rowIndex => $rowLabel) { + foreach ($columnsToDisplay as $columnIndex => $columnName) { + if ($this->isComparing) { + foreach ($seriesLabels as $seriesIndex => $seriesLabel) { + $wholeSeriesLabel = $this->getComparisonSeriesLabelFromCompareSeries($seriesLabel, $columnName, $rowLabel); + + $allSeriesData[$wholeSeriesLabel] = []; + + $metricIndex = $rowIndex * count($columnsToDisplay) + $columnIndex; + $seriesMetadata[$wholeSeriesLabel] = [ + 'metricIndex' => $metricIndex, + 'seriesIndex' => $seriesIndex, + ]; + + $seriesUnits[$wholeSeriesLabel] = $units[$columnName]; + + list($periodIndex, $segmentIndex) = DataComparisonFilter::getIndividualComparisonRowIndices($table, $seriesIndex); + $seriesToXAxis[] = $periodIndex; + } + } else { + $seriesLabel = $this->getSeriesLabel($rowLabel, $columnName); + $seriesUnits[$seriesLabel] = $units[$columnName]; + } + } + } + + return [$seriesMetadata, $seriesUnits, $seriesLabels, $seriesToXAxis]; + } } diff --git a/plugins/CoreVisualizations/Visualizations/Graph.php b/plugins/CoreVisualizations/Visualizations/Graph.php index f7767d9527..bc37ea8747 100644 --- a/plugins/CoreVisualizations/Visualizations/Graph.php +++ b/plugins/CoreVisualizations/Visualizations/Graph.php @@ -63,6 +63,14 @@ abstract class Graph extends Visualization $this->requestConfig->request_parameters_to_modify['format_metrics'] = 1; + // if addTotalRow was called in GenerateGraphHTML, add a row containing totals of + // different metrics + if ($this->config->add_total_row) { + $this->requestConfig->request_parameters_to_modify['totals'] = 1; + $this->requestConfig->request_parameters_to_modify['keep_totals_row'] = 1; + $this->requestConfig->request_parameters_to_modify['keep_totals_row_label'] = Piwik::translate('General_Total'); + } + $this->metricsFormatter = new Numeric(); } diff --git a/plugins/CoreVisualizations/Visualizations/HtmlTable.php b/plugins/CoreVisualizations/Visualizations/HtmlTable.php index 4bf717fd1b..a07cc6af2b 100644 --- a/plugins/CoreVisualizations/Visualizations/HtmlTable.php +++ b/plugins/CoreVisualizations/Visualizations/HtmlTable.php @@ -42,6 +42,15 @@ class HtmlTable extends Visualization public function beforeLoadDataTable() { $this->checkRequestIsNotForMultiplePeriods(); + + if ($this->isComparing()) { + // forward the comparisonIdSubtables var if present so it will be used when next/prev links are clicked + $comparisonIdSubtables = Common::getRequestVar('comparisonIdSubtables', false, 'string'); + if (!empty($comparisonIdSubtables)) { + $comparisonIdSubtables = Common::unsanitizeInputValue($comparisonIdSubtables); + $this->config->custom_parameters['comparisonIdSubtables'] = $comparisonIdSubtables; + } + } } public function beforeRender() @@ -96,6 +105,12 @@ class HtmlTable extends Visualization $this->config->columns_to_display = $this->dataTable->getColumns(); } + if ($this->isComparing() + && !empty($this->dataTable) + ) { + $this->assignTemplateVar('comparisonTotals', $this->dataTable->getMetadata('comparisonTotals')); + } + // Note: This needs to be done right before rendering, as otherwise some plugins might change the columns to display again if ($this->isFlattened()) { $dimensions = $this->dataTable->getMetadata('dimensions'); @@ -144,6 +159,11 @@ class HtmlTable extends Visualization }); } } + + $this->assignTemplateVar('segmentTitlePretty', $this->dataTable->getMetadata('segmentPretty')); + + $period = $this->dataTable->getMetadata('period'); + $this->assignTemplateVar('periodTitlePretty', $period ? $period->getLocalizedShortString() : ''); } public function beforeGenericFiltersAreAppliedToLoadedDataTable() @@ -212,6 +232,11 @@ class HtmlTable extends Visualization return null; } + public function supportsComparison() + { + return true; + } + protected function isFlattened() { return $this->requestConfig->flat || Common::getRequestVar('flat', ''); diff --git a/plugins/CoreVisualizations/Visualizations/JqplotGraph/Bar.php b/plugins/CoreVisualizations/Visualizations/JqplotGraph/Bar.php index 579016f122..0da05ea68e 100644 --- a/plugins/CoreVisualizations/Visualizations/JqplotGraph/Bar.php +++ b/plugins/CoreVisualizations/Visualizations/JqplotGraph/Bar.php @@ -50,6 +50,11 @@ class Bar extends JqplotGraph protected function makeDataGenerator($properties) { - return JqplotDataGenerator::factory('bar', $properties); + return JqplotDataGenerator::factory('bar', $properties, $this); + } + + public function supportsComparison() + { + return true; } } diff --git a/plugins/CoreVisualizations/Visualizations/JqplotGraph/Evolution.php b/plugins/CoreVisualizations/Visualizations/JqplotGraph/Evolution.php index 5de7fc5114..337bb233b2 100644 --- a/plugins/CoreVisualizations/Visualizations/JqplotGraph/Evolution.php +++ b/plugins/CoreVisualizations/Visualizations/JqplotGraph/Evolution.php @@ -9,8 +9,11 @@ namespace Piwik\Plugins\CoreVisualizations\Visualizations\JqplotGraph; +use Piwik\API\Request as ApiRequest; use Piwik\Common; use Piwik\DataTable; +use Piwik\Period; +use Piwik\Period\Factory; use Piwik\Period\Range; use Piwik\Plugins\CoreVisualizations\JqplotDataGenerator; use Piwik\Plugins\CoreVisualizations\Visualizations\JqplotGraph; @@ -43,7 +46,9 @@ class Evolution extends JqplotGraph public function beforeLoadDataTable() { - $this->calculateEvolutionDateRange(); + if (!$this->isComparing()) { + $this->calculateEvolutionDateRange(); + } parent::beforeLoadDataTable(); @@ -55,6 +60,33 @@ class Evolution extends JqplotGraph } $this->config->custom_parameters['columns'] = $this->config->columns_to_display; + + if ($this->isComparing()) { + $requestArray = $this->request->getRequestArray(); + $requestArray = ApiRequest::getRequestArrayFromString($requestArray); + + $requestingPeriod = Factory::build($requestArray['period'], $requestArray['date']); + + $this->requestConfig->request_parameters_to_modify['period'] = 'day'; + $this->requestConfig->request_parameters_to_modify['date'] = $requestingPeriod->getDateStart()->toString() . ',' . $requestingPeriod->getDateEnd()->toString(); + + if (!empty($requestArray['comparePeriods'])) { + foreach ($requestArray['comparePeriods'] as $index => $comparePeriod) { + $compareDate = $requestArray['compareDates'][$index]; + if (Period::isMultiplePeriod($compareDate, $comparePeriod)) { + continue; + } + + $comparePeriodObj = Factory::build($comparePeriod, $compareDate); + + $requestArray['comparePeriods'][$index] = 'day'; + $requestArray['compareDates'][$index] = $comparePeriodObj->getRangeString(); + } + + $this->requestConfig->request_parameters_to_modify['compareDates'] = $requestArray['compareDates']; + $this->requestConfig->request_parameters_to_modify['comparePeriods'] = $requestArray['comparePeriods']; + } + } } public function afterAllFiltersAreApplied() @@ -70,7 +102,7 @@ class Evolution extends JqplotGraph protected function makeDataGenerator($properties) { - return JqplotDataGenerator::factory('evolution', $properties); + return JqplotDataGenerator::factory('evolution', $properties, $this); } /** @@ -199,4 +231,9 @@ class Evolution extends JqplotGraph return ceil($paddedCount / $steps); } + + public function supportsComparison() + { + return true; + } } diff --git a/plugins/CoreVisualizations/Visualizations/JqplotGraph/Pie.php b/plugins/CoreVisualizations/Visualizations/JqplotGraph/Pie.php index 3fafdd5e7b..045047049f 100644 --- a/plugins/CoreVisualizations/Visualizations/JqplotGraph/Pie.php +++ b/plugins/CoreVisualizations/Visualizations/JqplotGraph/Pie.php @@ -55,6 +55,6 @@ class Pie extends JqplotGraph protected function makeDataGenerator($properties) { - return JqplotDataGenerator::factory('pie', $properties); + return JqplotDataGenerator::factory('pie', $properties, $this); } } diff --git a/plugins/CoreVisualizations/Visualizations/Sparkline.php b/plugins/CoreVisualizations/Visualizations/Sparkline.php index a2bdb09d2c..b59a89ba5d 100644 --- a/plugins/CoreVisualizations/Visualizations/Sparkline.php +++ b/plugins/CoreVisualizations/Visualizations/Sparkline.php @@ -11,7 +11,9 @@ namespace Piwik\Plugins\CoreVisualizations\Visualizations; use Exception; use Piwik\Common; use Piwik\DataTable; +use Piwik\Period; use Piwik\Plugin\ViewDataTable; +use Piwik\Site; /** * Reads the requested DataTable from the API and prepare data for the Sparkline view. @@ -21,6 +23,11 @@ class Sparkline extends ViewDataTable { const ID = 'sparkline'; + public function supportsComparison() + { + return true; + } + /** * @see ViewDataTable::main() * @return mixed @@ -29,22 +36,61 @@ class Sparkline extends ViewDataTable { // If period=range, we force the sparkline to draw daily data points $period = Common::getRequestVar('period'); - if ($period == 'range') { + $date = Common::getRequestVar('date'); + + if ($period == 'range' + || $this->isComparing() + ) { + $periodObj = Period\Factory::build($period, $date); $_GET['period'] = 'day'; + $_GET['date'] = $periodObj->getRangeString(); + } + + if ($this->isComparing()) { + $this->transformSingleComparisonPeriods(); } $this->loadDataTableFromAPI(); // then revert the hack for potentially subsequent getRequestVar $_GET['period'] = $period; + $_GET['date'] = $date; - $values = $this->getValuesFromDataTable($this->dataTable); - if (empty($values)) { - $values = array_fill(0, 30, 0); - } + $columnToPlot = $this->getColumnToPlot(); $graph = new \Piwik\Visualization\Sparkline(); - $graph->setValues($values); + + if ($this->isComparing()) { + $otherSeries = []; + + $comparisonSeries = $this->getComparisonSeries($this->dataTable); + foreach ($comparisonSeries as $seriesName) { + $otherSeries[$seriesName] = []; + } + + $this->dataTable->filter(function (DataTable $table) use ($comparisonSeries, &$otherSeries, $columnToPlot) { + foreach ($table->getRows() as $row) { + $comparisons = $row->getComparisons(); + if (empty($comparisons)) { + continue; + } + + foreach ($comparisons->getRows() as $comparisonRow) { + $compareSeriesPretty = $comparisonRow->getMetadata('compareSeriesPretty'); + $otherSeries[$compareSeriesPretty][] = $comparisonRow->getColumn($columnToPlot); + } + } + }); + + foreach ($otherSeries as $seriesValues) { + $seriesValues = $this->ensureValuesEvenIfEmpty($seriesValues); + $graph->addSeries($seriesValues); + } + } else { + $values = $this->getValuesFromDataTable($this->dataTable, $columnToPlot); + $values = $this->ensureValuesEvenIfEmpty($values); + $graph->addSeries($values); + } $height = Common::getRequestVar('height', 0, 'int'); if (!empty($height)) { @@ -100,7 +146,7 @@ class Sparkline extends ViewDataTable return $values; } - protected function getValuesFromDataTable($dataTable) + private function getColumnToPlot() { $columns = $this->config->columns_to_display; @@ -113,6 +159,11 @@ class Sparkline extends ViewDataTable } } + return $columnToPlot; + } + + protected function getValuesFromDataTable($dataTable, $columnToPlot) + { // a Set is returned when using the normal code path to request data from Archives, in all core plugins // however plugins can also return simple datatable, hence why the sparkline can accept both data types if ($this->dataTable instanceof DataTable\Map) { @@ -125,4 +176,41 @@ class Sparkline extends ViewDataTable return $values; } + + private function ensureValuesEvenIfEmpty(array $values) + { + if (empty($values)) { + return array_fill(0, 30, 0); + } + return $values; + } + + private function getComparisonSeries(DataTable\DataTableInterface $dataTable) + { + if ($dataTable instanceof DataTable\Map) { + return reset($dataTable->getDataTables())->getMetadata('comparisonSeries') ?: []; + } else { + return $dataTable->getMetadata('comparisonSeries') ?: []; + } + } + + private function transformSingleComparisonPeriods() + { + $comparePeriods = Common::getRequestVar('comparePeriods', $default = [], $type = 'array'); + $compareDates = Common::getRequestVar('compareDates', $default = [], $type = 'array'); + + foreach ($comparePeriods as $index => $comparePeriod) { + $compareDate = $compareDates[$index]; + if (Period::isMultiplePeriod($compareDate, $comparePeriod)) { + continue; + } + + $periodObj = Period\Factory::build($comparePeriod, $compareDate); + $comparePeriods[$index] = 'day'; + $compareDates[$index] = $periodObj->getRangeString(); + } + + $this->requestConfig->request_parameters_to_modify['comparePeriods'] = $comparePeriods; + $this->requestConfig->request_parameters_to_modify['compareDates'] = $compareDates; + } } diff --git a/plugins/CoreVisualizations/Visualizations/Sparklines.php b/plugins/CoreVisualizations/Visualizations/Sparklines.php index 2d11723bef..c3d8556e00 100644 --- a/plugins/CoreVisualizations/Visualizations/Sparklines.php +++ b/plugins/CoreVisualizations/Visualizations/Sparklines.php @@ -8,10 +8,14 @@ */ namespace Piwik\Plugins\CoreVisualizations\Visualizations; +use Piwik\API\Request; use Piwik\Common; use Piwik\DataTable; use Piwik\Metrics; +use Piwik\Period\Factory; use Piwik\Plugin\ViewDataTable; +use Piwik\Plugins\API\Filter\DataComparisonFilter; +use Piwik\SettingsPiwik; use Piwik\Url; use Piwik\View; @@ -48,6 +52,11 @@ class Sparklines extends ViewDataTable return new Sparklines\Config(); } + public function supportsComparison() + { + return true; + } + /** * @see ViewDataTable::main() * @return mixed @@ -82,6 +91,7 @@ class Sparklines extends ViewDataTable $view->titleAttributes = $this->config->title_attributes; $view->footerMessage = $this->config->show_footer_message; $view->areSparklinesLinkable = $this->config->areSparklinesLinkable(); + $view->isComparing = $this->isComparing(); $view->title = ''; if ($this->config->show_title) { @@ -103,13 +113,35 @@ class Sparklines extends ViewDataTable } } - $translations = $this->config->translations; - $firstRow = $data->getFirstRow(); + $comparisons = $firstRow->getComparisons(); + + $originalDate = Common::getRequestVar('date'); + $originalPeriod = Common::getRequestVar('period'); + + if ($this->isComparing() && !empty($comparisons)) { + $comparisonRows = []; + foreach ($comparisons->getRows() as $comparisonRow) { + $segment = $comparisonRow->getMetadata('compareSegment'); + if ($segment === false) { + $segment = Request::getRawSegmentFromRequest() ?: '';; + } - foreach ($this->config->getSparklineMetrics() as $sparklineMetric) { + $date = $comparisonRow->getMetadata('compareDate'); + $period = $comparisonRow->getMetadata('comparePeriod'); + + $comparisonRows[$segment][$period][$date] = $comparisonRow; + } + } + + foreach ($this->config->getSparklineMetrics() as $sparklineMetricIndex => $sparklineMetric) { $column = $sparklineMetric['columns']; $order = $sparklineMetric['order']; + $graphParams = $sparklineMetric['graphParams']; + + if (!isset($order)) { + $order = 1000; + } if ($column === 'label') { continue; @@ -120,31 +152,87 @@ class Sparklines extends ViewDataTable continue; } - if (!is_array($column)) { - $column = array($column); - } + $sparklineUrlParams = array( + 'columns' => $column, + 'module' => $this->requestConfig->getApiModuleToRequest(), + 'action' => $this->requestConfig->getApiMethodToRequest() + ); + + if ($this->isComparing() && !empty($comparisons)) { + $periodObj = Factory::build($originalPeriod, $originalDate); + + $sparklineUrlParams['compareSegments'] = []; + + $comparePeriods = $data->getMetadata('comparePeriods'); + $compareDates = $data->getMetadata('compareDates'); + + $compareSegments = $data->getMetadata('compareSegments'); + foreach ($compareSegments as $segmentIndex => $segment) { + $metrics = []; + $seriesIndices = []; - $values = array(); - $descriptions = array(); + foreach ($comparePeriods as $periodIndex => $period) { + $date = $compareDates[$periodIndex]; - foreach ($column as $col) { - $value = $firstRow->getColumn($col); + $compareRow = $comparisonRows[$segment][$period][$date]; + $segmentPretty = $compareRow->getMetadata('compareSegmentPretty'); + $periodPretty = $compareRow->getMetadata('comparePeriodPretty'); - if ($value === false) { - $value = 0; + $columnToUse = $this->removeUniqueVisitorsIfNotEnabledForPeriod($column, $period); + list($compareValues, $compareDescriptions, $evolutions) = $this->getValuesAndDescriptions($compareRow, $columnToUse); + + foreach ($compareValues as $i => $value) { + $metricInfo = [ + 'value' => $value, + 'description' => $compareDescriptions[$i], + 'group' => $periodPretty, + ]; + + if ($periodIndex > 0 + && isset($evolutions[$i]) + ) { + $metricInfo['evolution'] = $evolutions[$i]; + } + + $metrics[] = $metricInfo; + } + + $seriesIndices[] = DataComparisonFilter::getComparisonSeriesIndex($data, $periodIndex, $segmentIndex); + } + + // only set the title (which is the segment) if comparing more than one segment + $title = count($compareSegments) > 1 ? $segmentPretty : null; + + $params = array_merge($sparklineUrlParams, [ + 'segment' => $segment, + 'period' => $periodObj->getLabel(), + 'date' => $periodObj->getRangeString(), + ]); + $this->config->addSparkline($params, $metrics, $desc = null, null, ($order * 100) + $segmentIndex, $title, $sparklineMetricIndex, $seriesIndices, $graphParams); } + } else { + list($values, $descriptions) = $this->getValuesAndDescriptions($firstRow, $column); - $values[] = $value; - $descriptions[] = isset($translations[$col]) ? $translations[$col] : $col; - } + $metrics = []; + foreach ($values as $i => $value) { + $newMetric = [ + 'value' => $value, + 'description' => $descriptions[$i], + ]; - $sparklineUrlParams = array( - 'columns' => $column, - 'module' => $this->requestConfig->getApiModuleToRequest(), - 'action' => $this->requestConfig->getApiMethodToRequest() - ); + $metrics[] = $newMetric; + } - $this->config->addSparkline($sparklineUrlParams, $values, $descriptions, null, $order); + $evolution = null; + + $computeEvolution = $this->config->compute_evolution; + if ($computeEvolution) { + $evolution = $computeEvolution(array_combine($column, $values)); + $newMetric['evolution'] = $evolution; + } + + $this->config->addSparkline($sparklineUrlParams, $metrics, $desc = null, $evolution, $order, $title = null, $group = $sparklineMetricIndex, $seriesIndices = null, $graphParams); + } } } @@ -161,4 +249,44 @@ class Sparklines extends ViewDataTable $table->applyQueuedFilters(); } + + private function getValuesAndDescriptions(DataTable\Row $firstRow, $columns) + { + if (!is_array($columns)) { + $columns = array($columns); + } + + $translations = $this->config->translations; + + $values = array(); + $descriptions = array(); + $evolutions = []; + + foreach ($columns as $col) { + $value = $firstRow->getColumn($col); + + if ($value === false) { + $value = 0; + } + + $evolution = $firstRow->getColumn($col . '_change'); // for comparison rows + if ($evolution !== false) { + $evolutions[] = ['percent' => ltrim($evolution, '+'), 'tooltip' => '']; + } + + $values[] = $value; + $descriptions[] = isset($translations[$col]) ? $translations[$col] : $col; + } + + return [$values, $descriptions, $evolutions]; + } + + private function removeUniqueVisitorsIfNotEnabledForPeriod($columns, $period) + { + if (SettingsPiwik::isUniqueVisitorsEnabled($period)) { + return $columns; + } + + return array_diff($columns, ['nb_users', 'nb_uniq_visitors']); + } } diff --git a/plugins/CoreVisualizations/Visualizations/Sparklines/Config.php b/plugins/CoreVisualizations/Visualizations/Sparklines/Config.php index 86944ccc5a..e5fe3da1c0 100644 --- a/plugins/CoreVisualizations/Visualizations/Sparklines/Config.php +++ b/plugins/CoreVisualizations/Visualizations/Sparklines/Config.php @@ -12,6 +12,7 @@ use Piwik\Common; use Piwik\DataTable\Filter\CalculateEvolutionFilter; use Piwik\Metrics; use Piwik\NoAccessException; +use Piwik\Period\Factory; use Piwik\Period\Range; use Piwik\Site; use Piwik\Url; @@ -46,6 +47,15 @@ class Config extends \Piwik\ViewDataTable\Config */ public $title_attributes = array(); + /** + * If supplied, this function is used to compute the evolution percent displayed next to non-comparison sparkline views. + * + * The function is passed an array mapping column names with column values. + * + * @var callable + */ + public $compute_evolution = null; + public function __construct() { parent::__construct(); @@ -145,12 +155,15 @@ class Config extends \Piwik\ViewDataTable\Config * @param string|array $metricName Either one metric name (eg 'nb_visits') or an array of metric names * @param int|null $order Defines the order. The lower the order the earlier the sparkline will be displayed. * By default the sparkline will be appended to the end. + * @param array $graphParams The params to use when changing an associated evolution graph. By default this is determined + * from the sparkline URL, but sometimes the sparkline API method may not match the evolution graph API method. */ - public function addSparklineMetric($metricName, $order = null) + public function addSparklineMetric($metricName, $order = null, $graphParams = null) { $this->sparkline_metrics[] = array( 'columns' => $metricName, - 'order' => $order + 'order' => $order, + 'graphParams' => $graphParams, ); } @@ -167,7 +180,10 @@ class Config extends \Piwik\ViewDataTable\Config $this->sparklines[] = array( 'url' => '', 'metrics' => array(), - 'order' => $this->getSparklineOrder($order) + 'order' => $this->getSparklineOrder($order), + + // adding this group ensures the sparkline will be placed between individual sparklines, and not in their own group together + 'group' => 'placeholder' . count($this->sparklines), ); } @@ -194,62 +210,87 @@ class Config extends \Piwik\ViewDataTable\Config * 'tooltip' => '10 visits in 2015-07-26 compared to 20 visits in 2015-07-25') * @param int $order Defines the order. The lower the order the earlier the sparkline will be * displayed. By default the sparkline will be appended to the end. + * @param string $title The title of this specific sparkline. It is displayed on the left above the sparkline image. + * @param string $group The ID of the group for this sparkline. + * @param int $seriesIndices The indexes of each series displayed in the sparkline. This determines what color is used for each series. Mainly used for comparison. + * @param array $graphParams The params to use when changing an associated evolution graph. By default this is determined + * from the sparkline URL, but sometimes the sparkline API method may not match the evolution graph API method. * @throws \Exception In case an evolution parameter is set but has wrong data structure */ - public function addSparkline($requestParamsForSparkline, $value, $description, $evolution = null, $order = null) + public function addSparkline($requestParamsForSparkline, $metricInfos, $description, $evolution = null, $order = null, $title = null, $group = '', $seriesIndices = null, $graphParams = null) { $metrics = array(); - if (is_array($value)) { - $values = $value; + if ($description === null && is_array($metricInfos)) { + $metrics = $metricInfos; } else { - $values = array($value); - } + $value = $metricInfos; - if (!is_array($description)) { - $description = array($description); + if (is_array($value)) { + $values = $value; + } else { + $values = array($value); + } + + if (!is_array($description)) { + $description = array($description); + } + + if (count($values) === count($description)) { + foreach ($values as $index => $value) { + $metrics[] = array( + 'value' => $value, + 'description' => $description[$index] + ); + } + } else { + $msg = 'The number of values and descriptions need to be the same to add a sparkline. '; + $msg .= 'Values: ' . implode(', ', $values). ' Descriptions: ' . implode(', ', $description); + throw new \Exception($msg); + } } if (!empty($requestParamsForSparkline['columns']) && is_array($requestParamsForSparkline['columns']) - && count($requestParamsForSparkline['columns']) === count($values)) { + && count($requestParamsForSparkline['columns']) === count($metrics)) { $columns = array_values($requestParamsForSparkline['columns']); } elseif (!empty($requestParamsForSparkline['columns']) && is_string($requestParamsForSparkline['columns']) - && count($values) === 1) { + && count($metrics) === 1) { $columns = array($requestParamsForSparkline['columns']); } else{ $columns = array(); } - if (count($values) === count($description)) { - foreach ($values as $index => $value) { - $metrics[] = array( - 'column' => isset($columns[$index]) ? $columns[$index] : '', - 'value' => $value, - 'description' => $description[$index] - ); - } - } else { - $msg = 'The number of values and descriptions need to be the same to add a sparkline. '; - $msg .= 'Values: ' . implode(', ', $values). ' Descriptions: ' . implode(', ', $description); - throw new \Exception($msg); + foreach ($metrics as $index => $metricInfo) { + $metrics[$index]['column'] = isset($columns[$index]) ? $columns[$index] : ''; } if (empty($metrics)) { return; } + $groupedMetrics = []; + foreach ($metrics as $metricInfo) { + $metricGroup = isset($metricInfo['group']) ? $metricInfo['group'] : ''; + $groupedMetrics[$metricGroup][] = $metricInfo; + } + $sparkline = array( 'url' => $this->getUrlSparkline($requestParamsForSparkline), - 'metrics' => $metrics, - 'order' => $this->getSparklineOrder($order) + 'metrics' => $groupedMetrics, + 'order' => $this->getSparklineOrder($order), + 'title' => $title, + 'group' => $group, + 'seriesIndices' => $seriesIndices, + 'graphParams' => $graphParams, ); if (!empty($evolution)) { if (!is_array($evolution) || !array_key_exists('currentValue', $evolution) || - !array_key_exists('pastValue', $evolution)) { + !array_key_exists('pastValue', $evolution) + ) { throw new \Exception('In order to show an evolution in the sparklines view a currentValue and pastValue array key needs to be present'); } @@ -300,7 +341,12 @@ class Config extends \Piwik\ViewDataTable\Config return ($a['order'] < $b['order']) ? -1 : 1; }); - return $this->sparklines; + $sparklines = []; + foreach ($this->sparklines as $sparkline) { + $group = $sparkline['group']; + $sparklines[$group][] = $sparkline; + } + return $sparklines; } private function getSparklineOrder($order) @@ -390,7 +436,13 @@ class Config extends \Piwik\ViewDataTable\Config throw new NoAccessException("Website not initialized, check that you are logged in and/or using the correct token_auth."); } - $paramDate = Range::getRelativeToEndDate($period, $range, $endDate, $site); + if (!isset($paramsToSet['date']) + || !Range::isMultiplePeriod($paramsToSet['date'], $period) + ) { + $paramDate = Range::getRelativeToEndDate($period, $range, $endDate, $site); + } else { + $paramDate = $paramsToSet['date']; + } $params = array_merge($paramsToSet, array('date' => $paramDate)); return $params; diff --git a/plugins/CoreVisualizations/angularjs/single-metric-view/single-metric-view.component.js b/plugins/CoreVisualizations/angularjs/single-metric-view/single-metric-view.component.js index 353a5faebc..c7f2c41dc6 100644 --- a/plugins/CoreVisualizations/angularjs/single-metric-view/single-metric-view.component.js +++ b/plugins/CoreVisualizations/angularjs/single-metric-view/single-metric-view.component.js @@ -164,7 +164,7 @@ function getLastPeriodDate() { var RangePeriod = piwikPeriods.get('range'); var result = RangePeriod.getLastNRange(piwik.period, 2, piwik.currentDateString).startDate; - return $.datepicker.formatDate('yy-mm-dd', result); + return piwikPeriods.format(result); } function setWidgetTitle() { @@ -269,7 +269,7 @@ function getPastPeriodStr() { var startDate = piwikPeriods.get('range').getLastNRange(piwik.period, 2, piwik.currentDateString).startDate; var dateRange = piwikPeriods.get(piwik.period).parse(startDate).getDateRange(); - return $.datepicker.formatDate('yy-mm-dd', dateRange[0]) + ',' + $.datepicker.formatDate('yy-mm-dd', dateRange[1]); + return piwikPeriods.format(dateRange[0]) + ',' + piwikPeriods.format(dateRange[1]); } function isIdGoalSet() { diff --git a/plugins/CoreVisualizations/javascripts/jqplot.js b/plugins/CoreVisualizations/javascripts/jqplot.js index a509f401a6..c53cdcb493 100644 --- a/plugins/CoreVisualizations/javascripts/jqplot.js +++ b/plugins/CoreVisualizations/javascripts/jqplot.js @@ -634,9 +634,7 @@ function rowEvolutionGetMetricNameFromRow(tr) * Sets the colors used to render this graph. */ _setColors: function () { - var colorManager = piwik.ColorManager, - seriesColorNames = ['series1', 'series2', 'series3', 'series4', 'series5', - 'series6', 'series7', 'series8', 'series9', 'series10']; + var colorManager = piwik.ColorManager; var viewDataTable = $('#' + this.workingDivId).data('uiControlObject').param['viewDataTable']; @@ -651,11 +649,31 @@ function rowEvolutionGetMetricNameFromRow(tr) var namespace = graphType + '-graph-colors'; - this.jqplotParams.seriesColors = colorManager.getColors(namespace, seriesColorNames, true); + this._setSeriesColors(namespace); + this.jqplotParams.grid.background = colorManager.getColor(namespace, 'grid-background'); this.jqplotParams.grid.borderColor = colorManager.getColor(namespace, 'grid-border'); this.tickColor = colorManager.getColor(namespace, 'ticks'); this.singleMetricColor = colorManager.getColor(namespace, 'single-metric-label') + }, + + _setSeriesColors: function (namespace) { + var colorManager = piwik.ColorManager, + seriesColorNames = ['series0', 'series1', 'series2', 'series3', 'series4', 'series5', + 'series6', 'series7', 'series8', 'series9', 'series10']; + + var comparisonService = piwikHelper.getAngularDependency('piwikComparisonsService'); + if (comparisonService.isComparing() && typeof this.jqplotParams.series[0].seriesIndex !== 'undefined') { + namespace = 'comparison-series-color'; + + seriesColorNames = []; + this.jqplotParams.series.forEach(function (s) { + var seriesColorName = comparisonService.getSeriesColorName(s.seriesIndex, s.metricIndex); + seriesColorNames.push(seriesColorName); + }); + } + + this.jqplotParams.seriesColors = colorManager.getColors(namespace, seriesColorNames, true); } }); @@ -1040,7 +1058,9 @@ RowEvolutionSeriesToggle.prototype.beforeReplot = function () { c.markerRenderer.init(); var position = series.gridData[tick]; - c.markerRenderer.draw(position[0], position[1], c.piwikHighlightCanvas._ctx); + if (typeof position !== 'undefined') { + c.markerRenderer.draw(position[0], position[1], c.piwikHighlightCanvas._ctx); + } } } diff --git a/plugins/CoreVisualizations/javascripts/jqplotBarGraph.js b/plugins/CoreVisualizations/javascripts/jqplotBarGraph.js index cd21308f73..24af4c2e9d 100644 --- a/plugins/CoreVisualizations/javascripts/jqplotBarGraph.js +++ b/plugins/CoreVisualizations/javascripts/jqplotBarGraph.js @@ -22,6 +22,9 @@ _setJqplotParameters: function (params) { JqplotGraphDataTable.prototype._setJqplotParameters.call(this, params); + var barMargin = this.data[0].length > 10 ? 2 : 10; + var minBarWidth = 10; + this.jqplotParams.seriesDefaults = { renderer: $.jqplot.BarRenderer, rendererOptions: { @@ -29,7 +32,7 @@ shadowDepth: 2, shadowAlpha: .2, fillToZero: true, - barMargin: this.data[0].length > 10 ? 2 : 10 + barMargin: barMargin } }; @@ -48,6 +51,18 @@ this.jqplotParams.canvasLegend = { show: true }; + + var comparisonService = piwikHelper.getAngularDependency('piwikComparisonsService'); + if (comparisonService.isComparing()) { + var seriesCount = this.jqplotParams.series.length; + var dataCount = this.data[0].length; + + var totalBars = seriesCount * dataCount; + var totalMinWidth = (minBarWidth + barMargin) * totalBars + 50; + + this.$element.find('.piwik-graph').css('min-width', totalMinWidth + 'px'); + this.$element.css('overflow-x', 'scroll'); + } }, _bindEvents: function () { diff --git a/plugins/CoreVisualizations/javascripts/jqplotEvolutionGraph.js b/plugins/CoreVisualizations/javascripts/jqplotEvolutionGraph.js index b77add4962..012e5e8bc1 100644 --- a/plugins/CoreVisualizations/javascripts/jqplotEvolutionGraph.js +++ b/plugins/CoreVisualizations/javascripts/jqplotEvolutionGraph.js @@ -96,19 +96,53 @@ .on('jqplotPiwikTickOver', function (e, tick) { lastTick = tick; var label; - if (typeof self.jqplotParams.axes.xaxis.labels != 'undefined') { - label = self.jqplotParams.axes.xaxis.labels[tick]; - } else { - label = self.jqplotParams.axes.xaxis.ticks[tick]; - } - var text = []; - for (var d = 0; d < self.data.length; d++) { - var value = self.formatY(self.data[d][tick], d); + var dataByAxis = {}; + for (var d = 0; d < self.data.length; ++d) { + var valueUnformatted = self.data[d][tick]; + if (typeof valueUnformatted === 'undefined' || valueUnformatted === null) { + continue; + } + + var axis = self.jqplotParams.series[d]._xaxis || 'xaxis'; + if (!dataByAxis[axis]) { + dataByAxis[axis] = []; + } + + var value = self.formatY(valueUnformatted, d); var series = self.jqplotParams.series[d].label; - text.push('<strong>' + value + '</strong> ' + piwikHelper.htmlEntities(series)); + + var seriesColor = self.jqplotParams.seriesColors[d]; + + dataByAxis[axis].push('<span class="tooltip-series-color" style="background-color: ' + seriesColor + ';"/>' + '<strong>' + value + '</strong> ' + piwikHelper.htmlEntities(series)); + } + + var xAxisCount = 0; + Object.keys(self.jqplotParams.axes).forEach(function (axis) { + if (axis.substring(0, 1) === 'x') { + ++xAxisCount; + } + }); + + var content = ''; + for (var i = 0; i < xAxisCount; ++i) { + var axisName = i === 0 ? 'xaxis' : 'x' + (i + 1) + 'axis'; + if (!dataByAxis[axisName] || !dataByAxis[axisName].length) { + continue; + } + + if (typeof self.jqplotParams.axes[axisName].labels != 'undefined') { + label = self.jqplotParams.axes[axisName].labels[tick]; + } else { + label = self.jqplotParams.axes[axisName].ticks[tick]; + } + + if (typeof label === 'undefined') { // sanity check + continue; + } + + content += '<h3 class="evolution-tooltip-header">'+piwikHelper.htmlEntities(label)+'</h3>'+dataByAxis[axisName].join('<br />'); } - var content = '<h3>'+piwikHelper.htmlEntities(label)+'</h3>'+text.join('<br />'); $(this).tooltip({ track: true, diff --git a/plugins/CoreVisualizations/stylesheets/dataTableVisualizations.less b/plugins/CoreVisualizations/stylesheets/dataTableVisualizations.less index ff2118b952..15edf722c0 100644 --- a/plugins/CoreVisualizations/stylesheets/dataTableVisualizations.less +++ b/plugins/CoreVisualizations/stylesheets/dataTableVisualizations.less @@ -23,4 +23,18 @@ } .widget .dataTableVizBar .jqplot-graph { padding: 0 10px 10px 10px; +} + +.tooltip-series-color { + display: inline-block; + width: 11px; + height: 11px; + border-radius: 6px; + margin-right: 3px; + position: relative; + top: 1px; +} + +.ui-tooltip h3.evolution-tooltip-header { + margin-top: 4px; }
\ No newline at end of file diff --git a/plugins/CoreVisualizations/templates/_dataTableViz_htmlTable.twig b/plugins/CoreVisualizations/templates/_dataTableViz_htmlTable.twig index 38c2101168..a9ac3f4dae 100644 --- a/plugins/CoreVisualizations/templates/_dataTableViz_htmlTable.twig +++ b/plugins/CoreVisualizations/templates/_dataTableViz_htmlTable.twig @@ -19,11 +19,13 @@ </tr> {% endif %} {% else %} + {% set rowIndex = properties.filter_offset|default(0) + 1 %} {%- for rowId, row in dataTable.getRows() -%} {%- set rowHasSubtable = not subtablesAreDisabled and row.getIdSubDataTable() and properties.subtable_controller_action is not null -%} {%- set rowSubtableId = row.getMetadata('idsubdatatable_in_db')|default(row.getIdSubDataTable()) -%} {%- set isSummaryRow = rowId == constant('Piwik\\DataTable::ID_SUMMARY_ROW') or row.getMetadata('is_summary') -%} {%- set shouldHighlightRow = isSummaryRow and properties.highlight_summary_row -%} + {% set dimensions = dataTable.getMetadata('dimensions')|default([]) %} {# display this row if it doesn't have a subtable or if we don't replace the row with the subtable #} {%- set showRow = subtablesAreDisabled @@ -36,21 +38,38 @@ {% if row.getMetadata('segment') is not false %} data-segment-filter="{{ row.getMetadata('segment')|e('html_attr') }}"{% endif %} {% if row.getMetadata('url') is not false %} data-url-label="{{ row.getMetadata('url')|rawSafeDecoded }}"{% endif %} data-row-metadata="{{ row.getMetadata|json_encode|e('html_attr') }}" - class="{{ row.getMetadata('css_class') }} {% if rowHasSubtable %}subDataTable{% endif %}{% if shouldHighlightRow %} highlight{% endif %}{% if isSummaryRow %} summaryRow{% endif %}" + class="{{ row.getMetadata('css_class') }} {% if rowHasSubtable %}subDataTable{% endif %}{% if shouldHighlightRow %} highlight{% endif %}{% if isSummaryRow %} summaryRow{% endif %} {% if isComparing %}parentComparisonRow{% endif %}" {% if rowHasSubtable %}title="{{ 'CoreHome_ClickRowToExpandOrContract'|translate }}"{% endif %}> {% for column in properties.columns_to_display %} {% set cellAttributes = visualization.getCellHtmlAttributes(row, column) %} - <td {% if cellAttributes is not empty %}{% for name, value in cellAttributes %}{{ name|e('html') }}="{{ value|e('html_attr') }}" {% endfor %}{% endif %}> + <td class="{% if column =='label' or column in dimensions %}label{% else %}column{% endif %} {{ cellAttributes.class|default|e('html_attr') }}" + {% if cellAttributes is not empty %}{% for name, value in cellAttributes %}{{ name|e('html') }}="{{ value|e('html_attr') }}" {% endfor %}{% endif %} + > + {% if isComparing and column == 'label' %} + <span class="prefix-numeral">{{ rowIndex }}.</span> + {% endif %} + {% include "@CoreHome/_dataTableCell.twig" with properties %} </td> {% endfor %} </tr> + + {% if row.getComparisons() %} + {% include "@CoreVisualizations/_dataTableViz_htmlTable_comparisons.twig" with { + 'comparedRow': row, + 'dataTable': row.getComparisons(), + 'rootDataTable': dataTable, + 'dimensions': dimensions, + } %} + {% endif %} {% endif %} {# display subtable if present and showing expanded datatable #} {% if properties.show_expanded|default(false) and rowHasSubtable %} {% include "@CoreVisualizations/_dataTableViz_htmlTable.twig" with {'dataTable': row.getSubtable(), 'idSubtable': rowSubtableId} %} {% endif %} + + {% set rowIndex = rowIndex + 1 %} {%- endfor -%} {% if dataTable.getTotalsRow and properties.show_totals_row %} {% set row = dataTable.getTotalsRow %} @@ -58,7 +77,7 @@ <tr class="{{ row.getMetadata('css_class') }} totalsRow" title="Total values for this table"> {% for column in properties.columns_to_display %} - <td> + <td class="{% if column =='label' %}label{% else %}column{% endif %}"> {% include "@CoreHome/_dataTableCell.twig" with properties %} </td> {% endfor %} diff --git a/plugins/CoreVisualizations/templates/_dataTableViz_htmlTable_comparisons.twig b/plugins/CoreVisualizations/templates/_dataTableViz_htmlTable_comparisons.twig new file mode 100644 index 0000000000..fec4652ed4 --- /dev/null +++ b/plugins/CoreVisualizations/templates/_dataTableViz_htmlTable_comparisons.twig @@ -0,0 +1,83 @@ +{% if properties.columns_to_display is not empty %} +{% set lastSeenComparePeriod = false %} +{% set comparedPeriodPretty = dataTable.getFirstRow().getMetadata('comparePeriodPretty') %} +{% set isComparingSegments = rootDataTable.getMetadata('compareSegments')|default([])|length > 1 %} +{% set isComparingPeriods = rootDataTable.getMetadata('comparePeriods')|default([])|length > 1 %} +{%- for rowId, row in dataTable.getRows() -%} + {% set comparePeriod = row.getMetadata('comparePeriodPretty') %} + {% if lastSeenComparePeriod != comparePeriod and isComparingSegments and isComparingPeriods %} + <tr class="comparePeriod"> + <td class="label"> + {{ comparePeriod }} + </td> + + {# add extra empty columns for sticky column scrolling to work #} + {% for column in properties.columns_to_display %} + {% if column != 'label' %} + <td class="column"> </td> + {% endif %} + {% endfor %} + </tr> + {% endif %} + {% set overrideParams = {} %} + + {% if row.getMetadata('compareSegment')|default is not empty %}{% set overrideParams = overrideParams|merge({ segment: row.getMetadata('compareSegment'), compareSegments: '' }) %}{% endif %} + {% if row.getMetadata('comparePeriod')|default is not empty %}{% set overrideParams = overrideParams|merge({ period: row.getMetadata('comparePeriod'), comparePeriods: '' }) %}{% endif %} + {% if row.getMetadata('compareDate')|default is not empty %}{% set overrideParams = overrideParams|merge({ date: row.getMetadata('compareDate'), compareDates: '' }) %}{% endif %} + {% set idSubtable = row.getMetadata('idsubdatatable') %} + {% set seriesIndex = loop.index0 %} + <tr + {% if idSubtable != false %}data-idsubtable="{{ idSubtable|e('html_attr') }}"{% endif %} + data-comparison-series="{{ seriesIndex }}" + class="comparisonRow" + data-param-override="{{ overrideParams|json_encode|e('html_attr') }}" data-label="{{ comparedRow.getColumn('label')|e('html_attr') }}" + {% if row.getMetadata('segment') is not false %}data-segment-filter="{{ row.getMetadata('segment')|e('html_attr') }}"{% endif %} + > + <td class="label"> + {% if isComparingSegments %} + {%- set comparisonLabel = row.getMetadata('compareSegmentPretty') -%} + {% else %} + {%- set comparisonLabel = comparePeriod -%} + {% endif %} + <span class="label">{{ comparisonLabel }}</span> + </td> + {% for dimension in dimensions %} + {% if loop.index0 != 0 %} + <td class="label"> + + </td> + {% endif %} + {% endfor %} + {% for column in properties.columns_to_display %} + {% if column != 'label' and column not in dimensions %} + {% set columnValue = row.getColumn(column) %} + <td class="column"> + {% set rowComparisonTotals = comparisonTotals[seriesIndex].totals|default(null) %} + + {% set comparisonTooltipSuffix = '' %} + {% set columnChange = false %} + {% if row.getColumn(column ~ '_change')|default is not empty %} + {% set columnChange = row.getColumn(column ~ '_change')|default('+0%') %} + {% set comparisonTooltipSuffix = 'General_ComparisonRatioTooltip'|translate(columnChange, row.getMetadata('compareSegmentPretty'), comparedPeriodPretty) %} + {% endif %} + + {% include "@CoreVisualizations/_dataTableViz_htmlTable_ratio.twig" with { + 'changePercentage': columnChange, + 'totals': rowComparisonTotals, + 'label': comparedRow.getColumn('label'), + 'labelColumn': properties.columns_to_display|first, + 'changePercantage': columnChange, + 'forceZero': true, + 'tooltipSuffix': comparisonTooltipSuffix, + 'translations': properties.translations, + 'segmentTitlePretty': row.getMetadata('compareSegmentPretty'), + 'periodTitlePretty': row.getMetadata('comparePeriodPretty') + } %} + <span class="value">{{ columnValue|default(0)|number(2,0)|rawSafeDecoded }}</span> + </td> + {% endif %} + {% endfor %} + </tr> + {% set lastSeenComparePeriod = comparePeriod %} +{%- endfor -%} +{% endif %}
\ No newline at end of file diff --git a/plugins/CoreVisualizations/templates/_dataTableViz_htmlTable_ratio.twig b/plugins/CoreVisualizations/templates/_dataTableViz_htmlTable_ratio.twig new file mode 100644 index 0000000000..8d14bb126d --- /dev/null +++ b/plugins/CoreVisualizations/templates/_dataTableViz_htmlTable_ratio.twig @@ -0,0 +1,26 @@ +{% if column in properties.report_ratio_columns and (column in totals|keys or forceZero|default) -%} + {% set reportTotal = totals[column]|default(0) %} + {% if siteTotalRow|default is not empty %} + {% set siteTotal = siteTotalRow.getColumn(column) %} + {% elseif siteSummary is defined and siteSummary is not empty and siteSummary.getFirstRow %} + {% set siteTotal = siteSummary.getFirstRow.getColumn(column) %} + {% else %} + {% set siteTotal = 0 %} + {% endif %} + + {% set rowPercentage = row.getColumn(column)|percentage(reportTotal, 1) %} + {% set metricTitle = translations[column]|default(column) %} + + {% set reportRatioTooltip = 'General_ReportRatioTooltip'|translate(label, rowPercentage|e('html_attr'), reportTotal|e('html_attr'), metricTitle|e('html_attr'), '"' ~ segmentTitlePretty ~ '"', translations[labelColumn]|default(labelColumn)|e('html_attr')) %} + + {% if siteTotal and siteTotal > reportTotal %} + {% set totalPercentage = row.getColumn(column)|percentage(siteTotal, 1) %} + {% set totalRatioTooltip = 'General_TotalRatioTooltip'|translate(totalPercentage, siteTotal|number(2,0), metricTitle, periodTitlePretty) %} + {% else %} + {% set totalRatioTooltip = '' %} + {% endif %} + + <span class="ratio" + title="{{ reportRatioTooltip|rawSafeDecoded|raw }} {{ totalRatioTooltip|rawSafeDecoded|e('html_attr') }}{% if tooltipSuffix|default is not empty %}<br/><br/> {{ tooltipSuffix|rawSafeDecoded|e('html_attr') }}{% endif %}" + > {{ rowPercentage }} {% if changePercantage|default is not empty %}({{ changePercentage }}){% endif %}</span> +{%- endif %} diff --git a/plugins/CoreVisualizations/templates/_dataTableViz_sparklines.twig b/plugins/CoreVisualizations/templates/_dataTableViz_sparklines.twig index 5c8390d914..e1932c523b 100644 --- a/plugins/CoreVisualizations/templates/_dataTableViz_sparklines.twig +++ b/plugins/CoreVisualizations/templates/_dataTableViz_sparklines.twig @@ -10,12 +10,23 @@ <div class="row"> <div class="col m6"> {% endif %} - - {% for key, sparkline in sparklines %} - {% if key is even %} + {% if sparklines|length == 1 %} + {% for key, sparkline in sparklines|first %} + {% if loop.index0 is even %} {{ macros.singleSparkline(sparkline, allMetricsDocumentation, areSparklinesLinkable) }} {% endif %} {% endfor %} + {% else %} + {% for group in sparklines %} + {% if loop.index0 is even %} + <div> + {% for key, sparkline in group %} + {{ macros.singleSparkline(sparkline, allMetricsDocumentation, areSparklinesLinkable) }} + {% endfor %} + </div> + {% endif %} + {% endfor %} + {% endif %} {% if not isWidget %} <br style="clear:left"/> @@ -23,11 +34,23 @@ <div class="col m6"> {% endif %} + {% if sparklines|length == 1 %} {% for key, sparkline in sparklines %} - {% if key is odd %} + {% if loop.index0 is odd %} {{ macros.singleSparkline(sparkline, allMetricsDocumentation, areSparklinesLinkable) }} {% endif %} {% endfor %} + {% else %} + {% for group in sparklines %} + {% if loop.index0 is odd %} + <div> + {% for key, sparkline in group %} + {{ macros.singleSparkline(sparkline, allMetricsDocumentation, areSparklinesLinkable) }} + {% endfor %} + </div> + {% endif %} + {% endfor %} + {% endif %} <br style="clear:left"/> diff --git a/plugins/CoreVisualizations/templates/macros.twig b/plugins/CoreVisualizations/templates/macros.twig index 7562b88f78..bc754f2c19 100644 --- a/plugins/CoreVisualizations/templates/macros.twig +++ b/plugins/CoreVisualizations/templates/macros.twig @@ -1,36 +1,52 @@ + +{% macro sparklineEvolution(evolution) %} + {% set evolutionPretty = evolution.percent %} + + {% if evolution.percent < 0 %} + {% set evolutionClass = 'negative-evolution' %} + {% set evolutionIcon = 'arrow_down.png' %} + {% elseif evolution.percent == 0 %} + {% set evolutionClass = 'neutral-evolution' %} + {% set evolutionIcon = 'stop.png' %} + {% else %} + {% set evolutionClass = 'positive-evolution' %} + {% set evolutionIcon = 'arrow_up.png' %} + {% set evolutionPretty = '+' ~ evolution.percent %} + {% endif %} + + <span class="metricEvolution" title="{{ evolution.tooltip|rawSafeDecoded|e('html_attr') }}"> + <img style="padding-right:4px" src="plugins/MultiSites/images/{{ evolutionIcon }}"/> + <strong class="{{ evolutionClass }}">{{ evolutionPretty }}</strong></span> +{% endmacro %} + {% macro singleSparkline(sparkline, allMetricsDocumentation, areSparklinesLinkable) %} - <div class="sparkline {% if areSparklinesLinkable is defined and not areSparklinesLinkable %}notLinkable{% endif %}"> - {% if sparkline.url %}{{ sparkline(sparkline.url)|raw }}{% endif %} + <div class="sparkline {% if areSparklinesLinkable is defined and not areSparklinesLinkable %}notLinkable{% endif %}" + {% if sparkline.seriesIndices|default is not empty %}data-series-indices="{{ sparkline.seriesIndices|json_encode|e('html_attr') }}"{% endif %} + {% if sparkline.graphParams|default is not empty %}data-graph-params="{{ sparkline.graphParams|json_encode|e('html_attr') }}"{% endif %} + > + <div> + {% if sparkline.title|default is not empty %}<h6 class="sparkline-title" title="{{ sparkline.title|rawSafeDecoded|e('html_attr') }}">{{ sparkline.title }}</h6>{% endif %} + {% if sparkline.url %}{{ sparkline(sparkline.url)|raw }}{% endif %} + </div> <div> - {% for metric in sparkline.metrics %} - <span {% if allMetricsDocumentation[metric.column] is defined and allMetricsDocumentation[metric.column] %}title="{{ allMetricsDocumentation[metric.column] }}"{% endif %}> - {% if '%s' in metric.description -%} - {{ metric.description|translate("<strong>"~metric.value~"</strong>")|raw }} - {%- else %} - <strong>{{ metric.value }}</strong> {{ metric.description }} - {%- endif %}{% if not loop.last %}, {% endif %} - </span> + {% for groupName, group in sparkline.metrics %} + {% if groupName is not empty %}<span class="metric-group-title">{{ groupName }}</span>{% endif %} + {% for metric in group %} + <span class="sparkline-metrics" {% if allMetricsDocumentation[metric.column] is defined and allMetricsDocumentation[metric.column] %}title="{{ allMetricsDocumentation[metric.column] }}"{% endif %}> + {% if '%s' in metric.description -%} + {{ metric.description|translate("<strong>"~metric.value~"</strong>")|raw }} + {%- else %} + <strong>{{ metric.value }}</strong> {{ metric.description }} + {%- endif %}{% if not loop.last %}, {% endif %} + </span> + {% if metric.evolution is defined %} + {{ _self.sparklineEvolution(metric.evolution) }} + {% endif %} + {% endfor %} {% endfor %} {% if sparkline.evolution is defined %} - - {% set evolutionPretty = sparkline.evolution.percent %} - - {% if sparkline.evolution.percent < 0 %} - {% set evolutionClass = 'negative-evolution' %} - {% set evolutionIcon = 'arrow_down.png' %} - {% elseif sparkline.evolution.percent == 0 %} - {% set evolutionClass = 'neutral-evolution' %} - {% set evolutionIcon = 'stop.png' %} - {% else %} - {% set evolutionClass = 'positive-evolution' %} - {% set evolutionIcon = 'arrow_up.png' %} - {% set evolutionPretty = '+' ~ sparkline.evolution.percent %} - {% endif %} - - <span class="metricEvolution" title="{{ sparkline.evolution.tooltip }}"><img - style="padding-right:4px" src="plugins/MultiSites/images/{{ evolutionIcon }}"/> - <strong class="{{ evolutionClass }}">{{ evolutionPretty }}</strong></span> + {{ _self.sparklineEvolution(sparkline.evolution) }} {% endif %} </div> </div> diff --git a/plugins/CoreVisualizations/tests/Integration/SparklinesConfigTest.php b/plugins/CoreVisualizations/tests/Integration/SparklinesConfigTest.php index 33fb6be6ab..6847ac7b2c 100644 --- a/plugins/CoreVisualizations/tests/Integration/SparklinesConfigTest.php +++ b/plugins/CoreVisualizations/tests/Integration/SparklinesConfigTest.php @@ -65,12 +65,18 @@ class SparklinesConfigTest extends IntegrationTestCase $expectedSparkline = array( 'url' => '?period=day&date=2012-03-06,2012-04-04&idSite=1&module=CoreHome&action=renderMe&viewDataTable=sparkline', 'metrics' => array ( - array ('column' => '', 'value' => 10, 'description' => 'Visits'), + '' => [ + array ('value' => 10, 'description' => 'Visits', 'column' => ''), + ], ), - 'order' => 999 + 'order' => 999, + 'title' => null, + 'group' => '', + 'seriesIndices' => null, + 'graphParams' => null, ); - $this->assertSame(array($expectedSparkline), $this->config->getSortedSparklines()); + $this->assertSame(array($expectedSparkline), $this->config->getSortedSparklines()['']); } public function test_addSparkline_shouldAddAMinimalSparklineWithOneValueAndUseDefaultOrderWithColumn() @@ -79,10 +85,10 @@ class SparklinesConfigTest extends IntegrationTestCase $params['columns'] = 'nb_visits'; $this->config->addSparkline($params, $value = 10, $description = 'Visits'); - $expectedSparkline = array('column' => 'nb_visits', 'value' => 10, 'description' => 'Visits'); + $expectedSparkline = array('value' => 10, 'description' => 'Visits', 'column' => 'nb_visits'); $sparklines = $this->config->getSortedSparklines(); - $this->assertSame(array($expectedSparkline), $sparklines[0]['metrics']); + $this->assertSame(array($expectedSparkline), $sparklines[''][0]['metrics']['']); } public function test_addSparkline_shouldAddSparklineWithMultipleValues() @@ -92,9 +98,9 @@ class SparklinesConfigTest extends IntegrationTestCase $sparklines = $this->config->getSortedSparklines(); $this->assertSame(array ( - array ('column' => '', 'value' => 10, 'description' => 'Visits'), - array ('column' => '', 'value' => 20, 'description' => 'Actions'), - ), $sparklines[0]['metrics']); + array ('value' => 10, 'description' => 'Visits', 'column' => ''), + array ('value' => 20, 'description' => 'Actions', 'column' => ''), + ), $sparklines[''][0]['metrics']['']); } public function test_addSparkline_shouldAddSparklinesMultipleValuesWithColumns() @@ -105,12 +111,12 @@ class SparklinesConfigTest extends IntegrationTestCase $this->config->addSparkline($params, $values = array(10, 20), $description = array('Visits', 'Actions')); $expectedSparkline = array( - array ('column' => 'nb_visits', 'value' => 10, 'description' => 'Visits'), - array ('column' => 'nb_actions', 'value' => 20, 'description' => 'Actions') + array ('value' => 10, 'description' => 'Visits', 'column' => 'nb_visits'), + array ('value' => 20, 'description' => 'Actions', 'column' => 'nb_actions') ); $sparklines = $this->config->getSortedSparklines(); - $this->assertSame($expectedSparkline, $sparklines[0]['metrics']); + $this->assertSame($expectedSparkline, $sparklines[''][0]['metrics']['']); } /** @@ -133,7 +139,7 @@ class SparklinesConfigTest extends IntegrationTestCase $this->assertSame(array ( 'percent' => '-52.4%', 'tooltip' => '1 visit compared to 2 visits' - ), $sparklines[0]['evolution']); + ), $sparklines[''][0]['evolution']); } public function test_addSparkline_shouldAddOrder() @@ -142,7 +148,7 @@ class SparklinesConfigTest extends IntegrationTestCase $sparklines = $this->config->getSortedSparklines(); - $this->assertSame(42, $sparklines[0]['order']); + $this->assertSame(42, $sparklines[''][0]['order']); } public function test_addSparkline_shouldBeAbleToBuildSparklineUrlBasedOnGETparams() @@ -154,7 +160,166 @@ class SparklinesConfigTest extends IntegrationTestCase $sparklines = $this->config->getSortedSparklines(); - $this->assertSame('?columns=nb_visits&viewDataTable=sparkline&date=2012-03-06,2012-04-04', $sparklines[0]['url']); + $this->assertSame('?columns=nb_visits&viewDataTable=sparkline&date=2012-03-06,2012-04-04', $sparklines[''][0]['url']); + } + + public function test_addSparkline_shouldAddSparklinesWithGroups() + { + $this->config->addSparkline($this->sparklineParams(), $value = 10, $description = 'Visits', $evolution = null, $order = '4', $title = 'title1', $group = 'one'); + $this->config->addSparkline($this->sparklineParams(), $value = 11, $description = 'Visits1', $evolution = null, $order = '1', $title = 'title2', $group = 'one'); + $this->config->addSparkline($this->sparklineParams(), $value = 12, $description = 'Visits2', $evolution = null, $order = '3', $title = 'title3', $group = 'two'); + $this->config->addSparkline($this->sparklineParams(), $value = 13, $description = 'Visits3', $evolution = null, $order = '6', $title = 'title4', $group = 'two'); + + $sparklines = $this->config->getSortedSparklines(); + $expectedSparklines = [ + 'one' => [ + [ + 'url' => '?period=day&date=2012-03-06,2012-04-04&idSite=1&module=CoreHome&action=renderMe&viewDataTable=sparkline', + 'metrics' => [ + '' => [ + 0 => [ + 'value' => 11, + 'description' => 'Visits1', + 'column' => '', + ], + ], + ], + 'order' => 1, + 'title' => 'title2', + 'group' => 'one', + 'seriesIndices' => null, + 'graphParams' => null, + ], + [ + 'url' => '?period=day&date=2012-03-06,2012-04-04&idSite=1&module=CoreHome&action=renderMe&viewDataTable=sparkline', + 'metrics' => [ + '' => [ + 0 => [ + 'value' => 10, + 'description' => 'Visits', + 'column' => '', + ], + ], + ], + 'order' => 4, + 'title' => 'title1', + 'group' => 'one', + 'seriesIndices' => null, + 'graphParams' => null, + ], + ], + 'two' => [ + [ + 'url' => '?period=day&date=2012-03-06,2012-04-04&idSite=1&module=CoreHome&action=renderMe&viewDataTable=sparkline', + 'metrics' => [ + '' => [ + 0 => [ + 'value' => 12, + 'description' => 'Visits2', + 'column' => '', + ], + ], + ], + 'order' => 3, + 'title' => 'title3', + 'group' => 'two', + 'seriesIndices' => null, + 'graphParams' => null, + ], + [ + 'url' => '?period=day&date=2012-03-06,2012-04-04&idSite=1&module=CoreHome&action=renderMe&viewDataTable=sparkline', + 'metrics' => [ + '' => [ + 0 => [ + 'value' => 13, + 'description' => 'Visits3', + 'column' => '', + ], + ], + ], + 'order' => 6, + 'title' => 'title4', + 'group' => 'two', + 'seriesIndices' => null, + 'graphParams' => null, + ], + ], + ]; + + $this->assertSame($expectedSparklines, $sparklines); + } + + public function test_addSparkline_shouldAddSparklineMetricsWithGroups() + { + $metricInfos = [ + [ + 'value' => 'v1', + 'description' => 'd1', + 'group' => 'g1', + ], + [ + 'value' => 'v2', + 'description' => 'd3', + 'group' => 'g1', + ], + [ + 'value' => 'v3', + 'description' => 'd3', + 'group' => 'g3', + ], + [ + 'value' => 'v4', + 'description' => 'd4', + 'group' => 'g1', + ], + ]; + $this->config->addSparkline($this->sparklineParams(), $metricInfos, $description = null); + + $sparklines = $this->config->getSortedSparklines(); + $expectedSparklines = [ + '' => [ + [ + 'url' => '?period=day&date=2012-03-06,2012-04-04&idSite=1&module=CoreHome&action=renderMe&viewDataTable=sparkline', + 'metrics' => [ + 'g1' => [ + 0 => [ + 'value' => 'v1', + 'description' => 'd1', + 'group' => 'g1', + 'column' => '', + ], + 1 => [ + 'value' => 'v2', + 'description' => 'd3', + 'group' => 'g1', + 'column' => '', + ], + 2 => [ + 'value' => 'v4', + 'description' => 'd4', + 'group' => 'g1', + 'column' => '', + ], + ], + 'g3' => [ + 0 => [ + 'value' => 'v3', + 'description' => 'd3', + 'group' => 'g3', + 'column' => '', + ], + ], + ], + 'order' => 999, + 'title' => null, + 'group' => '', + 'seriesIndices' => null, + 'graphParams' => null, + ], + ], + ]; + + $this->assertSame($expectedSparklines, $sparklines); } private function sparklineParams($params = array()) diff --git a/plugins/CoreVisualizations/tests/Unit/SparklinesConfigTest.php b/plugins/CoreVisualizations/tests/Unit/SparklinesConfigTest.php index 1134df2f5c..e4759845fa 100644 --- a/plugins/CoreVisualizations/tests/Unit/SparklinesConfigTest.php +++ b/plugins/CoreVisualizations/tests/Unit/SparklinesConfigTest.php @@ -49,9 +49,9 @@ class SparklinesConfigTest extends \PHPUnit_Framework_TestCase $this->addFewSparklines(); $this->assertSame(array( - array('columns' => 'nb_visits', 'order' => null), - array('columns' => 'nb_unique_visitors', 'order' => 99), - array('columns' => array('nb_downloads', 'nb_outlinks'), 'order' => null), + array('columns' => 'nb_visits', 'order' => null, 'graphParams' => null), + array('columns' => 'nb_unique_visitors', 'order' => 99, 'graphParams' => null), + array('columns' => array('nb_downloads', 'nb_outlinks'), 'order' => null, 'graphParams' => null), ), $this->config->getSparklineMetrics()); } @@ -62,8 +62,8 @@ class SparklinesConfigTest extends \PHPUnit_Framework_TestCase $this->config->removeSparklineMetric('nb_unique_visitors'); $this->assertSame(array( - array('columns' => 'nb_visits', 'order' => null), - array('columns' => array('nb_downloads', 'nb_outlinks'), 'order' => null), + array('columns' => 'nb_visits', 'order' => null, 'graphParams' => null), + array('columns' => array('nb_downloads', 'nb_outlinks'), 'order' => null, 'graphParams' => null), ), $this->config->getSparklineMetrics()); } @@ -74,8 +74,8 @@ class SparklinesConfigTest extends \PHPUnit_Framework_TestCase $this->config->removeSparklineMetric(array('nb_downloads', 'nb_outlinks')); $this->assertSame(array( - array('columns' => 'nb_visits', 'order' => null), - array('columns' => 'nb_unique_visitors', 'order' => 99), + array('columns' => 'nb_visits', 'order' => null, 'graphParams' => null), + array('columns' => 'nb_unique_visitors', 'order' => 99, 'graphParams' => null), ), $this->config->getSparklineMetrics()); } @@ -86,9 +86,9 @@ class SparklinesConfigTest extends \PHPUnit_Framework_TestCase $this->config->replaceSparklineMetric('nb_unique_visitors', ''); $this->assertSame(array( - array('columns' => 'nb_visits', 'order' => null), - array('columns' => '', 'order' => 99), - array('columns' => array('nb_downloads', 'nb_outlinks'), 'order' => null), + array('columns' => 'nb_visits', 'order' => null, 'graphParams' => null), + array('columns' => '', 'order' => 99, 'graphParams' => null), + array('columns' => array('nb_downloads', 'nb_outlinks'), 'order' => null, 'graphParams' => null), ), $this->config->getSparklineMetrics()); } @@ -99,9 +99,9 @@ class SparklinesConfigTest extends \PHPUnit_Framework_TestCase $this->config->replaceSparklineMetric(array('nb_downloads', 'nb_outlinks'), ''); $this->assertSame(array( - array('columns' => 'nb_visits', 'order' => null), - array('columns' => 'nb_unique_visitors', 'order' => 99), - array('columns' => '', 'order' => null), + array('columns' => 'nb_visits', 'order' => null, 'graphParams' => null), + array('columns' => 'nb_unique_visitors', 'order' => 99, 'graphParams' => null), + array('columns' => '', 'order' => null, 'graphParams' => null), ), $this->config->getSparklineMetrics()); } @@ -113,10 +113,10 @@ class SparklinesConfigTest extends \PHPUnit_Framework_TestCase $this->config->addPlaceholder($order = 3); $this->assertSame(array( - array('url' => '', 'metrics' => array(), 'order' => 3), - array('url' => '', 'metrics' => array(), 'order' => 10), - array('url' => '', 'metrics' => array(), 'order' => 999), - array('url' => '', 'metrics' => array(), 'order' => 1001), + 'placeholder3' => [['url' => '', 'metrics' => array(), 'order' => 3, 'group' => 'placeholder3']], + 'placeholder1' => [['url' => '', 'metrics' => array(), 'order' => 10, 'group' => 'placeholder1']], + 'placeholder0' => [['url' => '', 'metrics' => array(), 'order' => 999, 'group' => 'placeholder0']], + 'placeholder2' => [['url' => '', 'metrics' => array(), 'order' => 1001, 'group' => 'placeholder2']], ), $this->config->getSortedSparklines()); } @@ -126,5 +126,4 @@ class SparklinesConfigTest extends \PHPUnit_Framework_TestCase $this->config->addSparklineMetric('nb_unique_visitors', 99); $this->config->addSparklineMetric(array('nb_downloads', 'nb_outlinks')); } - } diff --git a/plugins/DBStats/DBStats.php b/plugins/DBStats/DBStats.php index f75d4e8c95..47f83c0442 100644 --- a/plugins/DBStats/DBStats.php +++ b/plugins/DBStats/DBStats.php @@ -23,10 +23,16 @@ class DBStats extends \Piwik\Plugin public function registerEvents() { return array( - "TestingEnvironment.addHooks" => 'setupTestEnvironment' + "TestingEnvironment.addHooks" => 'setupTestEnvironment', + 'AssetManager.getStylesheetFiles' => 'getStylesheetFiles', ); } + public function getStylesheetFiles(&$stylesheets) + { + $stylesheets[] = 'plugins/DBStats/stylesheets/dbstats.less'; + } + public function setupTestEnvironment($environment) { Piwik::addAction("MySQLMetadataProvider.createDao", function (&$dao) { diff --git a/plugins/DBStats/stylesheets/dbstats.less b/plugins/DBStats/stylesheets/dbstats.less new file mode 100644 index 0000000000..b2c1c7e993 --- /dev/null +++ b/plugins/DBStats/stylesheets/dbstats.less @@ -0,0 +1,5 @@ +div.dataTable[data-report^=DBStats] { + table.dataTable th.label, table.dataTable td.label { + padding-left: 12px; + } +}
\ No newline at end of file diff --git a/plugins/DBStats/tests/UI/expected-screenshots/DBStats_admin_page.png b/plugins/DBStats/tests/UI/expected-screenshots/DBStats_admin_page.png index c59655a4d7..7f9341d765 100644 --- a/plugins/DBStats/tests/UI/expected-screenshots/DBStats_admin_page.png +++ b/plugins/DBStats/tests/UI/expected-screenshots/DBStats_admin_page.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:862978936eed6c7e784b2ebcd109a713672cc1af22073c281e8072d4422cbfc9 -size 226310 +oid sha256:22952c81c959f7c2bc15dd81c155425438961ba3810155fc1d79fc386911e1a4 +size 225924 diff --git a/plugins/Dashboard/javascripts/dashboardWidget.js b/plugins/Dashboard/javascripts/dashboardWidget.js index 749459005a..9d10183b19 100644 --- a/plugins/Dashboard/javascripts/dashboardWidget.js +++ b/plugins/Dashboard/javascripts/dashboardWidget.js @@ -146,10 +146,21 @@ } // Reading segment from hash tag (standard case) or from the URL (when embedding dashboard) - var segment = broadcast.getValueFromHash('segment') || broadcast.getValueFromUrl('segment'); - if (segment.length) { - this.widgetParameters.segment = segment; - } + ['segment'].forEach(function (paramName) { + var value = broadcast.getValueFromHash(paramName) || broadcast.getValueFromUrl(paramName); + if (value.length) { + self.widgetParameters[paramName] = value; + } + }); + + ['compareSegments', 'comparePeriods', 'compareDates'].forEach(function (paramName) { + var value = broadcast.getValueFromHash(paramName) || broadcast.getValueFromUrl(paramName); + if (value.length) { + self.widgetParameters[paramName] = value; + } else { + delete self.widgetParameters[paramName]; + } + }); if (!hideLoading) { $('.widgetContent', currentWidget).addClass('loading'); diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_create_new.png b/plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_create_new.png index 367473d945..0758e86dee 100644 --- a/plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_create_new.png +++ b/plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_create_new.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ac5b05a9f4ebfbc7d49375be488980272b9be6cf36041b97c072bf2f8450de03 -size 287799 +oid sha256:dd7a915297b1e0133b9a4ea0f2582e08de7b25dca0d6e31bc6907bb74aebb31a +size 287727 diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_removed.png b/plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_removed.png index 11b9123c87..187520fcaf 100644 --- a/plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_removed.png +++ b/plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_removed.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8e80edad1d12691eed9ac156e46bfbf5f50f4eeff4b69f5f0199f135e259620b -size 470940 +oid sha256:316535029886954e472f55900642ba3f7414d061a5dc582d98101a3f564e83c1 +size 470825 diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_widget_preview.png b/plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_widget_preview.png index 27bd0ba947..af0139a06b 100644 --- a/plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_widget_preview.png +++ b/plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_widget_preview.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:54f6f0f0a9c6d53ff8686e874c7536a3633eddec600f463f700e5b7f01a96870 -size 78171 +oid sha256:3d171c1f25f9e6c3d563d28e888a071e5aece8d16522d9e92a26189d1ac03baf +size 78152 diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_change_layout.png b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_change_layout.png index 3fea179b78..6f2b2ea43d 100644 --- a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_change_layout.png +++ b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_change_layout.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0a732ff6fca8b50b710c174e9c6bdc39e5ebc116aec52a4ad46aa09762bdc53e -size 28344 +oid sha256:734430cec12f1a1d50fdba2233767cce8b23a884390c363ca5b1900078256c46 +size 28347 diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_copied.png b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_copied.png index fa9c471761..69e5d2e373 100644 --- a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_copied.png +++ b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_copied.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9f7b84df4b2e445f5d81fac7a486b94df8288ebd1a732399f2461e2606ca8f83 -size 29499 +oid sha256:0abd087f4d36f11bab023eaac6fcf4c4f0466400f5a144f913d2c7c2551ec927 +size 29501 diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_create_new.png b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_create_new.png index 843781e99f..cd862e0a79 100644 --- a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_create_new.png +++ b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_create_new.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7484a597ebac3b46951cb6d793deb6404c84f46cac88fd0c09935774a11adcd9 -size 29658 +oid sha256:71aa611944407b45bc0327affe24edad35dde298d64589dfced48ac446309ebf +size 29659 diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_default_widget_selection_changed.png b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_default_widget_selection_changed.png index 1669f2d336..4fab81b58b 100644 --- a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_default_widget_selection_changed.png +++ b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_default_widget_selection_changed.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d6703682ea7e5aca3ec5417dc77366a2774e0a700b05d3e3813e7d8a0597fef8 -size 29006 +oid sha256:f27e517adceb88eb6b4a43e03309af013f31986ad90c424afaada6c1638392b0 +size 29010 diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_loaded.png b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_loaded.png index a56185dc51..d5ad244c7c 100644 --- a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_loaded.png +++ b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_loaded.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:76a9e2fb7470db5b710b852da2c68bda6d1bfbd6ac09bf51e63d8a55ff8a3bad -size 28628 +oid sha256:469bf405bc3cd4396f5a2ac8ea9a7ecf57446e88807ab1988a59947b0d6a017b +size 28577 diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_loaded_token_auth.png b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_loaded_token_auth.png index 5ee451d291..cb69af5416 100644 --- a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_loaded_token_auth.png +++ b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_loaded_token_auth.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c0a700ba89f44f95d8908095a360a1588288397d3a0e98b59d51e48e7a6dd7fd -size 693872 +oid sha256:9027e455792c410ef90909704194fc5b86eed3878c01ad2a1d74b2847798a9f9 +size 715131 diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_removed.png b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_removed.png index 5ee451d291..cb69af5416 100644 --- a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_removed.png +++ b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_removed.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c0a700ba89f44f95d8908095a360a1588288397d3a0e98b59d51e48e7a6dd7fd -size 693872 +oid sha256:9027e455792c410ef90909704194fc5b86eed3878c01ad2a1d74b2847798a9f9 +size 715131 diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_rename.png b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_rename.png index fe44ad8f2b..b58e73dd91 100644 --- a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_rename.png +++ b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_rename.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aaf59949126ca30c805c2f1d387f876e4cf3780fc951df9256d745f3b4586c24 -size 29007 +oid sha256:a19692edc4525595995dce157fb8e162fc87fe9984766ed8d7797036206ca48c +size 29011 diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_reset.png b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_reset.png index f57a1905e9..32c1ff560d 100644 --- a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_reset.png +++ b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_reset.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ce83173ae5194bc6b56e06f4d64637b1f678efad64861956622445ec647e4a20 -size 281844 +oid sha256:66ce0f6234d80c90741d0e9ff99d871c141b403101fee5616a2257bb945c2cc0 +size 281686 diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_rowevolution.png b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_rowevolution.png index c82945c71f..e8fc073272 100644 --- a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_rowevolution.png +++ b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_rowevolution.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:586e98a68becb0936e21b67ff433c65bdacbed176bd8362465c9628363ab32c4 -size 72431 +oid sha256:5d1c25dee11f60e08f34a88a081e4074278ce4a9f5a9d0996f062f4361de290e +size 72373 diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_segmented.png b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_segmented.png index de02e3b03a..9b328e1a9d 100644 --- a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_segmented.png +++ b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_segmented.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:993ae1d84c577d402680ef32951a3f0207f7a51cdcfb347a245b0771f5ea748a -size 31572 +oid sha256:171e0f85a9c19893127b98b84c8f92c986e77b0c829d3c41afb78063c8c4eee0 +size 31565 diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_widget_add_widget.png b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_widget_add_widget.png index ae4e681faa..65476f9531 100644 --- a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_widget_add_widget.png +++ b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_widget_add_widget.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3e1fc62ca9469d478f30d75750379974968a87204fef7faf00a400cb60bbfa34 -size 200735 +oid sha256:bb214ea5d1c825347a5fff7603e6990773e30d8a71b3248a24261143f00bb24c +size 201173 diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_widget_maximise.png b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_widget_maximise.png index ed300b8f42..224a9bf21c 100644 --- a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_widget_maximise.png +++ b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_widget_maximise.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9006aeee36f702ce455ac67d332ccb6470a31e211f0b4965f232a18bdf069446 -size 31300 +oid sha256:731057f7bf638d825c38f6f5428381f1e033c7e63e1be88b92dcaf6f0ac41fcd +size 31275 diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_widget_move.png b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_widget_move.png index 40877036d6..4bf82eadc5 100644 --- a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_widget_move.png +++ b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_widget_move.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d35d29d09c2fa108f9236290b66221073c617215c1cad17b26d8d5101371511a -size 28681 +oid sha256:495f12fac4ec25587e2cfe72d95b28799c309486dd285c7e8e3fecb4e4c5a313 +size 28633 diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_widget_move_removed.png b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_widget_move_removed.png index 40877036d6..4bf82eadc5 100644 --- a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_widget_move_removed.png +++ b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_widget_move_removed.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d35d29d09c2fa108f9236290b66221073c617215c1cad17b26d8d5101371511a -size 28681 +oid sha256:495f12fac4ec25587e2cfe72d95b28799c309486dd285c7e8e3fecb4e4c5a313 +size 28633 diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_widget_refresh.png b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_widget_refresh.png index 40877036d6..4bf82eadc5 100644 --- a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_widget_refresh.png +++ b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_widget_refresh.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d35d29d09c2fa108f9236290b66221073c617215c1cad17b26d8d5101371511a -size 28681 +oid sha256:495f12fac4ec25587e2cfe72d95b28799c309486dd285c7e8e3fecb4e4c5a313 +size 28633 diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_widget_unmaximise.png b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_widget_unmaximise.png index 3510aad611..a18864910a 100644 --- a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_widget_unmaximise.png +++ b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_widget_unmaximise.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f35259a6167d3fbdb213b446628910be4e3fcbe5159245cccec3a08f4138482b -size 30288 +oid sha256:d7aec7a34f19fe0a63126e7426b7cefe42d3ae3a7f71f176b0254e027acdad8c +size 30244 diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_widget_unminimise.png b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_widget_unminimise.png index 40877036d6..4bf82eadc5 100644 --- a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_widget_unminimise.png +++ b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_widget_unminimise.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d35d29d09c2fa108f9236290b66221073c617215c1cad17b26d8d5101371511a -size 28681 +oid sha256:495f12fac4ec25587e2cfe72d95b28799c309486dd285c7e8e3fecb4e4c5a313 +size 28633 diff --git a/plugins/Diagnostics/tests/Integration/Commands/AnalyzeArchiveTableTest.php b/plugins/Diagnostics/tests/Integration/Commands/AnalyzeArchiveTableTest.php index 844753bb10..8402b9506c 100644 --- a/plugins/Diagnostics/tests/Integration/Commands/AnalyzeArchiveTableTest.php +++ b/plugins/Diagnostics/tests/Integration/Commands/AnalyzeArchiveTableTest.php @@ -82,7 +82,7 @@ Total # Invalidated Archives: 0 Total # Temporary Archives: 0 Total # Error Archives: 0 Total # Segment Archives: 162 -Total Size of Blobs: 24.%d K +Total Size of Blobs: 25.%d K OUTPUT; diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_schedrep_html_tables_and_graph__ScheduledReports.generateReport_week.original.html b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_schedrep_html_tables_and_graph__ScheduledReports.generateReport_week.original.html index 150d212a52..f42b269e2c 100644 --- a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_schedrep_html_tables_and_graph__ScheduledReports.generateReport_week.original.html +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_schedrep_html_tables_and_graph__ScheduledReports.generateReport_week.original.html @@ -289,6 +289,11 @@ </a> </li> <li style="font-size:15px;line-height:24px;"> + <a href="#Referrers_get" style="text-decoration:none; color: #0d0d0d;"> + Referrers Overview + </a> + </li> + <li style="font-size:15px;line-height:24px;"> <a href="#Referrers_getReferrerType" style="text-decoration:none; color: #0d0d0d;"> Channel Type </a> @@ -4864,6 +4869,156 @@ Back to top ↑ </a></p> +<h2 id="Referrers_get" style=" color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, 'Helvetica Neue', sans-serif; font-size: 24pt; font-weight:normal; margin:45px 0 30px 0;"> + Referrers Overview +</h2> + + <img alt="" + src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAArwAAADICAIAAACF9KXqAAAEfklEQVR4nO3cS1biQABA0ehxJ+x/SayFHtCHTufHE+IHuHckEqoqTuqRCG+n02nYw+FwOB6Pew01DMM9o+24mIebHQC+yPvhcDhv0heTh/f71IDnHfeGTffqLLufFwC8lPefXsD38e4fAO7xMQzD8XhcvKI+fmu+uOMuvne//PL8kvPD8R2HyQHz114Oni/gvM75auezLK4kLmA86WTGxTEXB9yYBQAe0cfaE5ONeXGf3t4jLzcaJkdODhiPOdmeFxew2DfzWcYP136+esrz38/HWRw8zggAD+Tv7YnJ2/rfLG7AO+7T20MtPqsSAHg+/640rN2k+Kxf/g+Jlzy67UwfJa0AYHertyduM7ksf8MB32DjNse237B4APgp/3164uabFM+3g37FGT3fXwmAl7J6pWHxwwtrB4yv+c+3xvGzvUuuLmBjlo3DNj7UsHhGG0uaDFgWCQCP622vb4R8PuX+hU9GAPA6dv6fhkfnsgEArHGlAQBIXuhrpAGAe4gGACARDQBAIhoAgEQ0AACJaAAAEtEAACSiAQBIRAMAkIgGACARDQBAIhoAgEQ0AACJaAAAEtEAACSiAQBIRAMAkIgGACARDQBAIhoAgEQ0AACJaAAAEtEAACSiAQBIRAMAkIgGACARDQBAIhoAgEQ0AACJaAAAEtEAACSiAQBIRAMAkIgGACARDQBAIhoAgEQ0AACJaAAAEtEAACSiAQBIRAMAkIgGACARDQBAIhoAgEQ0AACJaAAAEtEAACSiAQBIRAMAkIgGACARDQBAIhoAgEQ0AACJaAAAEtEAACSiAQBIRAMAkIgGACARDQBAIhoAgEQ0AACJaAAAEtEAACSiAQBIRAMAkIgGACARDQBAIhoAgEQ0AACJaAAAEtEAACSiAQBIRAMAkIgGACARDQBAIhoAgEQ0AACJaAAAEtEAACSiAQBIRAMAkIgGACARDQBAIhoAgEQ0AACJaAAAEtEAACSiAQBIRAMAkIgGACARDQBAIhoAgEQ0AACJaAAAEtEAACSiAQBIRAMAkIgGACARDQBAIhoAgEQ0AACJaAAAEtEAACSiAQBIRAMAkIgGACARDQBAIhoAgEQ0AACJaAAAEtEAACSiAQBIRAMAkIgGACARDQBAIhoAgEQ0AACJaAAAEtEAACSiAQBIRAMAkIgGACARDQBAIhoAgEQ0AACJaAAAEtEAACSiAQBIRAMAkIgGACARDQBAIhoAgEQ0AACJaAAAEtEAACSiAQBIRAMAkIgGACARDQBAIhoAgEQ0AACJaAAAEtEAACSiAQBIRAMAkIgGACARDQBAIhoAgEQ0AACJaAAAEtEAACSiAQBIRAMAkIgGACARDQBAIhoAgEQ0AACJaAAAEtEAACSiAQBIRAMAkIgGACARDQBAIhoAgEQ0AACJaAAAEtEAACSiAQBIRAMAkIgGACARDQBAIhoAgEQ0AACJaAAAEtEAACSiAQBIRAMAkIgGACARDQBAIhoAgEQ0AACJaAAAEtEAACSiAQBIRAMAkIgGACARDQBA8gczQRugNvGghAAAAABJRU5ErkJggg==" + height="200" + width="700" + margin="0 auto"/> + + <br/> + <br/> + + <table style="border-collapse:collapse; border:1px solid rgb(231,231,231); padding:5px;"> + <thead style="background-color: #f2f2f2;"> + <th style="border-bottom:1px solid rgb(231,231,231);font-size: 15px;text-align: left;font-weight:normal;padding:13px 0 13px 10px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, 'Helvetica Neue', sans-serif;"> + Name + </th> + <th style="border-bottom:1px solid rgb(231,231,231);font-size: 15px;text-align: left;font-weight:normal;padding:13px 0 13px 10px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, 'Helvetica Neue', sans-serif; text-align:right;"> + Value + </th> + </thead> + <tbody> + + <tr style=";"> + <td style="padding:17px 15px;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, 'Helvetica Neue', sans-serif;"> + Visitors from Search Engines </td> + <td style="padding:17px 15px; text-align:right;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, 'Helvetica Neue', sans-serif;"> + 0 + </td> + </tr> + + <tr style="background-color: #f2f2f2;"> + <td style="padding:17px 15px;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, 'Helvetica Neue', sans-serif;"> + Visitors from Social Networks </td> + <td style="padding:17px 15px; text-align:right;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, 'Helvetica Neue', sans-serif;"> + 0 + </td> + </tr> + + <tr style=";"> + <td style="padding:17px 15px;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, 'Helvetica Neue', sans-serif;"> + Visitors from Direct Entry </td> + <td style="padding:17px 15px; text-align:right;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, 'Helvetica Neue', sans-serif;"> + 5 + </td> + </tr> + + <tr style="background-color: #f2f2f2;"> + <td style="padding:17px 15px;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, 'Helvetica Neue', sans-serif;"> + Visitors from Websites </td> + <td style="padding:17px 15px; text-align:right;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, 'Helvetica Neue', sans-serif;"> + 0 + </td> + </tr> + + <tr style=";"> + <td style="padding:17px 15px;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, 'Helvetica Neue', sans-serif;"> + Visitors from Campaigns </td> + <td style="padding:17px 15px; text-align:right;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, 'Helvetica Neue', sans-serif;"> + 0 + </td> + </tr> + + <tr style="background-color: #f2f2f2;"> + <td style="padding:17px 15px;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, 'Helvetica Neue', sans-serif;"> + Distinct search engines </td> + <td style="padding:17px 15px; text-align:right;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, 'Helvetica Neue', sans-serif;"> + 0 + </td> + </tr> + + <tr style=";"> + <td style="padding:17px 15px;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, 'Helvetica Neue', sans-serif;"> + Distinct social networks </td> + <td style="padding:17px 15px; text-align:right;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, 'Helvetica Neue', sans-serif;"> + 0 + </td> + </tr> + + <tr style="background-color: #f2f2f2;"> + <td style="padding:17px 15px;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, 'Helvetica Neue', sans-serif;"> + Distinct keywords </td> + <td style="padding:17px 15px; text-align:right;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, 'Helvetica Neue', sans-serif;"> + 0 + </td> + </tr> + + <tr style=";"> + <td style="padding:17px 15px;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, 'Helvetica Neue', sans-serif;"> + Distinct websites </td> + <td style="padding:17px 15px; text-align:right;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, 'Helvetica Neue', sans-serif;"> + 0 + </td> + </tr> + + <tr style="background-color: #f2f2f2;"> + <td style="padding:17px 15px;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, 'Helvetica Neue', sans-serif;"> + Distinct campaigns </td> + <td style="padding:17px 15px; text-align:right;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, 'Helvetica Neue', sans-serif;"> + 0 + </td> + </tr> + + <tr style=";"> + <td style="padding:17px 15px;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, 'Helvetica Neue', sans-serif;"> + Percent of Visitors from Direct Entry </td> + <td style="padding:17px 15px; text-align:right;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, 'Helvetica Neue', sans-serif;"> + 100% + </td> + </tr> + + <tr style="background-color: #f2f2f2;"> + <td style="padding:17px 15px;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, 'Helvetica Neue', sans-serif;"> + Percent of Visitors from Search Engines </td> + <td style="padding:17px 15px; text-align:right;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, 'Helvetica Neue', sans-serif;"> + 0% + </td> + </tr> + + <tr style=";"> + <td style="padding:17px 15px;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, 'Helvetica Neue', sans-serif;"> + Percent of Visitors from Campaigns </td> + <td style="padding:17px 15px; text-align:right;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, 'Helvetica Neue', sans-serif;"> + 0% + </td> + </tr> + + <tr style="background-color: #f2f2f2;"> + <td style="padding:17px 15px;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, 'Helvetica Neue', sans-serif;"> + Percent of Visitors from Social Networks </td> + <td style="padding:17px 15px; text-align:right;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, 'Helvetica Neue', sans-serif;"> + 0% + </td> + </tr> + + <tr style=";"> + <td style="padding:17px 15px;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, 'Helvetica Neue', sans-serif;"> + Percent of Visitors from Websites </td> + <td style="padding:17px 15px; text-align:right;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, 'Helvetica Neue', sans-serif;"> + 0% + </td> + </tr> + </tbody> + </table> + <p style="width: 100%; text-align:center;"> + <a style="color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, 'Helvetica Neue', sans-serif;; text-decoration:none; font-size: 9pt;" href="#reportTop"> + Back to top ↑ + </a></p> + <h2 id="Referrers_getReferrerType" style=" color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, 'Helvetica Neue', sans-serif; font-size: 24pt; font-weight:normal; margin:45px 0 30px 0;"> Channel Type </h2> diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_schedrep_html_tables_only__ScheduledReports.generateReport_week.original.html b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_schedrep_html_tables_only__ScheduledReports.generateReport_week.original.html index 162148fd66..58363ff845 100644 --- a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_schedrep_html_tables_only__ScheduledReports.generateReport_week.original.html +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_schedrep_html_tables_only__ScheduledReports.generateReport_week.original.html @@ -289,6 +289,11 @@ </a> </li> <li style="font-size:15px;line-height:24px;"> + <a href="#Referrers_get" style="text-decoration:none; color: #0d0d0d;"> + Referrers Overview + </a> + </li> + <li style="font-size:15px;line-height:24px;"> <a href="#Referrers_getReferrerType" style="text-decoration:none; color: #0d0d0d;"> Channel Type </a> @@ -4612,6 +4617,149 @@ Back to top ↑ </a></p> +<h2 id="Referrers_get" style=" color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, 'Helvetica Neue', sans-serif; font-size: 24pt; font-weight:normal; margin:45px 0 30px 0;"> + Referrers Overview +</h2> + + + + <table style="border-collapse:collapse; border:1px solid rgb(231,231,231); padding:5px;"> + <thead style="background-color: #f2f2f2;"> + <th style="border-bottom:1px solid rgb(231,231,231);font-size: 15px;text-align: left;font-weight:normal;padding:13px 0 13px 10px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, 'Helvetica Neue', sans-serif;"> + Name + </th> + <th style="border-bottom:1px solid rgb(231,231,231);font-size: 15px;text-align: left;font-weight:normal;padding:13px 0 13px 10px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, 'Helvetica Neue', sans-serif; text-align:right;"> + Value + </th> + </thead> + <tbody> + + <tr style=";"> + <td style="padding:17px 15px;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, 'Helvetica Neue', sans-serif;"> + Visitors from Search Engines </td> + <td style="padding:17px 15px; text-align:right;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, 'Helvetica Neue', sans-serif;"> + 0 + </td> + </tr> + + <tr style="background-color: #f2f2f2;"> + <td style="padding:17px 15px;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, 'Helvetica Neue', sans-serif;"> + Visitors from Social Networks </td> + <td style="padding:17px 15px; text-align:right;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, 'Helvetica Neue', sans-serif;"> + 0 + </td> + </tr> + + <tr style=";"> + <td style="padding:17px 15px;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, 'Helvetica Neue', sans-serif;"> + Visitors from Direct Entry </td> + <td style="padding:17px 15px; text-align:right;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, 'Helvetica Neue', sans-serif;"> + 5 + </td> + </tr> + + <tr style="background-color: #f2f2f2;"> + <td style="padding:17px 15px;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, 'Helvetica Neue', sans-serif;"> + Visitors from Websites </td> + <td style="padding:17px 15px; text-align:right;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, 'Helvetica Neue', sans-serif;"> + 0 + </td> + </tr> + + <tr style=";"> + <td style="padding:17px 15px;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, 'Helvetica Neue', sans-serif;"> + Visitors from Campaigns </td> + <td style="padding:17px 15px; text-align:right;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, 'Helvetica Neue', sans-serif;"> + 0 + </td> + </tr> + + <tr style="background-color: #f2f2f2;"> + <td style="padding:17px 15px;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, 'Helvetica Neue', sans-serif;"> + Distinct search engines </td> + <td style="padding:17px 15px; text-align:right;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, 'Helvetica Neue', sans-serif;"> + 0 + </td> + </tr> + + <tr style=";"> + <td style="padding:17px 15px;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, 'Helvetica Neue', sans-serif;"> + Distinct social networks </td> + <td style="padding:17px 15px; text-align:right;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, 'Helvetica Neue', sans-serif;"> + 0 + </td> + </tr> + + <tr style="background-color: #f2f2f2;"> + <td style="padding:17px 15px;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, 'Helvetica Neue', sans-serif;"> + Distinct keywords </td> + <td style="padding:17px 15px; text-align:right;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, 'Helvetica Neue', sans-serif;"> + 0 + </td> + </tr> + + <tr style=";"> + <td style="padding:17px 15px;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, 'Helvetica Neue', sans-serif;"> + Distinct websites </td> + <td style="padding:17px 15px; text-align:right;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, 'Helvetica Neue', sans-serif;"> + 0 + </td> + </tr> + + <tr style="background-color: #f2f2f2;"> + <td style="padding:17px 15px;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, 'Helvetica Neue', sans-serif;"> + Distinct campaigns </td> + <td style="padding:17px 15px; text-align:right;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, 'Helvetica Neue', sans-serif;"> + 0 + </td> + </tr> + + <tr style=";"> + <td style="padding:17px 15px;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, 'Helvetica Neue', sans-serif;"> + Percent of Visitors from Direct Entry </td> + <td style="padding:17px 15px; text-align:right;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, 'Helvetica Neue', sans-serif;"> + 100% + </td> + </tr> + + <tr style="background-color: #f2f2f2;"> + <td style="padding:17px 15px;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, 'Helvetica Neue', sans-serif;"> + Percent of Visitors from Search Engines </td> + <td style="padding:17px 15px; text-align:right;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, 'Helvetica Neue', sans-serif;"> + 0% + </td> + </tr> + + <tr style=";"> + <td style="padding:17px 15px;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, 'Helvetica Neue', sans-serif;"> + Percent of Visitors from Campaigns </td> + <td style="padding:17px 15px; text-align:right;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, 'Helvetica Neue', sans-serif;"> + 0% + </td> + </tr> + + <tr style="background-color: #f2f2f2;"> + <td style="padding:17px 15px;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, 'Helvetica Neue', sans-serif;"> + Percent of Visitors from Social Networks </td> + <td style="padding:17px 15px; text-align:right;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, 'Helvetica Neue', sans-serif;"> + 0% + </td> + </tr> + + <tr style=";"> + <td style="padding:17px 15px;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, 'Helvetica Neue', sans-serif;"> + Percent of Visitors from Websites </td> + <td style="padding:17px 15px; text-align:right;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, 'Helvetica Neue', sans-serif;"> + 0% + </td> + </tr> + </tbody> + </table> + <p style="width: 100%; text-align:center;"> + <a style="color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, 'Helvetica Neue', sans-serif;; text-decoration:none; font-size: 9pt;" href="#reportTop"> + Back to top ↑ + </a></p> + <h2 id="Referrers_getReferrerType" style=" color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, 'Helvetica Neue', sans-serif; font-size: 24pt; font-weight:normal; margin:45px 0 30px 0;"> Channel Type </h2> diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_schedrep_in_csv__ScheduledReports.generateReport_week.original.csv b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_schedrep_in_csv__ScheduledReports.generateReport_week.original.csv index 29e371ff87..b0dc0f05e0 100644 --- a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_schedrep_in_csv__ScheduledReports.generateReport_week.original.csv +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_schedrep_in_csv__ScheduledReports.generateReport_week.original.csv @@ -303,6 +303,10 @@ Returning Visits nb_uniq_visitors_returning,nb_users_returning,nb_visits_returning,nb_actions_returning,max_actions_returning,bounce_rate_returning,nb_actions_per_visit_returning,avg_time_on_site_returning 1,0,4,12,6,25%,3,00:25:32 +Referrers Overview +Referrers_visitorsFromSearchEngines,Referrers_visitorsFromSocialNetworks,Referrers_visitorsFromDirectEntry,Referrers_visitorsFromWebsites,Referrers_visitorsFromCampaigns,Referrers_distinctSearchEngines,Referrers_distinctSocialNetworks,Referrers_distinctKeywords,Referrers_distinctWebsites,Referrers_distinctCampaigns,Referrers_visitorsFromDirectEntry_percent,Referrers_visitorsFromSearchEngines_percent,Referrers_visitorsFromCampaigns_percent,Referrers_visitorsFromSocialNetworks_percent,Referrers_visitorsFromWebsites_percent +0,0,5,0,0,0,0,0,0,0,100%,0%,0%,0%,0% + Channel Type label,nb_visits,nb_actions,revenue,nb_actions_per_visit,avg_time_on_site,bounce_rate Direct Entry,5,16,"$13,361.11",3.2,00:22:49,20% diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_schedrep_in_pdf_tables_only__ScheduledReports.generateReport_week.original.pdf b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_schedrep_in_pdf_tables_only__ScheduledReports.generateReport_week.original.pdf Binary files differindex eca7f7e68e..bb0ce53170 100644 --- a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_schedrep_in_pdf_tables_only__ScheduledReports.generateReport_week.original.pdf +++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_schedrep_in_pdf_tables_only__ScheduledReports.generateReport_week.original.pdf diff --git a/plugins/ExamplePlugin/tests/System/expected/test___API.get_day.xml b/plugins/ExamplePlugin/tests/System/expected/test___API.get_day.xml index 213b91b66a..47964dea53 100644 --- a/plugins/ExamplePlugin/tests/System/expected/test___API.get_day.xml +++ b/plugins/ExamplePlugin/tests/System/expected/test___API.get_day.xml @@ -15,6 +15,22 @@ <bounce_rate_returning>0%</bounce_rate_returning> <nb_actions_per_visit_returning>0</nb_actions_per_visit_returning> <avg_time_on_site_returning>0</avg_time_on_site_returning> + <Referrers_visitorsFromSearchEngines>0</Referrers_visitorsFromSearchEngines> + <Referrers_visitorsFromSocialNetworks>0</Referrers_visitorsFromSocialNetworks> + <Referrers_visitorsFromDirectEntry>2</Referrers_visitorsFromDirectEntry> + <Referrers_visitorsFromWebsites>0</Referrers_visitorsFromWebsites> + <Referrers_visitorsFromCampaigns>0</Referrers_visitorsFromCampaigns> + <Referrers_distinctSearchEngines>0</Referrers_distinctSearchEngines> + <Referrers_distinctSocialNetworks>0</Referrers_distinctSocialNetworks> + <Referrers_distinctKeywords>0</Referrers_distinctKeywords> + <Referrers_distinctWebsites>0</Referrers_distinctWebsites> + <Referrers_distinctWebsitesUrls>0</Referrers_distinctWebsitesUrls> + <Referrers_distinctCampaigns>0</Referrers_distinctCampaigns> + <Referrers_visitorsFromDirectEntry_percent>100%</Referrers_visitorsFromDirectEntry_percent> + <Referrers_visitorsFromSearchEngines_percent>0%</Referrers_visitorsFromSearchEngines_percent> + <Referrers_visitorsFromCampaigns_percent>0%</Referrers_visitorsFromCampaigns_percent> + <Referrers_visitorsFromSocialNetworks_percent>0%</Referrers_visitorsFromSocialNetworks_percent> + <Referrers_visitorsFromWebsites_percent>0%</Referrers_visitorsFromWebsites_percent> <nb_conversions>1</nb_conversions> <nb_visits_converted>1</nb_visits_converted> <revenue>2541</revenue> diff --git a/plugins/Goals/API.php b/plugins/Goals/API.php index 6ee19e66c1..b09519e37b 100644 --- a/plugins/Goals/API.php +++ b/plugins/Goals/API.php @@ -440,7 +440,7 @@ class API extends \Piwik\Plugin\API * @param bool $showAllGoalSpecificMetrics whether to show all goal specific metrics when no goal is set * @return DataTable */ - public function get($idSite, $period, $date, $segment = false, $idGoal = false, $columns = array(), $showAllGoalSpecificMetrics = false) + public function get($idSite, $period, $date, $segment = false, $idGoal = false, $columns = array(), $showAllGoalSpecificMetrics = false, $compare = false) { Piwik::checkUserHasViewAccess($idSite); @@ -479,6 +479,19 @@ class API extends \Piwik\Plugin\API } } + // if we are comparing, this will be queried with format_metrics=0, but we will eventually need to format the metrics. + // unfortunately, we can't do that since the processed metric information is in the GetMetrics report. in this case, + // we queue the filter so it will eventually be formatted. + if (!empty($compare)) { + $getMetricsReport = ReportsProvider::factory('Goals', 'getMetrics'); + $table->queueFilter(function (DataTable $t) use ($getMetricsReport) { + $t->setMetadata(Metrics\Formatter::PROCESSED_METRICS_FORMATTED_FLAG, false); + + $formatter = new Metrics\Formatter(); + $formatter->formatMetrics($t, $getMetricsReport, $metricsToFormat = null, $formatAll = true); + }); + } + return $table; } diff --git a/plugins/ImageGraph/StaticGraph/Pie.php b/plugins/ImageGraph/StaticGraph/Pie.php index 63d7bf0752..6bae617232 100644 --- a/plugins/ImageGraph/StaticGraph/Pie.php +++ b/plugins/ImageGraph/StaticGraph/Pie.php @@ -23,4 +23,9 @@ class Pie extends PieGraph $this->pieConfig ); } + + public function supportsComparison() + { + return false; + } } diff --git a/plugins/Insights/tests/UI/expected-screenshots/Insights_initial.png b/plugins/Insights/tests/UI/expected-screenshots/Insights_initial.png index f670d55409..458c9c05c6 100644 --- a/plugins/Insights/tests/UI/expected-screenshots/Insights_initial.png +++ b/plugins/Insights/tests/UI/expected-screenshots/Insights_initial.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2bc504a2c6e1ea9a920dd044b094ba81ab4b40f023e74d98b3ea7a6bfe9283a7 -size 152748 +oid sha256:f5be2ace3fdaaff48711714d4d3df88c062051871e8e2bf8e30f93e0159de774 +size 146699 diff --git a/plugins/Intl/Intl.php b/plugins/Intl/Intl.php index b8e0030240..8f44e8d06a 100644 --- a/plugins/Intl/Intl.php +++ b/plugins/Intl/Intl.php @@ -10,5 +10,19 @@ namespace Piwik\Plugins\Intl; class Intl extends \Piwik\Plugin { + public function getListHooksRegistered() + { + return [ + 'Translate.getClientSideTranslationKeys' => 'getClientSideTranslationKeys', + ]; + } + public function getClientSideTranslationKeys(&$translationKeys) + { + $translationKeys[] = 'Intl_PeriodDay'; + $translationKeys[] = 'Intl_PeriodMonth'; + $translationKeys[] = 'Intl_PeriodWeek'; + $translationKeys[] = 'Intl_PeriodYear'; + $translationKeys[] = 'CoreHome_PeriodRange'; + } } diff --git a/plugins/IntranetMeasurable/tests/System/expected/test__intranet__API.get_day.xml b/plugins/IntranetMeasurable/tests/System/expected/test__intranet__API.get_day.xml index 6673fc4a55..779aa606bc 100644 --- a/plugins/IntranetMeasurable/tests/System/expected/test__intranet__API.get_day.xml +++ b/plugins/IntranetMeasurable/tests/System/expected/test__intranet__API.get_day.xml @@ -15,6 +15,22 @@ <bounce_rate_returning>0%</bounce_rate_returning> <nb_actions_per_visit_returning>0</nb_actions_per_visit_returning> <avg_time_on_site_returning>0</avg_time_on_site_returning> + <Referrers_visitorsFromSearchEngines>0</Referrers_visitorsFromSearchEngines> + <Referrers_visitorsFromSocialNetworks>0</Referrers_visitorsFromSocialNetworks> + <Referrers_visitorsFromDirectEntry>2</Referrers_visitorsFromDirectEntry> + <Referrers_visitorsFromWebsites>0</Referrers_visitorsFromWebsites> + <Referrers_visitorsFromCampaigns>0</Referrers_visitorsFromCampaigns> + <Referrers_distinctSearchEngines>0</Referrers_distinctSearchEngines> + <Referrers_distinctSocialNetworks>0</Referrers_distinctSocialNetworks> + <Referrers_distinctKeywords>0</Referrers_distinctKeywords> + <Referrers_distinctWebsites>0</Referrers_distinctWebsites> + <Referrers_distinctWebsitesUrls>0</Referrers_distinctWebsitesUrls> + <Referrers_distinctCampaigns>0</Referrers_distinctCampaigns> + <Referrers_visitorsFromDirectEntry_percent>100%</Referrers_visitorsFromDirectEntry_percent> + <Referrers_visitorsFromSearchEngines_percent>0%</Referrers_visitorsFromSearchEngines_percent> + <Referrers_visitorsFromCampaigns_percent>0%</Referrers_visitorsFromCampaigns_percent> + <Referrers_visitorsFromSocialNetworks_percent>0%</Referrers_visitorsFromSocialNetworks_percent> + <Referrers_visitorsFromWebsites_percent>0%</Referrers_visitorsFromWebsites_percent> <nb_conversions>0</nb_conversions> <nb_visits_converted>0</nb_visits_converted> <revenue>0</revenue> diff --git a/plugins/IntranetMeasurable/tests/System/expected/test__notIntranet__API.get_day.xml b/plugins/IntranetMeasurable/tests/System/expected/test__notIntranet__API.get_day.xml index 416534aa46..ad303275b1 100644 --- a/plugins/IntranetMeasurable/tests/System/expected/test__notIntranet__API.get_day.xml +++ b/plugins/IntranetMeasurable/tests/System/expected/test__notIntranet__API.get_day.xml @@ -15,6 +15,22 @@ <bounce_rate_returning>0%</bounce_rate_returning> <nb_actions_per_visit_returning>0</nb_actions_per_visit_returning> <avg_time_on_site_returning>0</avg_time_on_site_returning> + <Referrers_visitorsFromSearchEngines>0</Referrers_visitorsFromSearchEngines> + <Referrers_visitorsFromSocialNetworks>0</Referrers_visitorsFromSocialNetworks> + <Referrers_visitorsFromDirectEntry>1</Referrers_visitorsFromDirectEntry> + <Referrers_visitorsFromWebsites>0</Referrers_visitorsFromWebsites> + <Referrers_visitorsFromCampaigns>0</Referrers_visitorsFromCampaigns> + <Referrers_distinctSearchEngines>0</Referrers_distinctSearchEngines> + <Referrers_distinctSocialNetworks>0</Referrers_distinctSocialNetworks> + <Referrers_distinctKeywords>0</Referrers_distinctKeywords> + <Referrers_distinctWebsites>0</Referrers_distinctWebsites> + <Referrers_distinctWebsitesUrls>0</Referrers_distinctWebsitesUrls> + <Referrers_distinctCampaigns>0</Referrers_distinctCampaigns> + <Referrers_visitorsFromDirectEntry_percent>100%</Referrers_visitorsFromDirectEntry_percent> + <Referrers_visitorsFromSearchEngines_percent>0%</Referrers_visitorsFromSearchEngines_percent> + <Referrers_visitorsFromCampaigns_percent>0%</Referrers_visitorsFromCampaigns_percent> + <Referrers_visitorsFromSocialNetworks_percent>0%</Referrers_visitorsFromSocialNetworks_percent> + <Referrers_visitorsFromWebsites_percent>0%</Referrers_visitorsFromWebsites_percent> <nb_conversions>0</nb_conversions> <nb_visits_converted>0</nb_visits_converted> <revenue>0</revenue> diff --git a/plugins/Live/Live.php b/plugins/Live/Live.php index 29be212907..e1bf4f828f 100644 --- a/plugins/Live/Live.php +++ b/plugins/Live/Live.php @@ -34,9 +34,16 @@ class Live extends \Piwik\Plugin 'Live.renderVisitorDetails' => 'renderVisitorDetails', 'Live.renderVisitorIcons' => 'renderVisitorIcons', 'Template.jsGlobalVariables' => 'addJsGlobalVariables', + 'API.getPagesComparisonsDisabledFor' => 'getPagesComparisonsDisabledFor', ); } + public function getPagesComparisonsDisabledFor(&$pages) + { + $pages[] = 'General_Visitors.Live_VisitorLog'; + $pages[] = 'General_Visitors.General_RealTime'; + } + public function addJsGlobalVariables(&$out) { $actionsToDisplayCollapsed = (int)StaticContainer::get('Live.pageViewActionsToDisplayCollapsed'); diff --git a/plugins/Live/javascripts/rowaction.js b/plugins/Live/javascripts/rowaction.js index 061637eff4..ebaed67b0c 100644 --- a/plugins/Live/javascripts/rowaction.js +++ b/plugins/Live/javascripts/rowaction.js @@ -78,15 +78,22 @@ this.performAction(segment, tr, e); }; - DataTable_RowActions_SegmentVisitorLog.prototype.performAction = function (segment, tr, e) { + DataTable_RowActions_SegmentVisitorLog.prototype.performAction = function (segment, tr, e, originalRow) { var apiMethod = this.dataTable.param.module + '.' + this.dataTable.param.action; var extraParams = {}; + if (this.dataTable.param.date && this.dataTable.param.period) { extraParams = {date: this.dataTable.param.date, period: this.dataTable.param.period}; } + var paramOverride = $(originalRow || tr).data('param-override'); + if (typeof paramOverride !== 'object') { + paramOverride = {}; + } + $.extend(extraParams, paramOverride); + $.each(this.dataTable.param, function (index, value) { // we automatically add fields like idDimension, idGoal etc. if (index !== 'idSite' && index.indexOf('id') === 0 && $.isNumeric(value)) { @@ -133,7 +140,7 @@ }, isAvailableOnRow: function (dataTableParams, tr) { - var value = getRawSegmentValueFromRow(tr) + var value = getRawSegmentValueFromRow(tr); if ('undefined' === (typeof value)) { return false; } diff --git a/plugins/Live/stylesheets/live.less b/plugins/Live/stylesheets/live.less index eb9d64cdb3..9a0e4730a6 100644 --- a/plugins/Live/stylesheets/live.less +++ b/plugins/Live/stylesheets/live.less @@ -94,6 +94,12 @@ width: ~"calc(100% + 40px)"; } +#visitsTotal table.dataTable { + td:first-child, th:first-child { + padding-left: 14px; + } +} + .visitsLiveFooter img { vertical-align: middle; } @@ -600,6 +606,11 @@ a.visitor-log-visitor-profile-link { } } +.reporting-page #widgetLivewidget .theWidgetContent .visitsLiveFooter { + margin-left: -9px; + margin-right: -2px; +} + .refresh-divider { display: none; } diff --git a/plugins/Login/tests/Integration/PasswordResetterTest.php b/plugins/Login/tests/Integration/PasswordResetterTest.php index 3d870f64fe..607316f91f 100644 --- a/plugins/Login/tests/Integration/PasswordResetterTest.php +++ b/plugins/Login/tests/Integration/PasswordResetterTest.php @@ -203,7 +203,7 @@ class PasswordResetterTest extends IntegrationTestCase 'observers.global' => [ ['Mail.send', function (Mail $mail) { $body = $mail->getBodyHtml(true); - preg_match('/resetToken=3D([a-zA-Z0-9=\s]+)<\/p>/', $body, $matches); + preg_match('/resetToken[=\s]*3D([a-zA-Z0-9=\s]+)<\/p>/', $body, $matches); if (!empty($matches[1])) { $capturedToken = $matches[1]; $capturedToken = preg_replace('/=\s*/', '', $capturedToken); diff --git a/plugins/Morpheus/images/compare.svg b/plugins/Morpheus/images/compare.svg new file mode 100644 index 0000000000..efefc43677 --- /dev/null +++ b/plugins/Morpheus/images/compare.svg @@ -0,0 +1,6 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"> + <!-- + Source: https://fonts.gstatic.com/s/i/materialicons/compare/v1/24px.svg?download=true (Apache license version 2.0.) + --> + <path d="M0 0h24v24H0z" fill="none"/><path d="M10 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h5v2h2V1h-2v2zm0 15H5l5-6v6zm9-15h-5v2h5v13l-5-6v9h5c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z"/> +</svg>
\ No newline at end of file diff --git a/plugins/Morpheus/javascripts/ajaxHelper.js b/plugins/Morpheus/javascripts/ajaxHelper.js index 9f1d944849..56531ed1e6 100644 --- a/plugins/Morpheus/javascripts/ajaxHelper.js +++ b/plugins/Morpheus/javascripts/ajaxHelper.js @@ -166,7 +166,15 @@ function ajaxHelper() { params = broadcast.getValuesFromUrl(params); } + var arrayParams = ['compareSegments', 'comparePeriods', 'compareDates']; + for (var key in params) { + if (arrayParams.indexOf(key) !== -1 + && !params[key] + ) { + continue; + } + if(type.toLowerCase() == 'get') { this.getParams[key] = params[key]; } else if(type.toLowerCase() == 'post') { @@ -535,12 +543,9 @@ function ajaxHelper() { * @private */ this._mixinDefaultGetParams = function (params) { + var piwikUrl = piwikHelper.getAngularDependency('piwikUrl'); - if (window.location.hash) { - var segment = broadcast.getValueFromHash('segment', window.location.href.split('#')[1]); - } else { - var segment = broadcast.getValueFromUrl('segment'); - } + var segment = piwikUrl.getSearchParam('segment'); var defaultParams = { idSite: piwik.idSite || broadcast.getValueFromUrl('idSite'), diff --git a/plugins/Morpheus/stylesheets/base/colors.less b/plugins/Morpheus/stylesheets/base/colors.less index 8c82888f03..9a8150c330 100644 --- a/plugins/Morpheus/stylesheets/base/colors.less +++ b/plugins/Morpheus/stylesheets/base/colors.less @@ -31,14 +31,46 @@ @color-blue-brandSocialVeryLight: #00aced; @color-orange-brand: #ff9600; -@graph-colors-data-series1: #3450A3; -@graph-colors-data-series2: #43a047; -@graph-colors-data-series3: #ff7f00; -@graph-colors-data-series4: #d4291f; -@graph-colors-data-series5: #6a3d9a; -@graph-colors-data-series6: #b15928; -@graph-colors-data-series7: #fdbf6f; -@graph-colors-data-series8: #cab2d6; +// see https://material.io/design/color/#tools-for-picking-colors for source for colors +@graph-colors-data-series0: #0277BD; // light blue 50 series (800) +@graph-series0-shade1-color: #40C4FF; // (A200) +@graph-series0-shade2-color: #00B0FF; // (A400) +@graph-series0-shade3-color: #0091EA; // (A700) + +@graph-colors-data-series1: #FF8F00; // amber 50 series (800) +@graph-series1-shade1-color: #FFD740; // (A200) +@graph-series1-shade2-color: #FFC400; // (A400) +@graph-series1-shade3-color: #FFAB00; // (A700) + +@graph-colors-data-series2: #AD1457; // pink 50 series (800) +@graph-series2-shade1-color: #FF4081; // (A200) +@graph-series2-shade2-color: #F50057; // (A400) +@graph-series2-shade3-color: #C51162; // (A700) + +@graph-colors-data-series3: #6A1B9A; // purple 50 series (800) +@graph-series3-shade1-color: #E040FB; // (A200) +@graph-series3-shade2-color: #D500F9; // (A400) +@graph-series3-shade3-color: #AA00FF; // (A700) + +@graph-colors-data-series4: #558B2F; // light green 50 series (800) +@graph-series4-shade1-color: #B2FF59; // (A200) +@graph-series4-shade2-color: #76FF03; // (A400) +@graph-series4-shade3-color: #64DD17; // (A700) + +@graph-colors-data-series5: #00838F; // cyan 50 series (800) +@graph-series5-shade1-color: #18FFFF; // (A200) +@graph-series5-shade2-color: #00E5FF; // (A400) +@graph-series5-shade3-color: #00B8D4; // (A700) + +@graph-colors-data-series6: #283593; // indigo 50 series (800) +@graph-series6-shade1-color: #536DFE; // (A200) +@graph-series6-shade2-color: #3D5AFE; // (A400) +@graph-series6-shade3-color: #304FFE; // (A700) + +@graph-colors-data-series7: #D84315; // deep orange 50 series (800) +@graph-series7-shade1-color: #FF6E40; // (A200) +@graph-series7-shade2-color: #FF3D00; // (A400) +@graph-series7-shade3-color: #DD2C00; // (A700) @default-box-shade: 0 2px 3px 0 rgba(0,0,0,0.16), 0 0px 3px 0 rgba(0,0,0,0.12); diff --git a/plugins/Morpheus/stylesheets/main.less b/plugins/Morpheus/stylesheets/main.less index b9175b225e..659a5af4ac 100644 --- a/plugins/Morpheus/stylesheets/main.less +++ b/plugins/Morpheus/stylesheets/main.less @@ -382,7 +382,7 @@ table.dataTable { } } - tr { + tr:not(.subDataTableContainer) { td { border-bottom: 1px solid @color-silver-l95 !important; border-color: @color-silver-l95 !important; @@ -405,6 +405,17 @@ table.dataTable { border-left: 0; } + &:hover { + td:not(.cellSubDataTable):not(.parentComparisonRow) { + background-color: @color-silver-l95; + } + } + + &.label + td.column { + // first column after label should have a bit less padding + padding-left: 10px; + } + &.label .label { text-align: left; } @@ -476,23 +487,23 @@ table.dataTable { div.dataTableVizHtmlTable:not(.dataTableActions), div.dataTableVizAllColumns, div.dataTableVizGoals { - tr.subDataTable > td:first-child::before { + tr.subDataTable > td > span.label::before { display: inline-block; float: left; top: 0; width: 12px; height: 12px; - margin-left: -1px; + margin-left: -2px; margin-top: 3px; margin-right: 8px; content: ''; } - tr.subDataTable:not(.expanded) > td:first-child::before { + tr.subDataTable:not(.expanded) > td > span.label::before { background-image: url(plugins/Morpheus/images/plus.png); } - tr.subDataTable.expanded > td:first-child::before { + tr.subDataTable.expanded > td > span.label::before { background-image: url(plugins/Morpheus/images/minus.png); } } @@ -515,10 +526,25 @@ div.dataTableVizHtmlTable:not(.dataTableActions), } } + .UserCountryMap-btn-zoom { padding-left: 0; } +h6.sparkline-title { + margin-left: 2px; + text-transform: uppercase; + font-size: .8em; + font-weight: bold; + color: #999; + margin-bottom: 4px; + + max-width: 95px; + text-overflow: ellipsis; + white-space: nowrap; + overflow-x: hidden; +} + div.sparkline { display: -ms-flexbox; -ms-box-orient: horizontal; @@ -527,10 +553,11 @@ div.sparkline { display: -moz-flex; display: -ms-flex; display: flex; - -webkit-justify-content: space-around; - -moz-justify-content: space-around; - -ms-justify-content: space-around; - justify-content: space-around; + -webkit-justify-content: flex-start; + -moz-justify-content: flex-start; + -ms-justify-content: flex-start; + justify-content: flex-start; + align-items: center; border-bottom: 0; margin-bottom: 10px; @@ -542,8 +569,20 @@ div.sparkline { border-bottom: 1px dashed #c3c3c3; } } + + .metric-group-title { + display: block; + font-size: .7em; + text-transform: uppercase; + color: #999; + } + + .sparkline-metrics { + margin-bottom: 4px; + } } + div.sparkline img { -webkit-flex-shrink: 0; -moz-flex-shrink: 0; @@ -556,7 +595,6 @@ div.sparkline script + div { margin: 1px 0 0 1px; } - .widgetpreview-base li.widgetpreview-choosen { background: @color-silver-l95; position: relative; diff --git a/plugins/Morpheus/stylesheets/ui/_charts.less b/plugins/Morpheus/stylesheets/ui/_charts.less index f85008d8d2..917fe9bd14 100644 --- a/plugins/Morpheus/stylesheets/ui/_charts.less +++ b/plugins/Morpheus/stylesheets/ui/_charts.less @@ -1,3 +1,132 @@ +// comparison series colors & shades +.comparison-series-color[data-name=series0] { // series0 + color: @graph-colors-data-series0; +} + +.comparison-series-color[data-name=series0-shade1] { + color: @graph-series0-shade1-color; +} + +.comparison-series-color[data-name=series0-shade2] { + color: @graph-series0-shade2-color; +} + +.comparison-series-color[data-name=series0-shade3] { + color: @graph-series0-shade3-color; +} + +.comparison-series-color[data-name=series1] { // series1 + color: @graph-colors-data-series1; +} + +.comparison-series-color[data-name=series1-shade1] { + color: @graph-series1-shade1-color; +} + +.comparison-series-color[data-name=series1-shade2] { + color: @graph-series1-shade2-color; +} + +.comparison-series-color[data-name=series1-shade3] { + color: @graph-series1-shade3-color; +} + +.comparison-series-color[data-name=series2] { // series2 + color: @graph-colors-data-series2; +} + +.comparison-series-color[data-name=series2-shade1] { + color: @graph-series2-shade1-color; +} + +.comparison-series-color[data-name=series2-shade2] { + color: @graph-series2-shade2-color; +} + +.comparison-series-color[data-name=series2-shade3] { + color: @graph-series2-shade3-color; +} + +.comparison-series-color[data-name=series3] { // series3 + color: @graph-colors-data-series3; +} + +.comparison-series-color[data-name=series3-shade1] { + color: @graph-series3-shade1-color; +} + +.comparison-series-color[data-name=series3-shade2] { + color: @graph-series3-shade2-color; +} + +.comparison-series-color[data-name=series3-shade3] { + color: @graph-series3-shade3-color; +} + +.comparison-series-color[data-name=series4] { // series4 + color: @graph-colors-data-series4; +} + +.comparison-series-color[data-name=series4-shade1] { + color: @graph-series4-shade1-color; +} + +.comparison-series-color[data-name=series4-shade2] { + color: @graph-series4-shade2-color; +} + +.comparison-series-color[data-name=series4-shade3] { + color: @graph-series4-shade3-color; +} + +.comparison-series-color[data-name=series5] { // series5 + color: @graph-colors-data-series5; +} + +.comparison-series-color[data-name=series5-shade1] { + color: @graph-series5-shade1-color; +} + +.comparison-series-color[data-name=series5-shade2] { + color: @graph-series5-shade2-color; +} + +.comparison-series-color[data-name=series5-shade3] { + color: @graph-series5-shade3-color; +} + +.comparison-series-color[data-name=series6] { // series6 + color: @graph-colors-data-series6; +} + +.comparison-series-color[data-name=series6-shade1] { + color: @graph-series6-shade1-color; +} + +.comparison-series-color[data-name=series6-shade2] { + color: @graph-series6-shade2-color; +} + +.comparison-series-color[data-name=series6-shade3] { + color: @graph-series6-shade3-color; +} + +.comparison-series-color[data-name=series7] { // series7 + color: @graph-colors-data-series7; +} + +.comparison-series-color[data-name=series7-shade1] { + color: @graph-series7-shade1-color; +} + +.comparison-series-color[data-name=series7-shade2] { + color: @graph-series7-shade2-color; +} + +.comparison-series-color[data-name=series7-shade3] { + color: @graph-series7-shade3-color; +} + // bar graph colors .bar-graph-colors[data-name=grid-background] { color: @theme-color-background-contrast; @@ -12,35 +141,35 @@ } .bar-graph-colors[data-name=series1] { - color: @graph-colors-data-series1; + color: @graph-colors-data-series0; } .bar-graph-colors[data-name=series2] { - color: @graph-colors-data-series2; + color: @graph-colors-data-series1; } .bar-graph-colors[data-name=series3] { - color: @graph-colors-data-series3; + color: @graph-colors-data-series2; } .bar-graph-colors[data-name=series4] { - color: @graph-colors-data-series4; + color: @graph-colors-data-series3; } .bar-graph-colors[data-name=series5] { - color: @graph-colors-data-series5; + color: @graph-colors-data-series4; } -.bar-graph-colors[data-name=series6] { - color: @graph-colors-data-series6; +.bar-graph-colors[data-name=series5] { + color: @graph-colors-data-series5; } .bar-graph-colors[data-name=series7] { - color: @graph-colors-data-series7; + color: @graph-colors-data-series6; } .bar-graph-colors[data-name=series8] { - color: @graph-colors-data-series8; + color: @graph-colors-data-series7; } .bar-graph-colors[data-name=ticks] { @@ -61,35 +190,35 @@ } .pie-graph-colors[data-name=series1] { - color: @graph-colors-data-series1; + color: @graph-colors-data-series0; } .pie-graph-colors[data-name=series2] { - color: @graph-colors-data-series2; + color: @graph-colors-data-series1; } .pie-graph-colors[data-name=series3] { - color: @graph-colors-data-series3; + color: @graph-colors-data-series2; } .pie-graph-colors[data-name=series4] { - color: @graph-colors-data-series4; + color: @graph-colors-data-series3; } .pie-graph-colors[data-name=series5] { - color: @graph-colors-data-series5; + color: @graph-colors-data-series4; } .pie-graph-colors[data-name=series6] { - color: @graph-colors-data-series6; + color: @graph-colors-data-series5; } .pie-graph-colors[data-name=series7] { - color: @graph-colors-data-series7; + color: @graph-colors-data-series6; } .pie-graph-colors[data-name=series8] { - color: @graph-colors-data-series8; + color: @graph-colors-data-series7; } .pie-graph-colors[data-name=ticks] { @@ -105,35 +234,35 @@ // evolution graph colors .evolution-graph-colors[data-name=series1] { - color: @graph-colors-data-series1; + color: @graph-colors-data-series0; } .evolution-graph-colors[data-name=series2] { - color: @graph-colors-data-series2; + color: @graph-colors-data-series1; } .evolution-graph-colors[data-name=series3] { - color: @graph-colors-data-series3; + color: @graph-colors-data-series2; } .evolution-graph-colors[data-name=series4] { - color: @graph-colors-data-series4; + color: @graph-colors-data-series3; } .evolution-graph-colors[data-name=series5] { - color: @graph-colors-data-series5; + color: @graph-colors-data-series4; } .evolution-graph-colors[data-name=series6] { - color: @graph-colors-data-series6; + color: @graph-colors-data-series5; } .evolution-graph-colors[data-name=series7] { - color: @graph-colors-data-series7; + color: @graph-colors-data-series6; } .evolution-graph-colors[data-name=series8] { - color: @graph-colors-data-series8; + color: @graph-colors-data-series7; } .evolution-graph-colors[data-name=ticks] { diff --git a/plugins/Morpheus/stylesheets/uibase/_periodSelect.less b/plugins/Morpheus/stylesheets/uibase/_periodSelect.less index 023f278449..f14fc64fd2 100644 --- a/plugins/Morpheus/stylesheets/uibase/_periodSelect.less +++ b/plugins/Morpheus/stylesheets/uibase/_periodSelect.less @@ -56,10 +56,6 @@ padding: 0 0 4px 0; } -#periodMore { - overflow: hidden; -} - #periodString .period-date, #periodString .period-range { padding: 0 16px 0 0; @@ -77,3 +73,16 @@ #periodString label.selected-period-label { text-decoration: underline; } + +#periodString .compareCheckbox { + transform: scale(.8); + margin-left: -29px; + + .form-group { + margin: 15px 0 0; + } + + label { + padding-left: 25px; + } +} diff --git a/plugins/Morpheus/templates/dashboard.twig b/plugins/Morpheus/templates/dashboard.twig index cb7e7f5038..70419aedee 100644 --- a/plugins/Morpheus/templates/dashboard.twig +++ b/plugins/Morpheus/templates/dashboard.twig @@ -55,6 +55,8 @@ <div piwik-popover></div> + <piwik-comparisons></piwik-comparisons> + {% block content %} {% endblock %} diff --git a/plugins/MultiSites/MultiSites.php b/plugins/MultiSites/MultiSites.php index 4b7f614e63..c4fbd355f9 100644 --- a/plugins/MultiSites/MultiSites.php +++ b/plugins/MultiSites/MultiSites.php @@ -21,10 +21,16 @@ class MultiSites extends \Piwik\Plugin 'AssetManager.getStylesheetFiles' => 'getStylesheetFiles', 'AssetManager.getJavaScriptFiles' => 'getJsFiles', 'Translate.getClientSideTranslationKeys' => 'getClientSideTranslationKeys', - 'Metrics.getDefaultMetricTranslations' => 'addMetricTranslations' + 'Metrics.getDefaultMetricTranslations' => 'addMetricTranslations', + 'API.getPagesComparisonsDisabledFor' => 'getPagesComparisonsDisabledFor', ); } + public function getPagesComparisonsDisabledFor(&$pages) + { + $pages[] = 'MultiSites.index'; + } + public function addMetricTranslations(&$translations) { $appendix = " " . Piwik::translate('MultiSites_Evolution'); diff --git a/plugins/MultiSites/tests/UI/expected-screenshots/MultiSitesTest_all_websites.png b/plugins/MultiSites/tests/UI/expected-screenshots/MultiSitesTest_all_websites.png index d1a157df0a..2f963cb2d6 100644 --- a/plugins/MultiSites/tests/UI/expected-screenshots/MultiSitesTest_all_websites.png +++ b/plugins/MultiSites/tests/UI/expected-screenshots/MultiSitesTest_all_websites.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d1e5b0449e989a4286f4e7fb496c210995bda8c2812ca4024d915bfdac17589b -size 276562 +oid sha256:5b6962a43480f39fe6f47a7c4e9a5adb2ca394085aa67175be0010be6f321e1a +size 276667 diff --git a/plugins/MultiSites/tests/UI/expected-screenshots/MultiSitesTest_all_websites_changed_sort_order.png b/plugins/MultiSites/tests/UI/expected-screenshots/MultiSitesTest_all_websites_changed_sort_order.png index 7743f398a4..777f06010c 100644 --- a/plugins/MultiSites/tests/UI/expected-screenshots/MultiSitesTest_all_websites_changed_sort_order.png +++ b/plugins/MultiSites/tests/UI/expected-screenshots/MultiSitesTest_all_websites_changed_sort_order.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8033854fba0b193e69c7dd2d43f5c49782448a71ef8d729765ea2dc72d68df30 -size 55791 +oid sha256:f955e6c95975528ca310d8c01c7901e42f5179642d10afcdb19f91f5824dcfa5 +size 55795 diff --git a/plugins/MultiSites/tests/UI/expected-screenshots/MultiSitesTest_all_websites_page_1.png b/plugins/MultiSites/tests/UI/expected-screenshots/MultiSitesTest_all_websites_page_1.png index 31cfab6200..b86a3d9d5c 100644 --- a/plugins/MultiSites/tests/UI/expected-screenshots/MultiSitesTest_all_websites_page_1.png +++ b/plugins/MultiSites/tests/UI/expected-screenshots/MultiSitesTest_all_websites_page_1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9fcb7a08c93b58097b1d466e0de99d4e8e8187dd119ee917ecd6a17b20617c0d -size 72399 +oid sha256:1fceeb08862017f3e8ee9e5da8f638a98516b0026a210b7e69063e9d85bfcbd3 +size 72411 diff --git a/plugins/MultiSites/tests/UI/expected-screenshots/MultiSitesTest_all_websites_range.png b/plugins/MultiSites/tests/UI/expected-screenshots/MultiSitesTest_all_websites_range.png index 3f8ad7fa91..b8fb605c90 100644 --- a/plugins/MultiSites/tests/UI/expected-screenshots/MultiSitesTest_all_websites_range.png +++ b/plugins/MultiSites/tests/UI/expected-screenshots/MultiSitesTest_all_websites_range.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:61985bb6c6236078211f5acb5b44999475983ff3dd5caa2ddd54f0dc1d650500 -size 240821 +oid sha256:a950fff37e36248fcca1f0c1df67249fce25f32574c43fcbfe29516c0148da63 +size 240812 diff --git a/plugins/MultiSites/tests/UI/expected-screenshots/MultiSitesTest_all_websites_search.png b/plugins/MultiSites/tests/UI/expected-screenshots/MultiSitesTest_all_websites_search.png index eb335c1078..e17ef474a0 100644 --- a/plugins/MultiSites/tests/UI/expected-screenshots/MultiSitesTest_all_websites_search.png +++ b/plugins/MultiSites/tests/UI/expected-screenshots/MultiSitesTest_all_websites_search.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0d314c7b645eb175d69efff04e7b17db5cafbc9d0c56e19f9cbdb889dd453cc4 -size 55765 +oid sha256:9e324d4d241366a4fcd46f7a034326065cc99c653f92c1601f2754c1393f7670 +size 55766 diff --git a/plugins/Overlay/javascripts/rowaction.js b/plugins/Overlay/javascripts/rowaction.js index f4e733433d..6b76071e24 100644 --- a/plugins/Overlay/javascripts/rowaction.js +++ b/plugins/Overlay/javascripts/rowaction.js @@ -18,10 +18,10 @@ DataTable_RowActions_Overlay.prototype = new DataTable_RowAction; DataTable_RowActions_Overlay.registeredReports = []; DataTable_RowActions_Overlay.registerReport = function (handler) { DataTable_RowActions_Overlay.registeredReports.push(handler); -} +}; -DataTable_RowActions_Overlay.prototype.onClick = function (actionA, tr, e) { +DataTable_RowActions_Overlay.prototype.onClick = function (actionA, tr, e, originalRow) { if (!actionA.data('overlay-manipulated')) { actionA.data('overlay-manipulated', 1); @@ -48,6 +48,15 @@ DataTable_RowActions_Overlay.prototype.onClick = function (actionA, tr, e) { } } + var paramOverride = $(originalRow || tr).data('param-override'); + if (typeof paramOverride === 'object' && paramOverride.segment) { + if (segment) { + segment += ';' + paramOverride.segment; + } else { + segment = paramOverride.segment; + } + } + if (link) { var href = Overlay_Helper.getOverlayLink(this.dataTable.param.idSite, 'month', 'today', segment, link); diff --git a/plugins/Overlay/tests/UI/expected-screenshots/Overlay_row_evolution.png b/plugins/Overlay/tests/UI/expected-screenshots/Overlay_row_evolution.png index 0e5e4c914d..9dd85085bc 100644 --- a/plugins/Overlay/tests/UI/expected-screenshots/Overlay_row_evolution.png +++ b/plugins/Overlay/tests/UI/expected-screenshots/Overlay_row_evolution.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7057571fb17ba62fda1504aeb1aec0e3d2cf51c80ebc659ed8078dfcf0fc9d47 -size 102763 +oid sha256:ca43a135ec6a1900230b6d0c7d01c05252139d7ff4bfa04a281f7031d4d4c5af +size 102772 diff --git a/plugins/PrivacyManager/tests/UI/PrivacyManager_spec.js b/plugins/PrivacyManager/tests/UI/PrivacyManager_spec.js index 746006d906..a0c7447cb2 100644 --- a/plugins/PrivacyManager/tests/UI/PrivacyManager_spec.js +++ b/plugins/PrivacyManager/tests/UI/PrivacyManager_spec.js @@ -63,7 +63,7 @@ describe("PrivacyManager", function () { async function deleteDataSubjects() { - await page.click('.deleteDataSubjects input'); + await page.evaluate(() => $('.deleteDataSubjects input').click()); await page.waitFor(500); // wait for animation } @@ -267,7 +267,7 @@ describe("PrivacyManager", function () { it('should ask for confirmation before deleting any visit', async function() { await deleteDataSubjects(); - const modal = await page.$('.modal.open'); + const modal = await page.waitFor('.modal.open', { visible: true }); expect(await modal.screenshot()).to.matchImage('gdpr_tools_delete_visit_unconfirmed'); }); diff --git a/plugins/PrivacyManager/tests/UI/expected-screenshots/PrivacyManager_gdpr_tools_delete_visit_cancelled.png b/plugins/PrivacyManager/tests/UI/expected-screenshots/PrivacyManager_gdpr_tools_delete_visit_cancelled.png index 1f55f910fb..437ed20e1e 100644 --- a/plugins/PrivacyManager/tests/UI/expected-screenshots/PrivacyManager_gdpr_tools_delete_visit_cancelled.png +++ b/plugins/PrivacyManager/tests/UI/expected-screenshots/PrivacyManager_gdpr_tools_delete_visit_cancelled.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4f28961a08fcee83f90933ef4c6818bb8bc1962ecfbfa0caab54873575ff0e93 -size 579175 +oid sha256:c3d40d9a01a7e9d7bbb4e553c398c552b37e1a0d99687937742e59a48ba02c2a +size 542961 diff --git a/plugins/PrivacyManager/tests/UI/expected-screenshots/PrivacyManager_gdpr_tools_delete_visit_unconfirmed.png b/plugins/PrivacyManager/tests/UI/expected-screenshots/PrivacyManager_gdpr_tools_delete_visit_unconfirmed.png index d307935cac..f86a2bb290 100644 --- a/plugins/PrivacyManager/tests/UI/expected-screenshots/PrivacyManager_gdpr_tools_delete_visit_unconfirmed.png +++ b/plugins/PrivacyManager/tests/UI/expected-screenshots/PrivacyManager_gdpr_tools_delete_visit_unconfirmed.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:10df037298b049fe7aa175340049bd22aededb592127eb339395e4315fbd44b1 +oid sha256:85f5a6e7f0bf43cad861da1fd9a1ae7ceccda110091b25b023ddc0408f065151 size 9192 diff --git a/plugins/PrivacyManager/tests/UI/expected-screenshots/PrivacyManager_gdpr_tools_enrich_segment_by_ip.png b/plugins/PrivacyManager/tests/UI/expected-screenshots/PrivacyManager_gdpr_tools_enrich_segment_by_ip.png index 02a7343823..437ed20e1e 100644 --- a/plugins/PrivacyManager/tests/UI/expected-screenshots/PrivacyManager_gdpr_tools_enrich_segment_by_ip.png +++ b/plugins/PrivacyManager/tests/UI/expected-screenshots/PrivacyManager_gdpr_tools_enrich_segment_by_ip.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:20431e23602615276b4a2ca75326761232bc4343b989ac2bfd61eabfb64f5a0b -size 579646 +oid sha256:c3d40d9a01a7e9d7bbb4e553c398c552b37e1a0d99687937742e59a48ba02c2a +size 542961 diff --git a/plugins/PrivacyManager/tests/UI/expected-screenshots/PrivacyManager_gdpr_tools_uncheck_one_visit.png b/plugins/PrivacyManager/tests/UI/expected-screenshots/PrivacyManager_gdpr_tools_uncheck_one_visit.png index 1f55f910fb..1df77f8428 100644 --- a/plugins/PrivacyManager/tests/UI/expected-screenshots/PrivacyManager_gdpr_tools_uncheck_one_visit.png +++ b/plugins/PrivacyManager/tests/UI/expected-screenshots/PrivacyManager_gdpr_tools_uncheck_one_visit.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4f28961a08fcee83f90933ef4c6818bb8bc1962ecfbfa0caab54873575ff0e93 -size 579175 +oid sha256:980fb736468b6ddf3248ff249c7a5751fafeb3f71050ef78162174ca5e69af92 +size 543076 diff --git a/plugins/Referrers/API.php b/plugins/Referrers/API.php index dadd589458..ab82cd91f0 100644 --- a/plugins/Referrers/API.php +++ b/plugins/Referrers/API.php @@ -14,9 +14,13 @@ use Piwik\API\ResponseBuilder; use Piwik\Archive; use Piwik\Common; use Piwik\DataTable; +use Piwik\DataTable\Filter\ColumnCallbackAddColumnPercentage; use Piwik\Date; +use Piwik\Metrics; use Piwik\Piwik; +use Piwik\Plugin\ReportsProvider; use Piwik\Plugins\Referrers\DataTable\Filter\GroupDifferentSocialWritings; +use Piwik\Plugins\Referrers\Reports\Get; use Piwik\Site; /** @@ -30,6 +34,50 @@ use Piwik\Site; */ class API extends \Piwik\Plugin\API { + public function get($idSite, $period, $date, $segment = false, $columns = false) + { + Piwik::checkUserHasViewAccess($idSite); + + $dataTableReferrersType = $this->getReferrerType($idSite, $period, $date, $segment); + $dataTable = $this->createReferrerTypeTable($dataTableReferrersType); + + $archive = Archive::build($idSite, $period, $date, $segment); + + $numericArchives = $archive->getDataTableFromNumeric([ + Archiver::METRIC_DISTINCT_SEARCH_ENGINE_RECORD_NAME, + Archiver::METRIC_DISTINCT_SOCIAL_NETWORK_RECORD_NAME, + Archiver::METRIC_DISTINCT_KEYWORD_RECORD_NAME, + Archiver::METRIC_DISTINCT_WEBSITE_RECORD_NAME, + Archiver::METRIC_DISTINCT_URLS_RECORD_NAME, + Archiver::METRIC_DISTINCT_CAMPAIGN_RECORD_NAME, + ]); + $this->mergeNumericArchives($dataTable, $numericArchives); + + $totalVisits = array_sum($dataTableReferrersType->getColumn(Metrics::INDEX_NB_VISITS)); + + $percentColumns = [ + 'Referrers_visitorsFromDirectEntry', + 'Referrers_visitorsFromSearchEngines', + 'Referrers_visitorsFromCampaigns', + 'Referrers_visitorsFromSocialNetworks', + 'Referrers_visitorsFromWebsites', + ]; + foreach ($percentColumns as $column) { + $dataTable->filter(ColumnCallbackAddColumnPercentage::class, [ + $column . '_percent', + $column, + $totalVisits + ]); + } + + if (!empty($requestedColumns)) { + $requestedColumns = Piwik::getArrayFromApiParameter($columns); + $dataTable->filter(DataTable\Filter\ColumnDelete::class, [[], $requestedColumns]); + } + + return $dataTable; + } + /** * @param string $name * @param int $idSite @@ -669,4 +717,72 @@ class API extends \Piwik\Plugin\API $urlsTable = null; } + private function createReferrerTypeTable(DataTable\DataTableInterface $table) + { + if ($table instanceof DataTable) { + $nameToColumnId = array( + 'Referrers_visitorsFromSearchEngines' => Common::REFERRER_TYPE_SEARCH_ENGINE, + 'Referrers_visitorsFromSocialNetworks' => Common::REFERRER_TYPE_SOCIAL_NETWORK, + 'Referrers_visitorsFromDirectEntry' => Common::REFERRER_TYPE_DIRECT_ENTRY, + 'Referrers_visitorsFromWebsites' => Common::REFERRER_TYPE_WEBSITE, + 'Referrers_visitorsFromCampaigns' => Common::REFERRER_TYPE_CAMPAIGN, + ); + + $newRow = array(); + foreach ($nameToColumnId as $nameVar => $columnId) { + $value = 0; + $row = $table->getRowFromLabel($columnId); + if ($row !== false) { + $value = $row->getColumn(Metrics::INDEX_NB_VISITS); + } + $newRow[$nameVar] = $value; + } + + $result = new DataTable\Simple(); + $result->addRowFromSimpleArray($newRow); + return $result; + } else if ($table instanceof DataTable\Map) { + $result = new DataTable\Map(); + $result->setKeyName($table->getKeyName()); + foreach ($table->getDataTables() as $label => $childTable) { + $referrerTypeTable = $this->createReferrerTypeTable($childTable); + $result->addTable($referrerTypeTable, $label); + } + } else { + throw new \Exception("Unexpected DataTable type: " . get_class($table)); // sanity check + } + return $result; + } + + private function mergeNumericArchives(DataTable\DataTableInterface $table, DataTable\DataTableInterface $numericArchives = null) + { + if ($table instanceof DataTable) { + /** @var DataTable $numericArchives */ + if (empty($numericArchives)) { + return; + } + + $table->setAllTableMetadata($numericArchives->getAllTableMetadata()); + + if ($table->getRows() == 0) { + $table->addRow(new DataTable\Row()); + } + + if ($numericArchives->getRowsCount() == 0) { + return; + } + + $row = $table->getFirstRow(); + foreach ($numericArchives->getFirstRow() as $name => $value) { + $row->setColumn($name, $value); + } + } else if ($table instanceof DataTable\Map) { + foreach ($table->getDataTables() as $label => $childTable) { + $numericArchiveChildTable = $numericArchives->getTable($label); + $this->mergeNumericArchives($childTable, $numericArchiveChildTable); + } + } else { + throw new \Exception("Unexpected DataTable type: " . get_class($table)); // sanity check + } + } } diff --git a/plugins/Referrers/Controller.php b/plugins/Referrers/Controller.php index bc2d25fb7e..66184b8816 100644 --- a/plugins/Referrers/Controller.php +++ b/plugins/Referrers/Controller.php @@ -12,6 +12,7 @@ use Piwik\API\Request; use Piwik\Common; use Piwik\DataTable\Filter\CalculateEvolutionFilter; use Piwik\DataTable\Map; +use Piwik\FrontController; use Piwik\Metrics; use Piwik\NumberFormatter; use Piwik\Period\Range; @@ -41,213 +42,12 @@ class Controller extends \Piwik\Plugin\Controller public function getSparklines() { - $metrics = $this->getReferrersVisitorsByType(); - $distinctMetrics = $this->getDistinctReferrersMetrics(); + $_GET['forceView'] = '1'; + $_GET['viewDataTable'] = Sparklines::ID; - $numberFormatter = NumberFormatter::getInstance(); - - $totalVisits = array_sum($metrics); - foreach ($metrics as $name => $value) { - - // calculate percent of total, if there were any visits - if ($value != 0 && $totalVisits != 0) { - $percentName = $name . 'Percent'; - $metrics[$percentName] = round(($value / $totalVisits) * 100, 0); - } - } - - // calculate evolution for visit metrics & distinct metrics - list($lastPeriodDate, $ignore) = Range::getLastDate(); - if ($lastPeriodDate !== false) { - $date = Common::getRequestVar('date'); - $period = Common::getRequestVar('period'); - - $prettyDate = self::getPrettyDate($date, $period); - $prettyLastPeriodDate = self::getPrettyDate($lastPeriodDate, $period); - - // visit metrics - $previousValues = $this->getReferrersVisitorsByType($lastPeriodDate); - $metrics = $this->addEvolutionPropertiesToView($prettyDate, $metrics, $prettyLastPeriodDate, $previousValues); - - // distinct metrics - $previousValues = $this->getDistinctReferrersMetrics($lastPeriodDate); - $distinctMetrics = $this->addEvolutionPropertiesToView($prettyDate, $distinctMetrics, $prettyLastPeriodDate, $previousValues); - } - - /** @var Sparklines $view */ - $view = ViewDataTable\Factory::build(Sparklines::ID, $api = '', $controller = '', $force = true, $loadUserParams = false); - - // DIRECT ENTRY - $metrics['visitorsFromDirectEntry'] = $numberFormatter->formatNumber($metrics['visitorsFromDirectEntry']); - $values = array($metrics['visitorsFromDirectEntry']); - $descriptions = array(Piwik::translate('Referrers_TypeDirectEntries')); - - if (!empty($metrics['visitorsFromDirectEntryPercent'])) { - $metrics['visitorsFromDirectEntryPercent'] = $numberFormatter->formatPercent($metrics['visitorsFromDirectEntryPercent'], $precision = 1); - $values[] = $metrics['visitorsFromDirectEntryPercent']; - $descriptions[] = Piwik::translate('Referrers_XPercentOfVisits'); - } - - $directEntryParams = $this->getReferrerSparklineParams(Common::REFERRER_TYPE_DIRECT_ENTRY); - - $view->config->addSparkline($directEntryParams, $values, $descriptions, @$metrics['visitorsFromDirectEntryEvolution']); - - - // WEBSITES - $metrics['visitorsFromWebsites'] = $numberFormatter->formatNumber($metrics['visitorsFromWebsites']); - $values = array($metrics['visitorsFromWebsites']); - $descriptions = array(Piwik::translate('Referrers_TypeWebsites')); - - if (!empty($metrics['visitorsFromWebsitesPercent'])) { - $metrics['visitorsFromWebsitesPercent'] = $numberFormatter->formatPercent($metrics['visitorsFromWebsitesPercent'], $precision = 1); - $values[] = $metrics['visitorsFromWebsitesPercent']; - $descriptions[] = Piwik::translate('Referrers_XPercentOfVisits'); - } - - $searchEngineParams = $this->getReferrerSparklineParams(Common::REFERRER_TYPE_WEBSITE); - - $view->config->addSparkline($searchEngineParams, $values, $descriptions, @$metrics['visitorsFromWebsitesEvolution']); - - - // SEARCH ENGINES - $metrics['visitorsFromSearchEngines'] = $numberFormatter->formatNumber($metrics['visitorsFromSearchEngines']); - $values = array($metrics['visitorsFromSearchEngines']); - $descriptions = array(Piwik::translate('Referrers_TypeSearchEngines')); - - if (!empty($metrics['visitorsFromSearchEnginesPercent'])) { - $metrics['visitorsFromSearchEnginesPercent'] = $numberFormatter->formatPercent($metrics['visitorsFromSearchEnginesPercent'], $precision = 1); - $values[] = $metrics['visitorsFromSearchEnginesPercent']; - $descriptions[] = Piwik::translate('Referrers_XPercentOfVisits'); - } - $searchEngineParams = $this->getReferrerSparklineParams(Common::REFERRER_TYPE_SEARCH_ENGINE); - - $view->config->addSparkline($searchEngineParams, $values, $descriptions, @$metrics['visitorsFromSearchEnginesEvolution']); - - // SOCIAL NETWORKS - $metrics['visitorsFromSocialNetworks'] = $numberFormatter->formatNumber($metrics['visitorsFromSocialNetworks']); - $values = array($metrics['visitorsFromSocialNetworks']); - $descriptions = array(Piwik::translate('Referrers_TypeSocialNetworks')); - - if (!empty($metrics['visitorsFromSocialNetworksPercent'])) { - $metrics['visitorsFromSocialNetworksPercent'] = $numberFormatter->formatPercent($metrics['visitorsFromSocialNetworksPercent'], $precision = 1); - $values[] = $metrics['visitorsFromSocialNetworksPercent']; - $descriptions[] = Piwik::translate('Referrers_XPercentOfVisits'); - } - $socialNetworkParams = $this->getReferrerSparklineParams(Common::REFERRER_TYPE_SOCIAL_NETWORK); - - $view->config->addSparkline($socialNetworkParams, $values, $descriptions, @$metrics['visitorsFromSocialNetworksEvolution']); - - - // CAMPAIGNS - $metrics['visitorsFromCampaigns'] = $numberFormatter->formatNumber($metrics['visitorsFromCampaigns']); - $values = array($metrics['visitorsFromCampaigns']); - $descriptions = array(Piwik::translate('Referrers_TypeCampaigns')); - - if (!empty($metrics['visitorsFromCampaignsPercent'])) { - $metrics['visitorsFromCampaignsPercent'] = $numberFormatter->formatPercent($metrics['visitorsFromCampaignsPercent'], $precision = 1); - $values[] = $metrics['visitorsFromCampaignsPercent']; - $descriptions[] = Piwik::translate('Referrers_XPercentOfVisits'); - } - - $searchEngineParams = $this->getReferrerSparklineParams(Common::REFERRER_TYPE_CAMPAIGN); - - $view->config->addSparkline($searchEngineParams, $values, $descriptions, @$metrics['visitorsFromCampaignsEvolution']); - - - // DISTINCT SEARCH ENGINES - $sparklineParams = $this->getDistinctSparklineUrlParams('getLastDistinctSearchEnginesGraph'); - $value = $distinctMetrics['numberDistinctSearchEngines']; - $value = $numberFormatter->formatNumber($value); - $description = Piwik::translate('Referrers_DistinctSearchEngines'); - - $view->config->addSparkline($sparklineParams, $value, $description, @$distinctMetrics['numberDistinctSearchEnginesEvolution']); - - - // DISTINCT SOCIAL NETWORKS - $sparklineParams = $this->getDistinctSparklineUrlParams('getLastDistinctSocialNetworksGraph'); - $value = $distinctMetrics['numberDistinctSocialNetworks']; - $value = $numberFormatter->formatNumber($value); - $description = Piwik::translate('Referrers_DistinctSocialNetworks'); - - $view->config->addSparkline($sparklineParams, $value, $description, @$distinctMetrics['numberDistinctSocialNetworksEvolution']); - - - // DISTINCT WEBSITES - $sparklineParams = $this->getDistinctSparklineUrlParams('getLastDistinctWebsitesGraph'); - - $distinctMetrics['numberDistinctWebsites'] = $numberFormatter->formatNumber($distinctMetrics['numberDistinctWebsites']); - $distinctMetrics['numberDistinctWebsitesUrls'] = $numberFormatter->formatNumber($distinctMetrics['numberDistinctWebsitesUrls']); - - $values = array($distinctMetrics['numberDistinctWebsites'], $distinctMetrics['numberDistinctWebsitesUrls']); - $descriptions = array(Piwik::translate('Referrers_DistinctWebsites'), Piwik::translate('Referrers_UsingNDistinctUrls')); - - $view->config->addSparkline($sparklineParams, $values, $descriptions, @$distinctMetrics['numberDistinctWebsitesEvolution']); - - - // DISTINCT KEYWORDS - $sparklineParams = $this->getDistinctSparklineUrlParams('getLastDistinctKeywordsGraph'); - $value = $distinctMetrics['numberDistinctKeywords']; - $value = $numberFormatter->formatNumber($value); - $description = Piwik::translate('Referrers_DistinctKeywords'); - - $view->config->addSparkline($sparklineParams, $value, $description, @$distinctMetrics['numberDistinctKeywordsEvolution']); - - - // DISTINCT CAMPAIGNS - $sparklineParams = $this->getDistinctSparklineUrlParams('getLastDistinctCampaignsGraph'); - $value = $distinctMetrics['numberDistinctCampaigns']; - $value = $numberFormatter->formatNumber($value); - $description = Piwik::translate('Referrers_DistinctCampaigns'); - - $view->config->addSparkline($sparklineParams, $value, $description, @$distinctMetrics['numberDistinctCampaignsEvolution']); - - return $view->render(); - } - - private function getDistinctSparklineUrlParams($action) - { - return array('module' => $this->pluginName, 'action' => $action); + return FrontController::getInstance()->fetchDispatch('Referrers', 'get'); } - protected function getReferrersVisitorsByType($date = false) - { - if ($date === false) { - $date = Common::getRequestVar('date', false); - } - - // we disable the queued filters because here we want to get the visits coming from search engines - // if the filters were applied we would have to look up for a label looking like "Search Engines" - // which is not good when we have translations - $dataTableReferrersType = Request::processRequest( - "Referrers.getReferrerType", array('disable_queued_filters' => '1', 'date' => $date)); - - $nameToColumnId = array( - 'visitorsFromSearchEngines' => Common::REFERRER_TYPE_SEARCH_ENGINE, - 'visitorsFromSocialNetworks' => Common::REFERRER_TYPE_SOCIAL_NETWORK, - 'visitorsFromDirectEntry' => Common::REFERRER_TYPE_DIRECT_ENTRY, - 'visitorsFromWebsites' => Common::REFERRER_TYPE_WEBSITE, - 'visitorsFromCampaigns' => Common::REFERRER_TYPE_CAMPAIGN, - ); - $return = array(); - foreach ($nameToColumnId as $nameVar => $columnId) { - $value = 0; - $row = $dataTableReferrersType->getRowFromLabel($columnId); - if ($row !== false) { - $value = $row->getColumn(Metrics::INDEX_NB_VISITS); - } - $return[$nameVar] = $value; - } - return $return; - } - - protected $referrerTypeToLabel = array( - Common::REFERRER_TYPE_DIRECT_ENTRY => 'Referrers_DirectEntry', - Common::REFERRER_TYPE_SEARCH_ENGINE => 'Referrers_SearchEngines', - Common::REFERRER_TYPE_SOCIAL_NETWORK => 'Referrers_Socials', - Common::REFERRER_TYPE_WEBSITE => 'Referrers_Websites', - Common::REFERRER_TYPE_CAMPAIGN => 'Referrers_Campaigns', - ); - public function getEvolutionGraph($typeReferrer = false, array $columns = array(), array $defaultColumns = array()) { $view = $this->getLastUnitGraph($this->pluginName, __FUNCTION__, 'Referrers.getReferrerType'); @@ -369,84 +169,4 @@ class Controller extends \Piwik\Plugin\Controller return Piwik::translate($label); } - /** - * Returns the URL for the sparkline of visits with a specific referrer type. - * - * @param int $referrerType The referrer type. Referrer types are defined in Common class. - * @return string The URL that can be used to get a sparkline image. - */ - private function getReferrerSparklineParams($referrerType) - { - $totalRow = $this->translator->translate('General_Total'); - - return array( - 'columns' => array('nb_visits'), - 'rows' => array(self::getTranslatedReferrerTypeLabel($referrerType), $totalRow), - 'typeReferrer' => $referrerType, - 'module' => $this->pluginName, - 'action' => 'getReferrerType' - ); - } - - /** - * Returns an array containing the number of distinct referrers for each - * referrer type. - * - * @param bool|string $date The date to use when getting metrics. If false, the - * date query param is used. - * @return array The metrics. - */ - private function getDistinctReferrersMetrics($date = false) - { - $propertyToAccessorMapping = array( - 'numberDistinctSearchEngines' => 'getNumberOfDistinctSearchEngines', - 'numberDistinctSocialNetworks' => 'getNumberOfDistinctSocialNetworks', - 'numberDistinctKeywords' => 'getNumberOfDistinctKeywords', - 'numberDistinctWebsites' => 'getNumberOfDistinctWebsites', - 'numberDistinctWebsitesUrls' => 'getNumberOfDistinctWebsitesUrls', - 'numberDistinctCampaigns' => 'getNumberOfDistinctCampaigns', - ); - - $result = array(); - foreach ($propertyToAccessorMapping as $property => $method) { - $result[$property] = $this->getNumericValue('Referrers.' . $method, $date); - } - return $result; - } - - /** - * Utility method that calculates evolution values for a set of current & past values - * and sets properties on a View w/ HTML that displays the evolution percents. - * - * @param string $date The date of the current values. - * @param array $currentValues Array mapping view property names w/ present values. - * @param string $lastPeriodDate The date of the period in the past. - * @param array $previousValues Array mapping view property names w/ past values. Keys - * in this array should be the same as keys in $currentValues. - * @return array Added current values - */ - private function addEvolutionPropertiesToView($date, $currentValues, $lastPeriodDate, $previousValues) - { - foreach ($previousValues as $name => $pastValue) { - $currentValue = $currentValues[$name]; - $evolutionName = $name . 'Evolution'; - - $currentValueFormatted = NumberFormatter::getInstance()->format($currentValue); - $pastValueFormatted = NumberFormatter::getInstance()->format($pastValue); - - $currentValues[$evolutionName] = array( - 'currentValue' => $currentValue, - 'pastValue' => $pastValue, - 'tooltip' => Piwik::translate('General_EvolutionSummaryGeneric', array( - Piwik::translate('General_NVisits', $currentValueFormatted), - $date, - Piwik::translate('General_NVisits', $pastValueFormatted), - $lastPeriodDate, - CalculateEvolutionFilter::calculate($currentValue, $pastValue, $precision = 1) - )) - ); - } - - return $currentValues; - } } diff --git a/plugins/Referrers/Referrers.php b/plugins/Referrers/Referrers.php index 479323eeb4..5558d08918 100644 --- a/plugins/Referrers/Referrers.php +++ b/plugins/Referrers/Referrers.php @@ -10,6 +10,7 @@ namespace Piwik\Plugins\Referrers; use Piwik\Common; use Piwik\Piwik; +use Piwik\Plugins\Referrers\Reports\Get; use Piwik\Plugins\SitesManager\SiteUrls; /** @@ -33,9 +34,40 @@ class Referrers extends \Piwik\Plugin 'Tracker.setTrackerCacheGeneral' => 'setTrackerCacheGeneral', 'AssetManager.getJavaScriptFiles' => 'getJsFiles', 'AssetManager.getStylesheetFiles' => 'getStylesheetFiles', + 'API.getPagesComparisonsDisabledFor' => 'getPagesComparisonsDisabledFor', + 'Metrics.getDefaultMetricTranslations' => 'getDefaultMetricTranslations', ); } + public function getDefaultMetricTranslations(&$translations) + { + $translations['Referrers_visitorsFromSearchEngines'] = Piwik::translate('Referrers_VisitorsFromSearchEngines'); + $translations['Referrers_visitorsFromSearchEngines_percent'] = Piwik::translate('Referrers_PercentOfX', $translations['Referrers_visitorsFromSearchEngines']); + + $translations['Referrers_visitorsFromSocialNetworks'] = Piwik::translate('Referrers_VisitorsFromSocialNetworks'); + $translations['Referrers_visitorsFromSocialNetworks_percent'] = Piwik::translate('Referrers_PercentOfX', $translations['Referrers_visitorsFromSocialNetworks']); + + $translations['Referrers_visitorsFromDirectEntry'] = Piwik::translate('Referrers_VisitorsFromDirectEntry'); + $translations['Referrers_visitorsFromDirectEntry_percent'] = Piwik::translate('Referrers_PercentOfX', $translations['Referrers_visitorsFromDirectEntry']); + + $translations['Referrers_visitorsFromWebsites'] = Piwik::translate('Referrers_VisitorsFromWebsites'); + $translations['Referrers_visitorsFromWebsites_percent'] = Piwik::translate('Referrers_PercentOfX', $translations['Referrers_visitorsFromWebsites']); + + $translations['Referrers_visitorsFromCampaigns'] = Piwik::translate('Referrers_VisitorsFromCampaigns'); + $translations['Referrers_visitorsFromCampaigns_percent'] = Piwik::translate('Referrers_PercentOfX', $translations['Referrers_visitorsFromCampaigns']); + + $translations[Archiver::METRIC_DISTINCT_SEARCH_ENGINE_RECORD_NAME] = ucfirst(Piwik::translate('Referrers_DistinctSearchEngines')); + $translations[Archiver::METRIC_DISTINCT_SOCIAL_NETWORK_RECORD_NAME] = ucfirst(Piwik::translate('Referrers_DistinctSocialNetworks')); + $translations[Archiver::METRIC_DISTINCT_WEBSITE_RECORD_NAME] = ucfirst(Piwik::translate('Referrers_DistinctWebsites')); + $translations[Archiver::METRIC_DISTINCT_KEYWORD_RECORD_NAME] = ucfirst(Piwik::translate('Referrers_DistinctKeywords')); + $translations[Archiver::METRIC_DISTINCT_CAMPAIGN_RECORD_NAME] = ucfirst(Piwik::translate('Referrers_DistinctCampaigns')); + } + + public function getPagesComparisonsDisabledFor(&$pages) + { + $pages[] = 'Referrers_Referrers.Referrers_URLCampaignBuilder'; + } + public function getStylesheetFiles(&$stylesheets) { $stylesheets[] = 'plugins/Referrers/angularjs/campaign-builder/campaign-builder.directive.less'; diff --git a/plugins/Referrers/Reports/Get.php b/plugins/Referrers/Reports/Get.php new file mode 100644 index 0000000000..07c429cc6e --- /dev/null +++ b/plugins/Referrers/Reports/Get.php @@ -0,0 +1,200 @@ +<?php +/** + * Piwik - free/libre analytics platform + * + * @link https://matomo.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + * + */ + +namespace Piwik\Plugins\Referrers\Reports; + + +use Piwik\API\Request; +use Piwik\Common; +use Piwik\DataTable; +use Piwik\DataTable\Filter\CalculateEvolutionFilter; +use Piwik\DataTable\Filter\ColumnCallbackAddColumnPercentage; +use Piwik\Date; +use Piwik\NumberFormatter; +use Piwik\Period; +use Piwik\Period\Factory; +use Piwik\Period\Month; +use Piwik\Period\Range; +use Piwik\Piwik; +use Piwik\Plugin\ViewDataTable; +use Piwik\Plugins\CoreVisualizations\Visualizations\Sparklines; +use Piwik\Plugins\Referrers\Archiver; +use Piwik\Plugins\Referrers\Controller; +use Piwik\Report\ReportWidgetFactory; +use Piwik\Widget\WidgetsList; + +class Get extends Base +{ + const TOTAL_DIRECT_ENTRIES_METRIC_NAME = 'Referrers_directEntries'; + + protected function init() + { + parent::init(); + + $this->name = Piwik::translate('Referrers_ReferrersOverview'); + $this->documentation = ''; + $this->processedMetrics = [ + // none + ]; + $this->metrics = [ + 'Referrers_visitorsFromSearchEngines', + 'Referrers_visitorsFromSearchEngines_percent', + 'Referrers_visitorsFromSocialNetworks', + 'Referrers_visitorsFromSocialNetworks_percent', + 'Referrers_visitorsFromDirectEntry', + 'Referrers_visitorsFromDirectEntry_percent', + 'Referrers_visitorsFromWebsites', + 'Referrers_visitorsFromWebsites_percent', + 'Referrers_visitorsFromCampaigns', + 'Referrers_visitorsFromCampaigns_percent', + Archiver::METRIC_DISTINCT_SEARCH_ENGINE_RECORD_NAME, + Archiver::METRIC_DISTINCT_SOCIAL_NETWORK_RECORD_NAME, + Archiver::METRIC_DISTINCT_WEBSITE_RECORD_NAME, + Archiver::METRIC_DISTINCT_KEYWORD_RECORD_NAME, + Archiver::METRIC_DISTINCT_CAMPAIGN_RECORD_NAME, + ]; + } + + public function configureWidgets(WidgetsList $widgetsList, ReportWidgetFactory $factory) + { + // empty + } + + public function configureView(ViewDataTable $view) + { + if ($view->isViewDataTableId(Sparklines::ID) + && $view instanceof Sparklines + ) { + $this->addSparklineColumns($view); + $view->config->addTranslations($this->getSparklineTranslations()); + + // add evolution values + list($lastPeriodDate, $ignore) = Range::getLastDate(); + if ($lastPeriodDate !== false) { + $date = Common::getRequestVar('date'); + + /** @var DataTable $previousData */ + $previousData = Request::processRequest('Referrers.get', ['date' => $lastPeriodDate]); + $previousDataRow = $previousData->getFirstRow(); + + $view->config->compute_evolution = function ($columns) use ($date, $lastPeriodDate, $previousDataRow) { + $value = reset($columns); + $columnName = key($columns); + + if (!in_array($columnName, $this->metrics)) { + return; + } + + $pastValue = $previousDataRow->getColumn($columnName); + + $currentValueFormatted = NumberFormatter::getInstance()->format($value); + $pastValueFormatted = NumberFormatter::getInstance()->format($pastValue); + + return [ + 'currentValue' => $value, + 'pastValue' => $pastValue, + 'tooltip' => Piwik::translate('General_EvolutionSummaryGeneric', array( + Piwik::translate('General_NVisits', $currentValueFormatted), + $date, + Piwik::translate('General_NVisits', $pastValueFormatted), + $lastPeriodDate, + CalculateEvolutionFilter::calculate($value, $pastValue, $precision = 1) + )), + ]; + }; + } + } + } + + /** + * Returns the pretty date representation + * + * @param $date string + * @param $period string + * @return string Pretty date + */ + public static function getPrettyDate($date, $period) + { + return self::getCalendarPrettyDate(Factory::build($period, Date::factory($date))); + } + + /** + * Returns a prettified date string for use in period selector widget. + * + * @param Period $period The period to return a pretty string for. + * @return string + * @api + */ + public static function getCalendarPrettyDate($period) + { + if ($period instanceof Month) { + // show month name when period is for a month + + return $period->getLocalizedLongString(); + } else { + return $period->getPrettyString(); + } + } + + private function addSparklineColumns(Sparklines $view) + { + $directEntry = Controller::getTranslatedReferrerTypeLabel(Common::REFERRER_TYPE_DIRECT_ENTRY); + $directEntry = urlencode($directEntry); + + $website = Controller::getTranslatedReferrerTypeLabel(Common::REFERRER_TYPE_WEBSITE); + $website = urlencode($website); + + $searchEngine = Controller::getTranslatedReferrerTypeLabel(Common::REFERRER_TYPE_SEARCH_ENGINE); + $searchEngine = urlencode($searchEngine); + + $campaigns = Controller::getTranslatedReferrerTypeLabel(Common::REFERRER_TYPE_CAMPAIGN); + $campaigns = urlencode($campaigns); + + $socialNetworks = Controller::getTranslatedReferrerTypeLabel(Common::REFERRER_TYPE_SOCIAL_NETWORK); + $socialNetworks = urlencode($socialNetworks); + + $total = Piwik::translate('General_Total'); + + $view->config->addSparklineMetric(['Referrers_visitorsFromDirectEntry', 'Referrers_visitorsFromDirectEntry_percent'], 10, ['rows' => $directEntry . ',' . $total]); + $view->config->addSparklineMetric(['Referrers_visitorsFromWebsites', 'Referrers_visitorsFromWebsites_percent'], 20, ['rows' => $website . ',' . $total]); + $view->config->addSparklineMetric(['Referrers_visitorsFromSearchEngines', 'Referrers_visitorsFromSearchEngines_percent'], 30, ['rows' => $searchEngine . ',' . $total]); + $view->config->addSparklineMetric(['Referrers_visitorsFromSocialNetworks', 'Referrers_visitorsFromSocialNetworks_percent'], 40, ['rows' => $socialNetworks . ',' . $total]); + $view->config->addSparklineMetric(['Referrers_visitorsFromCampaigns', 'Referrers_visitorsFromCampaigns_percent'], 50, ['rows' => $campaigns . ',' . $total]); + $view->config->addSparklineMetric([Archiver::METRIC_DISTINCT_SEARCH_ENGINE_RECORD_NAME], 50); + $view->config->addSparklineMetric([Archiver::METRIC_DISTINCT_SOCIAL_NETWORK_RECORD_NAME], 60); + $view->config->addSparklineMetric([Archiver::METRIC_DISTINCT_WEBSITE_RECORD_NAME], 70); + $view->config->addSparklineMetric([Archiver::METRIC_DISTINCT_KEYWORD_RECORD_NAME], 80); + $view->config->addSparklineMetric([Archiver::METRIC_DISTINCT_CAMPAIGN_RECORD_NAME], 90); + } + + private function getSparklineTranslations() + { + $translations = [ + 'Referrers_visitorsFromDirectEntry' => Piwik::translate('Referrers_TypeDirectEntries'), + 'Referrers_visitorsFromWebsites' => Piwik::translate('Referrers_TypeWebsites'), + 'Referrers_visitorsFromSearchEngines' => Piwik::translate('Referrers_TypeSearchEngines'), + 'Referrers_visitorsFromSocialNetworks' => Piwik::translate('Referrers_TypeSocialNetworks'), + 'Referrers_visitorsFromCampaigns' => Piwik::translate('Referrers_TypeCampaigns'), + ]; + + foreach ($translations as $name => $label) { + $translations[$name . '_percent'] = Piwik::translate('Referrers_XPercentOfVisits'); + } + + $translations = array_merge($translations, [ + Archiver::METRIC_DISTINCT_SEARCH_ENGINE_RECORD_NAME => Piwik::translate('Referrers_DistinctSearchEngines'), + Archiver::METRIC_DISTINCT_SOCIAL_NETWORK_RECORD_NAME => Piwik::translate('Referrers_DistinctSocialNetworks'), + Archiver::METRIC_DISTINCT_WEBSITE_RECORD_NAME => Piwik::translate('Referrers_DistinctWebsites'), + Archiver::METRIC_DISTINCT_KEYWORD_RECORD_NAME => Piwik::translate('Referrers_DistinctKeywords'), + Archiver::METRIC_DISTINCT_CAMPAIGN_RECORD_NAME => Piwik::translate('Referrers_DistinctCampaigns'), + ]); + + return $translations; + } +}
\ No newline at end of file diff --git a/plugins/Referrers/lang/en.json b/plugins/Referrers/lang/en.json index 53019f50ec..0c4d889928 100644 --- a/plugins/Referrers/lang/en.json +++ b/plugins/Referrers/lang/en.json @@ -26,6 +26,7 @@ "DistinctSearchEngines": "distinct search engines", "DistinctSocialNetworks": "distinct social networks", "DistinctWebsites": "distinct websites", + "DistinctWebsiteUrls": "distinct website URLs", "EvolutionDocumentation": "This is an overview of the referrers that led visitors to your website.", "EvolutionDocumentationMoreInfo": "For more information about the different channel types, see the documentation of the %s table.", "Keywords": "Keywords", @@ -68,6 +69,12 @@ "WidgetSocials": "List of social networks", "WidgetTopKeywordsForPages": "Top Keywords for Page URL", "XPercentOfVisits": "%s of visits", - "Acquisition": "Acquisition" + "Acquisition": "Acquisition", + "VisitorsFromSearchEngines": "Visitors from Search Engines", + "PercentOfX": "Percent of %s", + "VisitorsFromSocialNetworks": "Visitors from Social Networks", + "VisitorsFromDirectEntry": "Visitors from Direct Entry", + "VisitorsFromWebsites": "Visitors from Websites", + "VisitorsFromCampaigns": "Visitors from Campaigns" } } diff --git a/plugins/Referrers/tests/System/expected/test_phpSerialized__Referrers.getReferrerType_year.original b/plugins/Referrers/tests/System/expected/test_phpSerialized__Referrers.getReferrerType_year.original Binary files differindex d1969c268a..96e76d807a 100644 --- a/plugins/Referrers/tests/System/expected/test_phpSerialized__Referrers.getReferrerType_year.original +++ b/plugins/Referrers/tests/System/expected/test_phpSerialized__Referrers.getReferrerType_year.original diff --git a/plugins/Referrers/tests/Unit/DataTable/Filter/UrlsFromWebsiteIdTest.php b/plugins/Referrers/tests/Unit/DataTable/Filter/UrlsFromWebsiteIdTest.php new file mode 100644 index 0000000000..47590690e2 --- /dev/null +++ b/plugins/Referrers/tests/Unit/DataTable/Filter/UrlsFromWebsiteIdTest.php @@ -0,0 +1,48 @@ +<?php +/** + * Piwik - free/libre analytics platform + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later + * + */ +namespace Piwik\Plugins\Referrers\tests\Unit\DataTable\Filter; + +use Piwik\DataTable; +use Piwik\Plugins\Referrers\DataTable\Filter\UrlsFromWebsiteId; + +require_once PIWIK_INCLUDE_PATH . '/plugins/Referrers/functions.php'; + +class UrlsFromWebsiteIdTest extends \PHPUnit_Framework_TestCase +{ + public function test_filter_ignoresDomainsPortssAndPortsInRecord() + { + $table = new DataTable(); + $table->addRowsFromSimpleArray([ + ['label' => 'https://abc.com/x/y/z', 'nb_visits' => 1], + ['label' => 'https://def.com/x/y/z', 'nb_visits' => 1], + ['label' => 'https://def.com/x/y/z/', 'nb_visits' => 1], + ['label' => 'http://abc.com:3000/x/y/z', 'nb_visits' => 1], + ['label' => 'http://abc.com/', 'nb_visits' => 1], + ]); + + $filter = new UrlsFromWebsiteId($table); + $filter->filter($table); + + $expected = [ + new DataTable\Row([ + DataTable\Row::COLUMNS => ['label' => 'x/y/z', 'nb_visits' => 3], + DataTable\Row::METADATA => ['url' => ''], + ]), + new DataTable\Row([ + DataTable\Row::COLUMNS => ['label' => 'x/y/z/', 'nb_visits' => 1], + DataTable\Row::METADATA => ['url' => ''], + ]), + new DataTable\Row([ + DataTable\Row::COLUMNS => ['label' => 'index', 'nb_visits' => 1], + DataTable\Row::METADATA => ['url' => ''], + ]), + ]; + $this->assertEquals($expected, array_values($table->getRows())); + } +}
\ No newline at end of file diff --git a/plugins/SegmentEditor/SegmentEditor.php b/plugins/SegmentEditor/SegmentEditor.php index c00e980c8e..7efc8dce91 100644 --- a/plugins/SegmentEditor/SegmentEditor.php +++ b/plugins/SegmentEditor/SegmentEditor.php @@ -11,6 +11,8 @@ namespace Piwik\Plugins\SegmentEditor; use Piwik\API\Request; use Piwik\ArchiveProcessor\PluginsArchiver; use Piwik\ArchiveProcessor\Rules; +use Piwik\Cache; +use Piwik\CacheId; use Piwik\Common; use Piwik\Config; use Piwik\Container\StaticContainer; @@ -267,6 +269,7 @@ class SegmentEditor extends \Piwik\Plugin $translationKeys[] = 'SegmentEditor_OperatorAND'; $translationKeys[] = 'SegmentEditor_OperatorOR'; $translationKeys[] = 'SegmentEditor_AddANDorORCondition'; + $translationKeys[] = 'SegmentEditor_DefaultAllVisits'; $translationKeys[] = 'General_OperationEquals'; $translationKeys[] = 'General_OperationNotEquals'; $translationKeys[] = 'General_OperationAtMost'; @@ -279,5 +282,25 @@ class SegmentEditor extends \Piwik\Plugin $translationKeys[] = 'General_OperationDoesNotContain'; $translationKeys[] = 'General_OperationStartsWith'; $translationKeys[] = 'General_OperationEndsWith'; + $translationKeys[] = 'General_Unknown'; + $translationKeys[] = 'SegmentEditor_ThisSegmentIsCompared'; + $translationKeys[] = 'SegmentEditor_ThisSegmentIsSelectedAndCannotBeCompared'; + $translationKeys[] = 'SegmentEditor_CompareThisSegment'; + } + + public static function getAllSegmentsForSite($idSite) + { + $cache = Cache::getTransientCache(); + $cacheKey = CacheId::siteAware('SegmentEditor_getAll', [$idSite]); + + $segments = $cache->fetch($cacheKey); + if (!is_array($segments)) { + $segments = Request::processRequest('SegmentEditor.getAll', ['idSite' => $idSite], $default = []); + usort($segments, function ($lhs, $rhs) { + return strcmp($lhs['name'], $rhs['name']); + }); + $cache->save($cacheKey, $segments); + } + return $segments; } } diff --git a/plugins/SegmentEditor/javascripts/Segmentation.js b/plugins/SegmentEditor/javascripts/Segmentation.js index ffa2ea9aa2..afc9b02906 100644 --- a/plugins/SegmentEditor/javascripts/Segmentation.js +++ b/plugins/SegmentEditor/javascripts/Segmentation.js @@ -80,6 +80,20 @@ Segmentation = (function($) { $(this.content).attr('title', title); }; + segmentation.prototype.markComparedSegments = function() { + var comparisonService = piwikHelper.getAngularDependency('piwikComparisonsService'); + var comparedSegments = comparisonService.getSegmentComparisons().map(function (comparison) { + return comparison.params.segment; + }); + + $('div.segmentList ul li[data-definition]', this.target).removeClass('comparedSegment').filter(function () { + var definition = $(this).attr('data-definition'); + return comparedSegments.indexOf(definition) !== -1 || comparedSegments.indexOf(decodeURIComponent(definition)) !== -1; + }).each(function () { + $(this).addClass('comparedSegment'); + }); + }; + segmentation.prototype.markCurrentSegment = function(){ var current = this.getSegment(); @@ -150,7 +164,14 @@ Segmentation = (function($) { (self.currentSegmentStr == "" ? " class='segmentSelected'" : "") + ' data-definition=""><span class="segname" tabindex="4">' + self.translations['SegmentEditor_DefaultAllVisits'] + ' ' + self.translations['General_DefaultAppended'] - + '</span></li> '; + + '</span>'; + var comparisonService = piwikHelper.getAngularDependency('piwikComparisonsService'); + if (comparisonService.isComparisonEnabled() + || comparisonService.isComparisonEnabled() === null // may not be initialized since this code is outside of angular + ) { + listHtml += '<span class="compareSegment allVisitsCompareSegment" title="' + _pk_translate('SegmentEditor_CompareThisSegment') + '"></span>'; + } + listHtml += '</li>'; var isVisibleToSuperUserNoticeAlreadyDisplayedOnce = false; var isVisibleToSuperUserNoticeShouldBeClosed = false; @@ -196,6 +217,11 @@ Segmentation = (function($) { if(self.segmentAccess == "write") { listHtml += '<span class="editSegment" title="'+ self.translations['General_Edit'].toLocaleLowerCase() +'"></span>'; } + if (comparisonService.isComparisonEnabled() + || comparisonService.isComparisonEnabled() === null // may not be initialized since this code is outside of angular + ) { + listHtml += '<span class="compareSegment" title="' + _pk_translate('SegmentEditor_CompareThisSegment') + '"></span>'; + } listHtml += '</li>'; } @@ -392,6 +418,20 @@ Segmentation = (function($) { e.preventDefault(); }); + self.target.on('click', '.compareSegment', function (e) { + e.stopPropagation(); + e.preventDefault(); + + var comparisonService = piwikHelper.getAngularDependency('piwikComparisonsService'); + comparisonService.addSegmentComparison({ + segment: $(e.target).closest('li').data('definition'), + }); + + self.markComparedSegments(); + + closeAllOpenLists(); + }); + self.target.on("click", ".segmentList li", function (e) { if ($(e.currentTarget).hasClass("grayed") !== true) { var idsegment = $(this).attr("data-idsegment"); @@ -764,6 +804,8 @@ Segmentation = (function($) { } this.initHtml = function() { + var self = this; + var html = getListHtml(); if(typeof self.content !== "undefined"){ @@ -775,6 +817,9 @@ Segmentation = (function($) { // assign content to object attribute to make it easil accesible through all widget methods this.markCurrentSegment(); + setTimeout(function () { + self.markComparedSegments(); + }); // Loading message var segmentIsSet = this.getSegment().length; @@ -794,6 +839,10 @@ Segmentation = (function($) { if (self.getSegment() != segment) { self.setSegment(segment); self.initHtml(); + } else { + setTimeout(function () { + self.markComparedSegments(); + }); } }); }); @@ -1044,6 +1093,8 @@ $(document).ready(function() { $('body').on('mouseup', this.onMouseUp); initTopControls(); + + piwikHelper.getAngularDependency('$rootScope').$emit('piwikSegmentationInited'); }; /** diff --git a/plugins/SegmentEditor/lang/en.json b/plugins/SegmentEditor/lang/en.json index 19446db9a6..8e3622801e 100644 --- a/plugins/SegmentEditor/lang/en.json +++ b/plugins/SegmentEditor/lang/en.json @@ -52,6 +52,9 @@ "CustomUnprocessedSegmentApiError5": "Please note that you can test whether your segment will work without having to wait for it to be processed by using the Live.getLastVisitsDetails API.", "CustomUnprocessedSegmentApiError6": "When using this API method, you will see which users and actions were matched by your &segment= parameter.", "CustomUnprocessedSegmentNoData": "To see data for this segment, you must create this segment manually in the Segment Editor, then wait a couple hours for preprocessing to complete.", - "AddThisToMatomo": "Add this segment to Matomo" + "AddThisToMatomo": "Add this segment to Matomo", + "ThisSegmentIsCompared": "This segment is currently compared.", + "ThisSegmentIsSelectedAndCannotBeCompared": "This segment is currently selected so cannot be selected to compare.", + "CompareThisSegment": "Compare this segment with the selected segment and period." } }
\ No newline at end of file diff --git a/plugins/SegmentEditor/stylesheets/segmentation.less b/plugins/SegmentEditor/stylesheets/segmentation.less index cd6b7632a7..a27e24fb0a 100644 --- a/plugins/SegmentEditor/stylesheets/segmentation.less +++ b/plugins/SegmentEditor/stylesheets/segmentation.less @@ -224,19 +224,46 @@ div.scrollable { margin-bottom: 5px; } -.segmentationContainer ul.submenu > li span.editSegment { - display: block; - float: right; - text-align: center; - margin-right: 4px; - font-weight: normal; - background: url(plugins/SegmentEditor/images/edit_segment.png) no-repeat; - width: 16px; - height: 16px; - .opacity(0.5); +.segmentationContainer ul.submenu > li { + span.editSegment, span.compareSegment { + display: block; + float: right; + text-align: center; + margin-right: 4px; + font-weight: normal; + width: 16px; + height: 16px; + .opacity(0.5); + + &:hover { + .opacity(1); + } + } + + span.editSegment { + background: url(plugins/SegmentEditor/images/edit_segment.png) no-repeat; + } + + span.compareSegment { + background: url(plugins/Morpheus/images/compare.svg) no-repeat; + background-size: cover; + + &.allVisitsCompareSegment { + margin-right: 24px; + } + } + + li.segmentSelected, li.comparedSegment { + span.compareSegment { + pointer-events: none; + opacity: 0.2; + } + } +} - &:hover { - .opacity(1); +html.comparisonsDisabled .segmentationContainer ul.submenu { + span.compareSegment { + display: none; } } diff --git a/plugins/SegmentEditor/templates/_segmentSelector.twig b/plugins/SegmentEditor/templates/_segmentSelector.twig index befd058c1c..528198ce5b 100644 --- a/plugins/SegmentEditor/templates/_segmentSelector.twig +++ b/plugins/SegmentEditor/templates/_segmentSelector.twig @@ -99,10 +99,10 @@ <input role="no" type="button" value="{{ 'General_No'|translate }}"/> </div> <div class="ui-confirm pleaseChangeBrowserAchivingDisabledSetting"> - <h2>{{ 'SegmentEditor_SegmentNotApplied'|translate(nameOfCurrentSegment)|raw }}</h2> + <h2>{{ 'SegmentEditor_SegmentNotApplied'|translate(nameOfCurrentSegment)|rawSafeDecoded|raw }}</h2> {% set segmentSetting %}{{ 'SegmentEditor_AutoArchivePreProcessed'|translate }}{% endset %} <p class="description"> - {{ 'SegmentEditor_SegmentNotAppliedMessage'|translate(nameOfCurrentSegment)|raw }} + {{ 'SegmentEditor_SegmentNotAppliedMessage'|translate(nameOfCurrentSegment)|rawSafeDecoded|raw }} <br/> {{ 'SegmentEditor_DataAvailableAtLaterDate'|translate }} {% if isSuperUser %} diff --git a/plugins/SegmentEditor/tests/UI/expected-screenshots/SegmentSelectorEditorTest_1_selector_open.png b/plugins/SegmentEditor/tests/UI/expected-screenshots/SegmentSelectorEditorTest_1_selector_open.png index e8b3aee3d2..c7e28dd23a 100644 --- a/plugins/SegmentEditor/tests/UI/expected-screenshots/SegmentSelectorEditorTest_1_selector_open.png +++ b/plugins/SegmentEditor/tests/UI/expected-screenshots/SegmentSelectorEditorTest_1_selector_open.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bd0d007b56acbd89e249dfabff444366c203f8868a66e9abd300e11e72c89362 -size 14931 +oid sha256:94ed357dba4c01adf66d7ee817b58f48c5ee5e19773de488586e7cf1948f2c85 +size 15404 diff --git a/plugins/SegmentEditor/tests/UI/expected-screenshots/SegmentSelectorEditorTest_complex_segment.png b/plugins/SegmentEditor/tests/UI/expected-screenshots/SegmentSelectorEditorTest_complex_segment.png index c5892ec6f6..cf0fa8d7fb 100644 --- a/plugins/SegmentEditor/tests/UI/expected-screenshots/SegmentSelectorEditorTest_complex_segment.png +++ b/plugins/SegmentEditor/tests/UI/expected-screenshots/SegmentSelectorEditorTest_complex_segment.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:617aa6d391021381d4e8338f22df769cc0748a309fb863b93cd6ac8b598fbec6 -size 130614 +oid sha256:73d5555874c628f5e2db4e0dfebafabb4e9eb78a101f0fed06559ebe9a1c0994 +size 130776 diff --git a/plugins/SegmentEditor/tests/UI/expected-screenshots/SegmentSelectorEditorTest_deleted.png b/plugins/SegmentEditor/tests/UI/expected-screenshots/SegmentSelectorEditorTest_deleted.png index e8b3aee3d2..c7e28dd23a 100644 --- a/plugins/SegmentEditor/tests/UI/expected-screenshots/SegmentSelectorEditorTest_deleted.png +++ b/plugins/SegmentEditor/tests/UI/expected-screenshots/SegmentSelectorEditorTest_deleted.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bd0d007b56acbd89e249dfabff444366c203f8868a66e9abd300e11e72c89362 -size 14931 +oid sha256:94ed357dba4c01adf66d7ee817b58f48c5ee5e19773de488586e7cf1948f2c85 +size 15404 diff --git a/plugins/SegmentEditor/tests/UI/expected-screenshots/SegmentSelectorEditorTest_saved.png b/plugins/SegmentEditor/tests/UI/expected-screenshots/SegmentSelectorEditorTest_saved.png index 806a821fb6..1e7dddd9dd 100644 --- a/plugins/SegmentEditor/tests/UI/expected-screenshots/SegmentSelectorEditorTest_saved.png +++ b/plugins/SegmentEditor/tests/UI/expected-screenshots/SegmentSelectorEditorTest_saved.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0370a2bb4439ad2eff9cab35eb342332ef57c02123432f525305c188c3751cc8 -size 16550 +oid sha256:8ddc2c7da46342c738ab78be5cf55a85b7afe1dc3232e16d159b3602f62a67bc +size 17090 diff --git a/plugins/SegmentEditor/tests/UI/expected-screenshots/SegmentSelectorEditorTest_updated.png b/plugins/SegmentEditor/tests/UI/expected-screenshots/SegmentSelectorEditorTest_updated.png index c954156536..856bc1dd54 100644 --- a/plugins/SegmentEditor/tests/UI/expected-screenshots/SegmentSelectorEditorTest_updated.png +++ b/plugins/SegmentEditor/tests/UI/expected-screenshots/SegmentSelectorEditorTest_updated.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8fe41b99e492db062653d6b96db653cad0354c7c99f8c9f65205b5a91dd8ff0f -size 16619 +oid sha256:2726e5cac039297dd41cc00ab853df5d216fb05aa9f34638e6e731b26b5e04db +size 17434 diff --git a/plugins/SegmentEditor/tests/UI/expected-screenshots/UnprocessedSegmentTest_custom_segment.png b/plugins/SegmentEditor/tests/UI/expected-screenshots/UnprocessedSegmentTest_custom_segment.png index 69c60e77f0..ba7c566010 100644 --- a/plugins/SegmentEditor/tests/UI/expected-screenshots/UnprocessedSegmentTest_custom_segment.png +++ b/plugins/SegmentEditor/tests/UI/expected-screenshots/UnprocessedSegmentTest_custom_segment.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dc5cc15887932ad1c9b9c1eba10e5f7cf30f091fa12d4ec62d1ffbf24b11785b -size 57284 +oid sha256:bd5e092fb0f2d3086c0d9457b469200105151b6f5bfc6862dd0ae64a248e7fa9 +size 57503 diff --git a/plugins/SegmentEditor/tests/UI/expected-screenshots/UnprocessedSegmentTest_unprocessed_segment.png b/plugins/SegmentEditor/tests/UI/expected-screenshots/UnprocessedSegmentTest_unprocessed_segment.png index 2f9485ca16..ba7c566010 100644 --- a/plugins/SegmentEditor/tests/UI/expected-screenshots/UnprocessedSegmentTest_unprocessed_segment.png +++ b/plugins/SegmentEditor/tests/UI/expected-screenshots/UnprocessedSegmentTest_unprocessed_segment.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:94ad972d91de1a1932bf09f7744d571595eb54b4c7ddca71eef53d918451368a -size 98802 +oid sha256:bd5e092fb0f2d3086c0d9457b469200105151b6f5bfc6862dd0ae64a248e7fa9 +size 57503 diff --git a/plugins/Transitions/Transitions.php b/plugins/Transitions/Transitions.php index 8649bf110f..7d6248fdc0 100644 --- a/plugins/Transitions/Transitions.php +++ b/plugins/Transitions/Transitions.php @@ -21,10 +21,16 @@ class Transitions extends \Piwik\Plugin return array( 'AssetManager.getStylesheetFiles' => 'getStylesheetFiles', 'AssetManager.getJavaScriptFiles' => 'getJsFiles', - 'Translate.getClientSideTranslationKeys' => 'getClientSideTranslationKeys' + 'Translate.getClientSideTranslationKeys' => 'getClientSideTranslationKeys', + 'API.getPagesComparisonsDisabledFor' => 'getPagesComparisonsDisabledFor', ); } + public function getPagesComparisonsDisabledFor(&$pages) + { + $pages[] = "General_Actions.Transitions_Transitions"; + } + public function getStylesheetFiles(&$stylesheets) { $stylesheets[] = 'plugins/Transitions/stylesheets/transitions.less'; diff --git a/plugins/Transitions/javascripts/transitions.js b/plugins/Transitions/javascripts/transitions.js index 2862263ff6..b67245203f 100644 --- a/plugins/Transitions/javascripts/transitions.js +++ b/plugins/Transitions/javascripts/transitions.js @@ -71,16 +71,24 @@ DataTable_RowActions_Transitions.prototype.performAction = function (label, tr, }; DataTable_RowActions_Transitions.prototype.doOpenPopover = function (link) { - var posSegment = (link+'').indexOf(':segment:'); - var segment = null; + var ALLOWED_OVERRIDE_PARAMS = ['segment', 'date', 'period', 'idSite']; - // handle and remove ':segment:$SEGMENT' from link - if (posSegment && posSegment > 0) { - segment = link.substring(posSegment + (':segment:'.length)); - link = link.substring(0, posSegment); + var parts = link.split(':'); + + var overrideParams = {}; + + var i = 0; + while (i < parts.length) { + var paramName = decodeURIComponent(parts[i]); + if (ALLOWED_OVERRIDE_PARAMS.indexOf(paramName) === -1) { + i += 1; + continue; + } + + overrideParams[paramName] = decodeURIComponent(parts[i + 1]); + parts.splice(i, 2); } - var parts = link.split(':'); if (parts.length < 2) { return; } @@ -90,7 +98,7 @@ DataTable_RowActions_Transitions.prototype.doOpenPopover = function (link) { var actionName = parts.join(':'); if (this.transitions === null) { - this.transitions = new Piwik_Transitions(actionType, actionName, this, segment); + this.transitions = new Piwik_Transitions(actionType, actionName, this, overrideParams); } else { this.transitions.reset(actionType, actionName, segment); } @@ -154,8 +162,8 @@ DataTable_RowActions_Registry.register({ // TRANSITIONS IMPLEMENTATION // -function Piwik_Transitions(actionType, actionName, rowAction, segment) { - this.reset(actionType, actionName, segment); +function Piwik_Transitions(actionType, actionName, rowAction, overrideParams) { + this.reset(actionType, actionName, overrideParams); this.rowAction = rowAction; this.ajax = new Piwik_Transitions_Ajax(); @@ -165,10 +173,10 @@ function Piwik_Transitions(actionType, actionName, rowAction, segment) { this.rightGroups = ['followingPages', 'followingSiteSearches', 'downloads', 'outlinks']; } -Piwik_Transitions.prototype.reset = function (actionType, actionName, segment) { +Piwik_Transitions.prototype.reset = function (actionType, actionName, overrideParams) { this.actionType = actionType; this.actionName = actionName; - this.segment = segment; + this.overrideParams = overrideParams; this.popover = null; this.canvas = null; @@ -230,7 +238,7 @@ Piwik_Transitions.prototype.showPopover = function (showEmbeddedInReport) { } // load the data - self.model.loadData(self.actionType, self.actionName, self.segment, function () { + self.model.loadData(self.actionType, self.actionName, self.overrideParams, function () { if (typeof Piwik_Transitions.popoverHtml == 'undefined') { // html not there yet callbackForHtml = bothLoaded; @@ -1316,7 +1324,7 @@ Piwik_Transitions_Model.prototype.htmlLoaded = function () { }; }; -Piwik_Transitions_Model.prototype.loadData = function (actionType, actionName, segment, callback) { +Piwik_Transitions_Model.prototype.loadData = function (actionType, actionName, overrideParams, callback) { var self = this; this.pageviews = 0; @@ -1362,8 +1370,8 @@ Piwik_Transitions_Model.prototype.loadData = function (actionType, actionName, s actionName: actionName, expanded: 1 }; - if (segment) { - params.segment = segment; + if (overrideParams) { + $.extend(params, overrideParams); } this.ajax.callApi('Transitions.getTransitionsForAction', params, diff --git a/plugins/TwoFactorAuth/tests/UI/expected-screenshots/TwoFactorAuth_logme_verified.png b/plugins/TwoFactorAuth/tests/UI/expected-screenshots/TwoFactorAuth_logme_verified.png index 1dbc4ef8fc..efa7e9b284 100644 --- a/plugins/TwoFactorAuth/tests/UI/expected-screenshots/TwoFactorAuth_logme_verified.png +++ b/plugins/TwoFactorAuth/tests/UI/expected-screenshots/TwoFactorAuth_logme_verified.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:18ce0e44ecb7a3b5bdac1b31cb4ba2f282930e6cd81a0e895c2a93a8d4e6e58b -size 184145 +oid sha256:c0861497c831b0efdd3571b0b712291bf496bde58112cb7d7edea1cdf5071bbd +size 184082 diff --git a/plugins/TwoFactorAuth/tests/UI/expected-screenshots/TwoFactorAuth_twofa_forced_step4.png b/plugins/TwoFactorAuth/tests/UI/expected-screenshots/TwoFactorAuth_twofa_forced_step4.png index 0ce7a55ca0..f7c196018d 100644 --- a/plugins/TwoFactorAuth/tests/UI/expected-screenshots/TwoFactorAuth_twofa_forced_step4.png +++ b/plugins/TwoFactorAuth/tests/UI/expected-screenshots/TwoFactorAuth_twofa_forced_step4.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a2f06d7f09046ad8d4d1648d5069b16bc3a0d62314111cd8dea5dfa27575c7fd -size 183162 +oid sha256:2da09422405154f3d93f1833795649913af4499eb2016664fa9a5874fa851c77 +size 183095 diff --git a/plugins/UserCountryMap/UserCountryMap.php b/plugins/UserCountryMap/UserCountryMap.php index b2b47e2a72..392df7291a 100644 --- a/plugins/UserCountryMap/UserCountryMap.php +++ b/plugins/UserCountryMap/UserCountryMap.php @@ -31,11 +31,17 @@ class UserCountryMap extends \Piwik\Plugin $hooks = array( 'AssetManager.getJavaScriptFiles' => 'getJsFiles', 'AssetManager.getStylesheetFiles' => 'getStylesheetFiles', - 'Translate.getClientSideTranslationKeys' => 'getClientSideTranslationKeys' + 'Translate.getClientSideTranslationKeys' => 'getClientSideTranslationKeys', + 'API.getPagesComparisonsDisabledFor' => 'getPagesComparisonsDisabledFor', ); return $hooks; } + public function getPagesComparisonsDisabledFor(&$pages) + { + $pages[] = 'General_Visitors.UserCountryMap_RealTimeMap'; + } + public function getJsFiles(&$jsFiles) { $jsFiles[] = "libs/bower_components/visibilityjs/lib/visibility.core.js"; diff --git a/plugins/Widgetize/stylesheets/widgetize.less b/plugins/Widgetize/stylesheets/widgetize.less index d4bc1d43a2..447c7dc7fe 100644 --- a/plugins/Widgetize/stylesheets/widgetize.less +++ b/plugins/Widgetize/stylesheets/widgetize.less @@ -47,4 +47,10 @@ body.widgetized { #pageFooter { margin-bottom: 0; } + + table.dataTable { + table-layout: fixed; + width: auto; + min-width: 100%; + } }
\ No newline at end of file |