Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/matomo-org/matomo.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authordiosmosis <diosmosis@users.noreply.github.com>2019-09-30 20:19:46 +0300
committerGitHub <noreply@github.com>2019-09-30 20:19:46 +0300
commit3f26e785f015d30d0aeea66aaf7484111b0dbfa9 (patch)
tree3a3d38441103ad8fafd012a027e327faed845817 /plugins/CoreHome
parent98837a7ac01f79f9e713471962699af74a54c6af (diff)
Compare segments and periods (in API and UI) (#14365)
* Allow row metadata to be datatables in API output. * Fill out initial DataComparisonFilter. * fixing some issues * couple more fixes * couple more fixes + initial system test * more fixes * Finish up segment comparison system test and get to pass. * Soft limit for number of segments/periods. * Add supportsComparison method. * Add UX code for adding/removing/seeing data comparisons + code to forward query parameters in AJAX requests + allow broadcast to handle multi-value query param values. * Start showing comparison tables in html tables. * Adding all comparison rows to html table visualization and adding "all visits" segment translation and add currently selected segment to comparisons table. * Show totals ratio for comparison rows. * finished poc html table visualization support for comparison * start working on comparisons support in graph visualizations * Some UX tweaks to htmltable and add comparisons to bar/pie graphs. * Getting comparisons to work in evolution graphs. * Get row evolution to work properly in comparison table. * Get segmented visitor log to work in comparison tables. * Fix regression in comparisons in evolution graph. * Get comparisons to work in actions datatable, fix twig issue that results in 100% cpu usage (when reading dataTable param w/ many rows & comparison tables), get overlay/transitions icons to appear, overlay should work properly. * Get transitions and overlay to work in comparison rows. * Fixing some datatable API output issues, fixing tests, support comparisons in subtables by forcing idSubtables of comparisons to be sent in request (makes UI work, but not pracitcal for API). * Remove typo. * apply original change * Allow All Visits default segment to be compared. * working on disabling currently compared segments. * Get currently compared segments code to work. * starting on refactoring datacomparisonfilter * Most of refactor done. * Get tests to pass and fix a bunch of datatable metadata consistency issues. * Modify evolution graph to modify compare parameters and show some sort of accurate comparison line graphs. * Set xaxis labels correctly in tooltips and make sure series data for comparisons is set correctly. * more fixes to displaying evolution comparisons where compared date ranges vary in length + make sure normal reports w/ no data display the no data message even when comparing * Show period type in comparison card. * Unsanitize compare segments. * Get correct period count. * Couple more fixes to evolution graph series labels, but still wonky. * Include comparison series label in comparison output so evolution graph has an easier time of building series data. * For multi period vs multi period show correct compareDate/comparePeriod for child tables. * Redesign period selector comparison section and get to work. * Allow plugins to disable comparisons for specific pages. * Start supporting comparison in sparkline visualization. * Get sparkline points & lengths to work correctly when comparing. * Fix comparison enabling check. * Pick series and shade colors. * Rewriting comparison card to show individual serieses. * Rewrite comparisons cards to only show segments as cards and individual serieses inside the cards. * Use comparison colors and shades in evolution graph + fix a couple bugs. * Tweak series colors and fix a couple regressions to comparison totals calculation. * Add ratio tooltip suffix explaining comparison percent. * fix typo * Forward comparison params in report export. * tweak series colors again + add tooltip with visitssummary metrics to comparison rows + fix a bug in using array query params in piwik-api + fix bug in formatting of comparison table metrics * Tooltip fixes, start on sparklines supporting comparison, modify comparison filter to only calculate change metrics against periods since they are time related. * Sparkline comparison support. * Tweak line thickness and set metric index properly in jqplot data generator. * In sparklines comparison, show evolution for compared period, rewrite top tooltip to be better, fix tooltip issues when multiple metrics used in evolution graph, and get comparison to previous period to work. * Update submodule * Make things look ok w/ a very long segment name, add numbers to compared datatable row labels, fix pie chart colors + a couple other regressions. * more bug fixes * Fix query param retrieval issue. * Do not throw if no comparison params specified, just do nothing. * try to fix a couple warnings * Another query param get fix. * Do not save comparison parameters. * pass by reference * fix JS error * DO not set compare params if not set in URL for dashboard widgets. * Fix comparison table styling in dashboard. * Expand bar graph if there are too many bars when comparing. * tweak comparison bar graph sizing * make sure flatten works w/ comparison * Apply compute processed metrics to comparison tables. * Hack to get Goals.get to be formatted during comparison. * Fix ordering of yunits in evolution graph. * If rows are selected, incorporate into comparison series names. * Format revenue properly in goals comparison sparklines. * First working attempt at adding Referrers.get method for use w/ Sparklines visualization. * get referrers sparklines to work w/ comparison * Finish using new referrers API method and get referrers sparklines/evolution graph to play well w/ each other in comparison mode. * Simplify table comparison view if only comparing periods, no segments. * Take into account visible rows when calculating series metric index. * Get comparison to work when totals rows are added to tables. * Show series color in evolution graph tooltip. * Fix error when loading row evolution/segmented visitor log for compared ranges. * fix regression in normal subtable loading * Fix row style * Forward comparisonIdSubtables parameter if present so it is used when changing limit/offset * Initialize the row index prefix to the filter offset. * Do not show period header if only segments compared in table. * Add UI tests and fix issues so they pass locally. * quick tweak * Fix PHP error * Updating screenshots * Fixing several bugs and updating expected screenshots. * Fix comparison tests and clear some TODO. * Prefix referrers metrics. * Revert "apply original change" This reverts commit 8f6ceb0430e5c7306a777498199ad7db21fd7175. * Show period label if comparing two periods of same type. * segment sanitization fixes * More segment fixes. * Another fix to the tooltip. * Fix related reports when comparing + make totals tooltip clearer + store segment + pretty title in datatable metadata so it does not have to be looked up every time. * Allow disabling comparisons for individual uses of visualizations. * Remove limit on hover for actions tables + fix subtable expansion for normal actions tables. * Make sure parameters are arrays. * Stricter check for empty parameters. * Allow first compared segment to be "removed". * several more fixes * Fixing table cell alignment and width and everything else that broke while making changes (hopefully). * Several fixes, including xss fixes and test fixes and bug fixes for comparisons. * more table css tweaks * Correct workings of previous period/year comparison + always convert periods to ranges when comparing in evolution graph. * Correct workings of previous period/year comparison + always convert periods to ranges when comparing in evolution graph + more css tweaks. * fix more test regressions * Forgot to add file * fix several TODO as well ass get comparison sparklines to have right colors in widgets. * Use DataTable metadata instead of getting available segments. * When comparing periods that do not uniformly support unique visitors, do not display unique visitors metric. * Small refactor and make sure sparklines shows over period w/o using lastN. * more refactoring and fixes * some more refactoring * Move comparison index math to helper methods. * Use piwikUrl.getSearchParam * Process comparison tables like normal tables in API.getProcessedReport. * remove some code redundancy * use new format date method * Add first working unit test for comparisons service. * Finish writing unit test for comparison service. * refactor comparisons service and fix a couple regressions * Fix more TODO items and refactoring. * Fill out more TODO. * Remove more TODO. * Fixing some tests. * another test fix * FIx some more tests. * More test fixes and regression fixes. * Do not add segments to summary rows in actions reports. * more test fixes * fix more tests * more test fixes * Fixing more tests. * Fixing more tests + debugging failing one. * Fix twig loop issue * Make sure empty compare params are not used in URL. * Remove cached request array. * Support comparison rows in multirow evolution popover and LabelFilter. * Tweak placement of some icons. * Forward current segment in reporting menu links. * Fix for split dimension view. * tweak css * Add more tests. * Add year to xlabels in evolution graph (but not xaxis tick). * applying review feedback * Apply more PR feedback * remove debugging code * tweak event docs * Fix test. * fix some test * fix a test and regression * updating tes files + fixing test * Fix regression * Fix dropdown z-index issue (or workaround really). * Fixing tests again. * Update screenshots * Fix bug and remove some debugging code * Apply review feedback. * Make sure ratio tooltips show in widgetized mode. * Fix some UI tests. * Fix tests * Fix a couple more tests.
Diffstat (limited to 'plugins/CoreHome')
-rw-r--r--plugins/CoreHome/CoreHome.php10
-rw-r--r--plugins/CoreHome/DataTableRowAction/MultiRowEvolution.php25
-rw-r--r--plugins/CoreHome/DataTableRowAction/RowEvolution.php54
-rw-r--r--plugins/CoreHome/angularjs/common/services/periods.js18
-rw-r--r--plugins/CoreHome/angularjs/common/services/piwik-api.js28
-rw-r--r--plugins/CoreHome/angularjs/common/services/piwik-api.spec.js14
-rw-r--r--plugins/CoreHome/angularjs/common/services/piwik-url.js43
-rw-r--r--plugins/CoreHome/angularjs/common/services/piwik.js4
-rw-r--r--plugins/CoreHome/angularjs/comparisons/comparisons.component.html28
-rw-r--r--plugins/CoreHome/angularjs/comparisons/comparisons.component.js149
-rw-r--r--plugins/CoreHome/angularjs/comparisons/comparisons.component.less74
-rw-r--r--plugins/CoreHome/angularjs/comparisons/comparisons.service.js353
-rw-r--r--plugins/CoreHome/angularjs/comparisons/comparisons.service.spec.js499
-rw-r--r--plugins/CoreHome/angularjs/date-range-picker/date-range-picker.component.js8
-rw-r--r--plugins/CoreHome/angularjs/period-selector/period-selector.controller.js164
-rw-r--r--plugins/CoreHome/angularjs/period-selector/period-selector.directive.html78
-rw-r--r--plugins/CoreHome/angularjs/period-selector/period-selector.directive.less81
-rw-r--r--plugins/CoreHome/angularjs/report-export/reportexport.directive.js21
-rw-r--r--plugins/CoreHome/angularjs/reporting-menu/reportingmenu.controller.js64
-rw-r--r--plugins/CoreHome/angularjs/reporting-page/reportingpage.controller.js28
-rw-r--r--plugins/CoreHome/angularjs/sparkline/sparkline.component.js18
-rw-r--r--plugins/CoreHome/angularjs/widget-bydimension-container/widget-bydimension-container.directive.less4
-rw-r--r--plugins/CoreHome/angularjs/widget-loader/widgetloader.directive.js19
-rw-r--r--plugins/CoreHome/javascripts/broadcast.js115
-rw-r--r--plugins/CoreHome/javascripts/dataTable.js136
-rw-r--r--plugins/CoreHome/javascripts/dataTable_rowactions.js71
-rw-r--r--plugins/CoreHome/javascripts/sparkline.js44
-rw-r--r--plugins/CoreHome/javascripts/uiControl.js6
-rw-r--r--plugins/CoreHome/stylesheets/dataTable.less3
-rw-r--r--plugins/CoreHome/stylesheets/dataTable/_comparisonsTable.less71
-rw-r--r--plugins/CoreHome/stylesheets/dataTable/_dataTable.less40
-rw-r--r--plugins/CoreHome/stylesheets/dataTable/_subDataTable.less8
-rw-r--r--plugins/CoreHome/stylesheets/jqplotColors.less150
-rw-r--r--plugins/CoreHome/templates/_dataTable.twig2
-rw-r--r--plugins/CoreHome/templates/_dataTableCell.twig31
-rw-r--r--plugins/CoreHome/templates/_dataTableHead.twig2
-rw-r--r--plugins/CoreHome/tests/UI/SingleMetricView_spec.js2
37 files changed, 2019 insertions, 446 deletions
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') }}"
- >&nbsp;{{ 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();