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:
Diffstat (limited to 'plugins')
-rw-r--r--plugins/API/API.php39
-rw-r--r--plugins/API/Filter/DataComparisonFilter.php590
-rw-r--r--plugins/API/Filter/DataComparisonFilter/ComparisonRowGenerator.php239
-rw-r--r--plugins/API/ProcessedReport.php37
-rw-r--r--plugins/API/RowEvolution.php20
-rw-r--r--plugins/API/tests/Integration/Filter/DataComparisonFilter/ComparisonRowGeneratorTest.php613
-rw-r--r--plugins/API/tests/Unit/XmlRendererTest.php5
-rw-r--r--plugins/Actions/DataTable/Filter/Actions.php46
-rw-r--r--plugins/Actions/javascripts/actionsDataTable.js22
-rw-r--r--plugins/Actions/javascripts/rowactions.js31
-rw-r--r--plugins/Actions/tests/UI/ActionsDataTable_spec.js1
-rw-r--r--plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_auto_expand.png4
-rw-r--r--plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_column_sorted.png4
-rw-r--r--plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_exclude_low_population.png4
-rw-r--r--plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_flattened.png4
-rw-r--r--plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_initial.png4
-rw-r--r--plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_pageview_percentages.png4
-rw-r--r--plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_search.png4
-rw-r--r--plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_search_closed.png4
-rw-r--r--plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_search_visible.png4
-rw-r--r--plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_segmented_visitor_log_hover.png4
-rw-r--r--plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_subtables_loaded.png4
-rw-r--r--plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_unflattened.png4
-rw-r--r--plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_unique_pageview_percentages.png4
m---------plugins/Bandwidth0
-rw-r--r--plugins/CoreAdminHome/tests/UI/expected-screenshots/TrackingFailures_manage_with_failures_delete_all_confirmed.png4
-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
-rw-r--r--plugins/CorePluginsAdmin/tests/UI/expected-screenshots/TagManagerTeaser_admin_page_disable.png4
-rw-r--r--plugins/CoreVisualizations/JqplotDataGenerator.php142
-rw-r--r--plugins/CoreVisualizations/JqplotDataGenerator/Chart.php46
-rw-r--r--plugins/CoreVisualizations/JqplotDataGenerator/Evolution.php191
-rw-r--r--plugins/CoreVisualizations/Visualizations/Graph.php8
-rw-r--r--plugins/CoreVisualizations/Visualizations/HtmlTable.php25
-rw-r--r--plugins/CoreVisualizations/Visualizations/JqplotGraph/Bar.php7
-rw-r--r--plugins/CoreVisualizations/Visualizations/JqplotGraph/Evolution.php41
-rw-r--r--plugins/CoreVisualizations/Visualizations/JqplotGraph/Pie.php2
-rw-r--r--plugins/CoreVisualizations/Visualizations/Sparkline.php102
-rw-r--r--plugins/CoreVisualizations/Visualizations/Sparklines.php170
-rw-r--r--plugins/CoreVisualizations/Visualizations/Sparklines/Config.php110
-rw-r--r--plugins/CoreVisualizations/angularjs/single-metric-view/single-metric-view.component.js4
-rw-r--r--plugins/CoreVisualizations/javascripts/jqplot.js30
-rw-r--r--plugins/CoreVisualizations/javascripts/jqplotBarGraph.js17
-rw-r--r--plugins/CoreVisualizations/javascripts/jqplotEvolutionGraph.js54
-rw-r--r--plugins/CoreVisualizations/stylesheets/dataTableVisualizations.less14
-rw-r--r--plugins/CoreVisualizations/templates/_dataTableViz_htmlTable.twig25
-rw-r--r--plugins/CoreVisualizations/templates/_dataTableViz_htmlTable_comparisons.twig83
-rw-r--r--plugins/CoreVisualizations/templates/_dataTableViz_htmlTable_ratio.twig26
-rw-r--r--plugins/CoreVisualizations/templates/_dataTableViz_sparklines.twig31
-rw-r--r--plugins/CoreVisualizations/templates/macros.twig72
-rw-r--r--plugins/CoreVisualizations/tests/Integration/SparklinesConfigTest.php193
-rw-r--r--plugins/CoreVisualizations/tests/Unit/SparklinesConfigTest.php35
-rw-r--r--plugins/DBStats/DBStats.php8
-rw-r--r--plugins/DBStats/stylesheets/dbstats.less5
-rw-r--r--plugins/DBStats/tests/UI/expected-screenshots/DBStats_admin_page.png4
-rw-r--r--plugins/Dashboard/javascripts/dashboardWidget.js19
-rw-r--r--plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_create_new.png4
-rw-r--r--plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_removed.png4
-rw-r--r--plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_widget_preview.png4
-rw-r--r--plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_change_layout.png4
-rw-r--r--plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_copied.png4
-rw-r--r--plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_create_new.png4
-rw-r--r--plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_default_widget_selection_changed.png4
-rw-r--r--plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_loaded.png4
-rw-r--r--plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_loaded_token_auth.png4
-rw-r--r--plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_removed.png4
-rw-r--r--plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_rename.png4
-rw-r--r--plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_reset.png4
-rw-r--r--plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_rowevolution.png4
-rw-r--r--plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_segmented.png4
-rw-r--r--plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_widget_add_widget.png4
-rw-r--r--plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_widget_maximise.png4
-rw-r--r--plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_widget_move.png4
-rw-r--r--plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_widget_move_removed.png4
-rw-r--r--plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_widget_refresh.png4
-rw-r--r--plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_widget_unmaximise.png4
-rw-r--r--plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_widget_unminimise.png4
-rw-r--r--plugins/Diagnostics/tests/Integration/Commands/AnalyzeArchiveTableTest.php2
-rw-r--r--plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_schedrep_html_tables_and_graph__ScheduledReports.generateReport_week.original.html155
-rw-r--r--plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_schedrep_html_tables_only__ScheduledReports.generateReport_week.original.html148
-rw-r--r--plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_schedrep_in_csv__ScheduledReports.generateReport_week.original.csv4
-rw-r--r--plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_schedrep_in_pdf_tables_only__ScheduledReports.generateReport_week.original.pdfbin586308 -> 588882 bytes
-rw-r--r--plugins/ExamplePlugin/tests/System/expected/test___API.get_day.xml16
-rw-r--r--plugins/Goals/API.php15
-rw-r--r--plugins/ImageGraph/StaticGraph/Pie.php5
-rw-r--r--plugins/Insights/tests/UI/expected-screenshots/Insights_initial.png4
-rw-r--r--plugins/Intl/Intl.php14
-rw-r--r--plugins/IntranetMeasurable/tests/System/expected/test__intranet__API.get_day.xml16
-rw-r--r--plugins/IntranetMeasurable/tests/System/expected/test__notIntranet__API.get_day.xml16
-rw-r--r--plugins/Live/Live.php7
-rw-r--r--plugins/Live/javascripts/rowaction.js11
-rw-r--r--plugins/Live/stylesheets/live.less11
-rw-r--r--plugins/Login/tests/Integration/PasswordResetterTest.php2
-rw-r--r--plugins/Morpheus/images/compare.svg6
-rw-r--r--plugins/Morpheus/javascripts/ajaxHelper.js15
-rw-r--r--plugins/Morpheus/stylesheets/base/colors.less48
-rw-r--r--plugins/Morpheus/stylesheets/main.less58
-rw-r--r--plugins/Morpheus/stylesheets/ui/_charts.less179
-rw-r--r--plugins/Morpheus/stylesheets/uibase/_periodSelect.less17
-rw-r--r--plugins/Morpheus/templates/dashboard.twig2
-rw-r--r--plugins/MultiSites/MultiSites.php8
-rw-r--r--plugins/MultiSites/tests/UI/expected-screenshots/MultiSitesTest_all_websites.png4
-rw-r--r--plugins/MultiSites/tests/UI/expected-screenshots/MultiSitesTest_all_websites_changed_sort_order.png4
-rw-r--r--plugins/MultiSites/tests/UI/expected-screenshots/MultiSitesTest_all_websites_page_1.png4
-rw-r--r--plugins/MultiSites/tests/UI/expected-screenshots/MultiSitesTest_all_websites_range.png4
-rw-r--r--plugins/MultiSites/tests/UI/expected-screenshots/MultiSitesTest_all_websites_search.png4
-rw-r--r--plugins/Overlay/javascripts/rowaction.js13
-rw-r--r--plugins/Overlay/tests/UI/expected-screenshots/Overlay_row_evolution.png4
-rw-r--r--plugins/PrivacyManager/tests/UI/PrivacyManager_spec.js4
-rw-r--r--plugins/PrivacyManager/tests/UI/expected-screenshots/PrivacyManager_gdpr_tools_delete_visit_cancelled.png4
-rw-r--r--plugins/PrivacyManager/tests/UI/expected-screenshots/PrivacyManager_gdpr_tools_delete_visit_unconfirmed.png2
-rw-r--r--plugins/PrivacyManager/tests/UI/expected-screenshots/PrivacyManager_gdpr_tools_enrich_segment_by_ip.png4
-rw-r--r--plugins/PrivacyManager/tests/UI/expected-screenshots/PrivacyManager_gdpr_tools_uncheck_one_visit.png4
-rw-r--r--plugins/Referrers/API.php116
-rw-r--r--plugins/Referrers/Controller.php288
-rw-r--r--plugins/Referrers/Referrers.php32
-rw-r--r--plugins/Referrers/Reports/Get.php200
-rw-r--r--plugins/Referrers/lang/en.json9
-rw-r--r--plugins/Referrers/tests/System/expected/test_phpSerialized__Referrers.getReferrerType_year.originalbin2898 -> 2955 bytes
-rw-r--r--plugins/Referrers/tests/Unit/DataTable/Filter/UrlsFromWebsiteIdTest.php48
-rw-r--r--plugins/SegmentEditor/SegmentEditor.php23
-rw-r--r--plugins/SegmentEditor/javascripts/Segmentation.js53
-rw-r--r--plugins/SegmentEditor/lang/en.json5
-rw-r--r--plugins/SegmentEditor/stylesheets/segmentation.less51
-rw-r--r--plugins/SegmentEditor/templates/_segmentSelector.twig4
-rw-r--r--plugins/SegmentEditor/tests/UI/expected-screenshots/SegmentSelectorEditorTest_1_selector_open.png4
-rw-r--r--plugins/SegmentEditor/tests/UI/expected-screenshots/SegmentSelectorEditorTest_complex_segment.png4
-rw-r--r--plugins/SegmentEditor/tests/UI/expected-screenshots/SegmentSelectorEditorTest_deleted.png4
-rw-r--r--plugins/SegmentEditor/tests/UI/expected-screenshots/SegmentSelectorEditorTest_saved.png4
-rw-r--r--plugins/SegmentEditor/tests/UI/expected-screenshots/SegmentSelectorEditorTest_updated.png4
-rw-r--r--plugins/SegmentEditor/tests/UI/expected-screenshots/UnprocessedSegmentTest_custom_segment.png4
-rw-r--r--plugins/SegmentEditor/tests/UI/expected-screenshots/UnprocessedSegmentTest_unprocessed_segment.png4
-rw-r--r--plugins/Transitions/Transitions.php8
-rw-r--r--plugins/Transitions/javascripts/transitions.js40
-rw-r--r--plugins/TwoFactorAuth/tests/UI/expected-screenshots/TwoFactorAuth_logme_verified.png4
-rw-r--r--plugins/TwoFactorAuth/tests/UI/expected-screenshots/TwoFactorAuth_twofa_forced_step4.png4
-rw-r--r--plugins/UserCountryMap/UserCountryMap.php8
-rw-r--r--plugins/Widgetize/stylesheets/widgetize.less6
173 files changed, 6276 insertions, 1181 deletions
diff --git a/plugins/API/API.php b/plugins/API/API.php
index cfdbe92985..8d21ab11a8 100644
--- a/plugins/API/API.php
+++ b/plugins/API/API.php
@@ -476,7 +476,7 @@ class API extends \Piwik\Plugin\API
* @param bool|int $idDimension
* @return array
*/
- public function getRowEvolution($idSite, $period, $date, $apiModule, $apiAction, $label = false, $segment = false, $column = false, $language = false, $idGoal = false, $legendAppendMetric = true, $labelUseAbsoluteUrl = true, $idDimension = false)
+ public function getRowEvolution($idSite, $period, $date, $apiModule, $apiAction, $label = false, $segment = false, $column = false, $language = false, $idGoal = false, $legendAppendMetric = true, $labelUseAbsoluteUrl = true, $idDimension = false, $labelSeries = false)
{
// check if site exists
$idSite = (int) $idSite;
@@ -504,7 +504,7 @@ class API extends \Piwik\Plugin\API
$rowEvolution = new RowEvolution();
return $rowEvolution->getRowEvolution($idSite, $period, $date, $apiModule, $apiAction, $label, $segment, $column,
- $language, $apiParameters, $legendAppendMetric, $labelUseAbsoluteUrl);
+ $language, $apiParameters, $legendAppendMetric, $labelUseAbsoluteUrl, $labelSeries);
}
/**
@@ -526,6 +526,10 @@ class API extends \Piwik\Plugin\API
foreach ($urls as $url) {
$params = Request::getRequestArrayFromString($url . '&format=php&serialize=0');
+ if (!empty($params['method']) && $params['method'] === 'API.getBulkRequest') {
+ continue;
+ }
+
if (isset($params['urls']) && $params['urls'] == $urls) {
// by default 'urls' is added to $params as Request::getRequestArrayFromString adds all $_GET/$_POST
// default parameters
@@ -620,6 +624,37 @@ class API extends \Piwik\Plugin\API
return $values;
}
+ /**
+ * Returns category/subcategory pairs as "CategoryId.SubcategoryId" for whom comparison features should
+ * be disabled.
+ *
+ * @return string[]
+ */
+ public function getPagesComparisonsDisabledFor()
+ {
+ $pages = [];
+
+ /**
+ * If your plugin has pages where you'd like comparison features to be disabled, you can add them
+ * via this event. Add the pages as "CategoryId.SubcategoryId".
+ *
+ * **Example**
+ *
+ * ```
+ * public function getPagesComparisonsDisabledFor(&$pages)
+ * {
+ * $pages[] = "General_Visitors.MyPlugin_MySubcategory";
+ * $pages[] = "MyPlugin.myControllerAction"; // if your plugin defines a whole page you want comparison disabled for
+ * }
+ * ```
+ *
+ * @param string[] &$pages
+ */
+ Piwik::postEvent('API.getPagesComparisonsDisabledFor', [&$pages]);
+
+ return $pages;
+ }
+
private function findSegment($segmentName, $idSite)
{
$segmentsMetadata = $this->getSegmentsMetadata($idSite, $_hideImplementationData = false);
diff --git a/plugins/API/Filter/DataComparisonFilter.php b/plugins/API/Filter/DataComparisonFilter.php
new file mode 100644
index 0000000000..78872f3b41
--- /dev/null
+++ b/plugins/API/Filter/DataComparisonFilter.php
@@ -0,0 +1,590 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+namespace Piwik\Plugins\API\Filter;
+
+use Piwik\API\Request;
+use Piwik\Common;
+use Piwik\Config;
+use Piwik\DataTable;
+use Piwik\DataTable\DataTableInterface;
+use Piwik\DataTable\Simple;
+use Piwik\Http\BadRequestException;
+use Piwik\Metrics;
+use Piwik\Period;
+use Piwik\Period\Factory;
+use Piwik\Piwik;
+use Piwik\Plugin\Report;
+use Piwik\Plugins\API\Filter\DataComparisonFilter\ComparisonRowGenerator;
+use Piwik\Segment;
+use Piwik\Segment\SegmentExpression;
+use Piwik\Site;
+
+/**
+ * Handles the API portion of the data comparison feature.
+ *
+ * If the `compareSegments`/`comparePeriods`/`compareDates` parameters are supplied this class will fetch
+ * the data to compare with and store this data next to each row in the root report.
+ *
+ * Additionally, `..._change` columns will be added that show the percentage change for a column. This is only
+ * done when comparing periods, since segments are subsets of visits, so it doesn't make sense to consider
+ * differences between them as "changes".
+ *
+ * ### Comparing multiple periods
+ *
+ * It is possible to compare multiple periods with other multiple periods. For example, it
+ * is possible to compare period=day, date=2018-02-03,2018-02-13 with period=day, date=2018-03-01,2018-03-15.
+ * When done, this filter will compare the first period in the first set w/ the first period in the second set,
+ * etc. So in the previous example, 2018-02-03 will be compared with 2018-03-01, 2018-02-04 with 2018-03-02, etc.
+ *
+ * ### Metadata
+ *
+ * This filter adds the following metadata to DataTables:
+ *
+ * - 'compareSegments': The list of segments being compared. The first entry will always be the value of the `segment` query param.
+ * - 'comparePeriods': The list of labels of periods being compared. The first entry will always be the value of the
+ * `period` query param.
+ * - 'compareDates': The list of dates being compared. The first entry will always be the value of the `date` query param.
+ * - 'comparisonSeries': Prettified labels for every comparison series in order.
+ *
+ * This filter adds the following metadata to rows in the comparison DataTables:
+ *
+ * - 'compareSegment': The segment of the data for the comparison row.
+ * - 'compareSegmentPretty': The prettified label for the segment.
+ * - 'comparePeriod': The period label for the data in the comparison row. This does not have to match a value in the
+ * `comparePeriods` query parameter if comparing multiple periods.
+ * - 'compareDate': The date for the period in the data in the comparison row. This does not have to match a value in the
+ * `compareDates` query parameter if comparing multiple periods.
+ * - 'comparePeriodPretty': The prettified label for the period.
+ * - 'compareSeriesPretty': Prettified label for the comparison data represented by the row. This will match an entry
+ * in the DataTable's `comparisonSeries` metadata.
+ */
+class DataComparisonFilter
+{
+ /**
+ * @var array
+ */
+ private $request;
+
+ /**
+ * @var int
+ */
+ private $segmentCompareLimit;
+
+ /**
+ * @var int
+ */
+ private $periodCompareLimit;
+
+ /**
+ * @var string
+ */
+ private $segmentName;
+
+ /**
+ * @var string[]
+ */
+ private $compareSegments;
+
+ /**
+ * @var string[]
+ */
+ private $compareDates;
+
+ /**
+ * @var string[]
+ */
+ private $comparePeriods;
+
+ /**
+ * @var int[]
+ */
+ private $compareSegmentIndices;
+
+ /**
+ * @var int[]
+ */
+ private $comparePeriodIndices;
+
+ /**
+ * @var bool
+ */
+ private $isRequestMultiplePeriod;
+
+ /**
+ * @var ComparisonRowGenerator
+ */
+ private $comparisonRowGenerator;
+
+ /**
+ * @var array
+ */
+ private $columnMappings;
+
+ public function __construct($request, Report $report = null)
+ {
+ $this->request = $request;
+
+ $generalConfig = Config::getInstance()->General;
+ $this->segmentCompareLimit = (int) $generalConfig['data_comparison_segment_limit'];
+ $this->checkComparisonLimit($this->segmentCompareLimit, 'data_comparison_segment_limit');
+
+ $this->periodCompareLimit = (int) $generalConfig['data_comparison_period_limit'];
+ $this->checkComparisonLimit($this->periodCompareLimit, 'data_comparison_period_limit');
+
+ $this->segmentName = $this->getSegmentNameFromReport($report);
+
+ $this->compareSegments = self::getCompareSegments();
+ if (count($this->compareSegments) > $this->segmentCompareLimit + 1) {
+ throw new BadRequestException(Piwik::translate('General_MaximumNumberOfSegmentsComparedIs', [$this->segmentCompareLimit]));
+ }
+
+ $this->compareDates = self::getCompareDates($request);
+ $this->comparePeriods = self::getComparePeriods($request);
+
+ if (count($this->compareDates) !== count($this->comparePeriods)) {
+ throw new BadRequestException(Piwik::translate('General_CompareDatesParamMustMatchComparePeriods', ['compareDates', 'comparePeriods']));
+ }
+
+ if (count($this->compareDates) > $this->periodCompareLimit + 1) {
+ throw new BadRequestException(Piwik::translate('General_MaximumNumberOfPeriodsComparedIs', [$this->periodCompareLimit]));
+ }
+
+ if (count($this->compareSegments) == 1
+ && count($this->comparePeriods) == 1
+ ) {
+ return;
+ }
+
+ $this->checkMultiplePeriodCompare();
+
+ // map segments/periods to their indexes in the query parameter arrays for comparisonIdSubtable matching
+ $this->compareSegmentIndices = array_flip($this->compareSegments);
+ foreach ($this->comparePeriods as $index => $period) {
+ $date = $this->compareDates[$index];
+ $this->comparePeriodIndices[$period][$date] = $index;
+ }
+
+ $this->columnMappings = $this->getColumnMappings();
+ $this->comparisonRowGenerator = new ComparisonRowGenerator($this->segmentName, $this->isRequestMultiplePeriod(), $this->columnMappings);
+ }
+
+ public static function isCompareParamsPresent($request = null)
+ {
+ return !empty(Common::getRequestVar('compareSegments', [], $type = 'array', $request))
+ || !empty(Common::getRequestVar('comparePeriods', [], $type = 'array', $request))
+ || !empty(Common::getRequestVar('compareDates', [], $type = 'array', $request));
+ }
+
+ /**
+ * @param DataTable\DataTableInterface $table
+ */
+ public function compare(DataTable\DataTableInterface $table)
+ {
+ if (empty($this->compareSegments)
+ && empty($this->comparePeriods)
+ ) {
+ return;
+ }
+
+ $method = Common::getRequestVar('method', $default = null, $type = 'string', $this->request);
+ if ($method == 'Live') {
+ throw new \Exception("Data comparison is not enabled for the Live API.");
+ }
+
+ // optimization, if empty, single table, don't need to make extra queries
+ if ($table->getRowsCount() == 0) {
+ return;
+ }
+
+ $comparisonSeries = [];
+
+ // fetch data first
+ $reportsToCompare = self::getReportsToCompare($this->compareSegments, $this->comparePeriods, $this->compareDates);
+ foreach ($reportsToCompare as $index => $modifiedParams) {
+ $compareMetadata = $this->getMetadataFromModifiedParams($modifiedParams);
+ $comparisonSeries[] = $compareMetadata['compareSeriesPretty'];
+
+ $compareTable = $this->requestReport($method, $modifiedParams);
+ $this->comparisonRowGenerator->compareTables($compareMetadata, $table, $compareTable);
+ }
+
+ // calculate changes (including processed metric changes)
+ // NOTE: it doesn't make to sense to calculate these values for segments, since segments are subsets of all visits, where periods are
+ // time periods (so things can change from one to another).
+ if (count($this->comparePeriods) > 1) {
+ $this->compareChangePercents($table);
+ }
+
+ // format comparison table metrics
+ $this->formatComparisonTables($table);
+
+ // add comparison parameters as metadata
+ $table->filter(function (DataTable $singleTable) use ($comparisonSeries) {
+ if (isset($this->compareSegments)) {
+ $singleTable->setMetadata('compareSegments', $this->compareSegments);
+ }
+
+ if (isset($this->comparePeriods)) {
+ $singleTable->setMetadata('comparePeriods', $this->comparePeriods);
+ }
+
+ if (isset($this->compareDates)) {
+ $singleTable->setMetadata('compareDates', $this->compareDates);
+ }
+
+ $singleTable->setMetadata('comparisonSeries', $comparisonSeries);
+ });
+ }
+
+ public static function getReportsToCompare($compareSegments, $comparePeriods, $compareDates)
+ {
+ $permutations = [];
+
+ // NOTE: the order of these loops determines the order of the rows in the comparison table. ie,
+ // if we loop over dates then segments, then we'll see comparison rows change segments before changing
+ // periods. this is because this loop determines in what order we fetch report data.
+ foreach ($compareDates as $index => $date) {
+ foreach ($compareSegments as $segment) {
+ $period = $comparePeriods[$index];
+
+ $params = [];
+ $params['segment'] = $segment;
+
+ if (!empty($period)
+ && !empty($date)
+ ) {
+ $params['date'] = $date;
+ $params['period'] = $period;
+ }
+
+ $permutations[] = $params;
+ }
+ }
+
+ return $permutations;
+ }
+
+ /**
+ * @param $paramsToModify
+ * @return DataTable
+ */
+ private function requestReport($method, $paramsToModify)
+ {
+ $params = array_merge(
+ [
+ 'filter_limit' => -1,
+ 'filter_offset' => 0,
+ 'filter_sort_column' => '',
+ 'filter_truncate' => -1,
+ 'compare' => 0,
+ 'totals' => 1,
+ 'disable_queued_filters' => 1,
+ 'format_metrics' => 0,
+ 'label' => '',
+ 'flat' => Common::getRequestVar('flat', 0, 'int', $this->request),
+ ],
+ $paramsToModify
+ );
+
+ $params['keep_totals_row'] = Common::getRequestVar('keep_totals_row', 0, 'int', $this->request);
+ $params['keep_totals_row_label'] = Common::getRequestVar('keep_totals_row_label', '', 'string', $this->request);
+
+ if (!isset($params['idSite'])) {
+ $params['idSite'] = Common::getRequestVar('idSite', null, 'string', $this->request);
+ }
+ if (!isset($params['period'])) {
+ $params['period'] = Common::getRequestVar('period', null, 'string', $this->request);
+ }
+ if (!isset($params['date'])) {
+ $params['date'] = Common::getRequestVar('date', null, 'string', $this->request);
+ }
+
+ $idSubtable = Common::getRequestVar('idSubtable', 0, 'int', $this->request);
+ if ($idSubtable > 0) {
+ $comparisonIdSubtables = Common::getRequestVar('comparisonIdSubtables', $default = false, 'json', $this->request);
+ if (empty($comparisonIdSubtables)) {
+ throw new \Exception("Comparing segments/periods with subtables only works when the comparison idSubtables are supplied as well.");
+ }
+
+ $segmentIndex = empty($paramsToModify['segment']) ? 0 : $this->compareSegmentIndices[$paramsToModify['segment']];
+ $periodIndex = empty($paramsToModify['period']) ? 0 : $this->comparePeriodIndices[$paramsToModify['period']][$paramsToModify['date']];
+ $seriesIndex = self::getComparisonSeriesIndex(null, $periodIndex, $segmentIndex, count($this->compareSegments));
+
+ if (!isset($comparisonIdSubtables[$seriesIndex])) {
+ throw new \Exception("Invalid comparisonIdSubtables parameter: no idSubtable found for segment $segmentIndex and period $periodIndex");
+ }
+
+ $comparisonIdSubtable = $comparisonIdSubtables[$seriesIndex];
+ if ($comparisonIdSubtable === -1) { // no subtable in comparison row
+ $table = new DataTable();
+ $table->setMetadata('site', new Site($params['idSite']));
+ $table->setMetadata('period', Period\Factory::build($params['period'], $params['date']));
+ return $table;
+ }
+
+ $params['idSubtable'] = $comparisonIdSubtable;
+ }
+
+ return Request::processRequest($method, $params);
+ }
+
+ private function formatComparisonTables(DataTableInterface $tableOrMap)
+ {
+ $tableOrMap->filter(function (DataTable $table) {
+ $rows = $table->getRows();
+
+ $totalRow = $table->getTotalsRow();
+ if ($totalRow) {
+ $rows[] = $totalRow;
+ }
+
+ foreach ($rows as $row) {
+ /** @var DataTable $comparisonTable */
+ $comparisonTable = $row->getComparisons();
+ if (!empty($comparisonTable)) { // sanity check
+ $columnMappings = $this->columnMappings;
+ $comparisonTable->filter(DataTable\Filter\ReplaceColumnNames::class, [$columnMappings]);
+ }
+
+ $subtable = $row->getSubtable();
+ if ($subtable) {
+ $this->formatComparisonTables($subtable);
+ }
+ }
+ });
+ }
+
+ private function checkComparisonLimit($n, $configName)
+ {
+ if ($n <= 1) {
+ throw new \Exception("The [General] $configName INI config option must be greater than 1.");
+ }
+ }
+
+ private function getMetadataFromModifiedParams($modifiedParams)
+ {
+ $metadata = [];
+
+ $period = isset($modifiedParams['period']) ? $modifiedParams['period'] : reset($this->comparePeriods);
+ $date = isset($modifiedParams['date']) ? $modifiedParams['date'] : reset($this->compareDates);
+ $segment = isset($modifiedParams['segment']) ? $modifiedParams['segment'] : reset($this->compareSegments);
+
+ $metadata['compareSegment'] = $segment;
+
+ $segmentObj = new Segment($segment, []);
+ $metadata['compareSegmentPretty'] = $segmentObj->getStoredSegmentName(false);
+
+ $metadata['comparePeriod'] = $period;
+ $metadata['compareDate'] = $date;
+
+ $prettyPeriod = Factory::build($period, $date)->getLocalizedLongString();
+ $metadata['comparePeriodPretty'] = ucfirst($prettyPeriod);
+
+ $metadata['compareSeriesPretty'] = self::getComparisonSeriesLabelSuffixFromParts(
+ $metadata['comparePeriodPretty'], $metadata['compareSegmentPretty']);
+
+ return $metadata;
+ }
+
+ private static function getComparisonSeriesLabelSuffixFromParts($periodPretty, $segmentPretty)
+ {
+ $comparisonLabels = [
+ $periodPretty,
+ $segmentPretty,
+ ];
+ $comparisonLabels = array_filter($comparisonLabels);
+
+ return '(' . implode(') (', $comparisonLabels) . ')';
+ }
+
+ private function getSegmentNameFromReport(Report $report = null)
+ {
+ if (empty($report)) {
+ return null;
+ }
+
+ $dimension = $report->getDimension();
+ if (empty($dimension)) {
+ return null;
+ }
+
+ $segments = $dimension->getSegments();
+ if (empty($segments)) {
+ return null;
+ }
+
+ /** @var \Piwik\Plugin\Segment $segment */
+ $segment = reset($segments);
+ $segmentName = $segment->getSegment();
+ return $segmentName;
+ }
+
+ private function checkMultiplePeriodCompare()
+ {
+ if ($this->isRequestMultiplePeriod()) {
+ foreach ($this->comparePeriods as $index => $period) {
+ if (!Period::isMultiplePeriod($this->compareDates[$index], $period)) {
+ throw new \Exception("Cannot compare: original request is multiple period and cannot be compared with single periods.");
+ }
+ }
+ } else {
+ foreach ($this->comparePeriods as $index => $period) {
+ if (Period::isMultiplePeriod($this->compareDates[$index], $period)) {
+ throw new \Exception("Cannot compare: original request is single period and cannot be compared with multiple periods.");
+ }
+ }
+ }
+ }
+
+ private function isRequestMultiplePeriod()
+ {
+ if ($this->isRequestMultiplePeriod === null) {
+ $period = Common::getRequestVar('period', $default = null, 'string', $this->request);
+ $date = Common::getRequestVar('date', $default = null, 'string', $this->request);
+
+ $this->isRequestMultiplePeriod = Period::isMultiplePeriod($date, $period);
+ }
+ return $this->isRequestMultiplePeriod;
+ }
+
+ private function compareChangePercents(DataTableInterface $result)
+ {
+ $segmentCount = count($this->compareSegments);
+
+ $result->filter(function (DataTable $table) use ($segmentCount) {
+ $rows = $table->getRows();
+
+ $totalRow = $table->getTotalsRow();
+ if ($totalRow) {
+ $rows[] = $totalRow;
+ }
+
+ foreach ($rows as $row) {
+ $comparisons = $row->getComparisons();
+ if (empty($comparisons)) {
+ continue;
+ }
+
+ /** @var DataTable\Row[] $rows */
+ $rows = array_values($comparisons->getRows());
+ foreach ($rows as $index => $compareRow) {
+ if ($index < $segmentCount) {
+ continue; // do not calculate for first period
+ }
+
+ list($periodIndex, $segmentIndex) = self::getIndividualComparisonRowIndices($table, $index, $segmentCount);
+
+ $otherPeriodRowIndex = $segmentIndex;
+ $otherPeriodRow = $comparisons[$otherPeriodRowIndex];
+
+ foreach ($compareRow->getColumns() as $name => $value) {
+ $valueToCompare = $otherPeriodRow ? $otherPeriodRow->getColumn($name) : 0;
+ $valueToCompare = $valueToCompare ?: 0;
+
+ $change = DataTable\Filter\CalculateEvolutionFilter::calculate($value, $valueToCompare, $precision = 1, $appendPercent = false);
+
+ if ($change >= 0) {
+ $change = '+' . $change;
+ }
+ $change .= '%';
+
+ $compareRow->addColumn($name . '_change', $change);
+ }
+ }
+ }
+ });
+ }
+
+ /**
+ * Returns the period and segment indices for a given comparison index.
+ *
+ * @param DataTable|null $table
+ * @param $comparisonRowIndex
+ * @param null $segmentCount
+ * @return array
+ */
+ public static function getIndividualComparisonRowIndices($table, $comparisonRowIndex, $segmentCount = null)
+ {
+ $segmentCount = $segmentCount ?: count($table->getMetadata('compareSegments'));
+ $segmentIndex = $comparisonRowIndex % $segmentCount;
+ $periodIndex = floor($comparisonRowIndex / $segmentCount);
+ return [$periodIndex, $segmentIndex];
+ }
+
+ /**
+ * Returns the series index for a comparison based on the period and segment indices.
+ *
+ * @param DataTable|null $table
+ * @param int $periodIndex
+ * @param int $segmentIndex
+ * @param int|null $segmentCount
+ * @return int
+ */
+ public static function getComparisonSeriesIndex($table, $periodIndex, $segmentIndex, $segmentCount = null)
+ {
+ $segmentCount = $segmentCount ?: count($table->getMetadata('compareSegments'));
+ return $periodIndex * $segmentCount + $segmentIndex;
+ }
+
+ private static function getCompareSegments($request = null)
+ {
+ $segments = Common::getRequestVar('compareSegments', $default = [], $type = 'array', $request);
+ array_unshift($segments, Common::getRequestVar('segment', '', 'string', $request));
+ $segments = Common::unsanitizeInputValues($segments);
+ return $segments;
+ }
+
+ private static function getComparePeriods($request = null)
+ {
+ $periods = Common::getRequestVar('comparePeriods', $default = [], $type = 'array', $request);
+ array_unshift($periods, Common::getRequestVar('period', '', 'string', $request));
+ return array_values($periods);
+ }
+
+ private static function getCompareDates($request = null)
+ {
+ $dates = Common::getRequestVar('compareDates', $default = [], $type = 'array', $request);
+ array_unshift($dates, Common::getRequestVar('date', '', 'string', $request));
+ return array_values($dates);
+ }
+
+ /**
+ * Returns the pretty series label for a specific comparison based on the currently set comparison query parameters.
+ *
+ * @param int $labelSeriesIndex The index of the comparison. Comparison series order is determined by {@see self::getReportsToCompare()}.
+ */
+ public static function getPrettyComparisonLabelFromSeriesIndex($labelSeriesIndex)
+ {
+ $compareSegments = self::getCompareSegments();
+ $comparePeriods = self::getComparePeriods();
+ $compareDates = self::getCompareDates();
+
+ list($periodIndex, $segmentIndex) = self::getIndividualComparisonRowIndices(null, $labelSeriesIndex, count($compareSegments));
+
+ $segmentObj = new Segment($compareSegments[$segmentIndex], []);
+ $prettySegment = $segmentObj->getStoredSegmentName(false);
+
+ $prettyPeriod = Factory::build($comparePeriods[$periodIndex], $compareDates[$periodIndex])->getLocalizedLongString();
+ $prettyPeriod = ucfirst($prettyPeriod);
+
+ return self::getComparisonSeriesLabelSuffixFromParts($prettyPeriod, $prettySegment);
+ }
+
+ private function getColumnMappings()
+ {
+ $allMappings = Metrics::getMappingFromIdToName();
+
+ $mappings = [];
+ foreach ($allMappings as $index => $name) {
+ $mappings[$index] = $name;
+ $mappings[$index . '_change'] = $name . '_change';
+ }
+ return $mappings;
+ }
+} \ No newline at end of file
diff --git a/plugins/API/Filter/DataComparisonFilter/ComparisonRowGenerator.php b/plugins/API/Filter/DataComparisonFilter/ComparisonRowGenerator.php
new file mode 100644
index 0000000000..eaed08c13b
--- /dev/null
+++ b/plugins/API/Filter/DataComparisonFilter/ComparisonRowGenerator.php
@@ -0,0 +1,239 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+namespace Piwik\Plugins\API\Filter\DataComparisonFilter;
+
+use Piwik\DataTable;
+use Piwik\DataTable\DataTableInterface;
+use Piwik\DataTable\Simple;
+use Piwik\Metrics;
+use Piwik\Period;
+use Piwik\Segment;
+use Piwik\Segment\SegmentExpression;
+
+class ComparisonRowGenerator
+{
+ /**
+ * @var bool
+ */
+ private $isRequestMultiplePeriod;
+
+ /**
+ * @var string
+ */
+ private $segmentNameForReport;
+
+ /**
+ * @var array
+ */
+ private $columnMappings;
+
+ public function __construct($segmentNameForReport, $isRequestMultiplePeriod, $columnMappings)
+ {
+ $this->segmentNameForReport = $segmentNameForReport;
+ $this->isRequestMultiplePeriod = $isRequestMultiplePeriod;
+ $this->columnMappings = $columnMappings;
+ }
+
+ public function compareTables($compareMetadata, DataTableInterface $tables, DataTableInterface $compareTables = null)
+ {
+ if ($tables instanceof DataTable) {
+ $this->compareTable($compareMetadata, $tables, $compareTables, $compareTables);
+ } else if ($tables instanceof DataTable\Map) {
+ $childTablesArray = array_values($tables->getDataTables());
+ $compareTablesArray = isset($compareTables) ? array_values($compareTables->getDataTables()) : [];
+
+ $isDatePeriod = $tables->getKeyName() == 'date';
+
+ foreach ($childTablesArray as $index => $childTable) {
+ $compareChildTable = isset($compareTablesArray[$index]) ? $compareTablesArray[$index] : null;
+ $this->compareTables($compareMetadata, $childTable, $compareChildTable);
+ }
+
+ // in case one of the compared periods has more periods than the main one, we want to fill the result with empty datatables
+ // so the comparison data is still present. this allows us to see that data in an evolution report.
+ if ($isDatePeriod) {
+ $lastTable = end($childTablesArray);
+
+ /** @var Period $lastPeriod */
+ $lastPeriod = $lastTable->getMetadata('period');
+ $periodType = $lastPeriod->getLabel();
+
+ for ($i = count($childTablesArray); $i < count($compareTablesArray); ++$i) {
+ $periodChangeCount = $i - count($childTablesArray) + 1;
+ $newPeriod = Period\Factory::build($periodType, $lastPeriod->getDateStart()->addPeriod($periodChangeCount, $periodType));
+
+ // create an empty table for the main request
+ $newTable = new DataTable();
+ $newTable->setAllTableMetadata($lastTable->getAllTableMetadata());
+ $newTable->setMetadata('period', $newPeriod);
+
+ if ($newPeriod->getLabel() === 'week' || $newPeriod->getLabel() === 'range') {
+ $periodLabel = $newPeriod->getRangeString();
+ } else {
+ $periodLabel = $newPeriod->getPrettyString();
+ }
+
+ $tables->addTable($newTable, $periodLabel);
+
+ // compare with the empty table
+ $compareTable = $compareTablesArray[$i];
+ $this->compareTables($compareMetadata, $newTable, $compareTable);
+ }
+ }
+ } else {
+ throw new \Exception("Unexpected DataTable type: " . get_class($tables));
+ }
+ }
+
+ private function compareTable($compareMetadata, DataTable $table, DataTable $rootCompareTable = null, DataTable $compareTable = null)
+ {
+ // if there are no rows in the table because the metrics are 0, add one so we can still set comparison values
+ if ($table->getRowsCount() == 0) {
+ $table->addRow(new DataTable\Row());
+ }
+
+ foreach ($table->getRows() as $row) {
+ $label = $row->getColumn('label');
+
+ $compareRow = null;
+ if ($compareTable instanceof Simple) {
+ $compareRow = $compareTable->getFirstRow() ?: null;
+ } else if ($compareTable instanceof DataTable) {
+ $compareRow = $compareTable->getRowFromLabel($label) ?: null;
+ }
+
+ $this->compareRow($table, $compareMetadata, $row, $compareRow, $rootCompareTable);
+ }
+
+ $totalsRow = $table->getTotalsRow();
+ if (!empty($totalsRow)) {
+ $compareRow = $compareTable ? $compareTable->getTotalsRow() : null;
+ $this->compareRow($table, $compareMetadata, $totalsRow, $compareRow, $rootCompareTable);
+ }
+
+ if ($compareTable) {
+ $totals = $compareTable->getMetadata('totals');
+ if (!empty($totals)) {
+ $totals = $this->replaceIndexesInTotals($totals);
+ $comparisonTotalsEntry = array_merge($compareMetadata, [
+ 'totals' => $totals,
+ ]);
+
+ $allTotalsTables = $table->getMetadata('comparisonTotals');
+ $allTotalsTables[] = $comparisonTotalsEntry;
+ $table->setMetadata('comparisonTotals', $allTotalsTables);
+ }
+ }
+ }
+
+ private function compareRow(DataTable $table, $compareMetadata, DataTable\Row $row, DataTable\Row $compareRow = null, DataTable $rootTable = null)
+ {
+ $comparisonDataTable = $row->getComparisons();
+ if (empty($comparisonDataTable)) {
+ $comparisonDataTable = new DataTable();
+ $comparisonDataTable->setMetadata(DataTable::EXTRA_PROCESSED_METRICS_METADATA_NAME,
+ $table->getMetadata(DataTable::EXTRA_PROCESSED_METRICS_METADATA_NAME));
+ $row->setComparisons($comparisonDataTable);
+ }
+
+ $this->addIndividualChildPrettifiedMetadata($compareMetadata, $rootTable);
+
+ $columns = [];
+ if ($compareRow) {
+ foreach ($compareRow as $name => $value) {
+ if (!is_numeric($value)
+ || $name == 'label'
+ ) {
+ continue;
+ }
+
+ $columns[$name] = $value;
+ }
+ } else {
+ foreach ($row as $name => $value) {
+ if (!is_numeric($value)
+ || $name == 'label'
+ ) {
+ continue;
+ }
+
+ $columns[$name] = 0;
+ }
+ }
+
+ $newRow = new DataTable\Row([
+ DataTable\Row::COLUMNS => $columns,
+ DataTable\Row::METADATA => $compareMetadata,
+ ]);
+
+ // set subtable
+ $newRow->setMetadata('idsubdatatable', -1);
+ if ($compareRow) {
+ $subtableId = $compareRow->getMetadata('idsubdatatable_in_db') ?: $compareRow->getIdSubDataTable();
+ if ($subtableId) {
+ $newRow->setMetadata('idsubdatatable', $subtableId);
+ }
+ }
+
+ // add segment metadatas
+ if ($row->getMetadata('segment')) {
+ $newSegment = $row->getMetadata('segment');
+ if ($newRow->getMetadata('compareSegment')) {
+ $newSegment = Segment::combine($newRow->getMetadata('compareSegment'), SegmentExpression::AND_DELIMITER, $newSegment);
+ }
+ $newRow->setMetadata('segment', $newSegment);
+ } else if ($this->segmentNameForReport
+ && $row->getMetadata('segmentValue') !== false
+ ) {
+ $segmentValue = $row->getMetadata('segmentValue');
+ $newRow->setMetadata('segment', sprintf('%s==%s', $this->segmentNameForReport, urlencode($segmentValue)));
+ }
+
+ $comparisonDataTable->addRow($newRow);
+
+ // recurse on subtable if there
+ $subtable = $row->getSubtable();
+ if ($subtable
+ && $compareRow
+ ) {
+ $this->compareTable($compareMetadata, $subtable, $rootTable, $compareRow->getSubtable());
+ }
+ }
+
+ private function addIndividualChildPrettifiedMetadata(array &$metadata, DataTable $parentTable = null)
+ {
+ if ($parentTable
+ && $this->isRequestMultiplePeriod
+ ) {
+ /** @var Period $period */
+ $period = $parentTable->getMetadata('period');
+ if (empty($period)) {
+ return;
+ }
+
+ $prettyPeriod = $period->getLocalizedLongString();
+ $metadata['comparePeriodPretty'] = ucfirst($prettyPeriod);
+
+ $metadata['comparePeriod'] = $period->getLabel();
+ $metadata['compareDate'] = $period->getDateStart()->toString();
+ }
+ }
+
+ private function replaceIndexesInTotals($totals)
+ {
+ foreach ($totals as $index => $value) {
+ if (isset($this->columnMappings[$index])) {
+ $name = $this->columnMappings[$index];
+ $totals[$name] = $totals[$index];
+ unset($totals[$index]);
+ }
+ }
+ return $totals;
+ }
+} \ No newline at end of file
diff --git a/plugins/API/ProcessedReport.php b/plugins/API/ProcessedReport.php
index 79e01da316..6e963ec416 100644
--- a/plugins/API/ProcessedReport.php
+++ b/plugins/API/ProcessedReport.php
@@ -600,15 +600,17 @@ class ProcessedReport
* - extract row metadata to a separate Simple $rowsMetadata
*
* @param int $idSite enables monetary value formatting based on site currency
- * @param Simple $simpleDataTable
+ * @param DataTable $simpleDataTable
* @param array $metadataColumns
* @param boolean $hasDimension
* @param bool $returnRawMetrics If set to true, the original metrics will be returned
* @param bool|null $formatMetrics
* @return array DataTable $enhancedDataTable filtered metrics with human readable format & Simple $rowsMetadata
*/
- private function handleSimpleDataTable($idSite, $simpleDataTable, $metadataColumns, $hasDimension, $returnRawMetrics = false, $formatMetrics = null)
+ private function handleSimpleDataTable($idSite, $simpleDataTable, $metadataColumns, $hasDimension, $returnRawMetrics = false, $formatMetrics = null, $keepMetadata = false)
{
+ $comparisonColumns = $this->getComparisonColumns($metadataColumns);
+
// new DataTable to store metadata
$rowsMetadata = new DataTable();
@@ -634,7 +636,11 @@ class ProcessedReport
}
}
- $enhancedRow = new Row();
+ $c = [];
+ if ($keepMetadata) {
+ $c[Row::METADATA] = $row->getMetadata();
+ }
+ $enhancedRow = new Row($c);
$enhancedDataTable->addRow($enhancedRow);
foreach ($rowMetrics as $columnName => $columnValue) {
@@ -669,11 +675,23 @@ class ProcessedReport
}
}
+ /** @var DataTable $comparisons */
+ $comparisons = $row->getComparisons();
+
+ if (!empty($comparisons)
+ && $comparisons->getRowsCount() > 0
+ ) {
+ list($newComparisons, $ignore) = $this->handleSimpleDataTable($idSite, $comparisons, $comparisonColumns, true, $returnRawMetrics, $formatMetrics, $keepMetadata = true);
+ $enhancedRow->setComparisons($newComparisons);
+ }
+
// If report has a dimension, extract metadata into a distinct DataTable
if ($hasDimension) {
$rowMetadata = $row->getMetadata();
$idSubDataTable = $row->getIdSubDataTable();
+ unset($rowMetadata[Row::COMPARISONS_METADATA_NAME]);
+
// always add a metadata row - even if empty, so the number of rows and metadata are equal and can be matched directly
$metadataRow = new Row();
$rowsMetadata->addRow($metadataRow);
@@ -830,6 +848,10 @@ class ProcessedReport
return $value;
}
+ if (strpos($columnName, '_change') !== false) { // comparison change columns are formatted by DataComparisonFilter
+ return $value == '0' ? '+0%' : $value;
+ }
+
// Display time in human readable
if (strpos($columnName, 'time_generation') !== false) {
return $formatter->getPrettyTimeFromSeconds($value, true);
@@ -853,4 +875,13 @@ class ProcessedReport
return $value;
}
+
+ private function getComparisonColumns(array $metadataColumns)
+ {
+ $result = $metadataColumns;
+ foreach ($metadataColumns as $columnName => $columnTranslation) {
+ $result[$columnName . '_change'] = Piwik::translate('General_ChangeInX', lcfirst($columnName));
+ }
+ return $result;
+ }
}
diff --git a/plugins/API/RowEvolution.php b/plugins/API/RowEvolution.php
index 9025eb3436..24e695c88b 100644
--- a/plugins/API/RowEvolution.php
+++ b/plugins/API/RowEvolution.php
@@ -19,6 +19,7 @@ use Piwik\DataTable\Filter\SafeDecodeLabel;
use Piwik\DataTable\Row;
use Piwik\Period;
use Piwik\Piwik;
+use Piwik\Plugins\API\Filter\DataComparisonFilter;
use Piwik\Site;
use Piwik\Url;
@@ -36,7 +37,7 @@ class RowEvolution
'getPageUrl'
);
- public function getRowEvolution($idSite, $period, $date, $apiModule, $apiAction, $label = false, $segment = false, $column = false, $language = false, $apiParameters = array(), $legendAppendMetric = true, $labelUseAbsoluteUrl = true)
+ public function getRowEvolution($idSite, $period, $date, $apiModule, $apiAction, $label = false, $segment = false, $column = false, $language = false, $apiParameters = array(), $legendAppendMetric = true, $labelUseAbsoluteUrl = true, $labelSeries = '')
{
// validation of requested $period & $date
if ($period == 'range') {
@@ -49,7 +50,7 @@ class RowEvolution
}
$label = DataTablePostProcessor::unsanitizeLabelParameter($label);
- $labels = Piwik::getArrayFromApiParameter($label);
+ $labels = Piwik::getArrayFromApiParameter($label, $onlyUnique = empty($labelSeries));
$metadata = $this->getRowEvolutionMetaData($idSite, $period, $date, $apiModule, $apiAction, $language, $apiParameters);
@@ -72,7 +73,8 @@ class RowEvolution
$labels,
$column,
$legendAppendMetric,
- $labelUseAbsoluteUrl
+ $labelUseAbsoluteUrl,
+ $labelSeries
);
} else {
$data = $this->getSingleRowEvolution(
@@ -420,8 +422,13 @@ class RowEvolution
/** Get row evolution for a multiple labels */
private function getMultiRowEvolution(DataTable\Map $dataTable, $metadata, $apiModule, $apiAction, $labels, $column,
$legendAppendMetric = true,
- $labelUseAbsoluteUrl = true)
+ $labelUseAbsoluteUrl = true,
+ $labelSeries = '')
{
+ $labelSeries = explode(',', $labelSeries);
+ $labelSeries = array_filter($labelSeries, 'strlen');
+ $labelSeries = array_map('intval', $labelSeries);
+
if (!isset($metadata['metrics'][$column])) {
// invalid column => use the first one that's available
$metrics = array_keys($metadata['metrics']);
@@ -455,6 +462,11 @@ class RowEvolution
$cleanLabel = $this->cleanOriginalLabel($label);
$actualLabels[$labelIdx] = $cleanLabel;
}
+
+ if (isset($labelSeries[$labelIdx])) {
+ $labelSeriesIndex = $labelSeries[$labelIdx];
+ $actualLabels[$labelIdx] .= ' ' . DataComparisonFilter::getPrettyComparisonLabelFromSeriesIndex($labelSeriesIndex);
+ }
}
// convert rows to be array($column.'_'.$labelIdx => $value) as opposed to
diff --git a/plugins/API/tests/Integration/Filter/DataComparisonFilter/ComparisonRowGeneratorTest.php b/plugins/API/tests/Integration/Filter/DataComparisonFilter/ComparisonRowGeneratorTest.php
new file mode 100644
index 0000000000..1f9f3f17ec
--- /dev/null
+++ b/plugins/API/tests/Integration/Filter/DataComparisonFilter/ComparisonRowGeneratorTest.php
@@ -0,0 +1,613 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+namespace Piwik\Plugins\API\tests\Integration\Filter\DataComparisonFilter;
+
+use Piwik\DataTable;
+use Piwik\Period\Factory;
+use Piwik\Plugins\API\Filter\DataComparisonFilter\ComparisonRowGenerator;
+use Piwik\Plugins\SegmentEditor\API;
+use Piwik\Tests\Framework\TestCase\IntegrationTestCase;
+
+class ComparisonRowGeneratorTest extends IntegrationTestCase
+{
+ const TEST_SEGMENT = 'browserCode==ff';
+ const OTHER_SEGMENT = 'operatingSystemCode=WIN';
+
+ protected static function beforeTableDataCached()
+ {
+ parent::beforeTableDataCached();
+
+ API::getInstance()->add('test segment', self::TEST_SEGMENT);
+ }
+
+ public function test_compareTables_shouldCompareTwoDataTablesCorrectly()
+ {
+ $table1 = $this->makeTable([
+ ['label' => 'row1', 'nb_visits' => 5, 'nb_actions' => 10],
+ ['label' => 'row2', 'nb_visits' => 10, 'nb_actions' => 25],
+ ['label' => 'row3', 'nb_visits' => 20],
+ ['label' => 'row4', 'nb_actions' => 30],
+ ]);
+
+ $table2 = $this->makeTable([
+ ['label' => 'row1', 'nb_visits' => 10, 'nb_actions' => 5],
+ ['label' => 'row3', 'somethingelse' => 25],
+ ]);
+
+ $compareMetadata = [
+ 'compareSegment' => self::TEST_SEGMENT,
+ 'comparePeriod' => 'day',
+ 'compareDate' => '2012-03-04',
+ ];
+
+ $comparisonRowGenerator = new ComparisonRowGenerator('reportSegment', false, []);
+ $comparisonRowGenerator->compareTables($compareMetadata, $table1, $table2);
+
+ $xmlContent = $this->toXml($table1);
+
+ $expectedXml = <<<END
+<?xml version="1.0" encoding="utf-8" ?>
+<result>
+ <row>
+ <label>row1</label>
+ <nb_visits>5</nb_visits>
+ <nb_actions>10</nb_actions>
+ <comparisons>
+ <row>
+ <nb_visits>10</nb_visits>
+ <nb_actions>5</nb_actions>
+ <compareSegment>browserCode==ff</compareSegment>
+ <comparePeriod>day</comparePeriod>
+ <compareDate>2012-03-04</compareDate>
+ <idsubdatatable>-1</idsubdatatable>
+ </row>
+ </comparisons>
+ </row>
+ <row>
+ <label>row2</label>
+ <nb_visits>10</nb_visits>
+ <nb_actions>25</nb_actions>
+ <comparisons>
+ <row>
+ <nb_visits>0</nb_visits>
+ <nb_actions>0</nb_actions>
+ <compareSegment>browserCode==ff</compareSegment>
+ <comparePeriod>day</comparePeriod>
+ <compareDate>2012-03-04</compareDate>
+ <idsubdatatable>-1</idsubdatatable>
+ </row>
+ </comparisons>
+ </row>
+ <row>
+ <label>row3</label>
+ <nb_visits>20</nb_visits>
+ <comparisons>
+ <row>
+ <somethingelse>25</somethingelse>
+ <compareSegment>browserCode==ff</compareSegment>
+ <comparePeriod>day</comparePeriod>
+ <compareDate>2012-03-04</compareDate>
+ <idsubdatatable>-1</idsubdatatable>
+ </row>
+ </comparisons>
+ </row>
+ <row>
+ <label>row4</label>
+ <nb_actions>30</nb_actions>
+ <comparisons>
+ <row>
+ <nb_actions>0</nb_actions>
+ <compareSegment>browserCode==ff</compareSegment>
+ <comparePeriod>day</comparePeriod>
+ <compareDate>2012-03-04</compareDate>
+ <idsubdatatable>-1</idsubdatatable>
+ </row>
+ </comparisons>
+ </row>
+</result>
+END;
+ $this->assertEquals($expectedXml, $xmlContent);
+ }
+
+ public function test_compareTables_shouldUseFirstTableRowsForComparisons()
+ {
+ $table1 = $this->makeTable([
+ ['label' => 'row1', 'nb_visits' => 10, 'nb_actions' => 5],
+ ['label' => 'row3', 'somethingelse' => 25],
+ ]);
+
+ $table2 = $this->makeTable([
+ ['label' => 'row1', 'nb_visits' => 5, 'nb_actions' => 10],
+ ['label' => 'row2', 'nb_visits' => 10, 'nb_actions' => 25],
+ ['label' => 'row3', 'nb_visits' => 20],
+ ['label' => 'row4', 'nb_actions' => 30],
+ ]);
+
+ $compareMetadata = [
+ 'compareSegment' => self::TEST_SEGMENT,
+ 'comparePeriod' => 'day',
+ 'compareDate' => '2012-03-04',
+ ];
+
+ $comparisonRowGenerator = new ComparisonRowGenerator('reportSegment', false, []);
+ $comparisonRowGenerator->compareTables($compareMetadata, $table1, $table2);
+
+ $xmlContent = $this->toXml($table1);
+
+ $expectedXml = <<<END
+<?xml version="1.0" encoding="utf-8" ?>
+<result>
+ <row>
+ <label>row1</label>
+ <nb_visits>10</nb_visits>
+ <nb_actions>5</nb_actions>
+ <comparisons>
+ <row>
+ <nb_visits>5</nb_visits>
+ <nb_actions>10</nb_actions>
+ <compareSegment>browserCode==ff</compareSegment>
+ <comparePeriod>day</comparePeriod>
+ <compareDate>2012-03-04</compareDate>
+ <idsubdatatable>-1</idsubdatatable>
+ </row>
+ </comparisons>
+ </row>
+ <row>
+ <label>row3</label>
+ <somethingelse>25</somethingelse>
+ <comparisons>
+ <row>
+ <nb_visits>20</nb_visits>
+ <compareSegment>browserCode==ff</compareSegment>
+ <comparePeriod>day</comparePeriod>
+ <compareDate>2012-03-04</compareDate>
+ <idsubdatatable>-1</idsubdatatable>
+ </row>
+ </comparisons>
+ </row>
+</result>
+END;
+ $this->assertEquals($expectedXml, $xmlContent);
+ }
+
+ public function test_compareTables_shouldCompareTwoDataTableMapsCorrectly()
+ {
+ $tableSet1 = $this->makeTableMap([
+ '2012-01-01' => [
+ ['label' => 'row1', 'nb_visits' => 10, 'nb_actions' => 15],
+ ['label' => 'row2', 'nb_visits' => 15, 'nb_actions' => 15],
+ ['label' => 'row3', 'nb_visits' => 20, 'nb_actions' => 10],
+ ],
+ '2012-02-01' => [
+ ['label' => 'row2', 'nb_visits' => 25, 'nb_actions' => 25],
+ ],
+ '2012-03-01' => [
+ ['label' => 'row3', 'nb_visits' => 20, 'nb_actions' => 10],
+ ['label' => 'row4', 'nb_visits' => 40, 'nb_actions' => 50],
+ ],
+ ]);
+
+ $tableSet2 = $this->makeTableMap([
+ '2012-01-01' => [
+ ['label' => 'row1', 'nb_visits' => 10, 'nb_actions' => 15],
+ ],
+ '2012-02-01' => [
+ ['label' => 'row2', 'nb_visits' => 15, 'nb_actions' => 15],
+ ['label' => 'row3', 'nb_visits' => 20, 'nb_actions' => 10],
+ ],
+ '2012-03-01' => [
+ ['label' => 'row2', 'nb_visits' => 25, 'nb_actions' => 25],
+ ['label' => 'row3', 'nb_visits' => 20, 'nb_actions' => 10],
+ ['label' => 'row4', 'nb_visits' => 40, 'nb_actions' => 50],
+ ],
+ ]);
+
+ $compareMetadata = [
+ 'compareSegment' => self::OTHER_SEGMENT,
+ 'comparePeriod' => 'month',
+ 'compareDate' => '2012-01-01,2012-03-01',
+ ];
+
+ $comparisonRowGenerator = new ComparisonRowGenerator('reportSegment', false, []);
+ $comparisonRowGenerator->compareTables($compareMetadata, $tableSet1, $tableSet2);
+
+ $xmlContent = $this->toXml($tableSet1);
+
+ $expectedXml = <<<END
+<?xml version="1.0" encoding="utf-8" ?>
+<results>
+ <result date="2012-01">
+ <row>
+ <label>row1</label>
+ <nb_visits>10</nb_visits>
+ <nb_actions>15</nb_actions>
+ <comparisons>
+ <row>
+ <nb_visits>10</nb_visits>
+ <nb_actions>15</nb_actions>
+ <compareSegment>operatingSystemCode=WIN</compareSegment>
+ <comparePeriod>month</comparePeriod>
+ <compareDate>2012-01-01,2012-03-01</compareDate>
+ <idsubdatatable>-1</idsubdatatable>
+ </row>
+ </comparisons>
+ </row>
+ <row>
+ <label>row2</label>
+ <nb_visits>15</nb_visits>
+ <nb_actions>15</nb_actions>
+ <comparisons>
+ <row>
+ <nb_visits>0</nb_visits>
+ <nb_actions>0</nb_actions>
+ <compareSegment>operatingSystemCode=WIN</compareSegment>
+ <comparePeriod>month</comparePeriod>
+ <compareDate>2012-01-01,2012-03-01</compareDate>
+ <idsubdatatable>-1</idsubdatatable>
+ </row>
+ </comparisons>
+ </row>
+ <row>
+ <label>row3</label>
+ <nb_visits>20</nb_visits>
+ <nb_actions>10</nb_actions>
+ <comparisons>
+ <row>
+ <nb_visits>0</nb_visits>
+ <nb_actions>0</nb_actions>
+ <compareSegment>operatingSystemCode=WIN</compareSegment>
+ <comparePeriod>month</comparePeriod>
+ <compareDate>2012-01-01,2012-03-01</compareDate>
+ <idsubdatatable>-1</idsubdatatable>
+ </row>
+ </comparisons>
+ </row>
+ </result>
+ <result date="2012-02">
+ <row>
+ <label>row2</label>
+ <nb_visits>25</nb_visits>
+ <nb_actions>25</nb_actions>
+ <comparisons>
+ <row>
+ <nb_visits>15</nb_visits>
+ <nb_actions>15</nb_actions>
+ <compareSegment>operatingSystemCode=WIN</compareSegment>
+ <comparePeriod>month</comparePeriod>
+ <compareDate>2012-01-01,2012-03-01</compareDate>
+ <idsubdatatable>-1</idsubdatatable>
+ </row>
+ </comparisons>
+ </row>
+ </result>
+ <result date="2012-03">
+ <row>
+ <label>row3</label>
+ <nb_visits>20</nb_visits>
+ <nb_actions>10</nb_actions>
+ <comparisons>
+ <row>
+ <nb_visits>20</nb_visits>
+ <nb_actions>10</nb_actions>
+ <compareSegment>operatingSystemCode=WIN</compareSegment>
+ <comparePeriod>month</comparePeriod>
+ <compareDate>2012-01-01,2012-03-01</compareDate>
+ <idsubdatatable>-1</idsubdatatable>
+ </row>
+ </comparisons>
+ </row>
+ <row>
+ <label>row4</label>
+ <nb_visits>40</nb_visits>
+ <nb_actions>50</nb_actions>
+ <comparisons>
+ <row>
+ <nb_visits>40</nb_visits>
+ <nb_actions>50</nb_actions>
+ <compareSegment>operatingSystemCode=WIN</compareSegment>
+ <comparePeriod>month</comparePeriod>
+ <compareDate>2012-01-01,2012-03-01</compareDate>
+ <idsubdatatable>-1</idsubdatatable>
+ </row>
+ </comparisons>
+ </row>
+ </result>
+</results>
+END;
+ $this->assertEquals($expectedXml, $xmlContent);
+ }
+
+ public function test_compareTables_shouldCompareTwoDataTaleMapsOfDifferentLengthsCorrectly_whenFirstIsLonger()
+ {
+ $tableSet1 = $this->makeTableMap([
+ '2012-01-01' => [
+ ['label' => 'row1', 'nb_visits' => 10, 'nb_actions' => 15],
+ ['label' => 'row2', 'nb_visits' => 15, 'nb_actions' => 15],
+ ['label' => 'row3', 'nb_visits' => 20, 'nb_actions' => 10],
+ ],
+ '2012-02-01' => [
+ ['label' => 'row2', 'nb_visits' => 25, 'nb_actions' => 25],
+ ],
+ '2012-03-01' => [
+ ['label' => 'row3', 'nb_visits' => 20, 'nb_actions' => 10],
+ ['label' => 'row4', 'nb_visits' => 40, 'nb_actions' => 50],
+ ],
+ ]);
+
+ $tableSet2 = $this->makeTableMap([
+ '2012-01-01' => [
+ ['label' => 'row1', 'nb_visits' => 10, 'nb_actions' => 15],
+ ],
+ '2012-02-01' => [
+ // empty
+ ],
+ ]);
+
+ $compareMetadata = [
+ 'compareSegment' => self::OTHER_SEGMENT,
+ 'comparePeriod' => 'month',
+ 'compareDate' => '2012-01-01,2012-03-01',
+ ];
+
+ $comparisonRowGenerator = new ComparisonRowGenerator('reportSegment', false, []);
+ $comparisonRowGenerator->compareTables($compareMetadata, $tableSet1, $tableSet2);
+
+ $xmlContent = $this->toXml($tableSet1);
+
+ $expectedXml = <<<END
+<?xml version="1.0" encoding="utf-8" ?>
+<results>
+ <result date="2012-01">
+ <row>
+ <label>row1</label>
+ <nb_visits>10</nb_visits>
+ <nb_actions>15</nb_actions>
+ <comparisons>
+ <row>
+ <nb_visits>10</nb_visits>
+ <nb_actions>15</nb_actions>
+ <compareSegment>operatingSystemCode=WIN</compareSegment>
+ <comparePeriod>month</comparePeriod>
+ <compareDate>2012-01-01,2012-03-01</compareDate>
+ <idsubdatatable>-1</idsubdatatable>
+ </row>
+ </comparisons>
+ </row>
+ <row>
+ <label>row2</label>
+ <nb_visits>15</nb_visits>
+ <nb_actions>15</nb_actions>
+ <comparisons>
+ <row>
+ <nb_visits>0</nb_visits>
+ <nb_actions>0</nb_actions>
+ <compareSegment>operatingSystemCode=WIN</compareSegment>
+ <comparePeriod>month</comparePeriod>
+ <compareDate>2012-01-01,2012-03-01</compareDate>
+ <idsubdatatable>-1</idsubdatatable>
+ </row>
+ </comparisons>
+ </row>
+ <row>
+ <label>row3</label>
+ <nb_visits>20</nb_visits>
+ <nb_actions>10</nb_actions>
+ <comparisons>
+ <row>
+ <nb_visits>0</nb_visits>
+ <nb_actions>0</nb_actions>
+ <compareSegment>operatingSystemCode=WIN</compareSegment>
+ <comparePeriod>month</comparePeriod>
+ <compareDate>2012-01-01,2012-03-01</compareDate>
+ <idsubdatatable>-1</idsubdatatable>
+ </row>
+ </comparisons>
+ </row>
+ </result>
+ <result date="2012-02">
+ <row>
+ <label>row2</label>
+ <nb_visits>25</nb_visits>
+ <nb_actions>25</nb_actions>
+ <comparisons>
+ <row>
+ <nb_visits>0</nb_visits>
+ <nb_actions>0</nb_actions>
+ <compareSegment>operatingSystemCode=WIN</compareSegment>
+ <comparePeriod>month</comparePeriod>
+ <compareDate>2012-01-01,2012-03-01</compareDate>
+ <idsubdatatable>-1</idsubdatatable>
+ </row>
+ </comparisons>
+ </row>
+ </result>
+ <result date="2012-03">
+ <row>
+ <label>row3</label>
+ <nb_visits>20</nb_visits>
+ <nb_actions>10</nb_actions>
+ <comparisons>
+ <row>
+ <nb_visits>0</nb_visits>
+ <nb_actions>0</nb_actions>
+ <compareSegment>operatingSystemCode=WIN</compareSegment>
+ <comparePeriod>month</comparePeriod>
+ <compareDate>2012-01-01,2012-03-01</compareDate>
+ <idsubdatatable>-1</idsubdatatable>
+ </row>
+ </comparisons>
+ </row>
+ <row>
+ <label>row4</label>
+ <nb_visits>40</nb_visits>
+ <nb_actions>50</nb_actions>
+ <comparisons>
+ <row>
+ <nb_visits>0</nb_visits>
+ <nb_actions>0</nb_actions>
+ <compareSegment>operatingSystemCode=WIN</compareSegment>
+ <comparePeriod>month</comparePeriod>
+ <compareDate>2012-01-01,2012-03-01</compareDate>
+ <idsubdatatable>-1</idsubdatatable>
+ </row>
+ </comparisons>
+ </row>
+ </result>
+</results>
+END;
+ $this->assertEquals($expectedXml, $xmlContent);
+ }
+
+ public function test_compareTables_shouldCompareTwoDataTaleMapsOfDifferentLengthsCorrectly_whenFirstIsShorter()
+ {
+ $tableSet1 = $this->makeTableMap([
+ '2012-01-01' => [
+ ['label' => 'row1', 'nb_visits' => 10, 'nb_actions' => 15],
+ ['label' => 'row2', 'nb_visits' => 15, 'nb_actions' => 15],
+ ['label' => 'row3', 'nb_visits' => 20, 'nb_actions' => 10],
+ ],
+ '2012-02-01' => [
+ // empty
+ ],
+ ]);
+
+ $tableSet2 = $this->makeTableMap([
+ '2012-01-01' => [
+ ['label' => 'row1', 'nb_visits' => 10, 'nb_actions' => 15],
+ ],
+ '2012-02-01' => [
+ ['label' => 'row2', 'nb_visits' => 15, 'nb_actions' => 15],
+ ['label' => 'row3', 'nb_visits' => 20, 'nb_actions' => 10],
+ ],
+ '2012-03-01' => [
+ ['label' => 'row2', 'nb_visits' => 25, 'nb_actions' => 25],
+ ['label' => 'row3', 'nb_visits' => 20, 'nb_actions' => 10],
+ ['label' => 'row4', 'nb_visits' => 40, 'nb_actions' => 50],
+ ],
+ ]);
+
+ $compareMetadata = [
+ 'compareSegment' => self::OTHER_SEGMENT,
+ 'comparePeriod' => 'month',
+ 'compareDate' => '2012-01-01,2012-03-01',
+ ];
+
+ $comparisonRowGenerator = new ComparisonRowGenerator('reportSegment', false, []);
+ $comparisonRowGenerator->compareTables($compareMetadata, $tableSet1, $tableSet2);
+
+ $xmlContent = $this->toXml($tableSet1);
+
+ $expectedXml = <<<END
+<?xml version="1.0" encoding="utf-8" ?>
+<results>
+ <result date="2012-01">
+ <row>
+ <label>row1</label>
+ <nb_visits>10</nb_visits>
+ <nb_actions>15</nb_actions>
+ <comparisons>
+ <row>
+ <nb_visits>10</nb_visits>
+ <nb_actions>15</nb_actions>
+ <compareSegment>operatingSystemCode=WIN</compareSegment>
+ <comparePeriod>month</comparePeriod>
+ <compareDate>2012-01-01,2012-03-01</compareDate>
+ <idsubdatatable>-1</idsubdatatable>
+ </row>
+ </comparisons>
+ </row>
+ <row>
+ <label>row2</label>
+ <nb_visits>15</nb_visits>
+ <nb_actions>15</nb_actions>
+ <comparisons>
+ <row>
+ <nb_visits>0</nb_visits>
+ <nb_actions>0</nb_actions>
+ <compareSegment>operatingSystemCode=WIN</compareSegment>
+ <comparePeriod>month</comparePeriod>
+ <compareDate>2012-01-01,2012-03-01</compareDate>
+ <idsubdatatable>-1</idsubdatatable>
+ </row>
+ </comparisons>
+ </row>
+ <row>
+ <label>row3</label>
+ <nb_visits>20</nb_visits>
+ <nb_actions>10</nb_actions>
+ <comparisons>
+ <row>
+ <nb_visits>0</nb_visits>
+ <nb_actions>0</nb_actions>
+ <compareSegment>operatingSystemCode=WIN</compareSegment>
+ <comparePeriod>month</comparePeriod>
+ <compareDate>2012-01-01,2012-03-01</compareDate>
+ <idsubdatatable>-1</idsubdatatable>
+ </row>
+ </comparisons>
+ </row>
+ </result>
+ <result date="2012-02">
+ <row>
+ <comparisons>
+ <row>
+ <compareSegment>operatingSystemCode=WIN</compareSegment>
+ <comparePeriod>month</comparePeriod>
+ <compareDate>2012-01-01,2012-03-01</compareDate>
+ <idsubdatatable>-1</idsubdatatable>
+ </row>
+ </comparisons>
+ </row>
+ </result>
+ <result date="2012-03">
+ <row>
+ <comparisons>
+ <row>
+ <compareSegment>operatingSystemCode=WIN</compareSegment>
+ <comparePeriod>month</comparePeriod>
+ <compareDate>2012-01-01,2012-03-01</compareDate>
+ <idsubdatatable>-1</idsubdatatable>
+ </row>
+ </comparisons>
+ </row>
+ </result>
+</results>
+END;
+ $this->assertEquals($expectedXml, $xmlContent);
+ }
+
+ private function makeTable(array $rows)
+ {
+ $table = new DataTable();
+ $table->addRowsFromSimpleArray($rows);
+ return $table;
+ }
+
+ private function makeTableMap(array $tableRows)
+ {
+ $result = new DataTable\Map();
+ $result->setKeyName('date');
+ foreach ($tableRows as $label => $rows) {
+ $period = Factory::build('month', $label);
+
+ $table = $this->makeTable($rows);
+ $table->setMetadata('period', $period);
+
+ $result->addTable($table, $period->getPrettyString());
+ }
+ return $result;
+ }
+
+ private function toXml(DataTable\DataTableInterface $table)
+ {
+ $renderer = new DataTable\Renderer\Xml();
+ $renderer->setTable($table);
+ return $renderer->render();
+ }
+} \ No newline at end of file
diff --git a/plugins/API/tests/Unit/XmlRendererTest.php b/plugins/API/tests/Unit/XmlRendererTest.php
index 833d303aac..a026f7778a 100644
--- a/plugins/API/tests/Unit/XmlRendererTest.php
+++ b/plugins/API/tests/Unit/XmlRendererTest.php
@@ -28,6 +28,11 @@ class XmlRendererTest extends \PHPUnit_Framework_TestCase
DataTable\Manager::getInstance()->deleteAll();
}
+ public function tearDown()
+ {
+ DataTable\Manager::getInstance()->deleteAll();
+ }
+
public function test_renderSuccess_shouldIncludeMessage()
{
$response = $this->builder->renderSuccess('ok');
diff --git a/plugins/Actions/DataTable/Filter/Actions.php b/plugins/Actions/DataTable/Filter/Actions.php
index c45716a98b..1295cae57e 100644
--- a/plugins/Actions/DataTable/Filter/Actions.php
+++ b/plugins/Actions/DataTable/Filter/Actions.php
@@ -54,29 +54,31 @@ class Actions extends BaseFilter
}
foreach ($dataTable->getRows() as $row) {
- $url = $row->getMetadata('url');
- $pageTitlePath = $row->getMetadata('page_title_path');
- $folderUrlStart = $row->getMetadata('folder_url_start');
- $label = $row->getColumn('label');
- if ($url) {
- $row->setMetadata('segmentValue', urldecode($url));
- } else if ($folderUrlStart) {
- $row->setMetadata('segment', 'pageUrl=^' . urlencode(urlencode($folderUrlStart)));
- } else if ($pageTitlePath) {
- if ($row->getIdSubDataTable()) {
- $row->setMetadata('segment', 'pageTitle=^' . urlencode(urlencode(trim(urldecode($pageTitlePath)))));
- } else {
- $row->setMetadata('segmentValue', trim(urldecode($pageTitlePath)));
- }
- } else if ($isPageTitleType && !in_array($label, [DataTable::LABEL_SUMMARY_ROW])) {
- // for older data w/o page_title_path metadata
- if ($row->getIdSubDataTable()) {
- $row->setMetadata('segment', 'pageTitle=^' . urlencode(urlencode(trim(urldecode($label)))));
- } else {
- $row->setMetadata('segmentValue', trim(urldecode($label)));
+ if (!$row->isSummaryRow()) {
+ $url = $row->getMetadata('url');
+ $pageTitlePath = $row->getMetadata('page_title_path');
+ $folderUrlStart = $row->getMetadata('folder_url_start');
+ $label = $row->getColumn('label');
+ if ($url) {
+ $row->setMetadata('segmentValue', urldecode($url));
+ } else if ($folderUrlStart) {
+ $row->setMetadata('segment', 'pageUrl=^' . urlencode(urlencode($folderUrlStart)));
+ } else if ($pageTitlePath) {
+ if ($row->getIdSubDataTable()) {
+ $row->setMetadata('segment', 'pageTitle=^' . urlencode(urlencode(trim(urldecode($pageTitlePath)))));
+ } else {
+ $row->setMetadata('segmentValue', trim(urldecode($pageTitlePath)));
+ }
+ } else if ($isPageTitleType && !in_array($label, [DataTable::LABEL_SUMMARY_ROW])) {
+ // for older data w/o page_title_path metadata
+ if ($row->getIdSubDataTable()) {
+ $row->setMetadata('segment', 'pageTitle=^' . urlencode(urlencode(trim(urldecode($label)))));
+ } else {
+ $row->setMetadata('segmentValue', trim(urldecode($label)));
+ }
+ } else if ($this->actionType == Action::TYPE_PAGE_URL && $urlPrefix) { // folder for older data w/ no folder URL metadata
+ $row->setMetadata('segment', 'pageUrl=^' . urlencode(urlencode($urlPrefix . '/' . $label)));
}
- } else if ($this->actionType == Action::TYPE_PAGE_URL && $urlPrefix) { // folder for older data w/ no folder URL metadata
- $row->setMetadata('segment', 'pageUrl=^' . urlencode(urlencode($urlPrefix . '/' . $label)));
}
// remove the default action name 'index' in the end of flattened urls and prepend $actionDelimiter
diff --git a/plugins/Actions/javascripts/actionsDataTable.js b/plugins/Actions/javascripts/actionsDataTable.js
index eb5a14ed01..f8de151d36 100644
--- a/plugins/Actions/javascripts/actionsDataTable.js
+++ b/plugins/Actions/javascripts/actionsDataTable.js
@@ -137,7 +137,8 @@
var imagePlusMinusHeight = 12;
$('td:first-child', rowsWithSubtables)
.each(function () {
- $(this).prepend('<img width="' + imagePlusMinusWidth + '" height="' + imagePlusMinusHeight + '" class="plusMinus" src="" />');
+ $('<img width="' + imagePlusMinusWidth + '" height="' + imagePlusMinusHeight + '" class="plusMinus" src="" />').insertBefore($(this).children('.label'));
+
if (self.param.filter_pattern_recursive) {
setImageMinus(this);
}
@@ -171,10 +172,7 @@
},
addOddAndEvenClasses: function(domElem) {
- // Add some styles on the cells
- // label (first column of a data row) or not
- $("tr:not(.hidden) td:first-child", domElem).addClass('label');
- $("tr:not(.hidden) td", domElem).slice(1).addClass('column');
+ // empty
},
handleRowActions: function (domElem, rows) {
@@ -190,7 +188,12 @@
var divIdToReplaceWithSubTable = 'subDataTable_' + idSubTable;
- var NextStyle = $(domElem).next().attr('class');
+ var $insertAfter = $(domElem).nextUntil(':not(.comparePeriod):not(.comparisonRow)').last();
+ if (!$insertAfter.length) {
+ $insertAfter = $(domElem);
+ }
+
+ var NextStyle = $insertAfter.next().attr('class');
var CurrentStyle = $(domElem).attr('class');
var currentRowLevel = getLevelFromClass(CurrentStyle);
@@ -205,7 +208,7 @@
self.disabledRowDom = $(domElem);
var numberOfColumns = $(domElem).children().length;
- $(domElem).after('\
+ $insertAfter.after('\
<tr id="' + divIdToReplaceWithSubTable + '" class="cellSubDataTable">\
<td colspan="' + numberOfColumns + '">\
<span class="loadingPiwik" style="display:inline"><img src="plugins/Morpheus/images/loading-blue.gif" /> Loading...</span>\
@@ -225,10 +228,13 @@
self.param.idSubtable = idSubTable;
self.param.action = self.props.subtable_controller_action;
+ var extraParams = {};
+ extraParams.comparisonIdSubtables = self.getComparisonIdSubtables($(domElem));
+
self.reloadAjaxDataTable(false, function (resp) {
self.actionsSubDataTableLoaded(resp, idSubTable);
self.repositionRowActions($(domElem));
- });
+ }, extraParams);
self.param.action = savedActionVariable;
self.restoreAllFilters(filtersToRestore);
diff --git a/plugins/Actions/javascripts/rowactions.js b/plugins/Actions/javascripts/rowactions.js
index 87b77868e4..422036aecc 100644
--- a/plugins/Actions/javascripts/rowactions.js
+++ b/plugins/Actions/javascripts/rowactions.js
@@ -20,6 +20,8 @@ $(function () {
function getLinkForTransitionAndOverlayPopover(tr)
{
+ tr = getRealRowIfComparisonRow(tr);
+
var link = tr.find('> td:first > a').attr('href');
link = $('<textarea>').html(link).val(); // remove html entities
return link;
@@ -31,11 +33,29 @@ $(function () {
return isPageUrlReport(dataTableParams);
},
isAvailableOnRow: function (dataTableParams, tr) {
+ tr = getRealRowIfComparisonRow(tr);
return isPageUrlReport(dataTableParams) && tr.find('> td:first span.label').parent().is('a')
},
- trigger: function (tr, e, subTableLabel) {
+ trigger: function (tr, e, subTableLabel, originalRow) {
+ var overrideParams = $.extend({}, $(originalRow || tr).data('param-override'));
+ if (typeof overrideParams !== 'object') {
+ overrideParams = {};
+ }
+
+ tr = getRealRowIfComparisonRow(tr);
+
var link = getLinkForTransitionAndOverlayPopover(tr);
- this.openPopover('url:' + link);
+ var popoverUrl = 'url:' + link;
+
+ Object.keys(overrideParams).forEach(function (paramName) {
+ if (!overrideParams[paramName]) {
+ return;
+ }
+
+ popoverUrl += ':' + encodeURIComponent(paramName) + ':' + encodeURIComponent(overrideParams[paramName]);
+ });
+
+ this.openPopover(popoverUrl);
}
});
@@ -66,4 +86,11 @@ $(function () {
});
}
+ function getRealRowIfComparisonRow(tr) {
+ if (tr.is('.comparisonRow')) {
+ var prevUntil = tr.prevUntil('.parentComparisonRow').prev();
+ return prevUntil.length ? prevUntil : tr.prev();
+ }
+ return tr;
+ }
}); \ No newline at end of file
diff --git a/plugins/Actions/tests/UI/ActionsDataTable_spec.js b/plugins/Actions/tests/UI/ActionsDataTable_spec.js
index 9c9f855544..bc4386281d 100644
--- a/plugins/Actions/tests/UI/ActionsDataTable_spec.js
+++ b/plugins/Actions/tests/UI/ActionsDataTable_spec.js
@@ -105,6 +105,7 @@ describe("ActionsDataTable", function () {
it("should show the search when clicking on the search icon", async function() {
await page.click('.dataTableAction.searchAction');
+ await page.mouse.move(-10, -10);
await page.waitFor(500);
expect(await page.screenshot({ fullPage: true })).to.matchImage('search_visible');
});
diff --git a/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_auto_expand.png b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_auto_expand.png
index 4f1e0fc4ae..3ff4a2ccd8 100644
--- a/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_auto_expand.png
+++ b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_auto_expand.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:6bbb83ed6c93fff400bc059649b000d8a8434d1bee5a03c9d60b06f08351ca44
-size 373126
+oid sha256:377222cc01db62a192d1d01524aaa651a25c73fff29a5a051b878a8dc56f3cf4
+size 376343
diff --git a/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_column_sorted.png b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_column_sorted.png
index 823d788aa2..ae4a0ad6d1 100644
--- a/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_column_sorted.png
+++ b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_column_sorted.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:19c00677ca3e15f163b7e59886d1d1349717918848dc105a94f8f49c51d593a9
-size 349049
+oid sha256:e4f9b43bd75105de4023baf9345cbe47f138e69df30ff4c745c7d058eba1b229
+size 352520
diff --git a/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_exclude_low_population.png b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_exclude_low_population.png
index dcaabb4970..11303bd0fc 100644
--- a/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_exclude_low_population.png
+++ b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_exclude_low_population.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:198000cebc6a53a3b9310fbf33a441fe70190dd86a573c28694f3e08656df483
-size 79732
+oid sha256:a28f6ab4021b92051d0d5e1d440d1afdcd198a7a1aa6064555a8795653d266a9
+size 79681
diff --git a/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_flattened.png b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_flattened.png
index 87a4c94bad..d59e2e3c67 100644
--- a/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_flattened.png
+++ b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_flattened.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:58f11c3535ffa459122e849ebd5c9c5d2d5207754f7a6c0897de88ccfea64356
-size 467791
+oid sha256:2cf9ac000f4f6abbab9ad5139a9b0cd01463aa32d68ace9ec072e4fe7df62e8e
+size 471736
diff --git a/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_initial.png b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_initial.png
index 91357e2854..dfd04fc0e8 100644
--- a/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_initial.png
+++ b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_initial.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:24a3a1a0cfe8f8b3a1258c4a09523f9204301d3458d94d822cb6b7a293615fcf
-size 348489
+oid sha256:9d529f2e6ef47420eff3159924c5de969b93ef4447ad45f27114f0c0203596b5
+size 351793
diff --git a/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_pageview_percentages.png b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_pageview_percentages.png
index 23278dc015..036bcb0d3e 100644
--- a/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_pageview_percentages.png
+++ b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_pageview_percentages.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:092f50f1d9b32b57ee047e3c6b8235007ac36e35f5f4a0f2e10bb875bfb44110
-size 67302
+oid sha256:4fc514392680028f608f8c4e391a9655181106813b72bdf38f7db06afe286fc2
+size 67063
diff --git a/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_search.png b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_search.png
index 2bfd563211..188ff83755 100644
--- a/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_search.png
+++ b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_search.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:eb076300fe4f89f96ed01e07d0a8af5637cff40d7de4c39255c2a31e493c4f12
-size 81492
+oid sha256:901b3ff98ab7314af98ce0f30f25d39b12faf00f576842c6c2834df46b0e0b96
+size 81557
diff --git a/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_search_closed.png b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_search_closed.png
index 50c2df9696..687fc77ed5 100644
--- a/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_search_closed.png
+++ b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_search_closed.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:730c0bd5c1e622c7fddedec312fbe74daecb21df021a15dc5e9dcabfcf717404
-size 59905
+oid sha256:c54831cd594cdb6d24e8772ac4757c1cda55d72cc52fcf5d0c66ace038033ac0
+size 59687
diff --git a/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_search_visible.png b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_search_visible.png
index 5e77179a22..c7012ffb9a 100644
--- a/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_search_visible.png
+++ b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_search_visible.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:75aed5872a02e45b9ece02d145819cf1c7434070a6dfbb3c9829f803fcd06322
-size 59910
+oid sha256:99e2cb16b5dc7475bb4dafe527d6f67c41d5bd087d2e4e90e7f9ccc147e5b95f
+size 65813
diff --git a/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_segmented_visitor_log_hover.png b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_segmented_visitor_log_hover.png
index 492c6f3b4f..1efa16a865 100644
--- a/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_segmented_visitor_log_hover.png
+++ b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_segmented_visitor_log_hover.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:c56217752d9be8b87d46287f62e21c1c87a94433124232c6db9e88a8e85ee532
-size 66616
+oid sha256:a1862e40dcf575461be061dc79be6b9e59f09e3af189460919ca1e7f5b22161f
+size 71556
diff --git a/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_subtables_loaded.png b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_subtables_loaded.png
index fc23b4e3d4..d0414cc612 100644
--- a/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_subtables_loaded.png
+++ b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_subtables_loaded.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:e734af53ed981781db46ee91fba9e0039c4d3c743b0dcd3c09d6dad3843ff9ea
-size 371468
+oid sha256:b006bd8903a40cda94780f77e59632871ad9c6264e77ad3291e31560bc829a0c
+size 419585
diff --git a/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_unflattened.png b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_unflattened.png
index 50c2df9696..687fc77ed5 100644
--- a/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_unflattened.png
+++ b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_unflattened.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:730c0bd5c1e622c7fddedec312fbe74daecb21df021a15dc5e9dcabfcf717404
-size 59905
+oid sha256:c54831cd594cdb6d24e8772ac4757c1cda55d72cc52fcf5d0c66ace038033ac0
+size 59687
diff --git a/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_unique_pageview_percentages.png b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_unique_pageview_percentages.png
index 6aeb2fab8b..036bcb0d3e 100644
--- a/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_unique_pageview_percentages.png
+++ b/plugins/Actions/tests/UI/expected-screenshots/ActionsDataTable_unique_pageview_percentages.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:752bb51c459aba94b8143e9d34c003a75c02dd87f5fb6c613b94ee6e716ecaed
-size 66900
+oid sha256:4fc514392680028f608f8c4e391a9655181106813b72bdf38f7db06afe286fc2
+size 67063
diff --git a/plugins/Bandwidth b/plugins/Bandwidth
-Subproject aafb2d9d8af8dc53b76ccf30b2ad7ed3af84ef2
+Subproject f36b18235c0dd1e33b2a917b9ee90faa609b75e
diff --git a/plugins/CoreAdminHome/tests/UI/expected-screenshots/TrackingFailures_manage_with_failures_delete_all_confirmed.png b/plugins/CoreAdminHome/tests/UI/expected-screenshots/TrackingFailures_manage_with_failures_delete_all_confirmed.png
index 18b1d26dd7..aa4611320b 100644
--- a/plugins/CoreAdminHome/tests/UI/expected-screenshots/TrackingFailures_manage_with_failures_delete_all_confirmed.png
+++ b/plugins/CoreAdminHome/tests/UI/expected-screenshots/TrackingFailures_manage_with_failures_delete_all_confirmed.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:97ff2802880e3f34cd07dabdb9f32e326f1e6f9cb83e35760525ef82d97dcb02
-size 24159
+oid sha256:7705f452376551b6bfa30d1eebbc47a6fe4e156a2b72d2a5f99697356516f754
+size 24706
diff --git a/plugins/CoreHome/CoreHome.php b/plugins/CoreHome/CoreHome.php
index fbcb7537b2..e55706314d 100644
--- a/plugins/CoreHome/CoreHome.php
+++ b/plugins/CoreHome/CoreHome.php
@@ -106,7 +106,6 @@ class CoreHome extends \Piwik\Plugin
$stylesheets[] = "plugins/CoreHome/stylesheets/dataTable.less";
$stylesheets[] = "plugins/CoreHome/stylesheets/cloud.less";
$stylesheets[] = "plugins/CoreHome/stylesheets/jquery.ui.autocomplete.css";
- $stylesheets[] = "plugins/CoreHome/stylesheets/jqplotColors.less";
$stylesheets[] = "plugins/CoreHome/stylesheets/sparklineColors.less";
$stylesheets[] = "plugins/CoreHome/stylesheets/promo.less";
$stylesheets[] = "plugins/CoreHome/stylesheets/color_manager.css";
@@ -130,6 +129,7 @@ class CoreHome extends \Piwik\Plugin
$stylesheets[] = "plugins/CoreHome/angularjs/dropdown-menu/dropdown-menu.directive.less";
$stylesheets[] = "plugins/CoreHome/angularjs/sparkline/sparkline.component.less";
$stylesheets[] = "plugins/CoreHome/angularjs/field-array/field-array.directive.less";
+ $stylesheets[] = "plugins/CoreHome/angularjs/comparisons/comparisons.component.less";
}
public function getJsFiles(&$jsFiles)
@@ -279,6 +279,9 @@ class CoreHome extends \Piwik\Plugin
$jsFiles[] = "plugins/CoreHome/angularjs/field-array/field-array.directive.js";
$jsFiles[] = "plugins/CoreHome/angularjs/field-array/field-array.controller.js";
+ $jsFiles[] = "plugins/CoreHome/angularjs/comparisons/comparisons.service.js";
+ $jsFiles[] = "plugins/CoreHome/angularjs/comparisons/comparisons.component.js";
+
// we have to load these CoreAdminHome files here. If we loaded them in CoreAdminHome,
// there would be JS errors as CoreAdminHome is loaded first. Meaning it is loaded before
// any angular JS file is loaded etc.
@@ -462,5 +465,10 @@ class CoreHome extends \Piwik\Plugin
$translationKeys[] = 'CoreHome_PageDownShortcutDescription';
$translationKeys[] = 'CoreHome_MacPageUp';
$translationKeys[] = 'CoreHome_MacPageDown';
+ $translationKeys[] = 'General_XComparedToY';
+ $translationKeys[] = 'General_ComparisonCardTooltip1';
+ $translationKeys[] = 'General_ComparisonCardTooltip2';
+ $translationKeys[] = 'General_Comparisons';
+ $translationKeys[] = 'General_ClickToRemoveComp';
}
}
diff --git a/plugins/CoreHome/DataTableRowAction/MultiRowEvolution.php b/plugins/CoreHome/DataTableRowAction/MultiRowEvolution.php
index f708515ebf..38fd48fa24 100644
--- a/plugins/CoreHome/DataTableRowAction/MultiRowEvolution.php
+++ b/plugins/CoreHome/DataTableRowAction/MultiRowEvolution.php
@@ -9,6 +9,7 @@
namespace Piwik\Plugins\CoreHome\DataTableRowAction;
use Piwik\Common;
+use Piwik\Context;
use Piwik\Piwik;
/**
@@ -68,4 +69,28 @@ class MultiRowEvolution extends RowEvolution
return parent::renderPopover($controller, $view);
}
+
+ protected function getRowEvolutionGraphFromController(\Piwik\Plugins\CoreHome\Controller $controller)
+ {
+ // the row evolution graphs should not compare serieses
+ return Context::executeWithQueryParameters(['compareSegments' => [], 'comparePeriods' => [], 'compareDates' => []], function () use ($controller) {
+ return parent::getRowEvolutionGraphFromController($controller);
+ });
+ }
+
+ public function getRowEvolutionGraph($graphType = false, $metrics = false)
+ {
+ // the row evolution graphs should not compare serieses
+ return Context::executeWithQueryParameters(['compareSegments' => [], 'comparePeriods' => [], 'compareDates' => []], function () use ($graphType, $metrics) {
+ return parent::getRowEvolutionGraph($graphType, $metrics);
+ });
+ }
+
+ protected function getSparkline($metric)
+ {
+ // the row evolution graphs should not compare serieses
+ return Context::executeWithQueryParameters(['compareSegments' => [], 'comparePeriods' => [], 'compareDates' => []], function () use ($metric) {
+ return parent::getSparkline($metric);
+ });
+ }
}
diff --git a/plugins/CoreHome/DataTableRowAction/RowEvolution.php b/plugins/CoreHome/DataTableRowAction/RowEvolution.php
index 68b490dee5..1da36555dc 100644
--- a/plugins/CoreHome/DataTableRowAction/RowEvolution.php
+++ b/plugins/CoreHome/DataTableRowAction/RowEvolution.php
@@ -17,6 +17,7 @@ use Piwik\Metrics;
use Piwik\NumberFormatter;
use Piwik\Period\Factory as PeriodFactory;
use Piwik\Piwik;
+use Piwik\Plugins\API\Filter\DataComparisonFilter;
use Piwik\Plugins\CoreVisualizations\Visualizations\JqplotGraph\Evolution as EvolutionViz;
use Piwik\Url;
use Piwik\ViewDataTable\Factory;
@@ -126,7 +127,7 @@ class RowEvolution
// render main evolution graph
$this->graphType = 'graphEvolution';
$this->graphMetrics = $this->availableMetrics;
- $view->graph = $controller->getRowEvolutionGraph($fetch = true, $rowEvolution = $this);
+ $view->graph = $this->getRowEvolutionGraphFromController($controller);
// render metrics overview
$view->metrics = $this->getMetricsToggles();
@@ -160,8 +161,9 @@ class RowEvolution
'period' => $this->period,
'date' => $this->date,
'format' => 'original',
- 'serialize' => '0'
+ 'serialize' => '0',
);
+
if (!empty($this->segment)) {
$parameters['segment'] = $this->segment;
}
@@ -170,11 +172,54 @@ class RowEvolution
$parameters['column'] = $column;
}
+ $isComparing = DataComparisonFilter::isCompareParamsPresent();
+ if ($isComparing) {
+ $compareDates = Common::getRequestVar('compareDates', [], 'array');
+ $comparePeriods = Common::getRequestVar('comparePeriods', [], 'array');
+ $compareSegments = Common::getRequestVar('compareSegments', [], 'array');
+
+ $totalSeriesCount = (count($compareSegments) + 1) * (count($comparePeriods) + 1);
+
+ $unmodifiedSeriesLabels = [];
+ for ($i = 0; $i < $totalSeriesCount; ++$i) {
+ $unmodifiedSeriesLabels[] = DataComparisonFilter::getPrettyComparisonLabelFromSeriesIndex($i);
+ }
+
+ $parameters['compare'] = 1;
+
+ foreach ($comparePeriods as $index => $period) {
+ $date = $compareDates[$index];
+
+ if ($period == 'range') {
+ $comparePeriods[$index] = 'day';
+ } else {
+ list($newDate, $lastN) = EvolutionViz::getDateRangeAndLastN($period, $date);
+ $compareDates[$index] = $newDate;
+ }
+ }
+
+ $parameters['compareDates'] = $compareDates;
+ $parameters['comparePeriods'] = $comparePeriods;
+ }
+
$url = Url::getQueryStringFromParameters($parameters);
$request = new Request($url);
$report = $request->process();
+ // at this point the report data will reference the comparison series labels for the changed compare periods/dates. We don't
+ // want to show this to users because they will not recognize the changed periods, so we have to replace them.
+ if ($isComparing) {
+ $modifiedSeriesLabels = reset($report['reportData']->getDataTables())->getMetadata('comparisonSeries');
+ $seriesMap = array_combine($modifiedSeriesLabels, $unmodifiedSeriesLabels);
+
+ foreach ($report['metadata']['metrics'] as $key => $metricInfo) {
+ foreach ($seriesMap as $modified => $unmodified) {
+ $report['metadata']['metrics'][$key]['name'] = str_replace($modified, $unmodified, $report['metadata']['metrics'][$key]['name']);
+ }
+ }
+ }
+
$this->extractEvolutionReport($report);
}
@@ -374,4 +419,9 @@ class RowEvolution
$fraction = substr(strrchr($value, "."), 1);
return strlen($fraction);
}
+
+ protected function getRowEvolutionGraphFromController(\Piwik\Plugins\CoreHome\Controller $controller)
+ {
+ return $controller->getRowEvolutionGraph($fetch = true, $rowEvolution = $this);
+ }
}
diff --git a/plugins/CoreHome/angularjs/common/services/periods.js b/plugins/CoreHome/angularjs/common/services/periods.js
index 3bc9861e79..6f2deadf73 100644
--- a/plugins/CoreHome/angularjs/common/services/periods.js
+++ b/plugins/CoreHome/angularjs/common/services/periods.js
@@ -59,7 +59,7 @@
DayPeriod.prototype = {
getPrettyString: function () {
- return $.datepicker.formatDate('yy-mm-dd', this.dateInPeriod);
+ return format(this.dateInPeriod);
},
getDateRange: function () {
@@ -83,8 +83,8 @@
WeekPeriod.prototype = {
getPrettyString: function () {
var weekDates = this.getDateRange(this.dateInPeriod);
- var startWeek = $.datepicker.formatDate('yy-mm-dd', weekDates[0]);
- var endWeek = $.datepicker.formatDate('yy-mm-dd', weekDates[1]);
+ var startWeek = format(weekDates[0]);
+ var endWeek = format(weekDates[1]);
return _pk_translate('General_DateRangeFromTo', [startWeek, endWeek]);
},
@@ -252,8 +252,8 @@
RangePeriod.prototype = {
getPrettyString: function () {
- var start = $.datepicker.formatDate('yy-mm-dd', this.startDate);
- var end = $.datepicker.formatDate('yy-mm-dd', this.endDate);
+ var start = format(this.startDate);
+ var end = format(this.endDate);
return _pk_translate('General_DateRangeFromTo', [start, end]);
},
@@ -271,7 +271,9 @@
isRecognizedPeriod: isRecognizedPeriod,
get: get,
parse: parse,
- parseDate: parseDate
+ parseDate: parseDate,
+ format: format,
+ RangePeriod: RangePeriod
};
function getAllLabels() {
@@ -310,6 +312,10 @@
};
}
+ function format(date) {
+ return $.datepicker.formatDate('yy-mm-dd', date);
+ }
+
function parseDate(strDate) {
if (strDate instanceof Date) {
return strDate;
diff --git a/plugins/CoreHome/angularjs/common/services/piwik-api.js b/plugins/CoreHome/angularjs/common/services/piwik-api.js
index 04160d5ee1..e2195f7ff1 100644
--- a/plugins/CoreHome/angularjs/common/services/piwik-api.js
+++ b/plugins/CoreHome/angularjs/common/services/piwik-api.js
@@ -11,9 +11,9 @@ var hasBlockedContent = false;
(function () {
angular.module('piwikApp.service').factory('piwikApi', piwikApiService);
- piwikApiService.$inject = ['$http', '$q', '$rootScope', 'piwik', '$window'];
+ piwikApiService.$inject = ['$http', '$q', '$rootScope', 'piwik', '$window', 'piwikUrl'];
- function piwikApiService ($http, $q, $rootScope, piwik, $window) {
+ function piwikApiService ($http, $q, $rootScope, piwik, $window, piwikUrl) {
var url = 'index.php';
var format = 'json';
@@ -33,7 +33,15 @@ var hasBlockedContent = false;
params = piwik.broadcast.getValuesFromUrl(params);
}
+ var arrayParams = ['compareSegments', 'comparePeriods', 'compareDates'];
+
for (var key in params) {
+ if (arrayParams.indexOf(key) !== -1
+ && !params[key]
+ ) {
+ continue;
+ }
+
getParams[key] = params[key];
}
}
@@ -95,9 +103,7 @@ var hasBlockedContent = false;
if (!angular.isDefined(response) || response === null) {
return $q.reject(null);
-
} else if (isErrorResponse(response)) {
-
createResponseErrorNotification(response, options);
return $q.reject(response.message || null);
@@ -132,9 +138,8 @@ var hasBlockedContent = false;
var ajaxCall = {
method: 'POST',
- url: url,
+ url: url + '?' + $.param(mixinDefaultGetParams(getParams)),
responseType: requestFormat,
- params: mixinDefaultGetParams(getParams),
data: $.param(getPostParams(postParams)),
timeout: requestPromise,
headers: headers
@@ -199,15 +204,16 @@ var hasBlockedContent = false;
* @private
*/
function mixinDefaultGetParams (getParamsToMixin) {
- var segment = piwik.broadcast.getValueFromHash('segment', $window.location.href.split('#')[1]);
-
// we have to decode the value manually because broadcast will not decode anything itself. if we don't,
// angular will encode it again before sending the value in an HTTP request.
- segment = decodeURIComponent(segment);
+ var segment = piwikUrl.getSearchParam('segment');
+ if (segment) {
+ segment = decodeURIComponent(segment);
+ }
var defaultParams = {
- idSite: piwik.idSite || piwik.broadcast.getValueFromUrl('idSite'),
- period: piwik.period || piwik.broadcast.getValueFromUrl('period'),
+ idSite: piwik.idSite || piwikUrl.getSearchParam('idSite'),
+ period: piwik.period || piwikUrl.getSearchParam('period'),
segment: segment
};
diff --git a/plugins/CoreHome/angularjs/common/services/piwik-api.spec.js b/plugins/CoreHome/angularjs/common/services/piwik-api.spec.js
index 1faf387ea3..e33fa329e2 100644
--- a/plugins/CoreHome/angularjs/common/services/piwik-api.spec.js
+++ b/plugins/CoreHome/angularjs/common/services/piwik-api.spec.js
@@ -55,7 +55,7 @@
piwikApi.fetch({
method: "SomePlugin.action"
}).then(function (response) {
- expect(response).to.equal("Request url: index.php?date=&format=JSON2&idSite=1&method=SomePlugin.action&module=API&period=day");
+ expect(response).to.equal("Request url: index.php?method=SomePlugin.action&module=API&format=JSON2&idSite=1&period=day&date=");
done();
}).catch(function (ex) {
@@ -115,7 +115,7 @@
piwikApi.fetch({
method: "SomePlugin.action"
}).then(function (response) {
- expect(response).to.equal("Request url: index.php?date=&format=JSON2&idSite=1&method=SomePlugin.action&module=API&period=day");
+ expect(response).to.equal("Request url: index.php?method=SomePlugin.action&module=API&format=JSON2&idSite=1&period=day&date=");
request1Done = true;
@@ -127,7 +127,7 @@
piwikApi.fetch({
method: "SomeOtherPlugin.action"
}).then(function (response) {
- expect(response).to.equal("Request url: index.php?date=&format=JSON2&idSite=1&method=SomeOtherPlugin.action&module=API&period=day");
+ expect(response).to.equal("Request url: index.php?method=SomeOtherPlugin.action&module=API&format=JSON2&idSite=1&period=day&date=");
request2Done = true;
@@ -160,7 +160,7 @@
piwikApi.fetch({
method: "SomeOtherPlugin.action"
}).then(function (response) {
- expect(response).to.equal("Request url: index.php?date=&format=JSON2&idSite=1&method=SomeOtherPlugin.action&module=API&period=day");
+ expect(response).to.equal("Request url: index.php?method=SomeOtherPlugin.action&module=API&format=JSON2&idSite=1&period=day&date=");
request2Done = true;
@@ -216,9 +216,9 @@
method: "SomeOtherPlugin.action"
}
]).then(function (response) {
- var restOfExpected = "index.php?date=&format=JSON2&idSite=1&method=API.getBulkRequest&" +
- "module=API&period=day - urls%5B%5D=%3Fmethod%3DSomePlugin.action%26param%3D" +
- "value&urls%5B%5D=%3Fmethod%3DSomeOtherPlugin.action&token_auth=100bf5eeeed1468f3f9d93750044d3dd";
+ var restOfExpected = "index.php?method=API.getBulkRequest&module=API&format=JSON2&idSite=1&period=day&date= - "
+ + "urls%5B%5D=%3Fmethod%3DSomePlugin.action%26param%3Dvalue&urls%5B%5D=%3Fmethod%3DSomeOtherPlugin.action"
+ + "&token_auth=100bf5eeeed1468f3f9d93750044d3dd";
expect(response.length).to.equal(2);
expect(response[0]).to.equal("Response #1: " + restOfExpected);
diff --git a/plugins/CoreHome/angularjs/common/services/piwik-url.js b/plugins/CoreHome/angularjs/common/services/piwik-url.js
index b7e4e52f14..dc45b7e390 100644
--- a/plugins/CoreHome/angularjs/common/services/piwik-url.js
+++ b/plugins/CoreHome/angularjs/common/services/piwik-url.js
@@ -7,12 +7,12 @@
(function () {
angular.module('piwikApp.service').service('piwikUrl', piwikUrl);
- piwikUrl.$inject = ['$location', 'piwik'];
+ piwikUrl.$inject = ['$location', 'piwik', '$window'];
/**
* Similar to angulars $location but works around some limitation. Use it if you need to access search params
*/
- function piwikUrl($location, piwik) {
+ function piwikUrl($location, piwik, $window) {
var model = {
getSearchParam: getSearchParam
@@ -22,42 +22,11 @@
function getSearchParam(paramName)
{
- if (paramName === 'segment') {
- var hash = window.location.href.split('#');
- if (hash && hash[1]) {
- return piwik.broadcast.getValueFromHash(paramName, hash[1]);
- }
-
- return broadcast.getValueFromUrl(paramName);
- }
-
- // available in global scope
- var search = $location.search();
-
- if (!search[paramName]) {
- // see https://github.com/angular/angular.js/issues/7239 (issue is resolved but problem still exists)
- var paramUrlValue = piwik.broadcast.getValueFromUrl(paramName);
- if (paramUrlValue !== false
- && paramUrlValue !== ''
- && paramUrlValue !== null
- && paramUrlValue !== undefined
- && paramName !== 'token_auth') {
- search[paramName] = paramUrlValue;
- } else {
- return paramUrlValue;
- }
- }
-
- if (search[paramName]) {
- var value = search[paramName];
-
- if (angular.isArray(search[paramName])) {
- // use last one. Eg when having period=day&period=year angular would otherwise return ['day', 'year']
- return value[value.length - 1];
- }
-
- return value;
+ var hash = $window.location.href.split('#');
+ if (hash && hash[1] && (new RegExp(paramName + '(\\[]|=)')).test(decodeURIComponent(hash[1]))) {
+ return broadcast.getValueFromHash(paramName, $window.location.href);
}
+ return broadcast.getValueFromUrl(paramName, $window.location.search);
}
}
})();
diff --git a/plugins/CoreHome/angularjs/common/services/piwik.js b/plugins/CoreHome/angularjs/common/services/piwik.js
index 3f6c2682bd..57bc0b580a 100644
--- a/plugins/CoreHome/angularjs/common/services/piwik.js
+++ b/plugins/CoreHome/angularjs/common/services/piwik.js
@@ -38,8 +38,8 @@
piwik.period = period;
var dateRange = piwikPeriods.parse(period, date).getDateRange();
- piwik.startDateString = $.datepicker.formatDate('yy-mm-dd', dateRange[0]);
- piwik.endDateString = $.datepicker.formatDate('yy-mm-dd', dateRange[1]);
+ piwik.startDateString = piwikPeriods.format(dateRange[0]);
+ piwik.endDateString = piwikPeriods.format(dateRange[1]);
updateDateInTitle(date, period);
diff --git a/plugins/CoreHome/angularjs/comparisons/comparisons.component.html b/plugins/CoreHome/angularjs/comparisons/comparisons.component.html
new file mode 100644
index 0000000000..f9fe8a3b2d
--- /dev/null
+++ b/plugins/CoreHome/angularjs/comparisons/comparisons.component.html
@@ -0,0 +1,28 @@
+<div ng-if="$ctrl.comparisonsService.isComparing()">
+ <h3>{{ 'General_Comparisons'|translate }}</h3>
+
+ <div class="comparison card" ng-repeat="comparison in $ctrl.comparisonsService.getSegmentComparisons() track by $index">
+ <div class="comparison-type">{{ 'General_Segment'|translate }}</div>
+
+ <div class="title" title="{{ comparison.title }}">
+ {{ comparison.title }}
+ </div>
+
+ <div class="comparison-period"
+ ng-repeat="periodComparison in $ctrl.comparisonsService.getPeriodComparisons() track by $index"
+ title="{{ $ctrl.getComparisonTooltip(comparison, periodComparison) }}"
+ >
+ <span class="comparison-dot" ng-style="{'background-color': $ctrl.comparisonsService.getSeriesColor(comparison, periodComparison)}"></span>
+ <span class="comparison-period-label">{{ periodComparison.title }} ({{ $ctrl.getComparisonPeriodType(periodComparison) }})</span>
+ </div>
+
+ <a class="remove-button" ng-click="$ctrl.comparisonsService.removeSegmentComparison($index)" ng-if="$ctrl.comparisonsService.getSegmentComparisons().length > 1">
+ <span class="icon icon-close" title="{{ 'General_ClickToRemoveComp'|translate }}"></span>
+ </a>
+ </div>
+
+ <div class="loadingPiwik" style="display:none;">
+ <img src="plugins/Morpheus/images/loading-blue.gif" alt="{{ 'General_LoadingData'|translate }}" />
+ {{ 'General_LoadingData'|translate }}
+ </div>
+</div> \ No newline at end of file
diff --git a/plugins/CoreHome/angularjs/comparisons/comparisons.component.js b/plugins/CoreHome/angularjs/comparisons/comparisons.component.js
new file mode 100644
index 0000000000..d4bdbdee8a
--- /dev/null
+++ b/plugins/CoreHome/angularjs/comparisons/comparisons.component.js
@@ -0,0 +1,149 @@
+/*!
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+(function () {
+ angular.module('piwikApp').component('piwikComparisons', {
+ templateUrl: 'plugins/CoreHome/angularjs/comparisons/comparisons.component.html?cb=' + piwik.cacheBuster,
+ bindings: {
+ // empty
+ },
+ controller: ComparisonsController
+ });
+
+ ComparisonsController.$inject = ['piwikComparisonsService', '$rootScope', 'piwikApi', '$element', 'piwikUrl'];
+
+ function ComparisonsController(comparisonsService, $rootScope, piwikApi, $element, piwikUrl) {
+ var vm = this;
+ var comparisonTooltips = null;
+
+ vm.comparisonsService = comparisonsService;
+ vm.$onInit = $onInit;
+ vm.$onDestroy = $onDestroy;
+ vm.comparisonHasSegment = comparisonHasSegment;
+ vm.getComparisonPeriodType = getComparisonPeriodType;
+ vm.getComparisonTooltip = getComparisonTooltip;
+
+ function $onInit() {
+ $rootScope.$on('piwikComparisonsChanged', onComparisonsChanged);
+
+ onComparisonsChanged();
+
+ setUpTooltips();
+ }
+
+ function $onDestroy() {
+ try {
+ $element.tooltip('destroy');
+ } catch (e) {
+ // ignore
+ }
+ }
+
+ function setUpTooltips() {
+ $element.tooltip({
+ track: true,
+ content: function() {
+ var title = $(this).attr('title');
+ return piwikHelper.escape(title.replace(/\n/g, '<br />'));
+ },
+ show: {delay: 200, duration: 200},
+ hide: false
+ });
+ }
+
+ function comparisonHasSegment(comparison) {
+ return typeof comparison.params.segment !== 'undefined';
+ }
+
+ function getComparisonPeriodType(comparison) {
+ var period = comparison.params.period;
+ if (period === 'range') {
+ return _pk_translate('CoreHome_PeriodRange');
+ }
+ var periodStr = _pk_translate('Intl_Period' + period.substring(0, 1).toUpperCase() + period.substring(1));
+ return periodStr.substring(0, 1).toUpperCase() + periodStr.substring(1);
+ }
+
+ function getComparisonTooltip(segmentComparison, periodComparison) {
+ if (!comparisonTooltips
+ || !Object.keys(comparisonTooltips).length
+ ) {
+ return undefined;
+ }
+
+ return comparisonTooltips[periodComparison.index][segmentComparison.index];
+ }
+
+ function onComparisonsChanged() {
+ comparisonTooltips = null;
+
+ if (!vm.comparisonsService.isComparing()) {
+ return;
+ }
+
+ var periodComparisons = comparisonsService.getPeriodComparisons();
+ var segmentComparisons = comparisonsService.getSegmentComparisons();
+ piwikApi.fetch({
+ method: 'API.getProcessedReport',
+ apiModule: 'VisitsSummary',
+ apiAction: 'get',
+ compare: '1',
+ compareSegments: piwikUrl.getSearchParam('compareSegments'),
+ comparePeriods: piwikUrl.getSearchParam('comparePeriods'),
+ compareDates: piwikUrl.getSearchParam('compareDates'),
+ format_metrics: '1',
+ }).then(function (report) {
+ comparisonTooltips = {};
+ periodComparisons.forEach(function (periodComp) {
+ comparisonTooltips[periodComp.index] = {};
+
+ segmentComparisons.forEach(function (segmentComp) {
+ comparisonTooltips[periodComp.index][segmentComp.index] = generateComparisonTooltip(report, periodComp, segmentComp);
+ });
+ });
+ });
+ }
+
+ function generateComparisonTooltip(visitsSummary, periodComp, segmentComp) {
+ if (!visitsSummary.reportData.comparisons) { // sanity check
+ return '';
+ }
+
+ var firstRowIndex = comparisonsService.getComparisonSeriesIndex(periodComp.index, 0);
+
+ var firstRow = visitsSummary.reportData.comparisons[firstRowIndex];
+
+ var comparisonRowIndex = comparisonsService.getComparisonSeriesIndex(periodComp.index, segmentComp.index);
+ var comparisonRow = visitsSummary.reportData.comparisons[comparisonRowIndex];
+
+ var firstPeriodRow = visitsSummary.reportData.comparisons[segmentComp.index];
+
+ var tooltip = '<div class="comparison-card-tooltip">';
+
+ var visitsPercent = ((comparisonRow.nb_visits / firstRow.nb_visits) * 100).toFixed(2) + '%';
+
+ tooltip += _pk_translate('General_ComparisonCardTooltip1', [
+ "'" + comparisonRow.compareSegmentPretty + "'",
+ comparisonRow.comparePeriodPretty,
+ visitsPercent,
+ comparisonRow.nb_visits,
+ firstRow.nb_visits
+ ]);
+ if (periodComp.index > 0) {
+ tooltip += '<br/><br/>';
+ tooltip += _pk_translate('General_ComparisonCardTooltip2', [
+ comparisonRow.nb_visits_change,
+ firstPeriodRow.compareSegmentPretty,
+ firstPeriodRow.comparePeriodPretty
+ ]);
+ }
+
+ tooltip += '</div>';
+ return tooltip;
+ }
+ }
+})();
diff --git a/plugins/CoreHome/angularjs/comparisons/comparisons.component.less b/plugins/CoreHome/angularjs/comparisons/comparisons.component.less
new file mode 100644
index 0000000000..c2eecb3a22
--- /dev/null
+++ b/plugins/CoreHome/angularjs/comparisons/comparisons.component.less
@@ -0,0 +1,74 @@
+piwik-comparisons {
+ h3 {
+ margin-top: 0;
+ margin-bottom: 0;
+ }
+
+ .comparison {
+ display: inline-block;
+ min-width: 164px;
+ margin-right: 16px;
+ padding: 14px;
+ background-color: white;
+ margin-bottom: 16px;
+ position: relative;
+ }
+
+ .comparison-type {
+ font-weight: bold;
+ text-transform: uppercase;
+ font-size: .7em;
+ color: #999;
+ }
+
+ .remove-button {
+ font-size: .6em;
+ color: #666;
+ position: absolute;
+ right: 12px;
+ top: 12px;
+ }
+
+ .title {
+ font-size: 1.1rem;
+ margin-top: 4px;
+ margin-bottom: 12px;
+
+ overflow-x: hidden;
+ max-width: 250px;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .comparison-period {
+ margin-top: 4px;
+
+ > span {
+ display: inline-block;
+ }
+
+ .comparison-dot {
+ width: 1em;
+ height: 1em;
+ display: inline-block;
+ border-radius: .5em;
+ }
+
+ .comparison-period-label {
+ position: relative;
+ top: -3px;
+ margin-left: 2px;
+ display: inline-block;
+ font-size: .85rem;
+ font-style: italic;
+ }
+ }
+}
+
+.comparison-card-tooltip {
+ p {
+ font-size: 1.1em;
+ line-height: 1.3em;
+ color: #fff;
+ }
+} \ No newline at end of file
diff --git a/plugins/CoreHome/angularjs/comparisons/comparisons.service.js b/plugins/CoreHome/angularjs/comparisons/comparisons.service.js
new file mode 100644
index 0000000000..800004a359
--- /dev/null
+++ b/plugins/CoreHome/angularjs/comparisons/comparisons.service.js
@@ -0,0 +1,353 @@
+/*!
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+(function () {
+ angular.module('piwikApp.service').factory('piwikComparisonsService', ComparisonFactory);
+
+ ComparisonFactory.$inject = ['$location', '$rootScope', 'piwikPeriods', 'piwikApi', 'piwikUrl'];
+
+ function ComparisonFactory($location, $rootScope, piwikPeriods, piwikApi, piwikUrl) {
+ var segmentComparisons = [];
+ var periodComparisons = [];
+ var comparisonsDisabledFor = [];
+ var isEnabled = null;
+
+ var SERIES_COLOR_COUNT = 8;
+ var SERIES_SHADE_COUNT = 3;
+
+ var colors = getAllSeriesColors();
+
+ $rootScope.$on('$locationChangeSuccess', updateComparisonsFromQueryParams);
+ $rootScope.$on('piwikSegmentationInited', updateComparisonsFromQueryParams);
+
+ if (!piwikHelper.isAngularRenderingThePage()) { // if we're, eg, widgetized
+ updateComparisonsFromQueryParams();
+ }
+
+ loadComparisonsDisabledFor();
+
+ return {
+ getComparisons: getComparisons,
+ removeSegmentComparison: removeSegmentComparison,
+ addSegmentComparison: addSegmentComparison,
+ isComparisonEnabled: isComparisonEnabled,
+ getSegmentComparisons: getSegmentComparisons,
+ getPeriodComparisons: getPeriodComparisons,
+ getSeriesColor: getSeriesColor,
+ getAllComparisonSeries: getAllComparisonSeries,
+ isComparing: isComparing,
+ isComparingPeriods: isComparingPeriods,
+ getIndividualComparisonRowIndices: getIndividualComparisonRowIndices,
+ getComparisonSeriesIndex: getComparisonSeriesIndex,
+ getSeriesColorName: getSeriesColorName
+ };
+
+ function getComparisons() {
+ return getSegmentComparisons().concat(getPeriodComparisons());
+ }
+
+ function isComparing() {
+ return isComparisonEnabled() && (segmentComparisons.length > 1 || periodComparisons.length > 1); // first two are for selected segment/period
+ }
+
+ function isComparingPeriods() {
+ return getPeriodComparisons().length > 1; // first is currently selected period
+ }
+
+ function getSegmentComparisons() {
+ if (!isComparisonEnabled()) {
+ return [];
+ }
+
+ return segmentComparisons;
+ }
+
+ function getPeriodComparisons() {
+ if (!isComparisonEnabled()) {
+ return [];
+ }
+
+ return periodComparisons;
+ }
+
+ function getSeriesColor(segmentComparison, periodComparison, metricIndex) {
+ metricIndex = metricIndex || 0;
+
+ var seriesIndex = getComparisonSeriesIndex(periodComparison.index, segmentComparison.index) % SERIES_COLOR_COUNT;
+ if (metricIndex === 0) {
+ return colors['series' + seriesIndex];
+ } else {
+ var shadeIndex = metricIndex % SERIES_SHADE_COUNT;
+ return colors['series' + seriesIndex + '-shade' + shadeIndex];
+ }
+ }
+
+ function getSeriesColorName(seriesIndex, metricIndex) {
+ var colorName = 'series' + (seriesIndex % SERIES_COLOR_COUNT);
+ if (metricIndex > 0) {
+ colorName += '-shade' + (metricIndex % SERIES_SHADE_COUNT);
+ }
+ return colorName;
+ }
+
+ function isComparisonEnabled() {
+ return isEnabled;
+ }
+
+ function getIndividualComparisonRowIndices(seriesIndex) {
+ var segmentCount = getSegmentComparisons().length;
+ var segmentIndex = seriesIndex % segmentCount;
+ var periodIndex = Math.floor(seriesIndex / segmentCount);
+
+ return {
+ segmentIndex: segmentIndex,
+ periodIndex: periodIndex,
+ };
+ }
+
+ function getComparisonSeriesIndex(periodIndex, segmentIndex) {
+ var segmentCount = getSegmentComparisons().length;
+ return periodIndex * segmentCount + segmentIndex;
+ }
+
+ function getAllComparisonSeries() {
+ var seriesInfo = [];
+
+ var seriesIndex = 0;
+ getPeriodComparisons().forEach(function (periodComp) {
+ getSegmentComparisons().forEach(function (segmentComp) {
+ seriesInfo.push({
+ index: seriesIndex,
+ params: $.extend({}, segmentComp.params, periodComp.params),
+ color: colors['series' + seriesIndex],
+ });
+ ++seriesIndex;
+ });
+ });
+ return seriesInfo;
+ }
+
+ function removeSegmentComparison(index) {
+ if (!isComparisonEnabled()) {
+ throw new Error('Comparison disabled.');
+ }
+
+ var newComparisons = [].concat(segmentComparisons);
+ newComparisons.splice(index, 1);
+
+ var extraParams = {};
+ if (index === 0) {
+ extraParams.segment = newComparisons[0].params.segment;
+ }
+
+ updateQueryParamsFromComparisons(newComparisons, periodComparisons, extraParams);
+ }
+
+ function addSegmentComparison(params) {
+ if (!isComparisonEnabled()) {
+ throw new Error('Comparison disabled.');
+ }
+
+ var newComparisons = segmentComparisons.concat([{ params: params }]);
+ updateQueryParamsFromComparisons(newComparisons, periodComparisons);
+ }
+
+ function updateQueryParamsFromComparisons(segmentComparisons, periodComparisons, extraParams) {
+ extraParams = extraParams || {};
+
+ // get unique segments/periods/dates from new Comparisons
+ var compareSegments = {};
+ var comparePeriodDatePairs = {};
+
+ var firstSegment = false;
+ var firstPeriod = false;
+
+ segmentComparisons.forEach(function (comparison) {
+ if (firstSegment) {
+ compareSegments[comparison.params.segment] = true;
+ } else {
+ firstSegment = true;
+ }
+ });
+
+ periodComparisons.forEach(function (comparison) {
+ if (firstPeriod) {
+ comparePeriodDatePairs[comparison.params.period + '|' + comparison.params.date] = true;
+ } else {
+ firstPeriod = true;
+ }
+ });
+
+ var comparePeriods = [];
+ var compareDates = [];
+ Object.keys(comparePeriodDatePairs).forEach(function (pair) {
+ var parts = pair.split('|');
+ comparePeriods.push(parts[0]);
+ compareDates.push(parts[1]);
+ });
+
+ var compareParams = {
+ compareSegments: Object.keys(compareSegments),
+ comparePeriods: comparePeriods,
+ compareDates: compareDates,
+ };
+
+ // change the page w/ these new param values
+ if (piwik.helper.isAngularRenderingThePage()) {
+ var search = $location.search();
+ var newSearch = $.extend({}, search, compareParams, extraParams);
+
+ delete newSearch['compareSegments[]'];
+ delete newSearch['comparePeriods[]'];
+ delete newSearch['compareDates[]'];
+
+ if (JSON.stringify(newSearch) !== JSON.stringify(search)) {
+ $location.search($.param(newSearch));
+ }
+
+ return;
+ }
+
+ var paramsToRemove = [];
+ ['compareSegments', 'comparePeriods', 'compareDates'].forEach(function (name) {
+ if (!compareParams[name].length) {
+ paramsToRemove.push(name);
+ }
+ });
+
+ // angular is not rendering the page (ie, we are in the embedded dashboard) or we need to change the segment
+ var url = $.param($.extend({}, extraParams));
+ var strHash = $.param($.extend({}, compareParams));
+ broadcast.propagateNewPage(url, undefined, strHash, paramsToRemove);
+ }
+
+ function updateComparisonsFromQueryParams() {
+ var title;
+ var availableSegments = [];
+ try {
+ availableSegments = $('.segmentEditorPanel').data('uiControlObject').impl.availableSegments || [];
+ } catch (e) {
+ // segment editor is not initialized yet
+ }
+
+ var compareSegments = piwikUrl.getSearchParam('compareSegments') || [];
+ compareSegments = compareSegments instanceof Array ? compareSegments : [compareSegments];
+
+ var comparePeriods = piwikUrl.getSearchParam('comparePeriods') || [];
+ comparePeriods = comparePeriods instanceof Array ? comparePeriods : [comparePeriods];
+
+ var compareDates = piwikUrl.getSearchParam('compareDates') || [];
+ compareDates = compareDates instanceof Array ? compareDates : [compareDates];
+
+ // add base comparisons
+ compareSegments.unshift(piwikUrl.getSearchParam('segment'));
+ comparePeriods.unshift(piwikUrl.getSearchParam('period'));
+ compareDates.unshift(piwikUrl.getSearchParam('date'));
+
+ var newSegmentComparisons = [];
+ compareSegments.forEach(function (segment, idx) {
+ var storedSegment = null;
+
+ availableSegments.forEach(function (s) {
+ if (s.definition === segment
+ || s.definition === decodeURIComponent(segment)
+ || decodeURIComponent(s.definition) === segment
+ ) {
+ storedSegment = s;
+ }
+ });
+
+ var segmentTitle = storedSegment ? storedSegment.name : _pk_translate('General_Unknown');
+ if (segment.trim() === '') {
+ segmentTitle = _pk_translate('SegmentEditor_DefaultAllVisits');
+ }
+
+ newSegmentComparisons.push({
+ params: {
+ segment: segment
+ },
+ title: piwikHelper.htmlDecode(segmentTitle),
+ index: idx
+ });
+ });
+
+ var newPeriodComparisons = [];
+ for (var i = 0; i < Math.min(compareDates.length, comparePeriods.length); ++i) {
+ try {
+ title = piwikPeriods.parse(comparePeriods[i], compareDates[i]).getPrettyString();
+ } catch (e) {
+ title = _pk_translate('General_Error');
+ }
+
+ newPeriodComparisons.push({
+ params: {
+ date: compareDates[i],
+ period: comparePeriods[i]
+ },
+ title: title,
+ index: i
+ });
+ }
+
+ checkEnabledForCurrentPage();
+ setComparisons(newSegmentComparisons, newPeriodComparisons);
+ }
+
+ function setComparisons(newSegmentComparisons, newPeriodComparisons) {
+ var oldSegmentComparisons = segmentComparisons;
+ var oldPeriodComparisons = periodComparisons;
+
+ segmentComparisons = newSegmentComparisons;
+ Object.freeze(segmentComparisons);
+
+ periodComparisons = newPeriodComparisons;
+ Object.freeze(periodComparisons);
+
+ if (JSON.stringify(oldPeriodComparisons) !== JSON.stringify(periodComparisons)
+ || JSON.stringify(oldSegmentComparisons) !== JSON.stringify(segmentComparisons)
+ ) {
+ $rootScope.$emit('piwikComparisonsChanged');
+ }
+ }
+
+ function checkEnabledForCurrentPage() {
+ // category/subcategory is not included on top bar pages, so in that case we use module/action
+ var category = piwikUrl.getSearchParam('category') || piwikUrl.getSearchParam('module');
+ var subcategory = piwikUrl.getSearchParam('subcategory') || piwikUrl.getSearchParam('action');
+
+ var id = category + "." + subcategory;
+ isEnabled = comparisonsDisabledFor.indexOf(id) === -1 && comparisonsDisabledFor.indexOf(category + ".*") === -1;
+
+ $('html').toggleClass('comparisonsDisabled', !isEnabled);
+ }
+
+ function loadComparisonsDisabledFor() {
+ piwikApi.fetch({
+ module: 'API',
+ method: 'API.getPagesComparisonsDisabledFor',
+ }).then(function (result) {
+ comparisonsDisabledFor = result;
+ checkEnabledForCurrentPage();
+ });
+ }
+
+ function getAllSeriesColors() {
+ var colorManager = piwik.ColorManager,
+ seriesColorNames = [];
+
+ for (var i = 0; i < SERIES_COLOR_COUNT; ++i) {
+ seriesColorNames.push('series' + i);
+ for (var j = 0; j < SERIES_SHADE_COUNT; ++j) {
+ seriesColorNames.push('series' + i + '-shade' + j);
+ }
+ }
+
+ return colorManager.getColors('comparison-series-color', seriesColorNames);
+ }
+ }
+
+})();
diff --git a/plugins/CoreHome/angularjs/comparisons/comparisons.service.spec.js b/plugins/CoreHome/angularjs/comparisons/comparisons.service.spec.js
new file mode 100644
index 0000000000..66841115e6
--- /dev/null
+++ b/plugins/CoreHome/angularjs/comparisons/comparisons.service.spec.js
@@ -0,0 +1,499 @@
+/*!
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+(function () {
+ describe('piwikComparisonsService', function () {
+ var DISABLED_PAGES = [
+ 'MyModule1.disabledPage',
+ 'MyModule2.disabledPage2',
+ 'MyModule3.*',
+ ];
+ var piwikComparisonsService;
+ var $httpBackend;
+ var $rootScope;
+ var $location;
+ var oldInjectorFn;
+ var $window;
+
+ beforeEach(module('piwikApp.service'));
+ beforeEach(module(function($provide) {
+ var loc = {};
+ Object.defineProperty(loc, 'href', {
+ get: function () {
+ return $location.absUrl();
+ },
+ set: function (v) {
+ $location.url(v);
+ },
+ });
+ Object.defineProperty(loc, 'search', {
+ get: function () {
+ return '?' + $location.absUrl().split('?')[1];
+ },
+ });
+
+ $window = {
+ location: loc,
+ };
+ $provide.value('$window', $window);
+ }));
+ beforeEach(inject(function($injector) {
+ oldInjectorFn = angular.element.prototype.injector;
+ angular.element.prototype.injector = function () { return $injector; };
+
+ window.piwik.ColorManager = {
+ getColors: function (ns, colors) {
+ var result = {};
+ colors.forEach(function (name) {
+ result[name] = ns + '.' + name;
+ });
+ return result;
+ }
+ };
+
+ piwik_translations = {
+ 'SegmentEditor_DefaultAllVisits': 'SegmentEditor_DefaultAllVisits',
+ 'Intl_PeriodDay': 'Intl_PeriodDay',
+ 'General_Unknown': 'General_Unknown',
+ 'General_DateRangeFromTo': 'General_DateRangeFromTo',
+ };
+
+ $rootScope = $injector.get('$rootScope');
+ $location = $injector.get('$location');
+ $httpBackend = $injector.get('$httpBackend');
+ }));
+ beforeEach(inject(function($injector) {
+ $httpBackend.whenPOST(function (url) {
+ return /API\.getPagesComparisonsDisabledFor/.test(url);
+ }).respond(function () {
+ return [200, DISABLED_PAGES];
+ });
+
+ piwikComparisonsService = $injector.get('piwikComparisonsService');
+
+ $httpBackend.flush();
+ }));
+ afterEach(function () {
+ angular.element.prototype.injector = oldInjectorFn;
+ });
+ afterEach (function () {
+ $httpBackend.verifyNoOutstandingExpectation ();
+ $httpBackend.verifyNoOutstandingRequest ();
+ });
+
+ describe('#getComparisons()', function () {
+ it('should return all comparisons in URL', function () {
+ $location.search('category=MyModule1&subcategory=enabledPage&date=2018-01-02&period=day&segment=abcdefg&compareDates[]=2018-03-04&comparePeriods[]=week&compareSegments[]=comparedsegment&compareSegments[]=');
+ $rootScope.$apply();
+
+ expect(piwikComparisonsService.getComparisons()).to.deep.equal([
+ {
+ params: {
+ segment: 'abcdefg',
+ },
+ title: 'General_Unknown',
+ index: 0,
+ },
+ {
+ params: {
+ segment: 'comparedsegment',
+ },
+ title: 'General_Unknown',
+ index: 1,
+ },
+ {
+ params: {
+ segment: '',
+ },
+ title: 'SegmentEditor_DefaultAllVisits',
+ index: 2,
+ },
+ {
+ params: {
+ date: '2018-01-02',
+ period: 'day'
+ },
+ title: '2018-01-02',
+ index: 0,
+ },
+ {
+ params: {
+ date: '2018-03-04',
+ period: 'week'
+ },
+ title: 'General_DateRangeFromTo',
+ index: 1,
+ },
+ ]);
+ });
+
+ it('should return base params if there are no comparisons', function () {
+ $location.search('category=MyModule1&subcategory=enabledPage&date=2018-01-02&period=day&segment=abcdefg');
+ $rootScope.$apply();
+
+ expect(piwikComparisonsService.getComparisons()).to.deep.equal([
+ {
+ params: {
+ segment: 'abcdefg'
+ },
+ title: 'General_Unknown',
+ index: 0,
+ },
+ {
+ params: {
+ date: '2018-01-02',
+ period: 'day'
+ },
+ title: '2018-01-02',
+ index: 0,
+ },
+ ]);
+ });
+
+ it('should return nothing if comparison is not enabled for the page', function () {
+ $location.search('category=MyModule1&subcategory=disabledPage&date=2018-01-02&period=day&segment=abcdefg&compareDates[]=2018-03-04&comparePeriods[]=week&compareSegments[]=comparedsegment&compareSegments[]=');
+ $rootScope.$apply();
+
+ expect(piwikComparisonsService.getComparisons()).to.deep.equal([]);
+ });
+ });
+
+ describe('#removeSegmentComparison()', function () {
+ it('should remove an existing segment comparison from the URL', function () {
+ $location.search('category=MyModule1&subcategory=enabledPage&date=2018-01-02&period=day&segment=abcdefg&compareDates[]=2018-03-04&comparePeriods[]=week&compareSegments[]=comparedsegment&compareSegments[]=');
+ $rootScope.$apply();
+
+ piwikComparisonsService.removeSegmentComparison(1);
+
+ expect($location.url()).to.equal('?category=MyModule1&subcategory=enabledPage&date=2018-01-02&period=day&segment=abcdefg&compareDates%5B%5D=2018-03-04&comparePeriods%5B%5D=week&compareSegments%5B%5D=comparedsegment&compareSegments%5B%5D=&updated=1#%3Fdate=2012-01-01,2012-01-02&period=range&comparePeriods%255B%255D=week&compareDates%255B%255D=2018-03-04');
+ });
+
+ it('should change the base comparison if the first segment is removed', function () {
+ $location.search('category=MyModule1&subcategory=enabledPage&date=2018-01-02&period=day&segment=abcdefg&compareDates[]=2018-03-04&comparePeriods[]=week&compareSegments[]=comparedsegment&compareSegments[]=');
+ $rootScope.$apply();
+
+ piwikComparisonsService.removeSegmentComparison(0);
+
+ expect($location.url()).to.equal('?category=MyModule1&subcategory=enabledPage&date=2018-01-02&period=day&segment=comparedsegment&compareDates%5B%5D=2018-03-04&comparePeriods%5B%5D=week&compareSegments%5B%5D=comparedsegment&compareSegments%5B%5D=&updated=1#%3Fdate=2012-01-01,2012-01-02&period=range&segment=comparedsegment&comparePeriods%255B%255D=week&compareDates%255B%255D=2018-03-04');
+ });
+ });
+
+ describe('#addSegmentComparison()', function () {
+ it('should add a new segment comparison to the URL', function () {
+ $location.search('category=MyModule1&subcategory=enabledPage&date=2018-01-02&period=day&segment=&compareDates[]=2018-03-04&comparePeriods[]=week&compareSegments[]=comparedsegment');
+ $rootScope.$apply();
+
+ piwikComparisonsService.addSegmentComparison({
+ segment: 'newsegment',
+ });
+
+ expect(piwikComparisonsService.getComparisons()).to.deep.equal([
+ {"params":{"segment":""},"title":"SegmentEditor_DefaultAllVisits","index":0},
+ {"params":{"segment":"comparedsegment"},"title":"General_Unknown","index":1},
+ {"params":{"date":"2018-01-02","period":"day"},"title":"2018-01-02","index":0},
+ {"params":{"date":"2018-03-04","period":"week"},"title":"General_DateRangeFromTo","index":1},
+ ]);
+ });
+
+ it('should add the all visits segment to the URL', function () {
+ $location.search('category=MyModule1&subcategory=enabledPage&date=2018-01-02&period=day&segment=abcdefg&compareDates[]=2018-03-04&comparePeriods[]=week&compareSegments[]=comparedsegment');
+ $rootScope.$apply();
+
+ piwikComparisonsService.addSegmentComparison({
+ segment: '',
+ });
+
+ expect(piwikComparisonsService.getComparisons()).to.deep.equal([
+ {"params":{"segment":"abcdefg"},"title":"General_Unknown","index":0},
+ {"params":{"segment":"comparedsegment"},"title":"General_Unknown","index":1},
+ {"params":{"date":"2018-01-02","period":"day"},"title":"2018-01-02","index":0},
+ {"params":{"date":"2018-03-04","period":"week"},"title":"General_DateRangeFromTo","index":1}
+ ]);
+ });
+
+ it('should add a new period comparison to the URL', function () {
+ $location.search('category=MyModule1&subcategory=enabledPage&date=2018-01-02&period=day&segment=&compareSegments[]=comparedsegment');
+ $rootScope.$apply();
+
+ piwikComparisonsService.addSegmentComparison({
+ period: 'month',
+ date: '2018-02-03',
+ });
+
+ expect(piwikComparisonsService.getComparisons()).to.deep.equal([
+ {"params":{"segment":""},"title":"SegmentEditor_DefaultAllVisits","index":0},
+ {"params":{"segment":"comparedsegment"},"title":"General_Unknown","index":1},
+ {"params":{"date":"2018-01-02","period":"day"},"title":"2018-01-02","index":0},
+ ]);
+ });
+
+ it('should add another period comparison to the URL if one is already there', function () {
+ $location.search('category=MyModule1&subcategory=enabledPage&date=2018-01-02&period=day&segment=&compareDates[]=2018-03-04&comparePeriods[]=week&compareSegments[]=comparedsegment');
+ $rootScope.$apply();
+
+ piwikComparisonsService.addSegmentComparison({
+ period: 'year',
+ date: '2018-02-03',
+ });
+
+ expect(piwikComparisonsService.getComparisons()).to.deep.equal([
+ {"params":{"segment":""},"title":"SegmentEditor_DefaultAllVisits","index":0},
+ {"params":{"segment":"comparedsegment"},"title":"General_Unknown","index":1},
+ {"params":{"date":"2018-01-02","period":"day"},"title":"2018-01-02","index":0},
+ {"params":{"date":"2018-03-04","period":"week"},"title":"General_DateRangeFromTo","index":1},
+ ]);
+ });
+ });
+
+ describe('#isComparisonEnabled()', function () {
+ it('should return true if comparison is enabled for the page', function () {
+ $location.search('category=MyModule1&subcategory=enabledPage&date=2018-01-02&period=day&segment=&compareDates[]=2018-03-04&comparePeriods[]=week&compareSegments[]=comparedsegment');
+ $rootScope.$apply();
+
+ expect(piwikComparisonsService.isComparisonEnabled()).to.be.true;
+ });
+
+ it('should return false if comparison is disabled for the page', function () {
+ $location.search('category=MyModule2&subcategory=disabledPage2&date=2018-01-02&period=day&segment=&compareDates[]=2018-03-04&comparePeriods[]=week&compareSegments[]=comparedsegment');
+ $rootScope.$apply();
+
+ expect(piwikComparisonsService.isComparisonEnabled()).to.be.false;
+ });
+
+ it('should return false if comparison is disabled for the entire category', function () {
+ $location.search('category=MyModule3&subcategory=enabledPage&date=2018-01-02&period=day&segment=&compareDates[]=2018-03-04&comparePeriods[]=week&compareSegments[]=comparedsegment');
+ $rootScope.$apply();
+
+ expect(piwikComparisonsService.isComparisonEnabled()).to.be.false;
+ });
+ });
+
+ describe('#getSegmentComparisons()', function () {
+ it('should return the segment comparisons only', function () {
+ $location.search('category=MyModule1&subcategory=enabledPage&date=2018-01-02&period=day&segment=abcdefg&compareDates[]=2018-03-04&comparePeriods[]=week&compareSegments[]=comparedsegment&compareSegments[]=');
+ $rootScope.$apply();
+
+ expect(piwikComparisonsService.getSegmentComparisons()).to.be.deep.equal([
+ {"params":{"segment":"abcdefg"},"title":"General_Unknown","index":0},
+ {"params":{"segment":"comparedsegment"},"title":"General_Unknown","index":1},
+ {"params":{"segment":""},"title":"SegmentEditor_DefaultAllVisits","index":2}
+ ]);
+ });
+
+ it('should return nothing if comparison is not enabled', function () {
+ $location.search('category=MyModule1&subcategory=disabledPage&date=2018-01-02&period=day&segment=abcdefg&compareDates[]=2018-03-04&comparePeriods[]=week&compareSegments[]=comparedsegment&compareSegments[]=');
+ $rootScope.$apply();
+
+ expect(piwikComparisonsService.getSegmentComparisons()).to.be.deep.equal([]);
+ });
+ });
+
+ describe('#getPeriodComparisons()', function () {
+ it('should return the period comparisons only', function () {
+ $location.search('category=MyModule1&subcategory=enabledPage&date=2018-01-02&period=day&segment=abcdefg&compareDates[]=2018-03-04&comparePeriods[]=week&compareSegments[]=comparedsegment&compareSegments[]=');
+ $rootScope.$apply();
+
+ expect(piwikComparisonsService.getPeriodComparisons()).to.be.deep.equal([
+ {
+ params: {
+ date: '2018-01-02',
+ period: 'day',
+ },
+ title: '2018-01-02',
+ index: 0,
+ },
+ {
+ params: {
+ date: '2018-03-04',
+ period: 'week',
+ },
+ title: 'General_DateRangeFromTo',
+ index: 1,
+ },
+ ]);
+ });
+
+ it('should return nothing if comparison is not enabled', function () {
+ $location.search('category=MyModule1&subcategory=disabledPage&date=2018-01-02&period=day&segment=abcdefg&compareDates[]=2018-03-04&comparePeriods[]=week&compareSegments[]=comparedsegment&compareSegments[]=');
+ $rootScope.$apply();
+
+ expect(piwikComparisonsService.getPeriodComparisons()).to.be.deep.equal([]);
+ });
+ });
+
+ describe('#getAllComparisonSeries()', function () {
+ it('should return all individual comparison serieses', function () {
+ $location.search('category=MyModule1&subcategory=enabledPage&date=2018-01-02&period=day&segment=abcdefg&compareDates[]=2018-03-04&comparePeriods[]=week&compareSegments[]=comparedsegment&compareSegments[]=');
+ $rootScope.$apply();
+
+ expect(piwikComparisonsService.getAllComparisonSeries()).to.be.deep.equal([
+ {
+ index: 0,
+ params: {
+ segment: 'abcdefg',
+ date: '2018-01-02',
+ period: 'day',
+ },
+ color: 'comparison-series-color.series0',
+ },
+ {
+ "index":1,
+ "params": {
+ "segment":"comparedsegment",
+ "date":"2018-01-02",
+ "period":"day"
+ },
+ color: 'comparison-series-color.series1',
+ },
+ {
+ "index":2,
+ "params": {
+ "segment":"",
+ "date":"2018-01-02",
+ "period":"day"
+ },
+ color: 'comparison-series-color.series2',
+ },
+ {
+ "index":3,
+ "params": {
+ "segment":"abcdefg",
+ "date":"2018-03-04",
+ "period":"week"
+ },
+ color: 'comparison-series-color.series3',
+ },
+ {
+ "index":4,
+ "params": {
+ "segment":"comparedsegment",
+ "date":"2018-03-04",
+ "period":"week"
+ },
+ color: 'comparison-series-color.series4',
+ },
+ {
+ "index":5,
+ "params": {
+ "segment":"",
+ "date":"2018-03-04",
+ "period":"week"
+ },
+ color: 'comparison-series-color.series5',
+ },
+ ]);
+ });
+
+ it('should return nothing if comparison is not enabled', function () {
+ $location.search('category=MyModule1&subcategory=disabledPage&date=2018-01-02&period=day&segment=abcdefg&compareDates[]=2018-03-04&comparePeriods[]=week&compareSegments[]=comparedsegment&compareSegments[]=');
+ $rootScope.$apply();
+
+ expect(piwikComparisonsService.getAllComparisonSeries()).to.be.deep.equal([]);
+ });
+ });
+
+ describe('#isComparing()', function () {
+ it('should return true if there are comparison parameters present', function () {
+ $location.search('category=MyModule1&subcategory=enabledPage&date=2018-01-02&period=day&segment=abcdefg&compareDates[]=2018-03-04&comparePeriods[]=week&compareSegments[]=comparedsegment&compareSegments[]=');
+ $rootScope.$apply();
+
+ expect(piwikComparisonsService.isComparing()).to.be.true;
+ });
+
+ it('should return true if there are segment comparisons but no period comparisons', function () {
+ $location.search('category=MyModule1&subcategory=enabledPage&date=2018-01-02&period=day&segment=abcdefg&compareSegments[]=comparedsegment&compareSegments[]=');
+ $rootScope.$apply();
+
+ expect(piwikComparisonsService.isComparing()).to.be.true;
+ });
+
+ it('should return true if there are period comparisons but no segment comparisons', function () {
+ $location.search('category=MyModule1&subcategory=enabledPage&date=2018-01-02&period=day&segment=abcdefg&compareDates[]=2018-03-04&comparePeriods[]=week');
+ $rootScope.$apply();
+
+ expect(piwikComparisonsService.isComparing()).to.be.true;
+ });
+
+ it('should return false if there are no comparison parameters present', function () {
+ $location.search('category=MyModule1&subcategory=enabledPage&date=2018-01-02&period=day&segment=abcdefg');
+ $rootScope.$apply();
+
+ expect(piwikComparisonsService.isComparing()).to.be.false;
+
+ $location.search('category=MyModule1&subcategory=enabledPage&date=2018-01-02&period=day');
+ $rootScope.$apply();
+
+ expect(piwikComparisonsService.isComparing()).to.be.false;
+ });
+
+ it('should return false if comparison is not enabled', function () {
+ $location.search('category=MyModule1&subcategory=disabledPage&date=2018-01-02&period=day&segment=abcdefg&compareDates[]=2018-03-04&comparePeriods[]=week&compareSegments[]=comparedsegment&compareSegments[]=');
+ $rootScope.$apply();
+
+ expect(piwikComparisonsService.isComparing()).to.be.false;
+ });
+ });
+
+ describe('#isComparingPeriods()', function () {
+ it('should return true if there are periods being compared', function () {
+ $location.search('category=MyModule1&subcategory=enabledPage&date=2018-01-02&period=day&segment=abcdefg&compareDates[]=2018-03-04&comparePeriods[]=week&compareSegments[]=comparedsegment&compareSegments[]=');
+ $rootScope.$apply();
+
+ expect(piwikComparisonsService.isComparingPeriods()).to.be.true;
+ });
+
+ it('should return false if there are no periods being compared, just segments', function () {
+ $location.search('category=MyModule1&subcategory=enabledPage&date=2018-01-02&period=day&segment=abcdefg&compareSegments[]=comparedsegment&compareSegments[]=');
+ $rootScope.$apply();
+
+ expect(piwikComparisonsService.isComparingPeriods()).to.be.false;
+ });
+
+ it('should return false if there is nothing being compared', function () {
+ $location.search('category=MyModule1&subcategory=enabledPage&date=2018-01-02&period=day&segment=abcdefg');
+ $rootScope.$apply();
+
+ expect(piwikComparisonsService.isComparingPeriods()).to.be.false;
+ });
+
+ it('should return false if comparing is not enabled', function () {
+ $location.search('category=MyModule1&subcategory=disabledPage&date=2018-01-02&period=day&segment=abcdefg&compareDates[]=2018-03-04&comparePeriods[]=week&compareSegments[]=comparedsegment&compareSegments[]=');
+ $rootScope.$apply();
+
+ expect(piwikComparisonsService.isComparingPeriods()).to.be.false;
+ });
+ });
+
+ describe('#getIndividualComparisonRowIndices()', function () {
+ it('should calculate the segment/period index from the given series index', function () {
+ $location.search('category=MyModule1&subcategory=enabledPage&date=2018-01-02&period=day&segment=abcdefg&compareDates[]=2018-03-04&comparePeriods[]=week&compareSegments[]=comparedsegment&compareSegments[]=');
+ $rootScope.$apply();
+
+ expect(piwikComparisonsService.getIndividualComparisonRowIndices(3)).to.be.deep.equal({
+ segmentIndex: 0,
+ periodIndex: 1,
+ });
+
+ expect(piwikComparisonsService.getIndividualComparisonRowIndices(0)).to.be.deep.equal({
+ segmentIndex: 0,
+ periodIndex: 0,
+ });
+ });
+ });
+
+ describe('#getComparisonSeriesIndex()', function () {
+ it('should return the comparison series index from the given segment & period indices', function () {
+ $location.search('category=MyModule1&subcategory=enabledPage&date=2018-01-02&period=day&segment=abcdefg&compareDates[]=2018-03-04&comparePeriods[]=week&compareSegments[]=comparedsegment&compareSegments[]=');
+ $rootScope.$apply();
+
+ expect(piwikComparisonsService.getComparisonSeriesIndex(1, 1)).to.be.deep.equal(4);
+
+ expect(piwikComparisonsService.getComparisonSeriesIndex(0, 1)).to.be.deep.equal(1);
+ });
+ });
+ });
+})(); \ No newline at end of file
diff --git a/plugins/CoreHome/angularjs/date-range-picker/date-range-picker.component.js b/plugins/CoreHome/angularjs/date-range-picker/date-range-picker.component.js
index c9c23bb3f2..95432ea6f7 100644
--- a/plugins/CoreHome/angularjs/date-range-picker/date-range-picker.component.js
+++ b/plugins/CoreHome/angularjs/date-range-picker/date-range-picker.component.js
@@ -32,9 +32,9 @@
controller: DateRangePickerController
});
- DateRangePickerController.$inject = [];
+ DateRangePickerController.$inject = ['piwikPeriods'];
- function DateRangePickerController() {
+ function DateRangePickerController(piwikPeriods) {
var vm = this;
vm.fromPickerSelectedDates = null;
@@ -118,7 +118,7 @@
function setStartRangeDate(date) {
vm.startDateInvalid = false;
- vm.startDate = $.datepicker.formatDate('yy-mm-dd', date);
+ vm.startDate = piwikPeriods.format(date);
vm.fromPickerSelectedDates = [date, date];
@@ -127,7 +127,7 @@
function setEndRangeDate(date) {
vm.endDateInvalid = false;
- vm.endDate = $.datepicker.formatDate('yy-mm-dd', date);
+ vm.endDate = piwikPeriods.format(date);
vm.toPickerSelectedDates = [date, date];
diff --git a/plugins/CoreHome/angularjs/period-selector/period-selector.controller.js b/plugins/CoreHome/angularjs/period-selector/period-selector.controller.js
index c48150e696..58ed8295e3 100644
--- a/plugins/CoreHome/angularjs/period-selector/period-selector.controller.js
+++ b/plugins/CoreHome/angularjs/period-selector/period-selector.controller.js
@@ -8,9 +8,9 @@
(function () {
angular.module('piwikApp').controller('PeriodSelectorController', PeriodSelectorController);
- PeriodSelectorController.$inject = ['piwik', '$location', 'piwikPeriods'];
+ PeriodSelectorController.$inject = ['piwik', '$location', 'piwikPeriods', 'piwikComparisonsService', '$rootScope', 'piwikUrl', '$element', '$timeout'];
- function PeriodSelectorController(piwik, $location, piwikPeriods) {
+ function PeriodSelectorController(piwik, $location, piwikPeriods, piwikComparisonsService, $rootScope, piwikUrl, $element, $timeout) {
var piwikMinDate = new Date(piwik.minDateYear, piwik.minDateMonth - 1, piwik.minDateDay),
piwikMaxDate = new Date(piwik.maxDateYear, piwik.maxDateMonth - 1, piwik.maxDateDay);
@@ -28,6 +28,11 @@
vm.isLoadingNewPage = false;
+ vm.isComparing = false;
+ vm.comparePeriodType = 'previousPeriod';
+ vm.compareStartDate = '';
+ vm.compareEndDate = '';
+
vm.getCurrentlyViewingText = getCurrentlyViewingText;
vm.changeViewedPeriod = changeViewedPeriod;
vm.setPiwikPeriodAndDate = setPiwikPeriodAndDate;
@@ -38,10 +43,28 @@
vm.onRangeChange = onRangeChange;
vm.isApplyEnabled = isApplyEnabled;
vm.$onInit = init;
+ vm.isComparisonEnabled = isComparisonEnabled;
+
+ $rootScope.$on('$locationChangeSuccess', setIsComparing);
function init() {
vm.updateSelectedValuesFromHash();
+ setIsComparing();
initTopControls(); // must be called when a top control changes width
+
+ handleZIndexPositionRelativeCompareDropdownIssue();
+ }
+
+ function handleZIndexPositionRelativeCompareDropdownIssue() {
+ $element.on('focus', '#comparePeriodToDropdown .select-dropdown', function () {
+ $element.addClass('compare-dropdown-open');
+ }).on('blur', '#comparePeriodToDropdown .select-dropdown', function () {
+ $element.removeClass('compare-dropdown-open');
+ });
+ }
+
+ function setIsComparing() {
+ vm.isComparing = piwikComparisonsService.isComparingPeriods();
}
function $onChanges(changesObj) {
@@ -68,6 +91,29 @@
return false;
}
+ if (vm.isComparing
+ && vm.comparePeriodType === 'custom'
+ && !isCompareRangeValid()
+ ) {
+ return false;
+ }
+
+ return true;
+ }
+
+ function isCompareRangeValid() {
+ try {
+ piwikPeriods.parseDate(vm.compareStartDate);
+ } catch (e) {
+ return false;
+ }
+
+ try {
+ piwikPeriods.parseDate(vm.compareEndDate);
+ } catch (e) {
+ return false;
+ }
+
return true;
}
@@ -78,8 +124,8 @@
}
function updateSelectedValuesFromHash() {
- var strDate = getQueryParamValue('date');
- var strPeriod = getQueryParamValue('period');
+ var strDate = piwikUrl.getSearchParam('date');
+ var strPeriod = piwikUrl.getSearchParam('period');
vm.periodValue = strPeriod;
vm.selectedPeriod = strPeriod;
@@ -97,16 +143,6 @@
}
}
- function getQueryParamValue(name) {
- // $location doesn't parse the URL before the hashbang, but it can hold the query param
- // values, if the page doesn't have the hashbang.
- var result = $location.search()[name];
- if (!result) {
- result = broadcast.getValueFromUrl(name);
- }
- return result;
- }
-
function getPeriodDisplayText(periodLabel) {
return piwikPeriods.get(periodLabel).getDisplayText();
}
@@ -142,6 +178,22 @@
function onApplyClicked() {
if (vm.selectedPeriod === 'range') {
+ var dateString = getSelectedDateString();
+ if (!dateString) {
+ return;
+ }
+
+ vm.periodValue = 'range';
+
+ propagateNewUrlParams(dateString, 'range');
+ return;
+ }
+
+ setPiwikPeriodAndDate(vm.selectedPeriod, vm.dateValue);
+ }
+
+ function getSelectedDateString() {
+ if (vm.selectedPeriod === 'range') {
var dateFrom = vm.startRangeDate,
dateTo = vm.endRangeDate,
oDateFrom = piwikPeriods.parseDate(dateFrom),
@@ -154,16 +206,13 @@
// TODO: use a notification instead?
$('#alert').find('h2').text(_pk_translate('General_InvalidDateRange'));
piwik.helper.modalConfirm('#alert', {});
- return;
+ return null;
}
- vm.periodValue = 'range';
-
- propagateNewUrlParams(dateFrom + ',' + dateTo, 'range');
- return;
+ return dateFrom + ',' + dateTo;
+ } else {
+ return formatDate(vm.dateValue);
}
-
- setPiwikPeriodAndDate(vm.selectedPeriod, vm.dateValue);
}
function setPiwikPeriodAndDate(period, date) {
@@ -184,17 +233,77 @@
vm.endRangeDate = formatDate(dateRange[1] > piwikMaxDate ? piwikMaxDate : dateRange[1]);
}
+ function getSelectedComparisonParams() {
+ var previousDate;
+
+ if (!vm.isComparing) {
+ return {};
+ }
+
+ if (vm.comparePeriodType === 'custom') {
+ return {
+ comparePeriods: ['range'],
+ compareDates: [vm.compareStartDate + ',' + vm.compareEndDate],
+ };
+ } else if (vm.comparePeriodType === 'previousPeriod') {
+ previousDate = getPreviousPeriodDateToSelectedPeriod();
+ return {
+ comparePeriods: [vm.selectedPeriod],
+ compareDates: [previousDate],
+ };
+ } else if (vm.comparePeriodType === 'previousYear') {
+ var dateStr = vm.selectedPeriod === 'range' ? (vm.startRangeDate + ',' + vm.endRangeDate) : vm.dateValue;
+ var currentDateRange = piwikPeriods.parse(vm.selectedPeriod, dateStr).getDateRange();
+ currentDateRange[0].setFullYear(currentDateRange[0].getFullYear() - 1);
+ currentDateRange[1].setFullYear(currentDateRange[1].getFullYear() - 1);
+
+ return {
+ comparePeriods: ['range'],
+ compareDates: [piwikPeriods.format(currentDateRange[0]) + ',' + piwikPeriods.format(currentDateRange[1])],
+ };
+ } else {
+ console.warn("Unknown compare period type: " + vm.comparePeriodType);
+ return {};
+ }
+ }
+
+ function getPreviousPeriodDateToSelectedPeriod() {
+ if (vm.selectedPeriod === 'range') {
+ var currentStartRange = piwikPeriods.parseDate(vm.startRangeDate);
+ var currentEndRange = piwikPeriods.parseDate(vm.endRangeDate);
+ var newEndDate = piwikPeriods.RangePeriod.getLastNRange('day', 2, currentStartRange).startDate;
+
+ var rangeSize = Math.floor((currentEndRange - currentStartRange) / 86400000);
+ var newRange = piwikPeriods.RangePeriod.getLastNRange('day', 1 + rangeSize, newEndDate);
+
+ return piwikPeriods.format(newRange.startDate) + ',' + piwikPeriods.format(newRange.endDate);
+ }
+
+ var newStartDate = piwikPeriods.RangePeriod.getLastNRange(vm.selectedPeriod, 2, vm.dateValue).startDate;
+ return piwikPeriods.format(newStartDate);
+ }
+
function propagateNewUrlParams(date, period) {
+ var compareParams = getSelectedComparisonParams();
+
if (piwik.helper.isAngularRenderingThePage()) {
vm.closePeriodSelector(); // defined in directive
var $search = $location.search();
- if (date !== $search.date || period !== $search.period) {
+ var isCurrentlyComparing = piwikUrl.getSearchParam('compareSegments') || piwikUrl.getSearchParam('comparePeriods');
+ if (date !== $search.date || period !== $search.period || vm.isComparing || isCurrentlyComparing) {
// eg when using back button the date might be actually already changed in the URL and we do not
// want to change the URL again
$search.date = date;
$search.period = period;
- $location.search($search);
+ $search.compareSegments = piwikUrl.getSearchParam('compareSegments') || [];
+ $.extend($search, compareParams);
+
+ delete $search['compareSegments[]'];
+ delete $search['comparePeriods[]'];
+ delete $search['compareDates[]'];
+
+ $location.search($.param($search));
}
return;
@@ -204,7 +313,8 @@
// not in an angular context (eg, embedded dashboard), so must actually
// change the URL
- broadcast.propagateNewPage('date=' + date + '&period=' + period);
+ var url = $.param($.extend({ date: date, period: period }, compareParams));
+ broadcast.propagateNewPage(url);
}
function isValidDate(d) {
@@ -216,7 +326,11 @@
}
function formatDate(date) {
- return $.datepicker.formatDate('yy-mm-dd', date);
+ return piwikPeriods.format(date);
+ }
+
+ function isComparisonEnabled() {
+ return piwikComparisonsService.isComparisonEnabled();
}
}
})();
diff --git a/plugins/CoreHome/angularjs/period-selector/period-selector.directive.html b/plugins/CoreHome/angularjs/period-selector/period-selector.directive.html
index ac7312c71c..6641d6714d 100644
--- a/plugins/CoreHome/angularjs/period-selector/period-selector.directive.html
+++ b/plugins/CoreHome/angularjs/period-selector/period-selector.directive.html
@@ -62,25 +62,71 @@
</label>
</p>
</div>
- <input
- type="submit"
- value="{{ 'General_Apply'|translate }}"
- id="calendarApply"
- class="btn"
- ng-click="periodSelector.onApplyClicked()"
- ng-disabled="!periodSelector.isApplyEnabled()"
- />
- <div id="ajaxLoadingCalendar" ng-if="periodSelector.isLoadingNewPage">
- <div class="loadingPiwik">
- <img src="plugins/Morpheus/images/loading-blue.gif" alt="{{ 'General_LoadingData'|translate }}" />{{ 'General_LoadingData'|translate }}
- </div>
- <div class="loadingSegment">
- {{ 'SegmentEditor_LoadingSegmentedDataMayTakeSomeTime'|translate }}
- </div>
- </div>
</div>
</td>
</tr>
</table>
+
+ <div class="compare-checkbox" ng-if="periodSelector.isComparisonEnabled()">
+ <input id="comparePeriodTo" type="checkbox" ng-model="periodSelector.isComparing"/>
+ <label for="comparePeriodTo">Compare to:</label>
+
+ <div
+ id="comparePeriodToDropdown"
+ piwik-field
+ name="comparePeriodToDropdown"
+ uicontrol="select"
+ options="[{key: 'custom', value: 'Custom'}, {key: 'previousPeriod', value: 'Previous period'}, {key: 'previousYear', value: 'Previous year'}]"
+ ng-model="periodSelector.comparePeriodType"
+ full-width="true"
+ disabled="!periodSelector.isComparing"
+ ></div>
+ </div>
+
+ <div class="compare-date-range" ng-if="periodSelector.isComparing && periodSelector.comparePeriodType == 'custom'">
+ <div>
+ <div
+ id="comparePeriodStartDate"
+ piwik-field
+ name="comparePeriodStartDate"
+ uicontrol="text"
+ ng-model="periodSelector.compareStartDate"
+ full-width="true"
+ title="Start Date"
+ placeholder="YYYY-MM-DD"
+ ></div>
+
+ <span class="compare-dates-separator"></span>
+
+ <div
+ id="comparePeriodEndDate"
+ piwik-field
+ name="comparePeriodEndDate"
+ uicontrol="text"
+ ng-model="periodSelector.compareEndDate"
+ full-width="true"
+ title="End Date"
+ placeholder="YYYY-MM-DD"
+ ></div>
+ </div>
+ </div>
+
+ <div class="apply-button-container">
+ <input
+ type="submit"
+ value="{{ 'General_Apply'|translate }}"
+ id="calendarApply"
+ class="btn"
+ ng-click="periodSelector.onApplyClicked()"
+ ng-disabled="!periodSelector.isApplyEnabled()"
+ />
+ </div>
+
+ <div id="ajaxLoadingCalendar" ng-if="periodSelector.isLoadingNewPage">
+ <div piwik-activity-indicator loading="true"></div>
+ <div class="loadingSegment">
+ {{ 'SegmentEditor_LoadingSegmentedDataMayTakeSomeTime'|translate }}
+ </div>
+ </div>
</div>
</div>
diff --git a/plugins/CoreHome/angularjs/period-selector/period-selector.directive.less b/plugins/CoreHome/angularjs/period-selector/period-selector.directive.less
index e44e0e0f2c..2cf453fd5b 100644
--- a/plugins/CoreHome/angularjs/period-selector/period-selector.directive.less
+++ b/plugins/CoreHome/angularjs/period-selector/period-selector.directive.less
@@ -1,10 +1,91 @@
[piwik-period-selector] {
display: inline-block;
+ &.compare-dropdown-open {
+ label[for=comparePeriodEndDate] {
+ visibility: hidden;
+ }
+
+ #comparePeriodEndDate .input-field {
+ position: static; // needed when dropdown is open, since dropdown content is position: absolute, and this is position: relative, which causes it to show up in the background of the dropdown
+ }
+ }
+
// overrides theme CSS that always hides .loadingPiwik. since we use an ng-if to make
// sure it's only shown when we're actually loading a new page, it's not necessary to
// forcefully hide it.
.loadingPiwik {
display: inline-block !important;
}
+
+ .compare-checkbox {
+ margin-bottom: 20px;
+ }
+
+ .compare-date-range {
+ padding-top: 20px;
+ }
+
+ #comparePeriodToDropdown {
+ height: 30px;
+ display: inline-block;
+ width: 60%;
+ transform: scale(.8);
+ margin-left: -5%;
+ margin-right: -5%;
+
+ .form-group {
+ margin: 0;
+ }
+
+ .input-field {
+ margin-top: 0;
+ }
+
+ .select-dropdown {
+ margin-bottom: 0;
+ }
+
+ .dropdown-content {
+ overflow-y: visible;
+ }
+ }
+
+ div#comparePeriodStartDate, div#comparePeriodEndDate {
+ display: inline-block;
+ width: calc(~"50% - 20px");
+
+ .form-group {
+ margin: 0;
+ }
+
+ .input-field {
+ margin-top: 0;
+ padding: 0;
+ }
+
+ .select-dropdown {
+ margin-bottom: 0;
+ }
+ }
+
+ .compare-dates-separator {
+ height: 1px;
+ margin-left: 8px;
+ margin-right: 8px;
+ background-color: @color-silver-l14;
+ width: 16px;
+ display: inline-block;
+ vertical-align: top;
+ margin-top: 30px;
+ }
+
+ label[for=comparePeriodTo] {
+ transform: scale(.9);
+ }
+
+ .apply-button-container {
+ text-align: right;
+ }
+
}
diff --git a/plugins/CoreHome/angularjs/report-export/reportexport.directive.js b/plugins/CoreHome/angularjs/report-export/reportexport.directive.js
index c197ce07eb..3599c4f6d8 100644
--- a/plugins/CoreHome/angularjs/report-export/reportexport.directive.js
+++ b/plugins/CoreHome/angularjs/report-export/reportexport.directive.js
@@ -94,6 +94,27 @@
exportUrlParams.period = period;
exportUrlParams.date = param_date;
+ if (dataTable.param.compareDates
+ && dataTable.param.compareDates.length
+ ) {
+ exportUrlParams.compareDates = dataTable.param.compareDates;
+ exportUrlParams.compare = '1';
+ }
+
+ if (dataTable.param.comparePeriods
+ && dataTable.param.comparePeriods.length
+ ) {
+ exportUrlParams.comparePeriods = dataTable.param.comparePeriods;
+ exportUrlParams.compare = '1';
+ }
+
+ if (dataTable.param.compareSegments
+ && dataTable.param.compareSegments.length
+ ) {
+ exportUrlParams.compareSegments = dataTable.param.compareSegments;
+ exportUrlParams.compare = '1';
+ }
+
if (typeof dataTable.param.filter_pattern != "undefined") {
exportUrlParams.filter_pattern = dataTable.param.filter_pattern;
}
diff --git a/plugins/CoreHome/angularjs/reporting-menu/reportingmenu.controller.js b/plugins/CoreHome/angularjs/reporting-menu/reportingmenu.controller.js
index d284034cd6..41623802bf 100644
--- a/plugins/CoreHome/angularjs/reporting-menu/reportingmenu.controller.js
+++ b/plugins/CoreHome/angularjs/reporting-menu/reportingmenu.controller.js
@@ -7,14 +7,18 @@
(function () {
angular.module('piwikApp').controller('ReportingMenuController', ReportingMenuController);
- ReportingMenuController.$inject = ['$scope', 'piwik', '$location', '$timeout', 'reportingMenuModel', '$rootScope'];
+ ReportingMenuController.$inject = ['$scope', 'piwik', '$location', '$timeout', 'reportingMenuModel', '$rootScope', 'piwikUrl'];
- function ReportingMenuController($scope, piwik, $location, $timeout, menuModel, $rootScope) {
+ function ReportingMenuController($scope, piwik, $location, $timeout, menuModel, $rootScope, piwikUrl) {
- var idSite = getUrlParam('idSite');
- var period = getUrlParam('period');
- var date = getUrlParam('date');
- var segment = getUrlParam('segment');
+ var idSite = piwikUrl.getSearchParam('idSite');
+ var period = piwikUrl.getSearchParam('period');
+ var date = piwikUrl.getSearchParam('date');
+ var segment = piwikUrl.getSearchParam('segment');
+
+ var comparePeriods = piwikUrl.getSearchParam('comparePeriods');
+ var compareDates = piwikUrl.getSearchParam('compareDates');
+ var compareSegments = piwikUrl.getSearchParam('compareSegments');
function markAllCategoriesAsInactive()
{
@@ -40,15 +44,6 @@
});
}
- function getUrlParam(param)
- {
- var value = piwik.broadcast.getValueFromHash(param);
- if (!value) {
- value = piwik.broadcast.getValueFromUrl(param);
- }
- return value;
- }
-
$scope.menuModel = menuModel;
// highlight the currently hovered subcategory (and category)
@@ -69,18 +64,29 @@
};
$scope.makeUrl = function (category, subcategory) {
+ var params = {
+ idSite: idSite,
+ period: period,
+ date: date,
+ segment: decodeURIComponent(segment),
+ category: category.id,
+ subcategory: subcategory.id,
+ };
+
+ if (compareDates) {
+ params.compareDates = compareDates;
+ }
- var url = 'idSite=' + encodeURIComponent(idSite);
- url += '&period=' + encodeURIComponent(period);
- url += '&date=' + encodeURIComponent(date);
- url += '&category=' + encodeURIComponent(category.id);
- url += '&subcategory=' + encodeURIComponent(subcategory.id);
+ if (comparePeriods) {
+ params.comparePeriods = comparePeriods;
+ }
- if (segment) {
- url+= '&segment='+ segment;
+ if (compareSegments) {
+ params.compareSegments = compareSegments;
}
- return url;
- }
+
+ return $.param(params);
+ };
$scope.loadCategory = function (category) {
if (category.active) {
@@ -146,9 +152,13 @@
var category = $search.category;
var subcategory = $search.subcategory;
- period = getUrlParam('period');
- date = getUrlParam('date');
- segment = getUrlParam('segment');
+ period = piwikUrl.getSearchParam('period');
+ date = piwikUrl.getSearchParam('date');
+ segment = piwikUrl.getSearchParam('segment');
+
+ comparePeriods = piwikUrl.getSearchParam('comparePeriods');
+ compareDates = piwikUrl.getSearchParam('compareDates');
+ compareSegments = piwikUrl.getSearchParam('compareSegments');
if (!category || !subcategory) {
return;
diff --git a/plugins/CoreHome/angularjs/reporting-page/reportingpage.controller.js b/plugins/CoreHome/angularjs/reporting-page/reportingpage.controller.js
index dca4f6ae4f..54785b189d 100644
--- a/plugins/CoreHome/angularjs/reporting-page/reportingpage.controller.js
+++ b/plugins/CoreHome/angularjs/reporting-page/reportingpage.controller.js
@@ -7,9 +7,9 @@
(function () {
angular.module('piwikApp').controller('ReportingPageController', ReportingPageController);
- ReportingPageController.$inject = ['$scope', 'piwik', '$rootScope', '$location', 'reportingPageModel', 'reportingPagesModel', 'notifications'];
+ ReportingPageController.$inject = ['$scope', 'piwik', '$rootScope', '$location', 'reportingPageModel', 'reportingPagesModel', 'notifications', 'piwikUrl'];
- function ReportingPageController($scope, piwik, $rootScope, $location, pageModel, pagesModel, notifications) {
+ function ReportingPageController($scope, piwik, $rootScope, $location, pageModel, pagesModel, notifications, piwikUrl) {
pageModel.resetPage();
$scope.pageModel = pageModel;
@@ -19,12 +19,19 @@
var currentDate = null;
var currentSegment = null;
+ var currentCompareDates = null;
+ var currentComparePeriods = null;
+ var currentCompareSegments = null;
+
function renderInitialPage()
{
var $search = $location.search();
currentPeriod = $search.period;
currentDate = $search.date;
currentSegment = $search.segment;
+ currentCompareSegments = piwikUrl.getSearchParam('compareSegments');
+ currentCompareDates = piwikUrl.getSearchParam('compareDates');
+ currentComparePeriods = piwikUrl.getSearchParam('comparePeriods');
$scope.renderPage($search.category, $search.subcategory);
}
@@ -59,7 +66,6 @@
}
pageModel.fetchPage(category, subcategory).then(function () {
-
if (!pageModel.page) {
var page = pagesModel.findPageInCategory(category);
if (page && page.subcategory) {
@@ -73,7 +79,7 @@
$scope.hasNoPage = !pageModel.page;
$scope.loading = false;
});
- }
+ };
$scope.loading = true; // we only set loading on initial load
@@ -89,11 +95,20 @@
var date = $search.date;
var segment = $search.segment;
+ // $location does not handle array parameters properly
+ var compareSegments = piwikUrl.getSearchParam('compareSegments');
+ var compareDates = piwikUrl.getSearchParam('compareDates');
+ var comparePeriods = piwikUrl.getSearchParam('comparePeriods');
+
if (category === currentCategory
&& subcategory === currentSubcategory
&& period === currentPeriod
&& date === currentDate
- && segment === currentSegment) {
+ && segment === currentSegment
+ && JSON.stringify(compareDates) === JSON.stringify(currentCompareDates)
+ && JSON.stringify(comparePeriods) === JSON.stringify(currentComparePeriods)
+ && JSON.stringify(compareSegments) === JSON.stringify(currentCompareSegments)
+ ) {
// this page is already loaded
return;
}
@@ -101,6 +116,9 @@
currentPeriod = period;
currentDate = date;
currentSegment = segment;
+ currentCompareDates = compareDates;
+ currentComparePeriods = comparePeriods;
+ currentCompareSegments = compareSegments;
$scope.renderPage(category, subcategory);
});
diff --git a/plugins/CoreHome/angularjs/sparkline/sparkline.component.js b/plugins/CoreHome/angularjs/sparkline/sparkline.component.js
index 42e3e4f699..6853164113 100644
--- a/plugins/CoreHome/angularjs/sparkline/sparkline.component.js
+++ b/plugins/CoreHome/angularjs/sparkline/sparkline.component.js
@@ -16,6 +16,7 @@
angular.module('piwikApp').component('piwikSparkline', {
template: '<img />',
bindings: {
+ seriesIndices: '<',
params: '<'
},
controller: SparklineController
@@ -34,12 +35,23 @@
}
function getSparklineUrl() {
+ var seriesIndices = vm.seriesIndices;
+ var sparklineColors = piwik.getSparklineColors();
+
+ if (seriesIndices) {
+ sparklineColors.lineColor = sparklineColors.lineColor.filter(function (c, index) {
+ return seriesIndices.indexOf(index) !== -1;
+ });
+ }
+
+ var colors = JSON.stringify(sparklineColors);
+
var defaultParams = {
forceView: '1',
viewDataTable: 'sparkline',
widget: $element.closest('[widgetId]').length ? '1' : '0',
showtitle: '1',
- colors: JSON.stringify(piwik.getSparklineColors()),
+ colors: colors,
random: Date.now(),
date: getDefaultDate()
};
@@ -67,8 +79,8 @@
dateRange[0] = piwikMinDate;
}
- var startDateStr = $.datepicker.formatDate('yy-mm-dd', dateRange[0]);
- var endDateStr = $.datepicker.formatDate('yy-mm-dd', dateRange[1]);
+ var startDateStr = piwikPeriods.format(dateRange[0]);
+ var endDateStr = piwikPeriods.format(dateRange[1]);
return startDateStr + ',' + endDateStr;
}
}
diff --git a/plugins/CoreHome/angularjs/widget-bydimension-container/widget-bydimension-container.directive.less b/plugins/CoreHome/angularjs/widget-bydimension-container/widget-bydimension-container.directive.less
index 698d2ca5ea..94b1a3d5d0 100644
--- a/plugins/CoreHome/angularjs/widget-bydimension-container/widget-bydimension-container.directive.less
+++ b/plugins/CoreHome/angularjs/widget-bydimension-container/widget-bydimension-container.directive.less
@@ -12,6 +12,10 @@
table.dataTable tr td.label {
max-width: 380px;
}
+
+ table.dataTable {
+ width: auto;
+ }
}
}
diff --git a/plugins/CoreHome/angularjs/widget-loader/widgetloader.directive.js b/plugins/CoreHome/angularjs/widget-loader/widgetloader.directive.js
index bda6b5d3b9..d741ac22e8 100644
--- a/plugins/CoreHome/angularjs/widget-loader/widgetloader.directive.js
+++ b/plugins/CoreHome/angularjs/widget-loader/widgetloader.directive.js
@@ -19,9 +19,9 @@
(function () {
angular.module('piwikApp').directive('piwikWidgetLoader', piwikWidgetLoader);
- piwikWidgetLoader.$inject = ['piwik', 'piwikUrl', '$http', '$compile', '$q', '$location', 'notifications', '$rootScope', '$timeout'];
+ piwikWidgetLoader.$inject = ['piwik', 'piwikUrl', '$http', '$compile', '$q', '$location', 'notifications', '$rootScope', '$timeout', 'piwikComparisonsService'];
- function piwikWidgetLoader(piwik, piwikUrl, $http, $compile, $q, $location, notifications, $rootScope, $timeout){
+ function piwikWidgetLoader(piwik, piwikUrl, $http, $compile, $q, $location, notifications, $rootScope, $timeout, piwikComparisonsService){
return {
restrict: 'A',
transclude: true,
@@ -71,6 +71,10 @@
var $urlParams = $location.search();
+ delete $urlParams['comparePeriods[]'];
+ delete $urlParams['compareDates[]'];
+ delete $urlParams['compareSegments[]'];
+
if ($.isEmptyObject($urlParams) || !$urlParams || !$urlParams['idSite']) {
// happens eg in exported widget etc when URL does not have #?...
$urlParams = {idSite: 'idSite', period: 'period',date: 'date'};
@@ -93,6 +97,17 @@
}
});
+ if (piwikComparisonsService.isComparisonEnabled()) {
+ ['comparePeriods', 'compareDates', 'compareSegments'].forEach(function (paramName) {
+ var value = piwikUrl.getSearchParam(paramName);
+ if (value) {
+ var map = {};
+ map[paramName] = value;
+ url += '&' + $.param(map);
+ }
+ });
+ }
+
if (!parameters || !('showtitle' in parameters)) {
url += '&showtitle=1';
}
diff --git a/plugins/CoreHome/javascripts/broadcast.js b/plugins/CoreHome/javascripts/broadcast.js
index 5e392b50dd..ef1b23ff28 100644
--- a/plugins/CoreHome/javascripts/broadcast.js
+++ b/plugins/CoreHome/javascripts/broadcast.js
@@ -242,9 +242,10 @@ var broadcast = {
}
if (disableHistory) {
- var newLocation = window.location.href.split('#')[0] + '#?' + currentHashStr;
+ var $window = piwikHelper.getAngularDependency('$window');
+ var newLocation = $window.location.href.split('#')[0] + '#?' + currentHashStr;
// window.location.replace changes the current url without pushing it on the browser's history stack
- window.location.replace(newLocation);
+ $window.location.replace(newLocation);
}
else {
// Let history know about this new Hash and load it.
@@ -306,20 +307,25 @@ var broadcast = {
* @param {string} str url with parameters to be updated
* @param {boolean} [showAjaxLoading] whether to show the ajax loading gif or not.
* @param {string} strHash additional parameters that should be updated on the hash
+ * @param {array} paramsToRemove Optional parameters to remove from the URL.
* @return {void}
*/
- propagateNewPage: function (str, showAjaxLoading, strHash) {
+ propagateNewPage: function (str, showAjaxLoading, strHash, paramsToRemove) {
// abort all existing ajax requests
globalAjaxQueue.abort();
+ paramsToRemove = paramsToRemove || [];
+
if (typeof showAjaxLoading === 'undefined' || showAjaxLoading) {
piwikHelper.showAjaxLoading();
}
var params_vals = str.split("&");
+ var $window = piwikHelper.getAngularDependency('$window');
+
// available in global scope
- var currentSearchStr = window.location.search;
+ var currentSearchStr = $window.location.search;
var currentHashStr = broadcast.getHashFromUrl();
if (!currentSearchStr) {
@@ -328,19 +334,41 @@ var broadcast = {
var oldUrl = currentSearchStr + currentHashStr;
- for (var i = 0; i < params_vals.length; i++) {
+ // remove all array query params that are currently set. if we don't do this the array parameters we add
+ // just get added to the existing parameters.
+ params_vals.forEach(function (param) {
+ if (/\[]=/.test(decodeURIComponent(param))) {
+ var paramName = decodeURIComponent(param).split('[]=')[0];
+ removeParam(paramName);
+ }
+ });
+
+ // remove parameters if needed
+ paramsToRemove.forEach(function (paramName) {
+ removeParam(paramName);
+ });
- if(params_vals[i].length == 0) {
- continue; // updating with empty string would destroy some values
+ // update/add parameters based on whether the parameter is an array param or not
+ params_vals.forEach(function (param) {
+ if(!param.length) {
+ return; // updating with empty string would destroy some values
}
- // update both the current search query and hash string
- currentSearchStr = broadcast.updateParamValue(params_vals[i], currentSearchStr);
+ if (/\[]=/.test(decodeURIComponent(param))) { // array param value
+ currentSearchStr = broadcast.addArrayParamValue(param, currentSearchStr);
+
+ if (currentHashStr.length !== 0) {
+ currentHashStr = broadcast.addArrayParamValue(param, currentHashStr);
+ }
+ } else {
+ // update both the current search query and hash string
+ currentSearchStr = broadcast.updateParamValue(param, currentSearchStr);
- if (currentHashStr.length != 0) {
- currentHashStr = broadcast.updateParamValue(params_vals[i], currentHashStr);
+ if (currentHashStr.length !== 0) {
+ currentHashStr = broadcast.updateParamValue(param, currentHashStr);
+ }
}
- }
+ });
var updatedUrl = new RegExp('&updated=([0-9]+)');
var updatedCounter = updatedUrl.exec(currentSearchStr);
@@ -367,16 +395,22 @@ var broadcast = {
if (event) {
event.preventDefault();
}
- })
+ });
}
if (oldUrl == newUrl) {
- window.location.reload();
+ $window.location.reload();
} else {
this.forceReload = true;
- window.location.href = newUrl;
+ $window.location.href = newUrl;
}
return false;
+
+ function removeParam(paramName) {
+ var paramRegex = new RegExp(paramName + '(\\[]|%5B%5D)?=[^&?#]*&?', 'gi');
+ currentSearchStr = currentSearchStr.replace(paramRegex, '');
+ currentHashStr = currentHashStr.replace(paramRegex, '');
+ }
},
/*************************************************
@@ -430,6 +464,21 @@ var broadcast = {
},
/**
+ * Adds a query param value. Use it to add an array parameter value where you don't want to remove an existing value first.
+ *
+ * @param newParamValue
+ * @param urlStr
+ */
+ addArrayParamValue: function (newParamValue, urlStr) {
+ if (urlStr.indexOf('?') === -1) {
+ urlStr += '?';
+ } else {
+ urlStr += '&';
+ }
+ return urlStr + newParamValue;
+ },
+
+ /**
* Loads a popover by adding a 'popover' query parameter to the current URL and
* indirectly executing the popover handler.
*
@@ -446,7 +495,6 @@ var broadcast = {
* handler.
*/
propagateNewPopoverParameter: function (handlerName, value) {
-
var $location = angular.element(document).injector().get('$location');
var popover = '';
@@ -467,7 +515,11 @@ var broadcast = {
}
}
- $location.search('popover', popover);
+ var $window = piwikHelper.getAngularDependency('$window');
+ var urlStr = $window.location.hash;
+ urlStr = broadcast.updateParamValue('popover=' + encodeURIComponent(popover), urlStr);
+ urlStr = urlStr.replace(/^[#?]+/, '');
+ $location.search(urlStr);
setTimeout(function () {
angular.element(document).injector().get('$rootScope').$apply();
@@ -726,11 +778,30 @@ var broadcast = {
var startStr = url.indexOf(lookFor);
if (startStr >= 0) {
- var endStr = url.indexOf("&", startStr);
- if (endStr == -1) {
+ return getSingleValue(startStr, url);
+ } else {
+ url = decodeURIComponent(url);
+
+ // try looking for multi value param
+ lookFor = param + '[]=';
+ startStr = url.indexOf(lookFor);
+ if (startStr >= 0) {
+ var result = [getSingleValue(startStr)];
+ while ((startStr = url.indexOf(lookFor, startStr + 1)) !== -1) {
+ result.push(getSingleValue(startStr));
+ }
+ return result;
+ } else {
+ return '';
+ }
+ }
+
+ function getSingleValue(startPos) {
+ var endStr = url.indexOf("&", startPos);
+ if (endStr === -1) {
endStr = url.length;
}
- var value = url.substring(startStr + param.length + 1, endStr);
+ var value = url.substring(startPos + lookFor.length, endStr);
// we sanitize values to add a protection layer against XSS
// &segment= value is not sanitized, since segments are designed to accept any user input
@@ -738,8 +809,6 @@ var broadcast = {
value = value.replace(/[^_%~\*\+\-\<\>!@\$\.()=,;0-9a-zA-Z]/gi, '');
}
return value;
- } else {
- return '';
}
},
@@ -763,7 +832,7 @@ var broadcast = {
var urlParts = url.split('#');
searchString = urlParts[0];
} else {
- searchString = location.search;
+ searchString = window.location.search;
}
return searchString;
}
diff --git a/plugins/CoreHome/javascripts/dataTable.js b/plugins/CoreHome/javascripts/dataTable.js
index 71dd8e78b7..dd00113ed7 100644
--- a/plugins/CoreHome/javascripts/dataTable.js
+++ b/plugins/CoreHome/javascripts/dataTable.js
@@ -107,7 +107,6 @@ $.extend(DataTable.prototype, UIControl.prototype, {
this.workingDivId = this._createDivId();
domElem.attr('id', this.workingDivId);
- this.maxNumRowsToHandleEvents = 255;
this.loadedSubDataTable = {};
this.isEmpty = $('.pk-emptyDataTable', domElem).length > 0;
this.bindEventsAndApplyStyle(domElem);
@@ -215,7 +214,7 @@ $.extend(DataTable.prototype, UIControl.prototype, {
// The ajax request contains the function callback to trigger if the request is successful or failed
// displayLoading = false When we don't want to display the Loading... DIV .loadingPiwik
// for example when the script add a Loading... it self and doesn't want to display the generic Loading
- reloadAjaxDataTable: function (displayLoading, callbackSuccess) {
+ reloadAjaxDataTable: function (displayLoading, callbackSuccess, extraParams) {
var self = this;
if (typeof displayLoading == "undefined") {
@@ -252,7 +251,7 @@ $.extend(DataTable.prototype, UIControl.prototype, {
var params = {};
for (var key in self.param) {
- if (typeof self.param[key] != "undefined" && self.param[key] != '') {
+ if (typeof self.param[key] != "undefined" && self.param[key] !== null && self.param[key] !== '') {
if (key == 'filter_column' || key == 'filter_column_recursive' ) {
// search in (metadata) `combinedLabel` when dimensions are shown separately in flattened tables
// needs to be overwritten for each request as switching a searched table might return no results
@@ -270,6 +269,9 @@ $.extend(DataTable.prototype, UIControl.prototype, {
}
ajaxRequest.addParams(params, 'get');
+ if (extraParams) {
+ ajaxRequest.addParams(extraParams, 'post');
+ }
ajaxRequest.withTokenInUrl();
ajaxRequest.setCallback(
@@ -410,6 +412,14 @@ $.extend(DataTable.prototype, UIControl.prototype, {
$domElem.width('');
parentDataTable.width('');
+ var $table = $('table.dataTable', domElem);
+ if ($table.closest('.reportsByDimensionView').length) {
+ var requiredTableWidth = $table.width() - 40; // 40 is padding on card content
+ if (domElem.width() > requiredTableWidth) {
+ domElem.css('max-width', requiredTableWidth + 'px');
+ }
+ }
+
var tableWidth = getTableWidth(domElem);
if (tableWidth <= maxTableWidth) {
@@ -443,7 +453,7 @@ $.extend(DataTable.prototype, UIControl.prototype, {
{
var labelWidth = minLabelWidth;
- var columnsInFirstRow = $('tr:nth-child(1) td:not(.label)', domElem);
+ var columnsInFirstRow = $('tbody tr:not(.parentComparisonRow):not(.comparePeriod):eq(0) td:not(.label)', domElem);
var widthOfAllColumns = 0;
columnsInFirstRow.each(function (index, column) {
@@ -465,7 +475,8 @@ $.extend(DataTable.prototype, UIControl.prototype, {
if (labelWidth > maxLabelWidth
&& !self.isWidgetized()
&& innerWidth !== domElem.width()
- && !self.isDashboard()) {
+ && !self.isDashboard()
+ ) {
labelWidth = maxLabelWidth; // prevent for instance table in Actions-Pages is not too wide
}
@@ -482,7 +493,6 @@ $.extend(DataTable.prototype, UIControl.prototype, {
}
var minWidthBody = $('tbody tr:nth-child(1) td.label', domElem).css('minWidth');
-
if (minWidthBody) {
minWidthBody = parseInt(minWidthBody, 10);
if (minWidthBody && minWidthBody > minWidth) {
@@ -528,8 +538,6 @@ $.extend(DataTable.prototype, UIControl.prototype, {
return labelWidth;
}
- setMaxTableWidthIfNeeded(domElem, 1200);
-
var isTableVisualization = this.jsViewDataTable
&& typeof this.jsViewDataTable === 'string'
&& typeof this.jsViewDataTable.indexOf === 'function'
@@ -560,6 +568,8 @@ $.extend(DataTable.prototype, UIControl.prototype, {
self.overflowContentIfNeeded(domElem);
}
+ setMaxTableWidthIfNeeded(domElem, 1200);
+
if (!self.windowResizeTableAttached) {
self.windowResizeTableAttached = true;
@@ -1451,36 +1461,16 @@ $.extend(DataTable.prototype, UIControl.prototype, {
//Apply some miscelleaneous style to the DataTable
applyCosmetics: function (domElem) {
- var self = this;
-
- // Add some styles on the cells even/odd
- // label (first column of a data row) or not
- $("th:first-child", domElem).addClass('label');
- $("td:first-child", domElem).addClass('label');
-
- var metadata = this.getReportMetadata();
-
- if (self.param.flat == "1" && self.param.show_dimensions == "1" && metadata.dimensions && Object.keys(metadata.dimensions).length > 1) {
- for (var i = 1; i < Object.keys(metadata.dimensions).length; i++) {
- $("th:nth-child("+(i+1)+")", domElem).addClass('label');
- $("td:nth-child("+(i+1)+")", domElem).addClass('label');
- }
- }
-
- $("tr td", domElem).addClass('column');
+ // empty
},
handleColumnHighlighting: function (domElem) {
- if (!this.canHandleRowEvents(domElem)) {
- return;
- }
-
var maxWidth = {};
var currentNthChild = null;
var self = this;
- // higlight all columns on hover
- $('td', domElem).hover(function() {
+ // give all values consistent width
+ $('td', domElem).each(function () {
var $this = $(this);
if ($this.hasClass('label')) {
return;
@@ -1492,16 +1482,29 @@ $.extend(DataTable.prototype, UIControl.prototype, {
if (!maxWidth[nthChild]) {
maxWidth[nthChild] = 0;
- rows.find("td:nth-child(" + (nthChild) + ").column .value").each(function (index, element) {
+ rows.find("td:nth-child(" + (nthChild) + ").column .value").add('> thead th:not(.label) .thDIV', table).each(function (index, element) {
var width = $(element).width();
if (width > maxWidth[nthChild]) {
maxWidth[nthChild] = width;
}
});
rows.find("td:nth-child(" + (nthChild) + ").column .value").each(function (index, element) {
- $(element).css({width: maxWidth[nthChild], display: 'inline-block'});
+ $(element).closest('td').css({width: maxWidth[nthChild]});
});
}
+ });
+
+ // higlight all columns on hover
+ $(domElem).on('mouseenter', 'td', function (e) {
+ e.stopPropagation();
+ var $this = $(e.target);
+ if ($this.hasClass('label')) {
+ return;
+ }
+
+ var table = $this.closest('table');
+ var nthChild = $this.parent('tr').children().index($(e.target)) + 1;
+ var rows = $('> tbody > tr', table);
if (currentNthChild === nthChild) {
return;
@@ -1511,8 +1514,10 @@ $.extend(DataTable.prototype, UIControl.prototype, {
rows.children("td:nth-child(" + (nthChild) + ")").addClass('highlight');
self.repositionRowActions($this.parent('tr'));
- }, function(event) {
- var $this = $(this);
+ });
+
+ $(domElem).on('mouseleave', 'td', function(event) {
+ var $this = $(event.target);
var table = $this.closest('table');
var $parentTr = $this.parent('tr');
var tr = $parentTr.children();
@@ -1531,6 +1536,21 @@ $.extend(DataTable.prototype, UIControl.prototype, {
});
},
+ getComparisonIdSubtables: function ($row) {
+ if ($row.is('.parentComparisonRow')) {
+ var comparisonRows = $row.nextUntil('.parentComparisonRow').filter('.comparisonRow');
+
+ var comparisonIdSubtables = {};
+ comparisonRows.each(function () {
+ var comparisonSeriesIndex = +$(this).data('comparison-series');
+ comparisonIdSubtables[comparisonSeriesIndex] = $(this).data('idsubtable');
+ });
+
+ return JSON.stringify(comparisonIdSubtables);
+ }
+ return undefined;
+ },
+
//behaviour for 'nested DataTable' (DataTable loaded on a click on a row)
handleSubDataTable: function (domElem) {
var self = this;
@@ -1544,12 +1564,17 @@ $.extend(DataTable.prototype, UIControl.prototype, {
// if the subDataTable is not already loaded
if (typeof self.loadedSubDataTable[divIdToReplaceWithSubTable] == "undefined") {
- var numberOfColumns = $(this).children().length;
+ var numberOfColumns = $(this).closest('table').find('thead tr').first().children().length;
+
+ var $insertAfter = $(this).nextUntil(':not(.comparePeriod):not(.comparisonRow)').last();
+ if (!$insertAfter.length) {
+ $insertAfter = $(this);
+ }
// at the end of the query it will replace the ID matching the new HTML table #ID
// we need to create this ID first
- $(this).after(
- '<tr>' +
+ var newRow = $insertAfter.after(
+ '<tr class="subDataTableContainer">' +
'<td colspan="' + numberOfColumns + '" class="cellSubDataTable">' +
'<div id="' + divIdToReplaceWithSubTable + '">' +
'<span class="loadingPiwik" style="display:inline"><img src="plugins/Morpheus/images/loading-blue.gif" />' + _pk_translate('General_Loading') + '</span>' +
@@ -1558,6 +1583,8 @@ $.extend(DataTable.prototype, UIControl.prototype, {
'</tr>'
);
+ piwikHelper.lazyScrollTo(newRow);
+
var savedActionVariable = self.param.action;
// reset all the filters from the Parent table
@@ -1570,9 +1597,12 @@ $.extend(DataTable.prototype, UIControl.prototype, {
delete self.param.totalRows;
+ var extraParams = {};
+ extraParams.comparisonIdSubtables = self.getComparisonIdSubtables($(this));
+
self.reloadAjaxDataTable(false, function(response) {
self.dataTableLoaded(response, divIdToReplaceWithSubTable);
- });
+ }, extraParams);
self.param.action = savedActionVariable;
delete self.param.idSubtable;
@@ -1580,14 +1610,15 @@ $.extend(DataTable.prototype, UIControl.prototype, {
self.loadedSubDataTable[divIdToReplaceWithSubTable] = true;
- $(this).next().toggle();
-
// when "loading..." is displayed, hide actions
// repositioning after loading is not easily possible
$(this).find('div.dataTableRowActions').hide();
+ } else {
+ var $toToggle = $(this).nextUntil('.subDataTableContainer').last();
+ $toToggle = $toToggle.length ? $toToggle : $(this);
+ $toToggle.next().toggle();
}
- $(this).next().toggle();
$(this).toggleClass('expanded');
self.repositionRowActions($(this));
}
@@ -1644,10 +1675,6 @@ $.extend(DataTable.prototype, UIControl.prototype, {
});
},
- canHandleRowEvents: function (domElem) {
- return domElem.find('table > tbody > tr').length <= this.maxNumRowsToHandleEvents;
- },
-
handleRowActions: function (domElem) {
this.doHandleRowActions(domElem.find('table > tbody > tr'));
},
@@ -1663,6 +1690,15 @@ $.extend(DataTable.prototype, UIControl.prototype, {
hide: false,
tooltipClass: 'small'
});
+ domElem.find('span.ratio').tooltip({
+ track: true,
+ content: function() {
+ var title = $(this).attr('title');
+ return piwikHelper.escape(title.replace(/\n/g, '<br />'));
+ },
+ show: {delay: 700, duration: 200},
+ hide: false
+ })
},
handleRelatedReports: function (domElem) {
@@ -1828,7 +1864,7 @@ $.extend(DataTable.prototype, UIControl.prototype, {
listenEvent = 'click';
}
- parent.on(listenEvent, 'tr', function () {
+ parent.on(listenEvent, 'tr:not(.subDataTableContainer)', function () {
var tr = this;
var $tr = $(tr);
var td = $tr.find('td.label:last');
@@ -1840,7 +1876,11 @@ $.extend(DataTable.prototype, UIControl.prototype, {
}
// if there are row actions, make sure the first column is not too narrow
- td.css('minWidth', '145px');
+ td.css('minWidth', $tr.is('.comparisonRow') ? '117px' : '145px');
+
+ if ($(this).is('.parentComparisonRow,.comparePeriod').length) {
+ return;
+ }
if (useTouchEvent && tr.actionsDom && tr.actionsDom.prop('rowActionsVisible')) {
tr.actionsDom.prop('rowActionsVisible', false);
diff --git a/plugins/CoreHome/javascripts/dataTable_rowactions.js b/plugins/CoreHome/javascripts/dataTable_rowactions.js
index 9d8751293b..a7272ad236 100644
--- a/plugins/CoreHome/javascripts/dataTable_rowactions.js
+++ b/plugins/CoreHome/javascripts/dataTable_rowactions.js
@@ -96,8 +96,8 @@ DataTable_RowActions_Registry.register({
isAvailableOnReport: function (dataTableParams) {
return (
typeof dataTableParams.disable_row_evolution == 'undefined'
- || dataTableParams.disable_row_evolution == "0"
- );
+ || dataTableParams.disable_row_evolution == "0"
+ );
},
isAvailableOnRow: function (dataTableParams, tr) {
@@ -144,7 +144,7 @@ DataTable_RowAction.prototype.initTr = function (tr) {
// API actions. For the label filter to work, we need to use the parent action.
// We use jQuery events to let subtables access their parents.
tr.bind(self.trEventName, function (e, params) {
- self.trigger($(this), params.originalEvent, params.label);
+ self.trigger($(this), params.originalEvent, params.label, params.originalRow);
});
};
@@ -152,7 +152,7 @@ DataTable_RowAction.prototype.initTr = function (tr) {
* This method is called from the click event and the tr event (see this.trEventName).
* It derives the label and calls performAction.
*/
-DataTable_RowAction.prototype.trigger = function (tr, e, subTableLabel) {
+DataTable_RowAction.prototype.trigger = function (tr, e, subTableLabel, originalRow) {
var label = this.getLabelFromTr(tr);
// if we have received the event from the sub table, add the label
@@ -166,7 +166,8 @@ DataTable_RowAction.prototype.trigger = function (tr, e, subTableLabel) {
if (subtable.is('.subDataTable')) {
subtable.closest('tr').prev().trigger(this.trEventName, {
label: label,
- originalEvent: e
+ originalEvent: e,
+ originalRow: tr
});
return;
}
@@ -186,20 +187,24 @@ DataTable_RowAction.prototype.trigger = function (tr, e, subTableLabel) {
}
ptr.trigger(this.trEventName, {
label: label,
- originalEvent: e
+ originalEvent: e,
+ originalRow: tr
});
return;
}
}
}
- this.performAction(label, tr, e);
+ this.performAction(label, tr, e, originalRow);
};
/** Get the label string from a tr dom element */
DataTable_RowAction.prototype.getLabelFromTr = function (tr) {
- var rowMetadata = this.getRowMetadata(tr);
+ if (tr.data('label')) {
+ return tr.data('label');
+ }
+ var rowMetadata = this.getRowMetadata(tr);
if (rowMetadata.combinedLabel) {
return '@' + rowMetadata.combinedLabel;
}
@@ -264,6 +269,7 @@ function DataTable_RowActions_RowEvolution(dataTable) {
/** The rows to be compared in multi row evolution */
this.multiEvolutionRows = [];
+ this.multiEvolutionRowsSeries = [];
}
/** Static helper method to launch row evolution from anywhere */
@@ -274,20 +280,38 @@ DataTable_RowActions_RowEvolution.launch = function (apiMethod, label) {
DataTable_RowActions_RowEvolution.prototype = new DataTable_RowAction;
-DataTable_RowActions_RowEvolution.prototype.performAction = function (label, tr, e) {
+DataTable_RowActions_RowEvolution.prototype.performAction = function (label, tr, e, originalRow) {
if (e.shiftKey) {
// only mark for multi row evolution if shift key is pressed
- this.addMultiEvolutionRow(label);
+ this.addMultiEvolutionRow(label, $(originalRow || tr).data('comparison-series'));
return;
}
- this.addMultiEvolutionRow(label);
+ this.addMultiEvolutionRow(label, $(originalRow || tr).data('comparison-series'));
// check whether we have rows marked for multi row evolution
- var extraParams = {};
+ var extraParams = $.extend({}, $(originalRow || tr).data('param-override'));
+ if (typeof extraParams !== 'object') {
+ extraParams = {};
+ }
+
if (this.multiEvolutionRows.length > 1) {
extraParams.action = 'getMultiRowEvolutionPopover';
label = this.multiEvolutionRows.join(',');
+
+ if (this.multiEvolutionRowsSeries.length > 1) { // when comparison is active
+ var piwikUrl = piwikHelper.getAngularDependency('piwikUrl');
+ extraParams.compareDates = piwikUrl.getSearchParam('compareDates');
+ extraParams.comparePeriods = piwikUrl.getSearchParam('comparePeriods');
+ extraParams.compareSegments = piwikUrl.getSearchParam('compareSegments');
+ extraParams.labelSeries = this.multiEvolutionRowsSeries.join(',');
+
+ // remove override period/date/segment since we are sending compare params so we can have the whole set of comparison
+ // serieses for LabelFilter
+ delete extraParams.period;
+ delete extraParams.date;
+ delete extraParams.segment;
+ }
}
$.each(this.dataTable.param, function (index, value) {
@@ -310,9 +334,27 @@ DataTable_RowActions_RowEvolution.prototype.performAction = function (label, tr,
this.openPopover(apiMethod, extraParams, label);
};
-DataTable_RowActions_RowEvolution.prototype.addMultiEvolutionRow = function (label) {
- if ($.inArray(label, this.multiEvolutionRows) == -1) {
+DataTable_RowActions_RowEvolution.prototype.addMultiEvolutionRow = function (label, seriesIndex) {
+ if (typeof seriesIndex !== 'undefined') {
+ var self = this;
+
+ var found = false;
+ this.multiEvolutionRows.forEach(function (rowLabel, index) {
+ var rowSeriesIndex = self.multiEvolutionRowsSeries[index];
+ if (label === rowLabel && seriesIndex === rowSeriesIndex) {
+ found = true;
+ return false;
+ }
+ });
+
+ if (!found) {
+ this.multiEvolutionRows.push(label);
+ this.multiEvolutionRowsSeries.push(seriesIndex);
+ }
+ } else if ($.inArray(label, this.multiEvolutionRows) === -1) {
this.multiEvolutionRows.push(label);
+
+ this.multiEvolutionRowsSeries = []; // for safety, make sure state is consistent
}
};
@@ -374,6 +416,7 @@ DataTable_RowActions_RowEvolution.prototype.showRowEvolution = function (apiMeth
Piwik_Popover.onClose(function () {
// reset rows marked for multi row evolution on close
self.multiEvolutionRows = [];
+ self.multiEvolutionRowsSeries = [];
});
if (self.dataTable !== null) {
diff --git a/plugins/CoreHome/javascripts/sparkline.js b/plugins/CoreHome/javascripts/sparkline.js
index 87ab401863..1a7f65622a 100644
--- a/plugins/CoreHome/javascripts/sparkline.js
+++ b/plugins/CoreHome/javascripts/sparkline.js
@@ -13,19 +13,36 @@ var sparklineDisplayHeight = 25;
var sparklineDisplayWidth = 100;
piwik.getSparklineColors = function () {
- return piwik.ColorManager.getColors('sparkline-colors', sparklineColorNames);
+ var colors = piwik.ColorManager.getColors('sparkline-colors', sparklineColorNames);
+
+ var comparisonService = piwikHelper.getAngularDependency('piwikComparisonsService');
+ if (comparisonService.isComparing()) {
+ var comparisons = comparisonService.getAllComparisonSeries();
+ colors.lineColor = comparisons.map(function (comp) { return comp.color; });
+ }
+
+ return colors;
};
// initializes each sparkline so they use colors defined in CSS
piwik.initSparklines = function() {
- $('.sparkline > img').each(function () {
+ $('.sparkline img').each(function () {
var $self = $(this);
if ($self.attr('src')) {
return;
}
- var colors = JSON.stringify(piwik.getSparklineColors());
+ var seriesIndices = $self.closest('.sparkline').data('series-indices');
+ var sparklineColors = piwik.getSparklineColors();
+
+ if (seriesIndices && sparklineColors.lineColor instanceof Array) {
+ sparklineColors.lineColor = sparklineColors.lineColor.filter(function (c, index) {
+ return seriesIndices.indexOf(index) !== -1;
+ });
+ }
+
+ var colors = JSON.stringify(sparklineColors);
var appendToSparklineUrl = '&colors=' + encodeURIComponent(colors);
// Append the token_auth to the URL if it was set (eg. embed dashboard)
@@ -40,8 +57,6 @@ piwik.initSparklines = function() {
};
window.initializeSparklines = function () {
- var sparklineUrlParamsToIgnore = ['module', 'action', 'idSite', 'period', 'date', 'showtitle', 'viewDataTable', 'forceView', 'random'];
-
$('.dataTableVizEvolution[data-report]').each(function () {
var graph = $(this);
@@ -73,16 +88,15 @@ window.initializeSparklines = function () {
$this.addClass('linked');
- var params = broadcast.getValuesFromUrl(sparklineUrl);
- for (var i = 0; i != sparklineUrlParamsToIgnore.length; ++i) {
- delete params[sparklineUrlParamsToIgnore[i]];
- }
- for (var key in params) {
- if (typeof params[key] == 'undefined') {
- // this happens for example with an empty segment parameter
- delete params[key];
- } else {
- params[key] = decodeURIComponent(params[key]);
+ var params = $this.data('graph-params') || {};
+ if (!Object.keys(params).length) {
+ var urlParams = broadcast.getValuesFromUrl(sparklineUrl);
+
+ if (urlParams.columns) {
+ params.columns = decodeURIComponent(urlParams.columns);
+ }
+ if (urlParams.rows) {
+ params.rows = decodeURIComponent(urlParams.rows);
}
}
diff --git a/plugins/CoreHome/javascripts/uiControl.js b/plugins/CoreHome/javascripts/uiControl.js
index 29b8f08711..4ffa385123 100644
--- a/plugins/CoreHome/javascripts/uiControl.js
+++ b/plugins/CoreHome/javascripts/uiControl.js
@@ -11,6 +11,8 @@
var exports = require('piwik/UI');
+ var ARRAY_PARAM_NAMES = ['compareDates', 'comparePeriods', 'compareSegments'];
+
/**
* Base type for Piwik UI controls. Provides functionality that all controls need (such as
* cleanup on destruction).
@@ -30,7 +32,9 @@
var params = JSON.parse($element.attr('data-params') || '{}');
for (var key in params) { // convert values in params that are arrays to comma separated string lists
- if (params[key] instanceof Array) {
+ if (params[key] instanceof Array
+ && ARRAY_PARAM_NAMES.indexOf(key) === -1
+ ) {
params[key] = params[key].join(',');
}
}
diff --git a/plugins/CoreHome/stylesheets/dataTable.less b/plugins/CoreHome/stylesheets/dataTable.less
index 06ad9623f9..2478032b75 100644
--- a/plugins/CoreHome/stylesheets/dataTable.less
+++ b/plugins/CoreHome/stylesheets/dataTable.less
@@ -4,4 +4,5 @@
@import "dataTable/_rowActions.less";
@import "dataTable/_subDataTable.less";
@import "dataTable/_tableConfiguration.less";
-@import "dataTable/_entityTable.less"; \ No newline at end of file
+@import "dataTable/_entityTable.less";
+@import "dataTable/_comparisonsTable.less"; \ No newline at end of file
diff --git a/plugins/CoreHome/stylesheets/dataTable/_comparisonsTable.less b/plugins/CoreHome/stylesheets/dataTable/_comparisonsTable.less
new file mode 100644
index 0000000000..aa5e3f861b
--- /dev/null
+++ b/plugins/CoreHome/stylesheets/dataTable/_comparisonsTable.less
@@ -0,0 +1,71 @@
+.theWidgetContent .card .card-content table.dataTable, table.dataTable {
+ tr.comparePeriod, tr.comparisonRow {
+ > td:first-child {
+ padding-left: 45px;
+ }
+ }
+
+ tr.parentComparisonRow {
+ .dataTableRowActions {
+ display: none !important;
+ }
+
+ > td:not(.label) > * {
+ visibility: hidden;
+ }
+ }
+
+ tr.comparePeriod {
+ .dataTableRowActions {
+ display: none !important;
+ }
+
+ td.label {
+ font-weight: bold;
+ color: @color-silver-l60;
+ }
+ }
+
+ td.label .prefix-numeral {
+ display: inline-block;
+ width: 20px;
+ }
+
+ .totalsRow,.summaryRow {
+ td.label .prefix-numeral {
+ visibility: hidden;
+ }
+ }
+
+ img {
+ margin-left: 1px;
+ }
+}
+
+tr.comparePeriod, tr.comparisonRow {
+ &.level0 td.label {
+ padding-left: 44px !important;
+ }
+
+ &.level1 td.label {
+ padding-left: 68px !important;
+ }
+
+ &.level2 td.label {
+ padding-left: 90px !important;
+ }
+}
+
+tr.parentComparisonRow {
+ td.label img.plusMinus {
+ margin-right: 8px;
+ }
+
+ &.level1 td.label {
+ padding-left: 44px !important;
+ }
+
+ &.level2 td.label {
+ padding-left: 68px !important;
+ }
+}
diff --git a/plugins/CoreHome/stylesheets/dataTable/_dataTable.less b/plugins/CoreHome/stylesheets/dataTable/_dataTable.less
index 2fbf404861..c0e280876d 100644
--- a/plugins/CoreHome/stylesheets/dataTable/_dataTable.less
+++ b/plugins/CoreHome/stylesheets/dataTable/_dataTable.less
@@ -49,6 +49,11 @@ table.dataTable img {
margin-left: 0.5em;
}
+table.dataTable tr:not(.parentComparisonRow) img.link {
+ position: relative;
+ top: 5px;
+}
+
table.subDataTable img.link {
transform: translateY(-50%);
}
@@ -83,7 +88,7 @@ body>.widget table.dataTable {
}
// for dataTables in report pages (not for widgets in the dashboard)
-.theWidgetContent .card .card-content table.dataTable {
+.theWidgetContent .card .card-content table.dataTable, table.dataTable, table.subDataTable {
th {
background: @theme-color-background-base !important;
@@ -163,11 +168,17 @@ table.dataTable span.label.highlighted {
font-style: italic;
}
+.widget div.dataTable:not(.isComparing) table.dataTable tbody > tr > td:not(.cellSubDataTable):first-child {
+ padding-left: 12px;
+}
+
/* the cell containing the subdatatable */
-table.dataTable .cellSubDataTable {
- margin: 0;
- border-left: 0;
- padding: 6px 12px 6px;
+table.dataTable tr td.cellSubDataTable {
+ &, .widget & {
+ margin: 0;
+ border-left: 0;
+ padding: 6px 20px 6px;
+ }
}
.cellSubDataTable > .dataTable {
@@ -445,13 +456,18 @@ tr.level0 td.label {
}
.widget {
- tr.level0 td.label {
+ div.dataTable:not(.isComparing) table.dataTable tr.level0 td.label,
+ div.dataTable:not(.isComparing) table.dataTable th {
padding-left: 12px !important;
}
}
tr.level1 td.label {
padding-left: 2.5em !important;
+
+ .widget & {
+ padding-left: 28px !important;
+ }
}
tr.level2 td.label {
@@ -493,7 +509,7 @@ tr.level12 td.label {
/* less right margins for the link image in the Pa*/
.dataTableActions table.dataTable img.link {
margin-right: 0.5em;
- margin-left: 0;
+ margin-left: 1px;
margin-bottom: 4px;
vertical-align: text-bottom;
}
@@ -505,7 +521,7 @@ table.dataTable td.label img {
tr td.label img.plusMinus {
margin-left: -3px;
- margin-right: 3px;
+ margin-right: 6px;
margin-top: 0;
}
@@ -615,9 +631,11 @@ div.dataTableScroller {
overflow-x: scroll;
}
-.theWidgetContent .card .card-content div.dataTableScroller {
- margin-left: -20px;
- width: ~"calc(100% + 40px)";
+.theWidgetContent .card .card-content, table.dataTable {
+ div.dataTableScroller {
+ margin-left: -20px;
+ width: ~"calc(100% + 40px)";
+ }
}
@media only screen and (min-width: 993px) {
diff --git a/plugins/CoreHome/stylesheets/dataTable/_subDataTable.less b/plugins/CoreHome/stylesheets/dataTable/_subDataTable.less
index 3dcfea86da..e7f2dcda37 100644
--- a/plugins/CoreHome/stylesheets/dataTable/_subDataTable.less
+++ b/plugins/CoreHome/stylesheets/dataTable/_subDataTable.less
@@ -31,14 +31,6 @@ table.subDataTable td.label, table.subDataTable td.column {
color: #2D2A27;
}
-table.subDataTable td.label {
- width: 80%;
-}
-
-table.subDataTable td.label {
- padding: 5px;
-}
-
/* are the following two supposed to be together? */
.subDataTable.dataTableFeatures {
padding-top: 0;
diff --git a/plugins/CoreHome/stylesheets/jqplotColors.less b/plugins/CoreHome/stylesheets/jqplotColors.less
deleted file mode 100644
index 6abb9a7424..0000000000
--- a/plugins/CoreHome/stylesheets/jqplotColors.less
+++ /dev/null
@@ -1,150 +0,0 @@
-// evolution graph colors
-.evolution-graph-colors[data-name=grid-background] {
- color: #fff;
-}
-
-.evolution-graph-colors[data-name=grid-border] {
- color: #f00;
-}
-
-.evolution-graph-colors[data-name=series1] {
- color: #5170AE;
-}
-
-.evolution-graph-colors[data-name=series2] {
- color: #F29007;
-}
-
-.evolution-graph-colors[data-name=series3] {
- color: #CC3399;
-}
-
-.evolution-graph-colors[data-name=series4] {
- color: #9933CC;
-}
-
-.evolution-graph-colors[data-name=series5] {
- color: #80a033;
-}
-
-.evolution-graph-colors[data-name=series6] {
- color: #246AD2;
-}
-
-.evolution-graph-colors[data-name=series7] {
- color: #FD16EA;
-}
-
-.evolution-graph-colors[data-name=series8] {
- color: #49C100;
-}
-
-.evolution-graph-colors[data-name=ticks] {
- color: #ccc;
-}
-
-.evolution-graph-colors[data-name=single-metric-label] {
- color: @theme-color-text-lighter;
-}
-
-// bar graph colors
-.bar-graph-colors[data-name=grid-background] {
- color: #fff;
-}
-
-.bar-graph-colors[data-name=grid-border] {
- color: #f00;
-}
-
-.bar-graph-colors[data-name=series1] {
- color: #5170AE;
-}
-
-.bar-graph-colors[data-name=series2] {
- color: #F3A010;
-}
-
-.bar-graph-colors[data-name=series3] {
- color: #CC3399;
-}
-
-.bar-graph-colors[data-name=series4] {
- color: #9933CC;
-}
-
-.bar-graph-colors[data-name=series5] {
- color: #80a033;
-}
-
-.bar-graph-colors[data-name=series6] {
- color: #246AD2;
-}
-
-.bar-graph-colors[data-name=series7] {
- color: #FD16EA;
-}
-
-.bar-graph-colors[data-name=series8] {
- color: #49C100;
-}
-
-.bar-graph-colors[data-name=ticks] {
- color: #ccc;
-}
-
-.bar-graph-colors[data-name=single-metric-label] {
- color: @theme-color-text-lighter;
-}
-
-// pie graph colors
-.pie-graph-colors[data-name=grid-background] {
- color: #fff;
-}
-
-.pie-graph-colors[data-name=grid-border] {
- color: #f00;
-}
-
-.pie-graph-colors[data-name=series1] {
- color: #59727F;
-}
-
-.pie-graph-colors[data-name=series2] {
- color: #7DAAC0;
-}
-
-.pie-graph-colors[data-name=series3] {
- color: #7F7259;
-}
-
-.pie-graph-colors[data-name=series4] {
- color: #C09E7D;
-}
-
-.pie-graph-colors[data-name=series5] {
- color: #9BB39B;
-}
-
-.pie-graph-colors[data-name=series6] {
- color: #B1D8B3;
-}
-
-.pie-graph-colors[data-name=series7] {
- color: #B39BA7;
-}
-
-.pie-graph-colors[data-name=series8] {
- color: #D8B1C5;
-}
-
-.pie-graph-colors[data-name=series9] {
- color: #A5A5A5;
-}
-
-.pie-graph-colors[data-name=ticks] {
- color: #ccc;
-}
-
-.pie-graph-colors[data-name=single-metric-label] {
- color: @theme-color-text-lighter;
-}
diff --git a/plugins/CoreHome/templates/_dataTable.twig b/plugins/CoreHome/templates/_dataTable.twig
index 29535d8bb8..14d52f4d1b 100644
--- a/plugins/CoreHome/templates/_dataTable.twig
+++ b/plugins/CoreHome/templates/_dataTable.twig
@@ -29,7 +29,7 @@
{% set summaryRowId = constant('Piwik\\DataTable::ID_SUMMARY_ROW') %}{# ID_SUMMARY_ROW #}
{% set isSubtable = javascriptVariablesToSet.idSubtable is defined and javascriptVariablesToSet.idSubtable != 0 %}
-<div class="dataTable {{ visualizationCssClass }} {{ properties.datatable_css_class|default('') }} {% if isSubtable %}subDataTable{% endif %}"
+<div class="dataTable {{ visualizationCssClass }} {{ properties.datatable_css_class|default('') }} {% if isSubtable %}subDataTable{% endif %} {% if isComparing|default(false) %}isComparing{% endif %}"
data-table-type="{{ properties.datatable_js_type }}"
data-report="{{ properties.report_id }}"
data-report-metadata="{{ reportMetdadata|json_encode|e('html_attr') }}"
diff --git a/plugins/CoreHome/templates/_dataTableCell.twig b/plugins/CoreHome/templates/_dataTableCell.twig
index 7aca38defd..7c72b57385 100644
--- a/plugins/CoreHome/templates/_dataTableCell.twig
+++ b/plugins/CoreHome/templates/_dataTableCell.twig
@@ -12,31 +12,14 @@
{% endif %}
{% set totals = dataTable.getMetadata('totals') %}
-{% if column in properties.report_ratio_columns and column in totals|keys -%}
- {% set labelColumn = columns_to_display|first %}
- {% set reportTotal = totals[column] %}
- {% if siteSummary is defined and siteSummary is not empty and siteSummary.getFirstRow %}
- {% set siteTotal = siteSummary.getFirstRow.getColumn(column) %}
- {% else %}
- {% set siteTotal = 0 %}
- {% endif %}
- {% set rowPercentage = row.getColumn(column)|percentage(reportTotal, 1) %}
- {% set metricTitle = translations[column]|default(column) %}
- {% set reportLabel = row.getColumn(labelColumn)|truncate(40)|rawSafeDecoded %}
-
- {% set reportRatioTooltip = 'General_ReportRatioTooltip'|translate(reportLabel, rowPercentage|e('html_attr'), reportTotal|e('html_attr'), metricTitle|e('html_attr'), translations[labelColumn]|default(labelColumn)|e('html_attr')) %}
-
- {% if siteTotal and siteTotal > reportTotal %}
- {% set totalPercentage = row.getColumn(column)|percentage(siteTotal, 1) %}
- {% set totalRatioTooltip = 'General_TotalRatioTooltip'|translate(totalPercentage, siteTotal|number(2,0), metricTitle) %}
- {% else %}
- {% set totalRatioTooltip = '' %}
- {% endif %}
+{% set labelColumn = columns_to_display|first %}
+{% set reportLabel = row.getColumn(labelColumn)|truncate(40)|rawSafeDecoded %}
+{% include "@CoreVisualizations/_dataTableViz_htmlTable_ratio.twig" with {
+ 'label': reportLabel,
+ 'labelColumn': labelColumn,
+ 'translations': properties.translations
+} %}
- <span class="ratio"
- title="{{ reportRatioTooltip|raw }} {{ totalRatioTooltip|e('html_attr') }}"
- >&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();
diff --git a/plugins/CorePluginsAdmin/tests/UI/expected-screenshots/TagManagerTeaser_admin_page_disable.png b/plugins/CorePluginsAdmin/tests/UI/expected-screenshots/TagManagerTeaser_admin_page_disable.png
index 5e966f788d..1576b7e720 100644
--- a/plugins/CorePluginsAdmin/tests/UI/expected-screenshots/TagManagerTeaser_admin_page_disable.png
+++ b/plugins/CorePluginsAdmin/tests/UI/expected-screenshots/TagManagerTeaser_admin_page_disable.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:087dd036e017afe506218fbd8f9d2458c736ce75a988193cd0f105df265e732c
-size 183739
+oid sha256:1be4ae68227c322e9ce3333f96ff963f47d90d7402e47f1fcdea556cdf0aabff
+size 183608
diff --git a/plugins/CoreVisualizations/JqplotDataGenerator.php b/plugins/CoreVisualizations/JqplotDataGenerator.php
index d725d22b04..e120764005 100644
--- a/plugins/CoreVisualizations/JqplotDataGenerator.php
+++ b/plugins/CoreVisualizations/JqplotDataGenerator.php
@@ -10,11 +10,16 @@
namespace Piwik\Plugins\CoreVisualizations;
use Exception;
+use Piwik\API\Request;
use Piwik\Common;
use Piwik\DataTable;
+use Piwik\DataTable\Row;
use Piwik\Metrics;
+use Piwik\Period\Factory;
use Piwik\Piwik;
+use Piwik\Plugins\API\Filter\DataComparisonFilter;
use Piwik\Plugins\CoreVisualizations\JqplotDataGenerator\Chart;
+use Piwik\Plugins\CoreVisualizations\Visualizations\JqplotGraph;
require_once PIWIK_INCLUDE_PATH . '/plugins/CoreVisualizations/JqplotDataGenerator/Evolution.php';
@@ -26,7 +31,7 @@ require_once PIWIK_INCLUDE_PATH . '/plugins/CoreVisualizations/JqplotDataGenerat
class JqplotDataGenerator
{
/**
- * View properties. @see Piwik\ViewDataTable for more info.
+ * View properties. @see \Piwik\ViewDataTable for more info.
*
* @var array
*/
@@ -34,6 +39,15 @@ class JqplotDataGenerator
protected $graphType;
+ protected $isComparing;
+
+ private $availableSegments;
+
+ /**
+ * @var JqplotGraph
+ */
+ protected $graph;
+
/**
* Creates a new JqplotDataGenerator instance for a graph type and view properties.
*
@@ -42,14 +56,14 @@ class JqplotDataGenerator
* @throws \Exception
* @return JqplotDataGenerator
*/
- public static function factory($type, $properties)
+ public static function factory($type, $properties, JqplotGraph $graph)
{
switch ($type) {
case 'evolution':
- return new JqplotDataGenerator\Evolution($properties, $type);
+ return new JqplotDataGenerator\Evolution($properties, $type, $graph);
case 'pie':
case 'bar':
- return new JqplotDataGenerator($properties, $type);
+ return new JqplotDataGenerator($properties, $type, $graph);
default:
throw new Exception("Unknown JqplotDataGenerator type '$type'.");
}
@@ -63,10 +77,14 @@ class JqplotDataGenerator
*
* @internal param \Piwik\Plugin\ViewDataTable $visualization
*/
- public function __construct($properties, $graphType)
+ public function __construct($properties, $graphType, JqplotGraph $graph)
{
$this->properties = $properties;
$this->graphType = $graphType;
+ $this->isComparing = $graph->isComparing();
+ $this->graph = $graph;
+
+ $this->availableSegments = Request::processRequest('SegmentEditor.getAll', $override = [], $default = []);
}
/**
@@ -80,12 +98,6 @@ class JqplotDataGenerator
$visualization = new Chart();
if ($dataTable->getRowsCount() > 0) {
- // if addTotalRow was called in GenerateGraphHTML, add a row containing totals of
- // different metrics
- if ($this->properties['add_total_row']) {
- $dataTable->queueFilter('AddSummaryRow', Piwik::translate('General_Total'));
- }
-
$dataTable->applyQueuedFilters();
$this->initChartObjectData($dataTable, $visualization);
}
@@ -101,47 +113,123 @@ class JqplotDataGenerator
{
$xLabels = $dataTable->getColumn('label');
- $columnNames = $this->properties['columns_to_display'];
- if (($labelColumnIndex = array_search('label', $columnNames)) !== false) {
- unset($columnNames[$labelColumnIndex]);
+ $columnsToDisplay = array_values($this->properties['columns_to_display']);
+ if (($labelColumnIndex = array_search('label', $columnsToDisplay)) !== false) {
+ unset($columnsToDisplay[$labelColumnIndex]);
}
- $columnNameToTranslation = $columnNameToValue = array();
- foreach ($columnNames as $columnName) {
- $columnNameToTranslation[$columnName] = @$this->properties['translations'][$columnName];
-
- $columnNameToValue[$columnName] = $dataTable->getColumn($columnName);
+ $seriesMetadata = null;
+ if ($this->isComparing) {
+ list($yLabels, $serieses, $seriesMetadata) = $this->getComparisonTableSerieses($dataTable, $columnsToDisplay);
+ } else {
+ list($yLabels, $serieses) = $this->getMainTableSerieses($dataTable, $columnsToDisplay);
}
$visualization->dataTable = $dataTable;
$visualization->properties = $this->properties;
$visualization->setAxisXLabels($xLabels);
- $visualization->setAxisYValues($columnNameToValue);
- $visualization->setAxisYLabels($columnNameToTranslation);
+ $visualization->setAxisYValues($serieses, $seriesMetadata);
+ $visualization->setAxisYLabels($yLabels);
- $units = $this->getUnitsForColumnsToDisplay();
+ $units = $this->getUnitsForSerieses($yLabels);
$visualization->setAxisYUnits($units);
}
- protected function getUnitsForColumnsToDisplay()
+ private function getMainTableSerieses(DataTable $dataTable, $columnNames)
+ {
+ $columnNameToTranslation = [];
+
+ foreach ($columnNames as $columnName) {
+ $columnNameToTranslation[$columnName] = @$this->properties['translations'][$columnName];
+ }
+
+ $columnNameToValue = array();
+ foreach ($columnNames as $columnName) {
+ $columnNameToValue[$columnName] = $dataTable->getColumn($columnName);
+ }
+
+ return [$columnNameToTranslation, $columnNameToValue];
+ }
+
+ private function getComparisonTableSerieses(DataTable $dataTable, $columnsToDisplay)
+ {
+ $seriesLabels = [];
+ $serieses = [];
+ $seriesMetadata = [];
+
+ $seriesIndices = [];
+
+ foreach ($dataTable->getRows() as $row) {
+ /** @var DataTable $comparisonTable */
+ $comparisonTable = $row->getComparisons();
+ if (empty($comparisonTable)) {
+ continue;
+ }
+
+ foreach ($comparisonTable->getRows() as $index => $compareRow) {
+ foreach ($columnsToDisplay as $columnIndex => $columnName) {
+ $seriesId = $columnName . '|' . $index;
+
+ if (!isset($seriesIndices[$seriesId])) {
+ $seriesIndices[$seriesId] = count($seriesIndices);
+ }
+
+ $seriesLabel = $this->getComparisonSeriesLabel($compareRow, $columnName);
+ $seriesLabels[$seriesId] = $seriesLabel;
+ $serieses[$seriesId][] = $compareRow->getColumn($columnName);
+
+ $seriesMetadata[$seriesId] = [
+ 'seriesIndex' => $seriesIndices[$seriesId],
+ 'metricIndex' => $columnIndex,
+ ];
+ }
+ }
+ }
+
+ return [$seriesLabels, $serieses, $seriesMetadata];
+ }
+
+ protected function getComparisonSeriesLabel(Row $compareRow, $columnName, $rowLabel = false)
+ {
+ return $this->getComparisonSeriesLabelFromCompareSeries($compareRow->getMetadata('compareSeriesPretty'), $columnName, $rowLabel);
+ }
+
+ protected function getComparisonSeriesLabelFromCompareSeries($compareSeriesPretty, $columnName, $rowLabel = false)
+ {
+ $columnTranslation = @$this->properties['translations'][$columnName];
+
+ if (empty($rowLabel)) {
+ $label = $columnTranslation;
+ } else {
+ $label = "$rowLabel ($columnTranslation)";
+ }
+
+ $label .= ' ' . $compareSeriesPretty;
+ return $label;
+ }
+
+ protected function getUnitsForSerieses($yLabels)
{
// derive units from column names
- $units = $this->deriveUnitsFromRequestedColumnNames();
+ $units = $this->deriveUnitsFromRequestedColumnNames($yLabels);
if (!empty($this->properties['y_axis_unit'])) {
$units = array_fill(0, count($units), $this->properties['y_axis_unit']);
}
return $units;
}
- private function deriveUnitsFromRequestedColumnNames()
+ private function deriveUnitsFromRequestedColumnNames($yLabels)
{
$idSite = Common::getRequestVar('idSite', null, 'int');
$units = array();
- foreach ($this->properties['columns_to_display'] as $columnName) {
+ foreach ($yLabels as $seriesId => $ignore) {
+ $parts = explode('|', $seriesId, 2);
+ $columnName = $parts[0];
+
$derivedUnit = Metrics::getUnit($columnName, $idSite);
- $units[$columnName] = empty($derivedUnit) ? false : $derivedUnit;
+ $units[$seriesId] = empty($derivedUnit) ? false : $derivedUnit;
}
return $units;
}
diff --git a/plugins/CoreVisualizations/JqplotDataGenerator/Chart.php b/plugins/CoreVisualizations/JqplotDataGenerator/Chart.php
index 44be4af356..2ac4b6f8d7 100644
--- a/plugins/CoreVisualizations/JqplotDataGenerator/Chart.php
+++ b/plugins/CoreVisualizations/JqplotDataGenerator/Chart.php
@@ -25,14 +25,16 @@ class Chart
// temporary
public $properties;
- public function setAxisXLabels($xLabels)
+ public function setAxisXLabels($xLabels, $xTicks = null, $index = 0)
{
+ $axisName = $this->getXAxis($index);
+
$xSteps = $this->properties['x_axis_step_size'];
$showAllTicks = $this->properties['show_all_ticks'];
- $this->axes['xaxis']['labels'] = array_values($xLabels);
+ $this->axes[$axisName]['labels'] = array_values($xLabels);
- $ticks = array_values($xLabels);
+ $ticks = array_values($xTicks ?: $xLabels);
if (!$showAllTicks) {
// unset labels so there are $xSteps number of blank ticks between labels
@@ -42,7 +44,7 @@ class Chart
}
}
}
- $this->axes['xaxis']['ticks'] = $ticks;
+ $this->axes[$axisName]['ticks'] = $ticks;
}
public function setAxisXOnClick(&$onClick)
@@ -50,14 +52,20 @@ class Chart
$this->axes['xaxis']['onclick'] = & $onClick;
}
- public function setAxisYValues(&$values)
+ public function setAxisYValues(&$values, $seriesMetadata = null)
{
foreach ($values as $label => &$data) {
- $this->series[] = array(
+ $seriesInfo = array(
'label' => $label,
- 'internalLabel' => $label
+ 'internalLabel' => $label,
);
+ if (isset($seriesMetadata[$label])) {
+ $seriesInfo = array_merge($seriesInfo, $seriesMetadata[$label]);
+ }
+
+ $this->series[] = $seriesInfo;
+
array_walk($data, function (&$v) {
$v = (float) Common::forceDotAsSeparatorForDecimalPoint($v);
});
@@ -116,4 +124,28 @@ class Chart
return $data;
}
+
+ public function setAxisXLabelsMultiple($xLabels, $seriesToXAxis, $ticks = null)
+ {
+ foreach ($xLabels as $index => $labels) {
+ $this->setAxisXLabels($labels, $ticks === null ? null : $ticks[$index], $index);
+ }
+
+ foreach ($seriesToXAxis as $seriesIndex => $xAxisIndex) {
+ $axisName = $this->getXAxis($xAxisIndex);
+
+ // don't actually set xaxis otherwise jqplot will show too many axes. however, we need the xaxis labels, so we add them
+ // to the jqplot config
+ $this->series[$seriesIndex]['_xaxis'] = $axisName;
+ }
+ }
+
+ private function getXAxis($index)
+ {
+ $axisName = 'xaxis';
+ if ($index != 0) {
+ $axisName = 'x' . ($index + 1) . 'axis';
+ }
+ return $axisName;
+ }
}
diff --git a/plugins/CoreVisualizations/JqplotDataGenerator/Evolution.php b/plugins/CoreVisualizations/JqplotDataGenerator/Evolution.php
index cae2577965..814903265b 100644
--- a/plugins/CoreVisualizations/JqplotDataGenerator/Evolution.php
+++ b/plugins/CoreVisualizations/JqplotDataGenerator/Evolution.php
@@ -11,6 +11,13 @@ namespace Piwik\Plugins\CoreVisualizations\JqplotDataGenerator;
use Piwik\Archive\DataTableFactory;
use Piwik\Common;
use Piwik\DataTable;
+use Piwik\DataTable\DataTableInterface;
+use Piwik\DataTable\Row;
+use Piwik\Date;
+use Piwik\Metrics;
+use Piwik\Period;
+use Piwik\Period\Factory;
+use Piwik\Plugins\API\Filter\DataComparisonFilter;
use Piwik\Plugins\CoreVisualizations\JqplotDataGenerator;
use Piwik\Url;
@@ -19,6 +26,18 @@ use Piwik\Url;
*/
class Evolution extends JqplotDataGenerator
{
+ protected function getUnitsForColumnsToDisplay()
+ {
+ $idSite = Common::getRequestVar('idSite', null, 'int');
+
+ $units = [];
+ foreach ($this->properties['columns_to_display'] as $columnName) {
+ $derivedUnit = Metrics::getUnit($columnName, $idSite);
+ $units[$columnName] = empty($derivedUnit) ? false : $derivedUnit;
+ }
+ return $units;
+ }
+
/**
* @param DataTable|DataTable\Map $dataTable
* @param Chart $visualization
@@ -33,11 +52,16 @@ class Evolution extends JqplotDataGenerator
return;
}
- // the X label is extracted from the 'period' object in the table's metadata
- $xLabels = array();
- foreach ($dataTable->getDataTables() as $metadataDataTable) {
- $xLabels[] = $metadataDataTable->getMetadata(DataTableFactory::TABLE_METADATA_PERIOD_INDEX)->getLocalizedShortString(); // eg. "Aug 2009"
- }
+ $dataTables = $dataTable->getDataTables();
+
+ // determine x labels based on both the displayed date range and the compared periods
+ /** @var Period[][] $xLabels */
+ $xLabels = [
+ [], // placeholder for first series
+ ];
+
+ $this->addComparisonXLabels($xLabels, reset($dataTables));
+ $this->addSelectedSeriesXLabels($xLabels, $dataTables);
$units = $this->getUnitsForColumnsToDisplay();
@@ -47,27 +71,37 @@ class Evolution extends JqplotDataGenerator
? : array(false) // make sure that a series is plotted even if there is no data
;
+ $columnsToDisplay = array_values($this->properties['columns_to_display']);
+
+ list($seriesMetadata, $seriesUnits, $seriesLabels, $seriesToXAxis) =
+ $this->getSeriesMetadata($rowsToDisplay, $columnsToDisplay, $units, $dataTables);
+
// collect series data to show. each row-to-display/column-to-display permutation creates a series.
$allSeriesData = array();
- $seriesUnits = array();
foreach ($rowsToDisplay as $rowLabel) {
- foreach ($this->properties['columns_to_display'] as $columnName) {
- $seriesLabel = $this->getSeriesLabel($rowLabel, $columnName);
- $seriesData = $this->getSeriesData($rowLabel, $columnName, $dataTable);
-
- $allSeriesData[$seriesLabel] = $seriesData;
- $seriesUnits[$seriesLabel] = $units[$columnName];
+ foreach ($columnsToDisplay as $columnName) {
+ if (!$this->isComparing) {
+ $this->setNonComparisonSeriesData($allSeriesData, $rowLabel, $columnName, $dataTable);
+ } else {
+ $this->setComparisonSeriesData($allSeriesData, $seriesLabels, $rowLabel, $columnName, $dataTable);
+ }
}
}
$visualization->dataTable = $dataTable;
$visualization->properties = $this->properties;
- $visualization->setAxisXLabels($xLabels);
- $visualization->setAxisYValues($allSeriesData);
+ $visualization->setAxisYValues($allSeriesData, $seriesMetadata);
$visualization->setAxisYUnits($seriesUnits);
- $dataTables = $dataTable->getDataTables();
+ $xLabelStrs = [];
+ $xAxisTicks = [];
+ foreach ($xLabels as $index => $seriesXLabels) {
+ $xLabelStrs[$index] = array_map(function (Period $p) { return $p->getLocalizedLongString(); }, $seriesXLabels);
+ $xAxisTicks[$index] = array_map(function (Period $p) { return $p->getLocalizedShortString(); }, $seriesXLabels);
+ }
+
+ $visualization->setAxisXLabelsMultiple($xLabelStrs, $seriesToXAxis, $xAxisTicks);
if ($this->isLinkEnabled()) {
$idSite = Common::getRequestVar('idSite', null, 'int');
@@ -144,4 +178,131 @@ class Evolution extends JqplotDataGenerator
}
return $linkEnabled;
}
+
+ /**
+ * Each period comparison shows data over different data points than the main series (eg, 2014-02-03,1014-02-06 compared w/ 2015-03-04,2015-03-15).
+ * Though we only display the selected period's x labels, we need to both have the labels for all these data points for tooltips and to stretch
+ * out the selected period x axis, in case it is shorter than one of the compared periods (as in the example above).
+ */
+ private function addComparisonXLabels(array &$xLabels, DataTable $table)
+ {
+ $comparePeriods = $table->getMetadata('comparePeriods') ?: [];
+ $compareDates = $table->getMetadata('compareDates') ?: [];
+
+ // get rid of selected period
+ array_shift($comparePeriods);
+ array_shift($compareDates);
+
+ foreach (array_values($comparePeriods) as $index => $period) {
+ $date = $compareDates[$index];
+
+ $range = Factory::build($period, $date);
+ foreach ($range->getSubperiods() as $subperiod) {
+ $xLabels[$index + 1][] = $subperiod;
+ }
+ }
+ }
+
+ /**
+ * @param array $xLabels
+ * @param DataTable[] $dataTables
+ * @throws \Exception
+ */
+ private function addSelectedSeriesXLabels(array &$xLabels, array $dataTables)
+ {
+ $xTicksCount = count($dataTables);
+ foreach ($xLabels as $labelSeries) {
+ $xTicksCount = max(count($labelSeries), $xTicksCount);
+ }
+
+ /** @var Date $startDate */
+ $startDate = reset($dataTables)->getMetadata(DataTableFactory::TABLE_METADATA_PERIOD_INDEX)->getDateStart();
+ $periodType = reset($dataTables)->getMetadata(DataTableFactory::TABLE_METADATA_PERIOD_INDEX)->getLabel();
+
+ for ($i = 0; $i < $xTicksCount; ++$i) {
+ $period = Factory::build($periodType, $startDate->addPeriod($i, $periodType));
+ $xLabels[0][] = $period;
+ }
+ }
+
+ private function setNonComparisonSeriesData(array &$allSeriesData, $rowLabel, $columnName, DataTable\Map $dataTable)
+ {
+ $seriesLabel = $this->getSeriesLabel($rowLabel, $columnName);
+
+ $seriesData = $this->getSeriesData($rowLabel, $columnName, $dataTable);
+ $allSeriesData[$seriesLabel] = $seriesData;
+ }
+
+ private function setComparisonSeriesData(array &$allSeriesData, array $seriesLabels, $rowLabel, $columnName, DataTable\Map $dataTable)
+ {
+ foreach ($dataTable->getDataTables() as $label => $childTable) {
+ // get the row for this label (use the first if $rowLabel is false)
+ if ($rowLabel === false) {
+ $row = $childTable->getFirstRow();
+ } else {
+ $row = $childTable->getRowFromLabel($rowLabel);
+ }
+
+ if (empty($row)
+ || empty($row->getComparisons())
+ ) {
+ foreach ($seriesLabels as $seriesIndex => $seriesLabelPrefix) {
+ $wholeSeriesLabel = $this->getComparisonSeriesLabelFromCompareSeries($seriesLabelPrefix, $columnName, $rowLabel);
+ $allSeriesData[$wholeSeriesLabel][] = 0;
+ }
+
+ continue;
+ }
+
+ /** @var DataTable $comparisonTable */
+ $comparisonTable = $row->getComparisons();
+ foreach ($comparisonTable->getRows() as $compareRow) {
+ $seriesLabel = $this->getComparisonSeriesLabel($compareRow, $columnName, $rowLabel);
+ $allSeriesData[$seriesLabel][] = $compareRow->getColumn($columnName);
+ }
+
+ $totalsRow = $comparisonTable->getTotalsRow();
+ if ($totalsRow) {
+ $seriesLabel = $this->getComparisonSeriesLabel($totalsRow, $columnName, $rowLabel);
+ $allSeriesData[$seriesLabel][] = $totalsRow->getColumn($columnName);
+ }
+ }
+ }
+
+ private function getSeriesMetadata(array $rowsToDisplay, array $columnsToDisplay, array $units, array $dataTables)
+ {
+ $seriesMetadata = null; // maps series labels to any metadata of the series
+ $seriesUnits = array(); // maps series labels to unit labels
+ $seriesToXAxis = []; // maps series index to x-axis index (groups of metrics for a single comparison will use the same x-axis)
+
+ $table = reset($dataTables);
+ $seriesLabels = $table->getMetadata('comparisonSeries') ?: [];
+ foreach ($rowsToDisplay as $rowIndex => $rowLabel) {
+ foreach ($columnsToDisplay as $columnIndex => $columnName) {
+ if ($this->isComparing) {
+ foreach ($seriesLabels as $seriesIndex => $seriesLabel) {
+ $wholeSeriesLabel = $this->getComparisonSeriesLabelFromCompareSeries($seriesLabel, $columnName, $rowLabel);
+
+ $allSeriesData[$wholeSeriesLabel] = [];
+
+ $metricIndex = $rowIndex * count($columnsToDisplay) + $columnIndex;
+ $seriesMetadata[$wholeSeriesLabel] = [
+ 'metricIndex' => $metricIndex,
+ 'seriesIndex' => $seriesIndex,
+ ];
+
+ $seriesUnits[$wholeSeriesLabel] = $units[$columnName];
+
+ list($periodIndex, $segmentIndex) = DataComparisonFilter::getIndividualComparisonRowIndices($table, $seriesIndex);
+ $seriesToXAxis[] = $periodIndex;
+ }
+ } else {
+ $seriesLabel = $this->getSeriesLabel($rowLabel, $columnName);
+ $seriesUnits[$seriesLabel] = $units[$columnName];
+ }
+ }
+ }
+
+ return [$seriesMetadata, $seriesUnits, $seriesLabels, $seriesToXAxis];
+ }
}
diff --git a/plugins/CoreVisualizations/Visualizations/Graph.php b/plugins/CoreVisualizations/Visualizations/Graph.php
index f7767d9527..bc37ea8747 100644
--- a/plugins/CoreVisualizations/Visualizations/Graph.php
+++ b/plugins/CoreVisualizations/Visualizations/Graph.php
@@ -63,6 +63,14 @@ abstract class Graph extends Visualization
$this->requestConfig->request_parameters_to_modify['format_metrics'] = 1;
+ // if addTotalRow was called in GenerateGraphHTML, add a row containing totals of
+ // different metrics
+ if ($this->config->add_total_row) {
+ $this->requestConfig->request_parameters_to_modify['totals'] = 1;
+ $this->requestConfig->request_parameters_to_modify['keep_totals_row'] = 1;
+ $this->requestConfig->request_parameters_to_modify['keep_totals_row_label'] = Piwik::translate('General_Total');
+ }
+
$this->metricsFormatter = new Numeric();
}
diff --git a/plugins/CoreVisualizations/Visualizations/HtmlTable.php b/plugins/CoreVisualizations/Visualizations/HtmlTable.php
index 4bf717fd1b..a07cc6af2b 100644
--- a/plugins/CoreVisualizations/Visualizations/HtmlTable.php
+++ b/plugins/CoreVisualizations/Visualizations/HtmlTable.php
@@ -42,6 +42,15 @@ class HtmlTable extends Visualization
public function beforeLoadDataTable()
{
$this->checkRequestIsNotForMultiplePeriods();
+
+ if ($this->isComparing()) {
+ // forward the comparisonIdSubtables var if present so it will be used when next/prev links are clicked
+ $comparisonIdSubtables = Common::getRequestVar('comparisonIdSubtables', false, 'string');
+ if (!empty($comparisonIdSubtables)) {
+ $comparisonIdSubtables = Common::unsanitizeInputValue($comparisonIdSubtables);
+ $this->config->custom_parameters['comparisonIdSubtables'] = $comparisonIdSubtables;
+ }
+ }
}
public function beforeRender()
@@ -96,6 +105,12 @@ class HtmlTable extends Visualization
$this->config->columns_to_display = $this->dataTable->getColumns();
}
+ if ($this->isComparing()
+ && !empty($this->dataTable)
+ ) {
+ $this->assignTemplateVar('comparisonTotals', $this->dataTable->getMetadata('comparisonTotals'));
+ }
+
// Note: This needs to be done right before rendering, as otherwise some plugins might change the columns to display again
if ($this->isFlattened()) {
$dimensions = $this->dataTable->getMetadata('dimensions');
@@ -144,6 +159,11 @@ class HtmlTable extends Visualization
});
}
}
+
+ $this->assignTemplateVar('segmentTitlePretty', $this->dataTable->getMetadata('segmentPretty'));
+
+ $period = $this->dataTable->getMetadata('period');
+ $this->assignTemplateVar('periodTitlePretty', $period ? $period->getLocalizedShortString() : '');
}
public function beforeGenericFiltersAreAppliedToLoadedDataTable()
@@ -212,6 +232,11 @@ class HtmlTable extends Visualization
return null;
}
+ public function supportsComparison()
+ {
+ return true;
+ }
+
protected function isFlattened()
{
return $this->requestConfig->flat || Common::getRequestVar('flat', '');
diff --git a/plugins/CoreVisualizations/Visualizations/JqplotGraph/Bar.php b/plugins/CoreVisualizations/Visualizations/JqplotGraph/Bar.php
index 579016f122..0da05ea68e 100644
--- a/plugins/CoreVisualizations/Visualizations/JqplotGraph/Bar.php
+++ b/plugins/CoreVisualizations/Visualizations/JqplotGraph/Bar.php
@@ -50,6 +50,11 @@ class Bar extends JqplotGraph
protected function makeDataGenerator($properties)
{
- return JqplotDataGenerator::factory('bar', $properties);
+ return JqplotDataGenerator::factory('bar', $properties, $this);
+ }
+
+ public function supportsComparison()
+ {
+ return true;
}
}
diff --git a/plugins/CoreVisualizations/Visualizations/JqplotGraph/Evolution.php b/plugins/CoreVisualizations/Visualizations/JqplotGraph/Evolution.php
index 5de7fc5114..337bb233b2 100644
--- a/plugins/CoreVisualizations/Visualizations/JqplotGraph/Evolution.php
+++ b/plugins/CoreVisualizations/Visualizations/JqplotGraph/Evolution.php
@@ -9,8 +9,11 @@
namespace Piwik\Plugins\CoreVisualizations\Visualizations\JqplotGraph;
+use Piwik\API\Request as ApiRequest;
use Piwik\Common;
use Piwik\DataTable;
+use Piwik\Period;
+use Piwik\Period\Factory;
use Piwik\Period\Range;
use Piwik\Plugins\CoreVisualizations\JqplotDataGenerator;
use Piwik\Plugins\CoreVisualizations\Visualizations\JqplotGraph;
@@ -43,7 +46,9 @@ class Evolution extends JqplotGraph
public function beforeLoadDataTable()
{
- $this->calculateEvolutionDateRange();
+ if (!$this->isComparing()) {
+ $this->calculateEvolutionDateRange();
+ }
parent::beforeLoadDataTable();
@@ -55,6 +60,33 @@ class Evolution extends JqplotGraph
}
$this->config->custom_parameters['columns'] = $this->config->columns_to_display;
+
+ if ($this->isComparing()) {
+ $requestArray = $this->request->getRequestArray();
+ $requestArray = ApiRequest::getRequestArrayFromString($requestArray);
+
+ $requestingPeriod = Factory::build($requestArray['period'], $requestArray['date']);
+
+ $this->requestConfig->request_parameters_to_modify['period'] = 'day';
+ $this->requestConfig->request_parameters_to_modify['date'] = $requestingPeriod->getDateStart()->toString() . ',' . $requestingPeriod->getDateEnd()->toString();
+
+ if (!empty($requestArray['comparePeriods'])) {
+ foreach ($requestArray['comparePeriods'] as $index => $comparePeriod) {
+ $compareDate = $requestArray['compareDates'][$index];
+ if (Period::isMultiplePeriod($compareDate, $comparePeriod)) {
+ continue;
+ }
+
+ $comparePeriodObj = Factory::build($comparePeriod, $compareDate);
+
+ $requestArray['comparePeriods'][$index] = 'day';
+ $requestArray['compareDates'][$index] = $comparePeriodObj->getRangeString();
+ }
+
+ $this->requestConfig->request_parameters_to_modify['compareDates'] = $requestArray['compareDates'];
+ $this->requestConfig->request_parameters_to_modify['comparePeriods'] = $requestArray['comparePeriods'];
+ }
+ }
}
public function afterAllFiltersAreApplied()
@@ -70,7 +102,7 @@ class Evolution extends JqplotGraph
protected function makeDataGenerator($properties)
{
- return JqplotDataGenerator::factory('evolution', $properties);
+ return JqplotDataGenerator::factory('evolution', $properties, $this);
}
/**
@@ -199,4 +231,9 @@ class Evolution extends JqplotGraph
return ceil($paddedCount / $steps);
}
+
+ public function supportsComparison()
+ {
+ return true;
+ }
}
diff --git a/plugins/CoreVisualizations/Visualizations/JqplotGraph/Pie.php b/plugins/CoreVisualizations/Visualizations/JqplotGraph/Pie.php
index 3fafdd5e7b..045047049f 100644
--- a/plugins/CoreVisualizations/Visualizations/JqplotGraph/Pie.php
+++ b/plugins/CoreVisualizations/Visualizations/JqplotGraph/Pie.php
@@ -55,6 +55,6 @@ class Pie extends JqplotGraph
protected function makeDataGenerator($properties)
{
- return JqplotDataGenerator::factory('pie', $properties);
+ return JqplotDataGenerator::factory('pie', $properties, $this);
}
}
diff --git a/plugins/CoreVisualizations/Visualizations/Sparkline.php b/plugins/CoreVisualizations/Visualizations/Sparkline.php
index a2bdb09d2c..b59a89ba5d 100644
--- a/plugins/CoreVisualizations/Visualizations/Sparkline.php
+++ b/plugins/CoreVisualizations/Visualizations/Sparkline.php
@@ -11,7 +11,9 @@ namespace Piwik\Plugins\CoreVisualizations\Visualizations;
use Exception;
use Piwik\Common;
use Piwik\DataTable;
+use Piwik\Period;
use Piwik\Plugin\ViewDataTable;
+use Piwik\Site;
/**
* Reads the requested DataTable from the API and prepare data for the Sparkline view.
@@ -21,6 +23,11 @@ class Sparkline extends ViewDataTable
{
const ID = 'sparkline';
+ public function supportsComparison()
+ {
+ return true;
+ }
+
/**
* @see ViewDataTable::main()
* @return mixed
@@ -29,22 +36,61 @@ class Sparkline extends ViewDataTable
{
// If period=range, we force the sparkline to draw daily data points
$period = Common::getRequestVar('period');
- if ($period == 'range') {
+ $date = Common::getRequestVar('date');
+
+ if ($period == 'range'
+ || $this->isComparing()
+ ) {
+ $periodObj = Period\Factory::build($period, $date);
$_GET['period'] = 'day';
+ $_GET['date'] = $periodObj->getRangeString();
+ }
+
+ if ($this->isComparing()) {
+ $this->transformSingleComparisonPeriods();
}
$this->loadDataTableFromAPI();
// then revert the hack for potentially subsequent getRequestVar
$_GET['period'] = $period;
+ $_GET['date'] = $date;
- $values = $this->getValuesFromDataTable($this->dataTable);
- if (empty($values)) {
- $values = array_fill(0, 30, 0);
- }
+ $columnToPlot = $this->getColumnToPlot();
$graph = new \Piwik\Visualization\Sparkline();
- $graph->setValues($values);
+
+ if ($this->isComparing()) {
+ $otherSeries = [];
+
+ $comparisonSeries = $this->getComparisonSeries($this->dataTable);
+ foreach ($comparisonSeries as $seriesName) {
+ $otherSeries[$seriesName] = [];
+ }
+
+ $this->dataTable->filter(function (DataTable $table) use ($comparisonSeries, &$otherSeries, $columnToPlot) {
+ foreach ($table->getRows() as $row) {
+ $comparisons = $row->getComparisons();
+ if (empty($comparisons)) {
+ continue;
+ }
+
+ foreach ($comparisons->getRows() as $comparisonRow) {
+ $compareSeriesPretty = $comparisonRow->getMetadata('compareSeriesPretty');
+ $otherSeries[$compareSeriesPretty][] = $comparisonRow->getColumn($columnToPlot);
+ }
+ }
+ });
+
+ foreach ($otherSeries as $seriesValues) {
+ $seriesValues = $this->ensureValuesEvenIfEmpty($seriesValues);
+ $graph->addSeries($seriesValues);
+ }
+ } else {
+ $values = $this->getValuesFromDataTable($this->dataTable, $columnToPlot);
+ $values = $this->ensureValuesEvenIfEmpty($values);
+ $graph->addSeries($values);
+ }
$height = Common::getRequestVar('height', 0, 'int');
if (!empty($height)) {
@@ -100,7 +146,7 @@ class Sparkline extends ViewDataTable
return $values;
}
- protected function getValuesFromDataTable($dataTable)
+ private function getColumnToPlot()
{
$columns = $this->config->columns_to_display;
@@ -113,6 +159,11 @@ class Sparkline extends ViewDataTable
}
}
+ return $columnToPlot;
+ }
+
+ protected function getValuesFromDataTable($dataTable, $columnToPlot)
+ {
// a Set is returned when using the normal code path to request data from Archives, in all core plugins
// however plugins can also return simple datatable, hence why the sparkline can accept both data types
if ($this->dataTable instanceof DataTable\Map) {
@@ -125,4 +176,41 @@ class Sparkline extends ViewDataTable
return $values;
}
+
+ private function ensureValuesEvenIfEmpty(array $values)
+ {
+ if (empty($values)) {
+ return array_fill(0, 30, 0);
+ }
+ return $values;
+ }
+
+ private function getComparisonSeries(DataTable\DataTableInterface $dataTable)
+ {
+ if ($dataTable instanceof DataTable\Map) {
+ return reset($dataTable->getDataTables())->getMetadata('comparisonSeries') ?: [];
+ } else {
+ return $dataTable->getMetadata('comparisonSeries') ?: [];
+ }
+ }
+
+ private function transformSingleComparisonPeriods()
+ {
+ $comparePeriods = Common::getRequestVar('comparePeriods', $default = [], $type = 'array');
+ $compareDates = Common::getRequestVar('compareDates', $default = [], $type = 'array');
+
+ foreach ($comparePeriods as $index => $comparePeriod) {
+ $compareDate = $compareDates[$index];
+ if (Period::isMultiplePeriod($compareDate, $comparePeriod)) {
+ continue;
+ }
+
+ $periodObj = Period\Factory::build($comparePeriod, $compareDate);
+ $comparePeriods[$index] = 'day';
+ $compareDates[$index] = $periodObj->getRangeString();
+ }
+
+ $this->requestConfig->request_parameters_to_modify['comparePeriods'] = $comparePeriods;
+ $this->requestConfig->request_parameters_to_modify['compareDates'] = $compareDates;
+ }
}
diff --git a/plugins/CoreVisualizations/Visualizations/Sparklines.php b/plugins/CoreVisualizations/Visualizations/Sparklines.php
index 2d11723bef..c3d8556e00 100644
--- a/plugins/CoreVisualizations/Visualizations/Sparklines.php
+++ b/plugins/CoreVisualizations/Visualizations/Sparklines.php
@@ -8,10 +8,14 @@
*/
namespace Piwik\Plugins\CoreVisualizations\Visualizations;
+use Piwik\API\Request;
use Piwik\Common;
use Piwik\DataTable;
use Piwik\Metrics;
+use Piwik\Period\Factory;
use Piwik\Plugin\ViewDataTable;
+use Piwik\Plugins\API\Filter\DataComparisonFilter;
+use Piwik\SettingsPiwik;
use Piwik\Url;
use Piwik\View;
@@ -48,6 +52,11 @@ class Sparklines extends ViewDataTable
return new Sparklines\Config();
}
+ public function supportsComparison()
+ {
+ return true;
+ }
+
/**
* @see ViewDataTable::main()
* @return mixed
@@ -82,6 +91,7 @@ class Sparklines extends ViewDataTable
$view->titleAttributes = $this->config->title_attributes;
$view->footerMessage = $this->config->show_footer_message;
$view->areSparklinesLinkable = $this->config->areSparklinesLinkable();
+ $view->isComparing = $this->isComparing();
$view->title = '';
if ($this->config->show_title) {
@@ -103,13 +113,35 @@ class Sparklines extends ViewDataTable
}
}
- $translations = $this->config->translations;
-
$firstRow = $data->getFirstRow();
+ $comparisons = $firstRow->getComparisons();
+
+ $originalDate = Common::getRequestVar('date');
+ $originalPeriod = Common::getRequestVar('period');
+
+ if ($this->isComparing() && !empty($comparisons)) {
+ $comparisonRows = [];
+ foreach ($comparisons->getRows() as $comparisonRow) {
+ $segment = $comparisonRow->getMetadata('compareSegment');
+ if ($segment === false) {
+ $segment = Request::getRawSegmentFromRequest() ?: '';;
+ }
- foreach ($this->config->getSparklineMetrics() as $sparklineMetric) {
+ $date = $comparisonRow->getMetadata('compareDate');
+ $period = $comparisonRow->getMetadata('comparePeriod');
+
+ $comparisonRows[$segment][$period][$date] = $comparisonRow;
+ }
+ }
+
+ foreach ($this->config->getSparklineMetrics() as $sparklineMetricIndex => $sparklineMetric) {
$column = $sparklineMetric['columns'];
$order = $sparklineMetric['order'];
+ $graphParams = $sparklineMetric['graphParams'];
+
+ if (!isset($order)) {
+ $order = 1000;
+ }
if ($column === 'label') {
continue;
@@ -120,31 +152,87 @@ class Sparklines extends ViewDataTable
continue;
}
- if (!is_array($column)) {
- $column = array($column);
- }
+ $sparklineUrlParams = array(
+ 'columns' => $column,
+ 'module' => $this->requestConfig->getApiModuleToRequest(),
+ 'action' => $this->requestConfig->getApiMethodToRequest()
+ );
+
+ if ($this->isComparing() && !empty($comparisons)) {
+ $periodObj = Factory::build($originalPeriod, $originalDate);
+
+ $sparklineUrlParams['compareSegments'] = [];
+
+ $comparePeriods = $data->getMetadata('comparePeriods');
+ $compareDates = $data->getMetadata('compareDates');
+
+ $compareSegments = $data->getMetadata('compareSegments');
+ foreach ($compareSegments as $segmentIndex => $segment) {
+ $metrics = [];
+ $seriesIndices = [];
- $values = array();
- $descriptions = array();
+ foreach ($comparePeriods as $periodIndex => $period) {
+ $date = $compareDates[$periodIndex];
- foreach ($column as $col) {
- $value = $firstRow->getColumn($col);
+ $compareRow = $comparisonRows[$segment][$period][$date];
+ $segmentPretty = $compareRow->getMetadata('compareSegmentPretty');
+ $periodPretty = $compareRow->getMetadata('comparePeriodPretty');
- if ($value === false) {
- $value = 0;
+ $columnToUse = $this->removeUniqueVisitorsIfNotEnabledForPeriod($column, $period);
+ list($compareValues, $compareDescriptions, $evolutions) = $this->getValuesAndDescriptions($compareRow, $columnToUse);
+
+ foreach ($compareValues as $i => $value) {
+ $metricInfo = [
+ 'value' => $value,
+ 'description' => $compareDescriptions[$i],
+ 'group' => $periodPretty,
+ ];
+
+ if ($periodIndex > 0
+ && isset($evolutions[$i])
+ ) {
+ $metricInfo['evolution'] = $evolutions[$i];
+ }
+
+ $metrics[] = $metricInfo;
+ }
+
+ $seriesIndices[] = DataComparisonFilter::getComparisonSeriesIndex($data, $periodIndex, $segmentIndex);
+ }
+
+ // only set the title (which is the segment) if comparing more than one segment
+ $title = count($compareSegments) > 1 ? $segmentPretty : null;
+
+ $params = array_merge($sparklineUrlParams, [
+ 'segment' => $segment,
+ 'period' => $periodObj->getLabel(),
+ 'date' => $periodObj->getRangeString(),
+ ]);
+ $this->config->addSparkline($params, $metrics, $desc = null, null, ($order * 100) + $segmentIndex, $title, $sparklineMetricIndex, $seriesIndices, $graphParams);
}
+ } else {
+ list($values, $descriptions) = $this->getValuesAndDescriptions($firstRow, $column);
- $values[] = $value;
- $descriptions[] = isset($translations[$col]) ? $translations[$col] : $col;
- }
+ $metrics = [];
+ foreach ($values as $i => $value) {
+ $newMetric = [
+ 'value' => $value,
+ 'description' => $descriptions[$i],
+ ];
- $sparklineUrlParams = array(
- 'columns' => $column,
- 'module' => $this->requestConfig->getApiModuleToRequest(),
- 'action' => $this->requestConfig->getApiMethodToRequest()
- );
+ $metrics[] = $newMetric;
+ }
- $this->config->addSparkline($sparklineUrlParams, $values, $descriptions, null, $order);
+ $evolution = null;
+
+ $computeEvolution = $this->config->compute_evolution;
+ if ($computeEvolution) {
+ $evolution = $computeEvolution(array_combine($column, $values));
+ $newMetric['evolution'] = $evolution;
+ }
+
+ $this->config->addSparkline($sparklineUrlParams, $metrics, $desc = null, $evolution, $order, $title = null, $group = $sparklineMetricIndex, $seriesIndices = null, $graphParams);
+ }
}
}
@@ -161,4 +249,44 @@ class Sparklines extends ViewDataTable
$table->applyQueuedFilters();
}
+
+ private function getValuesAndDescriptions(DataTable\Row $firstRow, $columns)
+ {
+ if (!is_array($columns)) {
+ $columns = array($columns);
+ }
+
+ $translations = $this->config->translations;
+
+ $values = array();
+ $descriptions = array();
+ $evolutions = [];
+
+ foreach ($columns as $col) {
+ $value = $firstRow->getColumn($col);
+
+ if ($value === false) {
+ $value = 0;
+ }
+
+ $evolution = $firstRow->getColumn($col . '_change'); // for comparison rows
+ if ($evolution !== false) {
+ $evolutions[] = ['percent' => ltrim($evolution, '+'), 'tooltip' => ''];
+ }
+
+ $values[] = $value;
+ $descriptions[] = isset($translations[$col]) ? $translations[$col] : $col;
+ }
+
+ return [$values, $descriptions, $evolutions];
+ }
+
+ private function removeUniqueVisitorsIfNotEnabledForPeriod($columns, $period)
+ {
+ if (SettingsPiwik::isUniqueVisitorsEnabled($period)) {
+ return $columns;
+ }
+
+ return array_diff($columns, ['nb_users', 'nb_uniq_visitors']);
+ }
}
diff --git a/plugins/CoreVisualizations/Visualizations/Sparklines/Config.php b/plugins/CoreVisualizations/Visualizations/Sparklines/Config.php
index 86944ccc5a..e5fe3da1c0 100644
--- a/plugins/CoreVisualizations/Visualizations/Sparklines/Config.php
+++ b/plugins/CoreVisualizations/Visualizations/Sparklines/Config.php
@@ -12,6 +12,7 @@ use Piwik\Common;
use Piwik\DataTable\Filter\CalculateEvolutionFilter;
use Piwik\Metrics;
use Piwik\NoAccessException;
+use Piwik\Period\Factory;
use Piwik\Period\Range;
use Piwik\Site;
use Piwik\Url;
@@ -46,6 +47,15 @@ class Config extends \Piwik\ViewDataTable\Config
*/
public $title_attributes = array();
+ /**
+ * If supplied, this function is used to compute the evolution percent displayed next to non-comparison sparkline views.
+ *
+ * The function is passed an array mapping column names with column values.
+ *
+ * @var callable
+ */
+ public $compute_evolution = null;
+
public function __construct()
{
parent::__construct();
@@ -145,12 +155,15 @@ class Config extends \Piwik\ViewDataTable\Config
* @param string|array $metricName Either one metric name (eg 'nb_visits') or an array of metric names
* @param int|null $order Defines the order. The lower the order the earlier the sparkline will be displayed.
* By default the sparkline will be appended to the end.
+ * @param array $graphParams The params to use when changing an associated evolution graph. By default this is determined
+ * from the sparkline URL, but sometimes the sparkline API method may not match the evolution graph API method.
*/
- public function addSparklineMetric($metricName, $order = null)
+ public function addSparklineMetric($metricName, $order = null, $graphParams = null)
{
$this->sparkline_metrics[] = array(
'columns' => $metricName,
- 'order' => $order
+ 'order' => $order,
+ 'graphParams' => $graphParams,
);
}
@@ -167,7 +180,10 @@ class Config extends \Piwik\ViewDataTable\Config
$this->sparklines[] = array(
'url' => '',
'metrics' => array(),
- 'order' => $this->getSparklineOrder($order)
+ 'order' => $this->getSparklineOrder($order),
+
+ // adding this group ensures the sparkline will be placed between individual sparklines, and not in their own group together
+ 'group' => 'placeholder' . count($this->sparklines),
);
}
@@ -194,62 +210,87 @@ class Config extends \Piwik\ViewDataTable\Config
* 'tooltip' => '10 visits in 2015-07-26 compared to 20 visits in 2015-07-25')
* @param int $order Defines the order. The lower the order the earlier the sparkline will be
* displayed. By default the sparkline will be appended to the end.
+ * @param string $title The title of this specific sparkline. It is displayed on the left above the sparkline image.
+ * @param string $group The ID of the group for this sparkline.
+ * @param int $seriesIndices The indexes of each series displayed in the sparkline. This determines what color is used for each series. Mainly used for comparison.
+ * @param array $graphParams The params to use when changing an associated evolution graph. By default this is determined
+ * from the sparkline URL, but sometimes the sparkline API method may not match the evolution graph API method.
* @throws \Exception In case an evolution parameter is set but has wrong data structure
*/
- public function addSparkline($requestParamsForSparkline, $value, $description, $evolution = null, $order = null)
+ public function addSparkline($requestParamsForSparkline, $metricInfos, $description, $evolution = null, $order = null, $title = null, $group = '', $seriesIndices = null, $graphParams = null)
{
$metrics = array();
- if (is_array($value)) {
- $values = $value;
+ if ($description === null && is_array($metricInfos)) {
+ $metrics = $metricInfos;
} else {
- $values = array($value);
- }
+ $value = $metricInfos;
- if (!is_array($description)) {
- $description = array($description);
+ if (is_array($value)) {
+ $values = $value;
+ } else {
+ $values = array($value);
+ }
+
+ if (!is_array($description)) {
+ $description = array($description);
+ }
+
+ if (count($values) === count($description)) {
+ foreach ($values as $index => $value) {
+ $metrics[] = array(
+ 'value' => $value,
+ 'description' => $description[$index]
+ );
+ }
+ } else {
+ $msg = 'The number of values and descriptions need to be the same to add a sparkline. ';
+ $msg .= 'Values: ' . implode(', ', $values). ' Descriptions: ' . implode(', ', $description);
+ throw new \Exception($msg);
+ }
}
if (!empty($requestParamsForSparkline['columns'])
&& is_array($requestParamsForSparkline['columns'])
- && count($requestParamsForSparkline['columns']) === count($values)) {
+ && count($requestParamsForSparkline['columns']) === count($metrics)) {
$columns = array_values($requestParamsForSparkline['columns']);
} elseif (!empty($requestParamsForSparkline['columns'])
&& is_string($requestParamsForSparkline['columns'])
- && count($values) === 1) {
+ && count($metrics) === 1) {
$columns = array($requestParamsForSparkline['columns']);
} else{
$columns = array();
}
- if (count($values) === count($description)) {
- foreach ($values as $index => $value) {
- $metrics[] = array(
- 'column' => isset($columns[$index]) ? $columns[$index] : '',
- 'value' => $value,
- 'description' => $description[$index]
- );
- }
- } else {
- $msg = 'The number of values and descriptions need to be the same to add a sparkline. ';
- $msg .= 'Values: ' . implode(', ', $values). ' Descriptions: ' . implode(', ', $description);
- throw new \Exception($msg);
+ foreach ($metrics as $index => $metricInfo) {
+ $metrics[$index]['column'] = isset($columns[$index]) ? $columns[$index] : '';
}
if (empty($metrics)) {
return;
}
+ $groupedMetrics = [];
+ foreach ($metrics as $metricInfo) {
+ $metricGroup = isset($metricInfo['group']) ? $metricInfo['group'] : '';
+ $groupedMetrics[$metricGroup][] = $metricInfo;
+ }
+
$sparkline = array(
'url' => $this->getUrlSparkline($requestParamsForSparkline),
- 'metrics' => $metrics,
- 'order' => $this->getSparklineOrder($order)
+ 'metrics' => $groupedMetrics,
+ 'order' => $this->getSparklineOrder($order),
+ 'title' => $title,
+ 'group' => $group,
+ 'seriesIndices' => $seriesIndices,
+ 'graphParams' => $graphParams,
);
if (!empty($evolution)) {
if (!is_array($evolution) ||
!array_key_exists('currentValue', $evolution) ||
- !array_key_exists('pastValue', $evolution)) {
+ !array_key_exists('pastValue', $evolution)
+ ) {
throw new \Exception('In order to show an evolution in the sparklines view a currentValue and pastValue array key needs to be present');
}
@@ -300,7 +341,12 @@ class Config extends \Piwik\ViewDataTable\Config
return ($a['order'] < $b['order']) ? -1 : 1;
});
- return $this->sparklines;
+ $sparklines = [];
+ foreach ($this->sparklines as $sparkline) {
+ $group = $sparkline['group'];
+ $sparklines[$group][] = $sparkline;
+ }
+ return $sparklines;
}
private function getSparklineOrder($order)
@@ -390,7 +436,13 @@ class Config extends \Piwik\ViewDataTable\Config
throw new NoAccessException("Website not initialized, check that you are logged in and/or using the correct token_auth.");
}
- $paramDate = Range::getRelativeToEndDate($period, $range, $endDate, $site);
+ if (!isset($paramsToSet['date'])
+ || !Range::isMultiplePeriod($paramsToSet['date'], $period)
+ ) {
+ $paramDate = Range::getRelativeToEndDate($period, $range, $endDate, $site);
+ } else {
+ $paramDate = $paramsToSet['date'];
+ }
$params = array_merge($paramsToSet, array('date' => $paramDate));
return $params;
diff --git a/plugins/CoreVisualizations/angularjs/single-metric-view/single-metric-view.component.js b/plugins/CoreVisualizations/angularjs/single-metric-view/single-metric-view.component.js
index 353a5faebc..c7f2c41dc6 100644
--- a/plugins/CoreVisualizations/angularjs/single-metric-view/single-metric-view.component.js
+++ b/plugins/CoreVisualizations/angularjs/single-metric-view/single-metric-view.component.js
@@ -164,7 +164,7 @@
function getLastPeriodDate() {
var RangePeriod = piwikPeriods.get('range');
var result = RangePeriod.getLastNRange(piwik.period, 2, piwik.currentDateString).startDate;
- return $.datepicker.formatDate('yy-mm-dd', result);
+ return piwikPeriods.format(result);
}
function setWidgetTitle() {
@@ -269,7 +269,7 @@
function getPastPeriodStr() {
var startDate = piwikPeriods.get('range').getLastNRange(piwik.period, 2, piwik.currentDateString).startDate;
var dateRange = piwikPeriods.get(piwik.period).parse(startDate).getDateRange();
- return $.datepicker.formatDate('yy-mm-dd', dateRange[0]) + ',' + $.datepicker.formatDate('yy-mm-dd', dateRange[1]);
+ return piwikPeriods.format(dateRange[0]) + ',' + piwikPeriods.format(dateRange[1]);
}
function isIdGoalSet() {
diff --git a/plugins/CoreVisualizations/javascripts/jqplot.js b/plugins/CoreVisualizations/javascripts/jqplot.js
index a509f401a6..c53cdcb493 100644
--- a/plugins/CoreVisualizations/javascripts/jqplot.js
+++ b/plugins/CoreVisualizations/javascripts/jqplot.js
@@ -634,9 +634,7 @@ function rowEvolutionGetMetricNameFromRow(tr)
* Sets the colors used to render this graph.
*/
_setColors: function () {
- var colorManager = piwik.ColorManager,
- seriesColorNames = ['series1', 'series2', 'series3', 'series4', 'series5',
- 'series6', 'series7', 'series8', 'series9', 'series10'];
+ var colorManager = piwik.ColorManager;
var viewDataTable = $('#' + this.workingDivId).data('uiControlObject').param['viewDataTable'];
@@ -651,11 +649,31 @@ function rowEvolutionGetMetricNameFromRow(tr)
var namespace = graphType + '-graph-colors';
- this.jqplotParams.seriesColors = colorManager.getColors(namespace, seriesColorNames, true);
+ this._setSeriesColors(namespace);
+
this.jqplotParams.grid.background = colorManager.getColor(namespace, 'grid-background');
this.jqplotParams.grid.borderColor = colorManager.getColor(namespace, 'grid-border');
this.tickColor = colorManager.getColor(namespace, 'ticks');
this.singleMetricColor = colorManager.getColor(namespace, 'single-metric-label')
+ },
+
+ _setSeriesColors: function (namespace) {
+ var colorManager = piwik.ColorManager,
+ seriesColorNames = ['series0', 'series1', 'series2', 'series3', 'series4', 'series5',
+ 'series6', 'series7', 'series8', 'series9', 'series10'];
+
+ var comparisonService = piwikHelper.getAngularDependency('piwikComparisonsService');
+ if (comparisonService.isComparing() && typeof this.jqplotParams.series[0].seriesIndex !== 'undefined') {
+ namespace = 'comparison-series-color';
+
+ seriesColorNames = [];
+ this.jqplotParams.series.forEach(function (s) {
+ var seriesColorName = comparisonService.getSeriesColorName(s.seriesIndex, s.metricIndex);
+ seriesColorNames.push(seriesColorName);
+ });
+ }
+
+ this.jqplotParams.seriesColors = colorManager.getColors(namespace, seriesColorNames, true);
}
});
@@ -1040,7 +1058,9 @@ RowEvolutionSeriesToggle.prototype.beforeReplot = function () {
c.markerRenderer.init();
var position = series.gridData[tick];
- c.markerRenderer.draw(position[0], position[1], c.piwikHighlightCanvas._ctx);
+ if (typeof position !== 'undefined') {
+ c.markerRenderer.draw(position[0], position[1], c.piwikHighlightCanvas._ctx);
+ }
}
}
diff --git a/plugins/CoreVisualizations/javascripts/jqplotBarGraph.js b/plugins/CoreVisualizations/javascripts/jqplotBarGraph.js
index cd21308f73..24af4c2e9d 100644
--- a/plugins/CoreVisualizations/javascripts/jqplotBarGraph.js
+++ b/plugins/CoreVisualizations/javascripts/jqplotBarGraph.js
@@ -22,6 +22,9 @@
_setJqplotParameters: function (params) {
JqplotGraphDataTable.prototype._setJqplotParameters.call(this, params);
+ var barMargin = this.data[0].length > 10 ? 2 : 10;
+ var minBarWidth = 10;
+
this.jqplotParams.seriesDefaults = {
renderer: $.jqplot.BarRenderer,
rendererOptions: {
@@ -29,7 +32,7 @@
shadowDepth: 2,
shadowAlpha: .2,
fillToZero: true,
- barMargin: this.data[0].length > 10 ? 2 : 10
+ barMargin: barMargin
}
};
@@ -48,6 +51,18 @@
this.jqplotParams.canvasLegend = {
show: true
};
+
+ var comparisonService = piwikHelper.getAngularDependency('piwikComparisonsService');
+ if (comparisonService.isComparing()) {
+ var seriesCount = this.jqplotParams.series.length;
+ var dataCount = this.data[0].length;
+
+ var totalBars = seriesCount * dataCount;
+ var totalMinWidth = (minBarWidth + barMargin) * totalBars + 50;
+
+ this.$element.find('.piwik-graph').css('min-width', totalMinWidth + 'px');
+ this.$element.css('overflow-x', 'scroll');
+ }
},
_bindEvents: function () {
diff --git a/plugins/CoreVisualizations/javascripts/jqplotEvolutionGraph.js b/plugins/CoreVisualizations/javascripts/jqplotEvolutionGraph.js
index b77add4962..012e5e8bc1 100644
--- a/plugins/CoreVisualizations/javascripts/jqplotEvolutionGraph.js
+++ b/plugins/CoreVisualizations/javascripts/jqplotEvolutionGraph.js
@@ -96,19 +96,53 @@
.on('jqplotPiwikTickOver', function (e, tick) {
lastTick = tick;
var label;
- if (typeof self.jqplotParams.axes.xaxis.labels != 'undefined') {
- label = self.jqplotParams.axes.xaxis.labels[tick];
- } else {
- label = self.jqplotParams.axes.xaxis.ticks[tick];
- }
- var text = [];
- for (var d = 0; d < self.data.length; d++) {
- var value = self.formatY(self.data[d][tick], d);
+ var dataByAxis = {};
+ for (var d = 0; d < self.data.length; ++d) {
+ var valueUnformatted = self.data[d][tick];
+ if (typeof valueUnformatted === 'undefined' || valueUnformatted === null) {
+ continue;
+ }
+
+ var axis = self.jqplotParams.series[d]._xaxis || 'xaxis';
+ if (!dataByAxis[axis]) {
+ dataByAxis[axis] = [];
+ }
+
+ var value = self.formatY(valueUnformatted, d);
var series = self.jqplotParams.series[d].label;
- text.push('<strong>' + value + '</strong> ' + piwikHelper.htmlEntities(series));
+
+ var seriesColor = self.jqplotParams.seriesColors[d];
+
+ dataByAxis[axis].push('<span class="tooltip-series-color" style="background-color: ' + seriesColor + ';"/>' + '<strong>' + value + '</strong> ' + piwikHelper.htmlEntities(series));
+ }
+
+ var xAxisCount = 0;
+ Object.keys(self.jqplotParams.axes).forEach(function (axis) {
+ if (axis.substring(0, 1) === 'x') {
+ ++xAxisCount;
+ }
+ });
+
+ var content = '';
+ for (var i = 0; i < xAxisCount; ++i) {
+ var axisName = i === 0 ? 'xaxis' : 'x' + (i + 1) + 'axis';
+ if (!dataByAxis[axisName] || !dataByAxis[axisName].length) {
+ continue;
+ }
+
+ if (typeof self.jqplotParams.axes[axisName].labels != 'undefined') {
+ label = self.jqplotParams.axes[axisName].labels[tick];
+ } else {
+ label = self.jqplotParams.axes[axisName].ticks[tick];
+ }
+
+ if (typeof label === 'undefined') { // sanity check
+ continue;
+ }
+
+ content += '<h3 class="evolution-tooltip-header">'+piwikHelper.htmlEntities(label)+'</h3>'+dataByAxis[axisName].join('<br />');
}
- var content = '<h3>'+piwikHelper.htmlEntities(label)+'</h3>'+text.join('<br />');
$(this).tooltip({
track: true,
diff --git a/plugins/CoreVisualizations/stylesheets/dataTableVisualizations.less b/plugins/CoreVisualizations/stylesheets/dataTableVisualizations.less
index ff2118b952..15edf722c0 100644
--- a/plugins/CoreVisualizations/stylesheets/dataTableVisualizations.less
+++ b/plugins/CoreVisualizations/stylesheets/dataTableVisualizations.less
@@ -23,4 +23,18 @@
}
.widget .dataTableVizBar .jqplot-graph {
padding: 0 10px 10px 10px;
+}
+
+.tooltip-series-color {
+ display: inline-block;
+ width: 11px;
+ height: 11px;
+ border-radius: 6px;
+ margin-right: 3px;
+ position: relative;
+ top: 1px;
+}
+
+.ui-tooltip h3.evolution-tooltip-header {
+ margin-top: 4px;
} \ No newline at end of file
diff --git a/plugins/CoreVisualizations/templates/_dataTableViz_htmlTable.twig b/plugins/CoreVisualizations/templates/_dataTableViz_htmlTable.twig
index 38c2101168..a9ac3f4dae 100644
--- a/plugins/CoreVisualizations/templates/_dataTableViz_htmlTable.twig
+++ b/plugins/CoreVisualizations/templates/_dataTableViz_htmlTable.twig
@@ -19,11 +19,13 @@
</tr>
{% endif %}
{% else %}
+ {% set rowIndex = properties.filter_offset|default(0) + 1 %}
{%- for rowId, row in dataTable.getRows() -%}
{%- set rowHasSubtable = not subtablesAreDisabled and row.getIdSubDataTable() and properties.subtable_controller_action is not null -%}
{%- set rowSubtableId = row.getMetadata('idsubdatatable_in_db')|default(row.getIdSubDataTable()) -%}
{%- set isSummaryRow = rowId == constant('Piwik\\DataTable::ID_SUMMARY_ROW') or row.getMetadata('is_summary') -%}
{%- set shouldHighlightRow = isSummaryRow and properties.highlight_summary_row -%}
+ {% set dimensions = dataTable.getMetadata('dimensions')|default([]) %}
{# display this row if it doesn't have a subtable or if we don't replace the row with the subtable #}
{%- set showRow = subtablesAreDisabled
@@ -36,21 +38,38 @@
{% if row.getMetadata('segment') is not false %} data-segment-filter="{{ row.getMetadata('segment')|e('html_attr') }}"{% endif %}
{% if row.getMetadata('url') is not false %} data-url-label="{{ row.getMetadata('url')|rawSafeDecoded }}"{% endif %}
data-row-metadata="{{ row.getMetadata|json_encode|e('html_attr') }}"
- class="{{ row.getMetadata('css_class') }} {% if rowHasSubtable %}subDataTable{% endif %}{% if shouldHighlightRow %} highlight{% endif %}{% if isSummaryRow %} summaryRow{% endif %}"
+ class="{{ row.getMetadata('css_class') }} {% if rowHasSubtable %}subDataTable{% endif %}{% if shouldHighlightRow %} highlight{% endif %}{% if isSummaryRow %} summaryRow{% endif %} {% if isComparing %}parentComparisonRow{% endif %}"
{% if rowHasSubtable %}title="{{ 'CoreHome_ClickRowToExpandOrContract'|translate }}"{% endif %}>
{% for column in properties.columns_to_display %}
{% set cellAttributes = visualization.getCellHtmlAttributes(row, column) %}
- <td {% if cellAttributes is not empty %}{% for name, value in cellAttributes %}{{ name|e('html') }}="{{ value|e('html_attr') }}" {% endfor %}{% endif %}>
+ <td class="{% if column =='label' or column in dimensions %}label{% else %}column{% endif %} {{ cellAttributes.class|default|e('html_attr') }}"
+ {% if cellAttributes is not empty %}{% for name, value in cellAttributes %}{{ name|e('html') }}="{{ value|e('html_attr') }}" {% endfor %}{% endif %}
+ >
+ {% if isComparing and column == 'label' %}
+ <span class="prefix-numeral">{{ rowIndex }}.</span>
+ {% endif %}
+
{% include "@CoreHome/_dataTableCell.twig" with properties %}
</td>
{% endfor %}
</tr>
+
+ {% if row.getComparisons() %}
+ {% include "@CoreVisualizations/_dataTableViz_htmlTable_comparisons.twig" with {
+ 'comparedRow': row,
+ 'dataTable': row.getComparisons(),
+ 'rootDataTable': dataTable,
+ 'dimensions': dimensions,
+ } %}
+ {% endif %}
{% endif %}
{# display subtable if present and showing expanded datatable #}
{% if properties.show_expanded|default(false) and rowHasSubtable %}
{% include "@CoreVisualizations/_dataTableViz_htmlTable.twig" with {'dataTable': row.getSubtable(), 'idSubtable': rowSubtableId} %}
{% endif %}
+
+ {% set rowIndex = rowIndex + 1 %}
{%- endfor -%}
{% if dataTable.getTotalsRow and properties.show_totals_row %}
{% set row = dataTable.getTotalsRow %}
@@ -58,7 +77,7 @@
<tr class="{{ row.getMetadata('css_class') }} totalsRow"
title="Total values for this table">
{% for column in properties.columns_to_display %}
- <td>
+ <td class="{% if column =='label' %}label{% else %}column{% endif %}">
{% include "@CoreHome/_dataTableCell.twig" with properties %}
</td>
{% endfor %}
diff --git a/plugins/CoreVisualizations/templates/_dataTableViz_htmlTable_comparisons.twig b/plugins/CoreVisualizations/templates/_dataTableViz_htmlTable_comparisons.twig
new file mode 100644
index 0000000000..fec4652ed4
--- /dev/null
+++ b/plugins/CoreVisualizations/templates/_dataTableViz_htmlTable_comparisons.twig
@@ -0,0 +1,83 @@
+{% if properties.columns_to_display is not empty %}
+{% set lastSeenComparePeriod = false %}
+{% set comparedPeriodPretty = dataTable.getFirstRow().getMetadata('comparePeriodPretty') %}
+{% set isComparingSegments = rootDataTable.getMetadata('compareSegments')|default([])|length > 1 %}
+{% set isComparingPeriods = rootDataTable.getMetadata('comparePeriods')|default([])|length > 1 %}
+{%- for rowId, row in dataTable.getRows() -%}
+ {% set comparePeriod = row.getMetadata('comparePeriodPretty') %}
+ {% if lastSeenComparePeriod != comparePeriod and isComparingSegments and isComparingPeriods %}
+ <tr class="comparePeriod">
+ <td class="label">
+ {{ comparePeriod }}
+ </td>
+
+ {# add extra empty columns for sticky column scrolling to work #}
+ {% for column in properties.columns_to_display %}
+ {% if column != 'label' %}
+ <td class="column">&nbsp;</td>
+ {% endif %}
+ {% endfor %}
+ </tr>
+ {% endif %}
+ {% set overrideParams = {} %}
+
+ {% if row.getMetadata('compareSegment')|default is not empty %}{% set overrideParams = overrideParams|merge({ segment: row.getMetadata('compareSegment'), compareSegments: '' }) %}{% endif %}
+ {% if row.getMetadata('comparePeriod')|default is not empty %}{% set overrideParams = overrideParams|merge({ period: row.getMetadata('comparePeriod'), comparePeriods: '' }) %}{% endif %}
+ {% if row.getMetadata('compareDate')|default is not empty %}{% set overrideParams = overrideParams|merge({ date: row.getMetadata('compareDate'), compareDates: '' }) %}{% endif %}
+ {% set idSubtable = row.getMetadata('idsubdatatable') %}
+ {% set seriesIndex = loop.index0 %}
+ <tr
+ {% if idSubtable != false %}data-idsubtable="{{ idSubtable|e('html_attr') }}"{% endif %}
+ data-comparison-series="{{ seriesIndex }}"
+ class="comparisonRow"
+ data-param-override="{{ overrideParams|json_encode|e('html_attr') }}" data-label="{{ comparedRow.getColumn('label')|e('html_attr') }}"
+ {% if row.getMetadata('segment') is not false %}data-segment-filter="{{ row.getMetadata('segment')|e('html_attr') }}"{% endif %}
+ >
+ <td class="label">
+ {% if isComparingSegments %}
+ {%- set comparisonLabel = row.getMetadata('compareSegmentPretty') -%}
+ {% else %}
+ {%- set comparisonLabel = comparePeriod -%}
+ {% endif %}
+ <span class="label">{{ comparisonLabel }}</span>
+ </td>
+ {% for dimension in dimensions %}
+ {% if loop.index0 != 0 %}
+ <td class="label">
+ &nbsp;
+ </td>
+ {% endif %}
+ {% endfor %}
+ {% for column in properties.columns_to_display %}
+ {% if column != 'label' and column not in dimensions %}
+ {% set columnValue = row.getColumn(column) %}
+ <td class="column">
+ {% set rowComparisonTotals = comparisonTotals[seriesIndex].totals|default(null) %}
+
+ {% set comparisonTooltipSuffix = '' %}
+ {% set columnChange = false %}
+ {% if row.getColumn(column ~ '_change')|default is not empty %}
+ {% set columnChange = row.getColumn(column ~ '_change')|default('+0%') %}
+ {% set comparisonTooltipSuffix = 'General_ComparisonRatioTooltip'|translate(columnChange, row.getMetadata('compareSegmentPretty'), comparedPeriodPretty) %}
+ {% endif %}
+
+ {% include "@CoreVisualizations/_dataTableViz_htmlTable_ratio.twig" with {
+ 'changePercentage': columnChange,
+ 'totals': rowComparisonTotals,
+ 'label': comparedRow.getColumn('label'),
+ 'labelColumn': properties.columns_to_display|first,
+ 'changePercantage': columnChange,
+ 'forceZero': true,
+ 'tooltipSuffix': comparisonTooltipSuffix,
+ 'translations': properties.translations,
+ 'segmentTitlePretty': row.getMetadata('compareSegmentPretty'),
+ 'periodTitlePretty': row.getMetadata('comparePeriodPretty')
+ } %}
+ <span class="value">{{ columnValue|default(0)|number(2,0)|rawSafeDecoded }}</span>
+ </td>
+ {% endif %}
+ {% endfor %}
+ </tr>
+ {% set lastSeenComparePeriod = comparePeriod %}
+{%- endfor -%}
+{% endif %} \ No newline at end of file
diff --git a/plugins/CoreVisualizations/templates/_dataTableViz_htmlTable_ratio.twig b/plugins/CoreVisualizations/templates/_dataTableViz_htmlTable_ratio.twig
new file mode 100644
index 0000000000..8d14bb126d
--- /dev/null
+++ b/plugins/CoreVisualizations/templates/_dataTableViz_htmlTable_ratio.twig
@@ -0,0 +1,26 @@
+{% if column in properties.report_ratio_columns and (column in totals|keys or forceZero|default) -%}
+ {% set reportTotal = totals[column]|default(0) %}
+ {% if siteTotalRow|default is not empty %}
+ {% set siteTotal = siteTotalRow.getColumn(column) %}
+ {% elseif siteSummary is defined and siteSummary is not empty and siteSummary.getFirstRow %}
+ {% set siteTotal = siteSummary.getFirstRow.getColumn(column) %}
+ {% else %}
+ {% set siteTotal = 0 %}
+ {% endif %}
+
+ {% set rowPercentage = row.getColumn(column)|percentage(reportTotal, 1) %}
+ {% set metricTitle = translations[column]|default(column) %}
+
+ {% set reportRatioTooltip = 'General_ReportRatioTooltip'|translate(label, rowPercentage|e('html_attr'), reportTotal|e('html_attr'), metricTitle|e('html_attr'), '"' ~ segmentTitlePretty ~ '"', translations[labelColumn]|default(labelColumn)|e('html_attr')) %}
+
+ {% if siteTotal and siteTotal > reportTotal %}
+ {% set totalPercentage = row.getColumn(column)|percentage(siteTotal, 1) %}
+ {% set totalRatioTooltip = 'General_TotalRatioTooltip'|translate(totalPercentage, siteTotal|number(2,0), metricTitle, periodTitlePretty) %}
+ {% else %}
+ {% set totalRatioTooltip = '' %}
+ {% endif %}
+
+ <span class="ratio"
+ title="{{ reportRatioTooltip|rawSafeDecoded|raw }} {{ totalRatioTooltip|rawSafeDecoded|e('html_attr') }}{% if tooltipSuffix|default is not empty %}<br/><br/> {{ tooltipSuffix|rawSafeDecoded|e('html_attr') }}{% endif %}"
+ >&nbsp;{{ rowPercentage }} {% if changePercantage|default is not empty %}({{ changePercentage }}){% endif %}</span>
+{%- endif %}
diff --git a/plugins/CoreVisualizations/templates/_dataTableViz_sparklines.twig b/plugins/CoreVisualizations/templates/_dataTableViz_sparklines.twig
index 5c8390d914..e1932c523b 100644
--- a/plugins/CoreVisualizations/templates/_dataTableViz_sparklines.twig
+++ b/plugins/CoreVisualizations/templates/_dataTableViz_sparklines.twig
@@ -10,12 +10,23 @@
<div class="row">
<div class="col m6">
{% endif %}
-
- {% for key, sparkline in sparklines %}
- {% if key is even %}
+ {% if sparklines|length == 1 %}
+ {% for key, sparkline in sparklines|first %}
+ {% if loop.index0 is even %}
{{ macros.singleSparkline(sparkline, allMetricsDocumentation, areSparklinesLinkable) }}
{% endif %}
{% endfor %}
+ {% else %}
+ {% for group in sparklines %}
+ {% if loop.index0 is even %}
+ <div>
+ {% for key, sparkline in group %}
+ {{ macros.singleSparkline(sparkline, allMetricsDocumentation, areSparklinesLinkable) }}
+ {% endfor %}
+ </div>
+ {% endif %}
+ {% endfor %}
+ {% endif %}
{% if not isWidget %}
<br style="clear:left"/>
@@ -23,11 +34,23 @@
<div class="col m6">
{% endif %}
+ {% if sparklines|length == 1 %}
{% for key, sparkline in sparklines %}
- {% if key is odd %}
+ {% if loop.index0 is odd %}
{{ macros.singleSparkline(sparkline, allMetricsDocumentation, areSparklinesLinkable) }}
{% endif %}
{% endfor %}
+ {% else %}
+ {% for group in sparklines %}
+ {% if loop.index0 is odd %}
+ <div>
+ {% for key, sparkline in group %}
+ {{ macros.singleSparkline(sparkline, allMetricsDocumentation, areSparklinesLinkable) }}
+ {% endfor %}
+ </div>
+ {% endif %}
+ {% endfor %}
+ {% endif %}
<br style="clear:left"/>
diff --git a/plugins/CoreVisualizations/templates/macros.twig b/plugins/CoreVisualizations/templates/macros.twig
index 7562b88f78..bc754f2c19 100644
--- a/plugins/CoreVisualizations/templates/macros.twig
+++ b/plugins/CoreVisualizations/templates/macros.twig
@@ -1,36 +1,52 @@
+
+{% macro sparklineEvolution(evolution) %}
+ {% set evolutionPretty = evolution.percent %}
+
+ {% if evolution.percent < 0 %}
+ {% set evolutionClass = 'negative-evolution' %}
+ {% set evolutionIcon = 'arrow_down.png' %}
+ {% elseif evolution.percent == 0 %}
+ {% set evolutionClass = 'neutral-evolution' %}
+ {% set evolutionIcon = 'stop.png' %}
+ {% else %}
+ {% set evolutionClass = 'positive-evolution' %}
+ {% set evolutionIcon = 'arrow_up.png' %}
+ {% set evolutionPretty = '+' ~ evolution.percent %}
+ {% endif %}
+
+ <span class="metricEvolution" title="{{ evolution.tooltip|rawSafeDecoded|e('html_attr') }}">
+ <img style="padding-right:4px" src="plugins/MultiSites/images/{{ evolutionIcon }}"/>
+ <strong class="{{ evolutionClass }}">{{ evolutionPretty }}</strong></span>
+{% endmacro %}
+
{% macro singleSparkline(sparkline, allMetricsDocumentation, areSparklinesLinkable) %}
- <div class="sparkline {% if areSparklinesLinkable is defined and not areSparklinesLinkable %}notLinkable{% endif %}">
- {% if sparkline.url %}{{ sparkline(sparkline.url)|raw }}{% endif %}
+ <div class="sparkline {% if areSparklinesLinkable is defined and not areSparklinesLinkable %}notLinkable{% endif %}"
+ {% if sparkline.seriesIndices|default is not empty %}data-series-indices="{{ sparkline.seriesIndices|json_encode|e('html_attr') }}"{% endif %}
+ {% if sparkline.graphParams|default is not empty %}data-graph-params="{{ sparkline.graphParams|json_encode|e('html_attr') }}"{% endif %}
+ >
+ <div>
+ {% if sparkline.title|default is not empty %}<h6 class="sparkline-title" title="{{ sparkline.title|rawSafeDecoded|e('html_attr') }}">{{ sparkline.title }}</h6>{% endif %}
+ {% if sparkline.url %}{{ sparkline(sparkline.url)|raw }}{% endif %}
+ </div>
<div>
- {% for metric in sparkline.metrics %}
- <span {% if allMetricsDocumentation[metric.column] is defined and allMetricsDocumentation[metric.column] %}title="{{ allMetricsDocumentation[metric.column] }}"{% endif %}>
- {% if '%s' in metric.description -%}
- {{ metric.description|translate("<strong>"~metric.value~"</strong>")|raw }}
- {%- else %}
- <strong>{{ metric.value }}</strong> {{ metric.description }}
- {%- endif %}{% if not loop.last %}, {% endif %}
- </span>
+ {% for groupName, group in sparkline.metrics %}
+ {% if groupName is not empty %}<span class="metric-group-title">{{ groupName }}</span>{% endif %}
+ {% for metric in group %}
+ <span class="sparkline-metrics" {% if allMetricsDocumentation[metric.column] is defined and allMetricsDocumentation[metric.column] %}title="{{ allMetricsDocumentation[metric.column] }}"{% endif %}>
+ {% if '%s' in metric.description -%}
+ {{ metric.description|translate("<strong>"~metric.value~"</strong>")|raw }}
+ {%- else %}
+ <strong>{{ metric.value }}</strong> {{ metric.description }}
+ {%- endif %}{% if not loop.last %}, {% endif %}
+ </span>
+ {% if metric.evolution is defined %}
+ {{ _self.sparklineEvolution(metric.evolution) }}
+ {% endif %}
+ {% endfor %}
{% endfor %}
{% if sparkline.evolution is defined %}
-
- {% set evolutionPretty = sparkline.evolution.percent %}
-
- {% if sparkline.evolution.percent < 0 %}
- {% set evolutionClass = 'negative-evolution' %}
- {% set evolutionIcon = 'arrow_down.png' %}
- {% elseif sparkline.evolution.percent == 0 %}
- {% set evolutionClass = 'neutral-evolution' %}
- {% set evolutionIcon = 'stop.png' %}
- {% else %}
- {% set evolutionClass = 'positive-evolution' %}
- {% set evolutionIcon = 'arrow_up.png' %}
- {% set evolutionPretty = '+' ~ sparkline.evolution.percent %}
- {% endif %}
-
- <span class="metricEvolution" title="{{ sparkline.evolution.tooltip }}"><img
- style="padding-right:4px" src="plugins/MultiSites/images/{{ evolutionIcon }}"/>
- <strong class="{{ evolutionClass }}">{{ evolutionPretty }}</strong></span>
+ {{ _self.sparklineEvolution(sparkline.evolution) }}
{% endif %}
</div>
</div>
diff --git a/plugins/CoreVisualizations/tests/Integration/SparklinesConfigTest.php b/plugins/CoreVisualizations/tests/Integration/SparklinesConfigTest.php
index 33fb6be6ab..6847ac7b2c 100644
--- a/plugins/CoreVisualizations/tests/Integration/SparklinesConfigTest.php
+++ b/plugins/CoreVisualizations/tests/Integration/SparklinesConfigTest.php
@@ -65,12 +65,18 @@ class SparklinesConfigTest extends IntegrationTestCase
$expectedSparkline = array(
'url' => '?period=day&date=2012-03-06,2012-04-04&idSite=1&module=CoreHome&action=renderMe&viewDataTable=sparkline',
'metrics' => array (
- array ('column' => '', 'value' => 10, 'description' => 'Visits'),
+ '' => [
+ array ('value' => 10, 'description' => 'Visits', 'column' => ''),
+ ],
),
- 'order' => 999
+ 'order' => 999,
+ 'title' => null,
+ 'group' => '',
+ 'seriesIndices' => null,
+ 'graphParams' => null,
);
- $this->assertSame(array($expectedSparkline), $this->config->getSortedSparklines());
+ $this->assertSame(array($expectedSparkline), $this->config->getSortedSparklines()['']);
}
public function test_addSparkline_shouldAddAMinimalSparklineWithOneValueAndUseDefaultOrderWithColumn()
@@ -79,10 +85,10 @@ class SparklinesConfigTest extends IntegrationTestCase
$params['columns'] = 'nb_visits';
$this->config->addSparkline($params, $value = 10, $description = 'Visits');
- $expectedSparkline = array('column' => 'nb_visits', 'value' => 10, 'description' => 'Visits');
+ $expectedSparkline = array('value' => 10, 'description' => 'Visits', 'column' => 'nb_visits');
$sparklines = $this->config->getSortedSparklines();
- $this->assertSame(array($expectedSparkline), $sparklines[0]['metrics']);
+ $this->assertSame(array($expectedSparkline), $sparklines[''][0]['metrics']['']);
}
public function test_addSparkline_shouldAddSparklineWithMultipleValues()
@@ -92,9 +98,9 @@ class SparklinesConfigTest extends IntegrationTestCase
$sparklines = $this->config->getSortedSparklines();
$this->assertSame(array (
- array ('column' => '', 'value' => 10, 'description' => 'Visits'),
- array ('column' => '', 'value' => 20, 'description' => 'Actions'),
- ), $sparklines[0]['metrics']);
+ array ('value' => 10, 'description' => 'Visits', 'column' => ''),
+ array ('value' => 20, 'description' => 'Actions', 'column' => ''),
+ ), $sparklines[''][0]['metrics']['']);
}
public function test_addSparkline_shouldAddSparklinesMultipleValuesWithColumns()
@@ -105,12 +111,12 @@ class SparklinesConfigTest extends IntegrationTestCase
$this->config->addSparkline($params, $values = array(10, 20), $description = array('Visits', 'Actions'));
$expectedSparkline = array(
- array ('column' => 'nb_visits', 'value' => 10, 'description' => 'Visits'),
- array ('column' => 'nb_actions', 'value' => 20, 'description' => 'Actions')
+ array ('value' => 10, 'description' => 'Visits', 'column' => 'nb_visits'),
+ array ('value' => 20, 'description' => 'Actions', 'column' => 'nb_actions')
);
$sparklines = $this->config->getSortedSparklines();
- $this->assertSame($expectedSparkline, $sparklines[0]['metrics']);
+ $this->assertSame($expectedSparkline, $sparklines[''][0]['metrics']['']);
}
/**
@@ -133,7 +139,7 @@ class SparklinesConfigTest extends IntegrationTestCase
$this->assertSame(array (
'percent' => '-52.4%',
'tooltip' => '1 visit compared to 2 visits'
- ), $sparklines[0]['evolution']);
+ ), $sparklines[''][0]['evolution']);
}
public function test_addSparkline_shouldAddOrder()
@@ -142,7 +148,7 @@ class SparklinesConfigTest extends IntegrationTestCase
$sparklines = $this->config->getSortedSparklines();
- $this->assertSame(42, $sparklines[0]['order']);
+ $this->assertSame(42, $sparklines[''][0]['order']);
}
public function test_addSparkline_shouldBeAbleToBuildSparklineUrlBasedOnGETparams()
@@ -154,7 +160,166 @@ class SparklinesConfigTest extends IntegrationTestCase
$sparklines = $this->config->getSortedSparklines();
- $this->assertSame('?columns=nb_visits&viewDataTable=sparkline&date=2012-03-06,2012-04-04', $sparklines[0]['url']);
+ $this->assertSame('?columns=nb_visits&viewDataTable=sparkline&date=2012-03-06,2012-04-04', $sparklines[''][0]['url']);
+ }
+
+ public function test_addSparkline_shouldAddSparklinesWithGroups()
+ {
+ $this->config->addSparkline($this->sparklineParams(), $value = 10, $description = 'Visits', $evolution = null, $order = '4', $title = 'title1', $group = 'one');
+ $this->config->addSparkline($this->sparklineParams(), $value = 11, $description = 'Visits1', $evolution = null, $order = '1', $title = 'title2', $group = 'one');
+ $this->config->addSparkline($this->sparklineParams(), $value = 12, $description = 'Visits2', $evolution = null, $order = '3', $title = 'title3', $group = 'two');
+ $this->config->addSparkline($this->sparklineParams(), $value = 13, $description = 'Visits3', $evolution = null, $order = '6', $title = 'title4', $group = 'two');
+
+ $sparklines = $this->config->getSortedSparklines();
+ $expectedSparklines = [
+ 'one' => [
+ [
+ 'url' => '?period=day&date=2012-03-06,2012-04-04&idSite=1&module=CoreHome&action=renderMe&viewDataTable=sparkline',
+ 'metrics' => [
+ '' => [
+ 0 => [
+ 'value' => 11,
+ 'description' => 'Visits1',
+ 'column' => '',
+ ],
+ ],
+ ],
+ 'order' => 1,
+ 'title' => 'title2',
+ 'group' => 'one',
+ 'seriesIndices' => null,
+ 'graphParams' => null,
+ ],
+ [
+ 'url' => '?period=day&date=2012-03-06,2012-04-04&idSite=1&module=CoreHome&action=renderMe&viewDataTable=sparkline',
+ 'metrics' => [
+ '' => [
+ 0 => [
+ 'value' => 10,
+ 'description' => 'Visits',
+ 'column' => '',
+ ],
+ ],
+ ],
+ 'order' => 4,
+ 'title' => 'title1',
+ 'group' => 'one',
+ 'seriesIndices' => null,
+ 'graphParams' => null,
+ ],
+ ],
+ 'two' => [
+ [
+ 'url' => '?period=day&date=2012-03-06,2012-04-04&idSite=1&module=CoreHome&action=renderMe&viewDataTable=sparkline',
+ 'metrics' => [
+ '' => [
+ 0 => [
+ 'value' => 12,
+ 'description' => 'Visits2',
+ 'column' => '',
+ ],
+ ],
+ ],
+ 'order' => 3,
+ 'title' => 'title3',
+ 'group' => 'two',
+ 'seriesIndices' => null,
+ 'graphParams' => null,
+ ],
+ [
+ 'url' => '?period=day&date=2012-03-06,2012-04-04&idSite=1&module=CoreHome&action=renderMe&viewDataTable=sparkline',
+ 'metrics' => [
+ '' => [
+ 0 => [
+ 'value' => 13,
+ 'description' => 'Visits3',
+ 'column' => '',
+ ],
+ ],
+ ],
+ 'order' => 6,
+ 'title' => 'title4',
+ 'group' => 'two',
+ 'seriesIndices' => null,
+ 'graphParams' => null,
+ ],
+ ],
+ ];
+
+ $this->assertSame($expectedSparklines, $sparklines);
+ }
+
+ public function test_addSparkline_shouldAddSparklineMetricsWithGroups()
+ {
+ $metricInfos = [
+ [
+ 'value' => 'v1',
+ 'description' => 'd1',
+ 'group' => 'g1',
+ ],
+ [
+ 'value' => 'v2',
+ 'description' => 'd3',
+ 'group' => 'g1',
+ ],
+ [
+ 'value' => 'v3',
+ 'description' => 'd3',
+ 'group' => 'g3',
+ ],
+ [
+ 'value' => 'v4',
+ 'description' => 'd4',
+ 'group' => 'g1',
+ ],
+ ];
+ $this->config->addSparkline($this->sparklineParams(), $metricInfos, $description = null);
+
+ $sparklines = $this->config->getSortedSparklines();
+ $expectedSparklines = [
+ '' => [
+ [
+ 'url' => '?period=day&date=2012-03-06,2012-04-04&idSite=1&module=CoreHome&action=renderMe&viewDataTable=sparkline',
+ 'metrics' => [
+ 'g1' => [
+ 0 => [
+ 'value' => 'v1',
+ 'description' => 'd1',
+ 'group' => 'g1',
+ 'column' => '',
+ ],
+ 1 => [
+ 'value' => 'v2',
+ 'description' => 'd3',
+ 'group' => 'g1',
+ 'column' => '',
+ ],
+ 2 => [
+ 'value' => 'v4',
+ 'description' => 'd4',
+ 'group' => 'g1',
+ 'column' => '',
+ ],
+ ],
+ 'g3' => [
+ 0 => [
+ 'value' => 'v3',
+ 'description' => 'd3',
+ 'group' => 'g3',
+ 'column' => '',
+ ],
+ ],
+ ],
+ 'order' => 999,
+ 'title' => null,
+ 'group' => '',
+ 'seriesIndices' => null,
+ 'graphParams' => null,
+ ],
+ ],
+ ];
+
+ $this->assertSame($expectedSparklines, $sparklines);
}
private function sparklineParams($params = array())
diff --git a/plugins/CoreVisualizations/tests/Unit/SparklinesConfigTest.php b/plugins/CoreVisualizations/tests/Unit/SparklinesConfigTest.php
index 1134df2f5c..e4759845fa 100644
--- a/plugins/CoreVisualizations/tests/Unit/SparklinesConfigTest.php
+++ b/plugins/CoreVisualizations/tests/Unit/SparklinesConfigTest.php
@@ -49,9 +49,9 @@ class SparklinesConfigTest extends \PHPUnit_Framework_TestCase
$this->addFewSparklines();
$this->assertSame(array(
- array('columns' => 'nb_visits', 'order' => null),
- array('columns' => 'nb_unique_visitors', 'order' => 99),
- array('columns' => array('nb_downloads', 'nb_outlinks'), 'order' => null),
+ array('columns' => 'nb_visits', 'order' => null, 'graphParams' => null),
+ array('columns' => 'nb_unique_visitors', 'order' => 99, 'graphParams' => null),
+ array('columns' => array('nb_downloads', 'nb_outlinks'), 'order' => null, 'graphParams' => null),
), $this->config->getSparklineMetrics());
}
@@ -62,8 +62,8 @@ class SparklinesConfigTest extends \PHPUnit_Framework_TestCase
$this->config->removeSparklineMetric('nb_unique_visitors');
$this->assertSame(array(
- array('columns' => 'nb_visits', 'order' => null),
- array('columns' => array('nb_downloads', 'nb_outlinks'), 'order' => null),
+ array('columns' => 'nb_visits', 'order' => null, 'graphParams' => null),
+ array('columns' => array('nb_downloads', 'nb_outlinks'), 'order' => null, 'graphParams' => null),
), $this->config->getSparklineMetrics());
}
@@ -74,8 +74,8 @@ class SparklinesConfigTest extends \PHPUnit_Framework_TestCase
$this->config->removeSparklineMetric(array('nb_downloads', 'nb_outlinks'));
$this->assertSame(array(
- array('columns' => 'nb_visits', 'order' => null),
- array('columns' => 'nb_unique_visitors', 'order' => 99),
+ array('columns' => 'nb_visits', 'order' => null, 'graphParams' => null),
+ array('columns' => 'nb_unique_visitors', 'order' => 99, 'graphParams' => null),
), $this->config->getSparklineMetrics());
}
@@ -86,9 +86,9 @@ class SparklinesConfigTest extends \PHPUnit_Framework_TestCase
$this->config->replaceSparklineMetric('nb_unique_visitors', '');
$this->assertSame(array(
- array('columns' => 'nb_visits', 'order' => null),
- array('columns' => '', 'order' => 99),
- array('columns' => array('nb_downloads', 'nb_outlinks'), 'order' => null),
+ array('columns' => 'nb_visits', 'order' => null, 'graphParams' => null),
+ array('columns' => '', 'order' => 99, 'graphParams' => null),
+ array('columns' => array('nb_downloads', 'nb_outlinks'), 'order' => null, 'graphParams' => null),
), $this->config->getSparklineMetrics());
}
@@ -99,9 +99,9 @@ class SparklinesConfigTest extends \PHPUnit_Framework_TestCase
$this->config->replaceSparklineMetric(array('nb_downloads', 'nb_outlinks'), '');
$this->assertSame(array(
- array('columns' => 'nb_visits', 'order' => null),
- array('columns' => 'nb_unique_visitors', 'order' => 99),
- array('columns' => '', 'order' => null),
+ array('columns' => 'nb_visits', 'order' => null, 'graphParams' => null),
+ array('columns' => 'nb_unique_visitors', 'order' => 99, 'graphParams' => null),
+ array('columns' => '', 'order' => null, 'graphParams' => null),
), $this->config->getSparklineMetrics());
}
@@ -113,10 +113,10 @@ class SparklinesConfigTest extends \PHPUnit_Framework_TestCase
$this->config->addPlaceholder($order = 3);
$this->assertSame(array(
- array('url' => '', 'metrics' => array(), 'order' => 3),
- array('url' => '', 'metrics' => array(), 'order' => 10),
- array('url' => '', 'metrics' => array(), 'order' => 999),
- array('url' => '', 'metrics' => array(), 'order' => 1001),
+ 'placeholder3' => [['url' => '', 'metrics' => array(), 'order' => 3, 'group' => 'placeholder3']],
+ 'placeholder1' => [['url' => '', 'metrics' => array(), 'order' => 10, 'group' => 'placeholder1']],
+ 'placeholder0' => [['url' => '', 'metrics' => array(), 'order' => 999, 'group' => 'placeholder0']],
+ 'placeholder2' => [['url' => '', 'metrics' => array(), 'order' => 1001, 'group' => 'placeholder2']],
), $this->config->getSortedSparklines());
}
@@ -126,5 +126,4 @@ class SparklinesConfigTest extends \PHPUnit_Framework_TestCase
$this->config->addSparklineMetric('nb_unique_visitors', 99);
$this->config->addSparklineMetric(array('nb_downloads', 'nb_outlinks'));
}
-
}
diff --git a/plugins/DBStats/DBStats.php b/plugins/DBStats/DBStats.php
index f75d4e8c95..47f83c0442 100644
--- a/plugins/DBStats/DBStats.php
+++ b/plugins/DBStats/DBStats.php
@@ -23,10 +23,16 @@ class DBStats extends \Piwik\Plugin
public function registerEvents()
{
return array(
- "TestingEnvironment.addHooks" => 'setupTestEnvironment'
+ "TestingEnvironment.addHooks" => 'setupTestEnvironment',
+ 'AssetManager.getStylesheetFiles' => 'getStylesheetFiles',
);
}
+ public function getStylesheetFiles(&$stylesheets)
+ {
+ $stylesheets[] = 'plugins/DBStats/stylesheets/dbstats.less';
+ }
+
public function setupTestEnvironment($environment)
{
Piwik::addAction("MySQLMetadataProvider.createDao", function (&$dao) {
diff --git a/plugins/DBStats/stylesheets/dbstats.less b/plugins/DBStats/stylesheets/dbstats.less
new file mode 100644
index 0000000000..b2c1c7e993
--- /dev/null
+++ b/plugins/DBStats/stylesheets/dbstats.less
@@ -0,0 +1,5 @@
+div.dataTable[data-report^=DBStats] {
+ table.dataTable th.label, table.dataTable td.label {
+ padding-left: 12px;
+ }
+} \ No newline at end of file
diff --git a/plugins/DBStats/tests/UI/expected-screenshots/DBStats_admin_page.png b/plugins/DBStats/tests/UI/expected-screenshots/DBStats_admin_page.png
index c59655a4d7..7f9341d765 100644
--- a/plugins/DBStats/tests/UI/expected-screenshots/DBStats_admin_page.png
+++ b/plugins/DBStats/tests/UI/expected-screenshots/DBStats_admin_page.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:862978936eed6c7e784b2ebcd109a713672cc1af22073c281e8072d4422cbfc9
-size 226310
+oid sha256:22952c81c959f7c2bc15dd81c155425438961ba3810155fc1d79fc386911e1a4
+size 225924
diff --git a/plugins/Dashboard/javascripts/dashboardWidget.js b/plugins/Dashboard/javascripts/dashboardWidget.js
index 749459005a..9d10183b19 100644
--- a/plugins/Dashboard/javascripts/dashboardWidget.js
+++ b/plugins/Dashboard/javascripts/dashboardWidget.js
@@ -146,10 +146,21 @@
}
// Reading segment from hash tag (standard case) or from the URL (when embedding dashboard)
- var segment = broadcast.getValueFromHash('segment') || broadcast.getValueFromUrl('segment');
- if (segment.length) {
- this.widgetParameters.segment = segment;
- }
+ ['segment'].forEach(function (paramName) {
+ var value = broadcast.getValueFromHash(paramName) || broadcast.getValueFromUrl(paramName);
+ if (value.length) {
+ self.widgetParameters[paramName] = value;
+ }
+ });
+
+ ['compareSegments', 'comparePeriods', 'compareDates'].forEach(function (paramName) {
+ var value = broadcast.getValueFromHash(paramName) || broadcast.getValueFromUrl(paramName);
+ if (value.length) {
+ self.widgetParameters[paramName] = value;
+ } else {
+ delete self.widgetParameters[paramName];
+ }
+ });
if (!hideLoading) {
$('.widgetContent', currentWidget).addClass('loading');
diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_create_new.png b/plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_create_new.png
index 367473d945..0758e86dee 100644
--- a/plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_create_new.png
+++ b/plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_create_new.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:ac5b05a9f4ebfbc7d49375be488980272b9be6cf36041b97c072bf2f8450de03
-size 287799
+oid sha256:dd7a915297b1e0133b9a4ea0f2582e08de7b25dca0d6e31bc6907bb74aebb31a
+size 287727
diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_removed.png b/plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_removed.png
index 11b9123c87..187520fcaf 100644
--- a/plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_removed.png
+++ b/plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_removed.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:8e80edad1d12691eed9ac156e46bfbf5f50f4eeff4b69f5f0199f135e259620b
-size 470940
+oid sha256:316535029886954e472f55900642ba3f7414d061a5dc582d98101a3f564e83c1
+size 470825
diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_widget_preview.png b/plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_widget_preview.png
index 27bd0ba947..af0139a06b 100644
--- a/plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_widget_preview.png
+++ b/plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_widget_preview.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:54f6f0f0a9c6d53ff8686e874c7536a3633eddec600f463f700e5b7f01a96870
-size 78171
+oid sha256:3d171c1f25f9e6c3d563d28e888a071e5aece8d16522d9e92a26189d1ac03baf
+size 78152
diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_change_layout.png b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_change_layout.png
index 3fea179b78..6f2b2ea43d 100644
--- a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_change_layout.png
+++ b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_change_layout.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:0a732ff6fca8b50b710c174e9c6bdc39e5ebc116aec52a4ad46aa09762bdc53e
-size 28344
+oid sha256:734430cec12f1a1d50fdba2233767cce8b23a884390c363ca5b1900078256c46
+size 28347
diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_copied.png b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_copied.png
index fa9c471761..69e5d2e373 100644
--- a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_copied.png
+++ b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_copied.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:9f7b84df4b2e445f5d81fac7a486b94df8288ebd1a732399f2461e2606ca8f83
-size 29499
+oid sha256:0abd087f4d36f11bab023eaac6fcf4c4f0466400f5a144f913d2c7c2551ec927
+size 29501
diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_create_new.png b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_create_new.png
index 843781e99f..cd862e0a79 100644
--- a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_create_new.png
+++ b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_create_new.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:7484a597ebac3b46951cb6d793deb6404c84f46cac88fd0c09935774a11adcd9
-size 29658
+oid sha256:71aa611944407b45bc0327affe24edad35dde298d64589dfced48ac446309ebf
+size 29659
diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_default_widget_selection_changed.png b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_default_widget_selection_changed.png
index 1669f2d336..4fab81b58b 100644
--- a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_default_widget_selection_changed.png
+++ b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_default_widget_selection_changed.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:d6703682ea7e5aca3ec5417dc77366a2774e0a700b05d3e3813e7d8a0597fef8
-size 29006
+oid sha256:f27e517adceb88eb6b4a43e03309af013f31986ad90c424afaada6c1638392b0
+size 29010
diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_loaded.png b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_loaded.png
index a56185dc51..d5ad244c7c 100644
--- a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_loaded.png
+++ b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_loaded.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:76a9e2fb7470db5b710b852da2c68bda6d1bfbd6ac09bf51e63d8a55ff8a3bad
-size 28628
+oid sha256:469bf405bc3cd4396f5a2ac8ea9a7ecf57446e88807ab1988a59947b0d6a017b
+size 28577
diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_loaded_token_auth.png b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_loaded_token_auth.png
index 5ee451d291..cb69af5416 100644
--- a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_loaded_token_auth.png
+++ b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_loaded_token_auth.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:c0a700ba89f44f95d8908095a360a1588288397d3a0e98b59d51e48e7a6dd7fd
-size 693872
+oid sha256:9027e455792c410ef90909704194fc5b86eed3878c01ad2a1d74b2847798a9f9
+size 715131
diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_removed.png b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_removed.png
index 5ee451d291..cb69af5416 100644
--- a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_removed.png
+++ b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_removed.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:c0a700ba89f44f95d8908095a360a1588288397d3a0e98b59d51e48e7a6dd7fd
-size 693872
+oid sha256:9027e455792c410ef90909704194fc5b86eed3878c01ad2a1d74b2847798a9f9
+size 715131
diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_rename.png b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_rename.png
index fe44ad8f2b..b58e73dd91 100644
--- a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_rename.png
+++ b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_rename.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:aaf59949126ca30c805c2f1d387f876e4cf3780fc951df9256d745f3b4586c24
-size 29007
+oid sha256:a19692edc4525595995dce157fb8e162fc87fe9984766ed8d7797036206ca48c
+size 29011
diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_reset.png b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_reset.png
index f57a1905e9..32c1ff560d 100644
--- a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_reset.png
+++ b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_reset.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:ce83173ae5194bc6b56e06f4d64637b1f678efad64861956622445ec647e4a20
-size 281844
+oid sha256:66ce0f6234d80c90741d0e9ff99d871c141b403101fee5616a2257bb945c2cc0
+size 281686
diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_rowevolution.png b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_rowevolution.png
index c82945c71f..e8fc073272 100644
--- a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_rowevolution.png
+++ b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_rowevolution.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:586e98a68becb0936e21b67ff433c65bdacbed176bd8362465c9628363ab32c4
-size 72431
+oid sha256:5d1c25dee11f60e08f34a88a081e4074278ce4a9f5a9d0996f062f4361de290e
+size 72373
diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_segmented.png b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_segmented.png
index de02e3b03a..9b328e1a9d 100644
--- a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_segmented.png
+++ b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_segmented.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:993ae1d84c577d402680ef32951a3f0207f7a51cdcfb347a245b0771f5ea748a
-size 31572
+oid sha256:171e0f85a9c19893127b98b84c8f92c986e77b0c829d3c41afb78063c8c4eee0
+size 31565
diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_widget_add_widget.png b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_widget_add_widget.png
index ae4e681faa..65476f9531 100644
--- a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_widget_add_widget.png
+++ b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_widget_add_widget.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:3e1fc62ca9469d478f30d75750379974968a87204fef7faf00a400cb60bbfa34
-size 200735
+oid sha256:bb214ea5d1c825347a5fff7603e6990773e30d8a71b3248a24261143f00bb24c
+size 201173
diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_widget_maximise.png b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_widget_maximise.png
index ed300b8f42..224a9bf21c 100644
--- a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_widget_maximise.png
+++ b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_widget_maximise.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:9006aeee36f702ce455ac67d332ccb6470a31e211f0b4965f232a18bdf069446
-size 31300
+oid sha256:731057f7bf638d825c38f6f5428381f1e033c7e63e1be88b92dcaf6f0ac41fcd
+size 31275
diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_widget_move.png b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_widget_move.png
index 40877036d6..4bf82eadc5 100644
--- a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_widget_move.png
+++ b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_widget_move.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:d35d29d09c2fa108f9236290b66221073c617215c1cad17b26d8d5101371511a
-size 28681
+oid sha256:495f12fac4ec25587e2cfe72d95b28799c309486dd285c7e8e3fecb4e4c5a313
+size 28633
diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_widget_move_removed.png b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_widget_move_removed.png
index 40877036d6..4bf82eadc5 100644
--- a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_widget_move_removed.png
+++ b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_widget_move_removed.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:d35d29d09c2fa108f9236290b66221073c617215c1cad17b26d8d5101371511a
-size 28681
+oid sha256:495f12fac4ec25587e2cfe72d95b28799c309486dd285c7e8e3fecb4e4c5a313
+size 28633
diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_widget_refresh.png b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_widget_refresh.png
index 40877036d6..4bf82eadc5 100644
--- a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_widget_refresh.png
+++ b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_widget_refresh.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:d35d29d09c2fa108f9236290b66221073c617215c1cad17b26d8d5101371511a
-size 28681
+oid sha256:495f12fac4ec25587e2cfe72d95b28799c309486dd285c7e8e3fecb4e4c5a313
+size 28633
diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_widget_unmaximise.png b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_widget_unmaximise.png
index 3510aad611..a18864910a 100644
--- a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_widget_unmaximise.png
+++ b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_widget_unmaximise.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:f35259a6167d3fbdb213b446628910be4e3fcbe5159245cccec3a08f4138482b
-size 30288
+oid sha256:d7aec7a34f19fe0a63126e7426b7cefe42d3ae3a7f71f176b0254e027acdad8c
+size 30244
diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_widget_unminimise.png b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_widget_unminimise.png
index 40877036d6..4bf82eadc5 100644
--- a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_widget_unminimise.png
+++ b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_widget_unminimise.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:d35d29d09c2fa108f9236290b66221073c617215c1cad17b26d8d5101371511a
-size 28681
+oid sha256:495f12fac4ec25587e2cfe72d95b28799c309486dd285c7e8e3fecb4e4c5a313
+size 28633
diff --git a/plugins/Diagnostics/tests/Integration/Commands/AnalyzeArchiveTableTest.php b/plugins/Diagnostics/tests/Integration/Commands/AnalyzeArchiveTableTest.php
index 844753bb10..8402b9506c 100644
--- a/plugins/Diagnostics/tests/Integration/Commands/AnalyzeArchiveTableTest.php
+++ b/plugins/Diagnostics/tests/Integration/Commands/AnalyzeArchiveTableTest.php
@@ -82,7 +82,7 @@ Total # Invalidated Archives: 0
Total # Temporary Archives: 0
Total # Error Archives: 0
Total # Segment Archives: 162
-Total Size of Blobs: 24.%d K
+Total Size of Blobs: 25.%d K
OUTPUT;
diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_schedrep_html_tables_and_graph__ScheduledReports.generateReport_week.original.html b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_schedrep_html_tables_and_graph__ScheduledReports.generateReport_week.original.html
index 150d212a52..f42b269e2c 100644
--- a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_schedrep_html_tables_and_graph__ScheduledReports.generateReport_week.original.html
+++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_schedrep_html_tables_and_graph__ScheduledReports.generateReport_week.original.html
@@ -289,6 +289,11 @@
</a>
</li>
<li style="font-size:15px;line-height:24px;">
+ <a href="#Referrers_get" style="text-decoration:none; color: #0d0d0d;">
+ Referrers Overview
+ </a>
+ </li>
+ <li style="font-size:15px;line-height:24px;">
<a href="#Referrers_getReferrerType" style="text-decoration:none; color: #0d0d0d;">
Channel Type
</a>
@@ -4864,6 +4869,156 @@
Back to top &#8593;
</a></p>
+<h2 id="Referrers_get" style=" color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, Oxygen-Sans, Cantarell, &#039;Helvetica Neue&#039;, sans-serif; font-size: 24pt; font-weight:normal; margin:45px 0 30px 0;">
+ Referrers Overview
+</h2>
+
+ <img alt=""
+ src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAArwAAADICAIAAACF9KXqAAAEfklEQVR4nO3cS1biQABA0ehxJ+x/SayFHtCHTufHE+IHuHckEqoqTuqRCG+n02nYw+FwOB6Pew01DMM9o+24mIebHQC+yPvhcDhv0heTh/f71IDnHfeGTffqLLufFwC8lPefXsD38e4fAO7xMQzD8XhcvKI+fmu+uOMuvne//PL8kvPD8R2HyQHz114Oni/gvM75auezLK4kLmA86WTGxTEXB9yYBQAe0cfaE5ONeXGf3t4jLzcaJkdODhiPOdmeFxew2DfzWcYP136+esrz38/HWRw8zggAD+Tv7YnJ2/rfLG7AO+7T20MtPqsSAHg+/640rN2k+Kxf/g+Jlzy67UwfJa0AYHertyduM7ksf8MB32DjNse237B4APgp/3164uabFM+3g37FGT3fXwmAl7J6pWHxwwtrB4yv+c+3xvGzvUuuLmBjlo3DNj7UsHhGG0uaDFgWCQCP622vb4R8PuX+hU9GAPA6dv6fhkfnsgEArHGlAQBIXuhrpAGAe4gGACARDQBAIhoAgEQ0AACJaAAAEtEAACSiAQBIRAMAkIgGACARDQBAIhoAgEQ0AACJaAAAEtEAACSiAQBIRAMAkIgGACARDQBAIhoAgEQ0AACJaAAAEtEAACSiAQBIRAMAkIgGACARDQBAIhoAgEQ0AACJaAAAEtEAACSiAQBIRAMAkIgGACARDQBAIhoAgEQ0AACJaAAAEtEAACSiAQBIRAMAkIgGACARDQBAIhoAgEQ0AACJaAAAEtEAACSiAQBIRAMAkIgGACARDQBAIhoAgEQ0AACJaAAAEtEAACSiAQBIRAMAkIgGACARDQBAIhoAgEQ0AACJaAAAEtEAACSiAQBIRAMAkIgGACARDQBAIhoAgEQ0AACJaAAAEtEAACSiAQBIRAMAkIgGACARDQBAIhoAgEQ0AACJaAAAEtEAACSiAQBIRAMAkIgGACARDQBAIhoAgEQ0AACJaAAAEtEAACSiAQBIRAMAkIgGACARDQBAIhoAgEQ0AACJaAAAEtEAACSiAQBIRAMAkIgGACARDQBAIhoAgEQ0AACJaAAAEtEAACSiAQBIRAMAkIgGACARDQBAIhoAgEQ0AACJaAAAEtEAACSiAQBIRAMAkIgGACARDQBAIhoAgEQ0AACJaAAAEtEAACSiAQBIRAMAkIgGACARDQBAIhoAgEQ0AACJaAAAEtEAACSiAQBIRAMAkIgGACARDQBAIhoAgEQ0AACJaAAAEtEAACSiAQBIRAMAkIgGACARDQBAIhoAgEQ0AACJaAAAEtEAACSiAQBIRAMAkIgGACARDQBAIhoAgEQ0AACJaAAAEtEAACSiAQBIRAMAkIgGACARDQBAIhoAgEQ0AACJaAAAEtEAACSiAQBIRAMAkIgGACARDQBAIhoAgEQ0AACJaAAAEtEAACSiAQBIRAMAkIgGACARDQBAIhoAgEQ0AACJaAAAEtEAACSiAQBIRAMAkIgGACARDQBAIhoAgEQ0AACJaAAAEtEAACSiAQBIRAMAkIgGACARDQBA8gczQRugNvGghAAAAABJRU5ErkJggg=="
+ height="200"
+ width="700"
+ margin="0 auto"/>
+
+ <br/>
+ <br/>
+
+ <table style="border-collapse:collapse; border:1px solid rgb(231,231,231); padding:5px;">
+ <thead style="background-color: #f2f2f2;">
+ <th style="border-bottom:1px solid rgb(231,231,231);font-size: 15px;text-align: left;font-weight:normal;padding:13px 0 13px 10px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, Oxygen-Sans, Cantarell, &#039;Helvetica Neue&#039;, sans-serif;">
+ &nbsp;Name&nbsp;&nbsp;
+ </th>
+ <th style="border-bottom:1px solid rgb(231,231,231);font-size: 15px;text-align: left;font-weight:normal;padding:13px 0 13px 10px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, Oxygen-Sans, Cantarell, &#039;Helvetica Neue&#039;, sans-serif; text-align:right;">
+ &nbsp;Value&nbsp;&nbsp;
+ </th>
+ </thead>
+ <tbody>
+
+ <tr style=";">
+ <td style="padding:17px 15px;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, Oxygen-Sans, Cantarell, &#039;Helvetica Neue&#039;, sans-serif;">
+ Visitors from Search Engines </td>
+ <td style="padding:17px 15px; text-align:right;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, Oxygen-Sans, Cantarell, &#039;Helvetica Neue&#039;, sans-serif;">
+ 0
+ </td>
+ </tr>
+
+ <tr style="background-color: #f2f2f2;">
+ <td style="padding:17px 15px;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, Oxygen-Sans, Cantarell, &#039;Helvetica Neue&#039;, sans-serif;">
+ Visitors from Social Networks </td>
+ <td style="padding:17px 15px; text-align:right;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, Oxygen-Sans, Cantarell, &#039;Helvetica Neue&#039;, sans-serif;">
+ 0
+ </td>
+ </tr>
+
+ <tr style=";">
+ <td style="padding:17px 15px;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, Oxygen-Sans, Cantarell, &#039;Helvetica Neue&#039;, sans-serif;">
+ Visitors from Direct Entry </td>
+ <td style="padding:17px 15px; text-align:right;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, Oxygen-Sans, Cantarell, &#039;Helvetica Neue&#039;, sans-serif;">
+ 5
+ </td>
+ </tr>
+
+ <tr style="background-color: #f2f2f2;">
+ <td style="padding:17px 15px;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, Oxygen-Sans, Cantarell, &#039;Helvetica Neue&#039;, sans-serif;">
+ Visitors from Websites </td>
+ <td style="padding:17px 15px; text-align:right;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, Oxygen-Sans, Cantarell, &#039;Helvetica Neue&#039;, sans-serif;">
+ 0
+ </td>
+ </tr>
+
+ <tr style=";">
+ <td style="padding:17px 15px;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, Oxygen-Sans, Cantarell, &#039;Helvetica Neue&#039;, sans-serif;">
+ Visitors from Campaigns </td>
+ <td style="padding:17px 15px; text-align:right;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, Oxygen-Sans, Cantarell, &#039;Helvetica Neue&#039;, sans-serif;">
+ 0
+ </td>
+ </tr>
+
+ <tr style="background-color: #f2f2f2;">
+ <td style="padding:17px 15px;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, Oxygen-Sans, Cantarell, &#039;Helvetica Neue&#039;, sans-serif;">
+ Distinct search engines </td>
+ <td style="padding:17px 15px; text-align:right;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, Oxygen-Sans, Cantarell, &#039;Helvetica Neue&#039;, sans-serif;">
+ 0
+ </td>
+ </tr>
+
+ <tr style=";">
+ <td style="padding:17px 15px;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, Oxygen-Sans, Cantarell, &#039;Helvetica Neue&#039;, sans-serif;">
+ Distinct social networks </td>
+ <td style="padding:17px 15px; text-align:right;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, Oxygen-Sans, Cantarell, &#039;Helvetica Neue&#039;, sans-serif;">
+ 0
+ </td>
+ </tr>
+
+ <tr style="background-color: #f2f2f2;">
+ <td style="padding:17px 15px;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, Oxygen-Sans, Cantarell, &#039;Helvetica Neue&#039;, sans-serif;">
+ Distinct keywords </td>
+ <td style="padding:17px 15px; text-align:right;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, Oxygen-Sans, Cantarell, &#039;Helvetica Neue&#039;, sans-serif;">
+ 0
+ </td>
+ </tr>
+
+ <tr style=";">
+ <td style="padding:17px 15px;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, Oxygen-Sans, Cantarell, &#039;Helvetica Neue&#039;, sans-serif;">
+ Distinct websites </td>
+ <td style="padding:17px 15px; text-align:right;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, Oxygen-Sans, Cantarell, &#039;Helvetica Neue&#039;, sans-serif;">
+ 0
+ </td>
+ </tr>
+
+ <tr style="background-color: #f2f2f2;">
+ <td style="padding:17px 15px;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, Oxygen-Sans, Cantarell, &#039;Helvetica Neue&#039;, sans-serif;">
+ Distinct campaigns </td>
+ <td style="padding:17px 15px; text-align:right;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, Oxygen-Sans, Cantarell, &#039;Helvetica Neue&#039;, sans-serif;">
+ 0
+ </td>
+ </tr>
+
+ <tr style=";">
+ <td style="padding:17px 15px;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, Oxygen-Sans, Cantarell, &#039;Helvetica Neue&#039;, sans-serif;">
+ Percent of Visitors from Direct Entry </td>
+ <td style="padding:17px 15px; text-align:right;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, Oxygen-Sans, Cantarell, &#039;Helvetica Neue&#039;, sans-serif;">
+ 100%
+ </td>
+ </tr>
+
+ <tr style="background-color: #f2f2f2;">
+ <td style="padding:17px 15px;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, Oxygen-Sans, Cantarell, &#039;Helvetica Neue&#039;, sans-serif;">
+ Percent of Visitors from Search Engines </td>
+ <td style="padding:17px 15px; text-align:right;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, Oxygen-Sans, Cantarell, &#039;Helvetica Neue&#039;, sans-serif;">
+ 0%
+ </td>
+ </tr>
+
+ <tr style=";">
+ <td style="padding:17px 15px;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, Oxygen-Sans, Cantarell, &#039;Helvetica Neue&#039;, sans-serif;">
+ Percent of Visitors from Campaigns </td>
+ <td style="padding:17px 15px; text-align:right;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, Oxygen-Sans, Cantarell, &#039;Helvetica Neue&#039;, sans-serif;">
+ 0%
+ </td>
+ </tr>
+
+ <tr style="background-color: #f2f2f2;">
+ <td style="padding:17px 15px;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, Oxygen-Sans, Cantarell, &#039;Helvetica Neue&#039;, sans-serif;">
+ Percent of Visitors from Social Networks </td>
+ <td style="padding:17px 15px; text-align:right;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, Oxygen-Sans, Cantarell, &#039;Helvetica Neue&#039;, sans-serif;">
+ 0%
+ </td>
+ </tr>
+
+ <tr style=";">
+ <td style="padding:17px 15px;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, Oxygen-Sans, Cantarell, &#039;Helvetica Neue&#039;, sans-serif;">
+ Percent of Visitors from Websites </td>
+ <td style="padding:17px 15px; text-align:right;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, Oxygen-Sans, Cantarell, &#039;Helvetica Neue&#039;, sans-serif;">
+ 0%
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ <p style="width: 100%; text-align:center;">
+ <a style="color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, Oxygen-Sans, Cantarell, &#039;Helvetica Neue&#039;, sans-serif;; text-decoration:none; font-size: 9pt;" href="#reportTop">
+ Back to top &#8593;
+ </a></p>
+
<h2 id="Referrers_getReferrerType" style=" color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, Oxygen-Sans, Cantarell, &#039;Helvetica Neue&#039;, sans-serif; font-size: 24pt; font-weight:normal; margin:45px 0 30px 0;">
Channel Type
</h2>
diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_schedrep_html_tables_only__ScheduledReports.generateReport_week.original.html b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_schedrep_html_tables_only__ScheduledReports.generateReport_week.original.html
index 162148fd66..58363ff845 100644
--- a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_schedrep_html_tables_only__ScheduledReports.generateReport_week.original.html
+++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_schedrep_html_tables_only__ScheduledReports.generateReport_week.original.html
@@ -289,6 +289,11 @@
</a>
</li>
<li style="font-size:15px;line-height:24px;">
+ <a href="#Referrers_get" style="text-decoration:none; color: #0d0d0d;">
+ Referrers Overview
+ </a>
+ </li>
+ <li style="font-size:15px;line-height:24px;">
<a href="#Referrers_getReferrerType" style="text-decoration:none; color: #0d0d0d;">
Channel Type
</a>
@@ -4612,6 +4617,149 @@
Back to top &#8593;
</a></p>
+<h2 id="Referrers_get" style=" color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, Oxygen-Sans, Cantarell, &#039;Helvetica Neue&#039;, sans-serif; font-size: 24pt; font-weight:normal; margin:45px 0 30px 0;">
+ Referrers Overview
+</h2>
+
+
+
+ <table style="border-collapse:collapse; border:1px solid rgb(231,231,231); padding:5px;">
+ <thead style="background-color: #f2f2f2;">
+ <th style="border-bottom:1px solid rgb(231,231,231);font-size: 15px;text-align: left;font-weight:normal;padding:13px 0 13px 10px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, Oxygen-Sans, Cantarell, &#039;Helvetica Neue&#039;, sans-serif;">
+ &nbsp;Name&nbsp;&nbsp;
+ </th>
+ <th style="border-bottom:1px solid rgb(231,231,231);font-size: 15px;text-align: left;font-weight:normal;padding:13px 0 13px 10px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, Oxygen-Sans, Cantarell, &#039;Helvetica Neue&#039;, sans-serif; text-align:right;">
+ &nbsp;Value&nbsp;&nbsp;
+ </th>
+ </thead>
+ <tbody>
+
+ <tr style=";">
+ <td style="padding:17px 15px;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, Oxygen-Sans, Cantarell, &#039;Helvetica Neue&#039;, sans-serif;">
+ Visitors from Search Engines </td>
+ <td style="padding:17px 15px; text-align:right;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, Oxygen-Sans, Cantarell, &#039;Helvetica Neue&#039;, sans-serif;">
+ 0
+ </td>
+ </tr>
+
+ <tr style="background-color: #f2f2f2;">
+ <td style="padding:17px 15px;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, Oxygen-Sans, Cantarell, &#039;Helvetica Neue&#039;, sans-serif;">
+ Visitors from Social Networks </td>
+ <td style="padding:17px 15px; text-align:right;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, Oxygen-Sans, Cantarell, &#039;Helvetica Neue&#039;, sans-serif;">
+ 0
+ </td>
+ </tr>
+
+ <tr style=";">
+ <td style="padding:17px 15px;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, Oxygen-Sans, Cantarell, &#039;Helvetica Neue&#039;, sans-serif;">
+ Visitors from Direct Entry </td>
+ <td style="padding:17px 15px; text-align:right;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, Oxygen-Sans, Cantarell, &#039;Helvetica Neue&#039;, sans-serif;">
+ 5
+ </td>
+ </tr>
+
+ <tr style="background-color: #f2f2f2;">
+ <td style="padding:17px 15px;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, Oxygen-Sans, Cantarell, &#039;Helvetica Neue&#039;, sans-serif;">
+ Visitors from Websites </td>
+ <td style="padding:17px 15px; text-align:right;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, Oxygen-Sans, Cantarell, &#039;Helvetica Neue&#039;, sans-serif;">
+ 0
+ </td>
+ </tr>
+
+ <tr style=";">
+ <td style="padding:17px 15px;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, Oxygen-Sans, Cantarell, &#039;Helvetica Neue&#039;, sans-serif;">
+ Visitors from Campaigns </td>
+ <td style="padding:17px 15px; text-align:right;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, Oxygen-Sans, Cantarell, &#039;Helvetica Neue&#039;, sans-serif;">
+ 0
+ </td>
+ </tr>
+
+ <tr style="background-color: #f2f2f2;">
+ <td style="padding:17px 15px;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, Oxygen-Sans, Cantarell, &#039;Helvetica Neue&#039;, sans-serif;">
+ Distinct search engines </td>
+ <td style="padding:17px 15px; text-align:right;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, Oxygen-Sans, Cantarell, &#039;Helvetica Neue&#039;, sans-serif;">
+ 0
+ </td>
+ </tr>
+
+ <tr style=";">
+ <td style="padding:17px 15px;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, Oxygen-Sans, Cantarell, &#039;Helvetica Neue&#039;, sans-serif;">
+ Distinct social networks </td>
+ <td style="padding:17px 15px; text-align:right;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, Oxygen-Sans, Cantarell, &#039;Helvetica Neue&#039;, sans-serif;">
+ 0
+ </td>
+ </tr>
+
+ <tr style="background-color: #f2f2f2;">
+ <td style="padding:17px 15px;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, Oxygen-Sans, Cantarell, &#039;Helvetica Neue&#039;, sans-serif;">
+ Distinct keywords </td>
+ <td style="padding:17px 15px; text-align:right;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, Oxygen-Sans, Cantarell, &#039;Helvetica Neue&#039;, sans-serif;">
+ 0
+ </td>
+ </tr>
+
+ <tr style=";">
+ <td style="padding:17px 15px;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, Oxygen-Sans, Cantarell, &#039;Helvetica Neue&#039;, sans-serif;">
+ Distinct websites </td>
+ <td style="padding:17px 15px; text-align:right;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, Oxygen-Sans, Cantarell, &#039;Helvetica Neue&#039;, sans-serif;">
+ 0
+ </td>
+ </tr>
+
+ <tr style="background-color: #f2f2f2;">
+ <td style="padding:17px 15px;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, Oxygen-Sans, Cantarell, &#039;Helvetica Neue&#039;, sans-serif;">
+ Distinct campaigns </td>
+ <td style="padding:17px 15px; text-align:right;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, Oxygen-Sans, Cantarell, &#039;Helvetica Neue&#039;, sans-serif;">
+ 0
+ </td>
+ </tr>
+
+ <tr style=";">
+ <td style="padding:17px 15px;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, Oxygen-Sans, Cantarell, &#039;Helvetica Neue&#039;, sans-serif;">
+ Percent of Visitors from Direct Entry </td>
+ <td style="padding:17px 15px; text-align:right;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, Oxygen-Sans, Cantarell, &#039;Helvetica Neue&#039;, sans-serif;">
+ 100%
+ </td>
+ </tr>
+
+ <tr style="background-color: #f2f2f2;">
+ <td style="padding:17px 15px;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, Oxygen-Sans, Cantarell, &#039;Helvetica Neue&#039;, sans-serif;">
+ Percent of Visitors from Search Engines </td>
+ <td style="padding:17px 15px; text-align:right;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, Oxygen-Sans, Cantarell, &#039;Helvetica Neue&#039;, sans-serif;">
+ 0%
+ </td>
+ </tr>
+
+ <tr style=";">
+ <td style="padding:17px 15px;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, Oxygen-Sans, Cantarell, &#039;Helvetica Neue&#039;, sans-serif;">
+ Percent of Visitors from Campaigns </td>
+ <td style="padding:17px 15px; text-align:right;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, Oxygen-Sans, Cantarell, &#039;Helvetica Neue&#039;, sans-serif;">
+ 0%
+ </td>
+ </tr>
+
+ <tr style="background-color: #f2f2f2;">
+ <td style="padding:17px 15px;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, Oxygen-Sans, Cantarell, &#039;Helvetica Neue&#039;, sans-serif;">
+ Percent of Visitors from Social Networks </td>
+ <td style="padding:17px 15px; text-align:right;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, Oxygen-Sans, Cantarell, &#039;Helvetica Neue&#039;, sans-serif;">
+ 0%
+ </td>
+ </tr>
+
+ <tr style=";">
+ <td style="padding:17px 15px;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, Oxygen-Sans, Cantarell, &#039;Helvetica Neue&#039;, sans-serif;">
+ Percent of Visitors from Websites </td>
+ <td style="padding:17px 15px; text-align:right;;border-bottom:1px solid rgb(231,231,231);font-size: 15px;font-variant-numeric: tabular-nums;padding:17px 15px;color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, Oxygen-Sans, Cantarell, &#039;Helvetica Neue&#039;, sans-serif;">
+ 0%
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ <p style="width: 100%; text-align:center;">
+ <a style="color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, Oxygen-Sans, Cantarell, &#039;Helvetica Neue&#039;, sans-serif;; text-decoration:none; font-size: 9pt;" href="#reportTop">
+ Back to top &#8593;
+ </a></p>
+
<h2 id="Referrers_getReferrerType" style=" color:#0d0d0d;font-family:-apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, Oxygen-Sans, Cantarell, &#039;Helvetica Neue&#039;, sans-serif; font-size: 24pt; font-weight:normal; margin:45px 0 30px 0;">
Channel Type
</h2>
diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_schedrep_in_csv__ScheduledReports.generateReport_week.original.csv b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_schedrep_in_csv__ScheduledReports.generateReport_week.original.csv
index 29e371ff87..b0dc0f05e0 100644
--- a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_schedrep_in_csv__ScheduledReports.generateReport_week.original.csv
+++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_schedrep_in_csv__ScheduledReports.generateReport_week.original.csv
@@ -303,6 +303,10 @@ Returning Visits
nb_uniq_visitors_returning,nb_users_returning,nb_visits_returning,nb_actions_returning,max_actions_returning,bounce_rate_returning,nb_actions_per_visit_returning,avg_time_on_site_returning
1,0,4,12,6,25%,3,00:25:32
+Referrers Overview
+Referrers_visitorsFromSearchEngines,Referrers_visitorsFromSocialNetworks,Referrers_visitorsFromDirectEntry,Referrers_visitorsFromWebsites,Referrers_visitorsFromCampaigns,Referrers_distinctSearchEngines,Referrers_distinctSocialNetworks,Referrers_distinctKeywords,Referrers_distinctWebsites,Referrers_distinctCampaigns,Referrers_visitorsFromDirectEntry_percent,Referrers_visitorsFromSearchEngines_percent,Referrers_visitorsFromCampaigns_percent,Referrers_visitorsFromSocialNetworks_percent,Referrers_visitorsFromWebsites_percent
+0,0,5,0,0,0,0,0,0,0,100%,0%,0%,0%,0%
+
Channel Type
label,nb_visits,nb_actions,revenue,nb_actions_per_visit,avg_time_on_site,bounce_rate
Direct Entry,5,16,"$13,361.11",3.2,00:22:49,20%
diff --git a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_schedrep_in_pdf_tables_only__ScheduledReports.generateReport_week.original.pdf b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_schedrep_in_pdf_tables_only__ScheduledReports.generateReport_week.original.pdf
index eca7f7e68e..bb0ce53170 100644
--- a/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_schedrep_in_pdf_tables_only__ScheduledReports.generateReport_week.original.pdf
+++ b/plugins/Ecommerce/tests/System/expected/test_ecommerceOrderWithItems_schedrep_in_pdf_tables_only__ScheduledReports.generateReport_week.original.pdf
Binary files differ
diff --git a/plugins/ExamplePlugin/tests/System/expected/test___API.get_day.xml b/plugins/ExamplePlugin/tests/System/expected/test___API.get_day.xml
index 213b91b66a..47964dea53 100644
--- a/plugins/ExamplePlugin/tests/System/expected/test___API.get_day.xml
+++ b/plugins/ExamplePlugin/tests/System/expected/test___API.get_day.xml
@@ -15,6 +15,22 @@
<bounce_rate_returning>0%</bounce_rate_returning>
<nb_actions_per_visit_returning>0</nb_actions_per_visit_returning>
<avg_time_on_site_returning>0</avg_time_on_site_returning>
+ <Referrers_visitorsFromSearchEngines>0</Referrers_visitorsFromSearchEngines>
+ <Referrers_visitorsFromSocialNetworks>0</Referrers_visitorsFromSocialNetworks>
+ <Referrers_visitorsFromDirectEntry>2</Referrers_visitorsFromDirectEntry>
+ <Referrers_visitorsFromWebsites>0</Referrers_visitorsFromWebsites>
+ <Referrers_visitorsFromCampaigns>0</Referrers_visitorsFromCampaigns>
+ <Referrers_distinctSearchEngines>0</Referrers_distinctSearchEngines>
+ <Referrers_distinctSocialNetworks>0</Referrers_distinctSocialNetworks>
+ <Referrers_distinctKeywords>0</Referrers_distinctKeywords>
+ <Referrers_distinctWebsites>0</Referrers_distinctWebsites>
+ <Referrers_distinctWebsitesUrls>0</Referrers_distinctWebsitesUrls>
+ <Referrers_distinctCampaigns>0</Referrers_distinctCampaigns>
+ <Referrers_visitorsFromDirectEntry_percent>100%</Referrers_visitorsFromDirectEntry_percent>
+ <Referrers_visitorsFromSearchEngines_percent>0%</Referrers_visitorsFromSearchEngines_percent>
+ <Referrers_visitorsFromCampaigns_percent>0%</Referrers_visitorsFromCampaigns_percent>
+ <Referrers_visitorsFromSocialNetworks_percent>0%</Referrers_visitorsFromSocialNetworks_percent>
+ <Referrers_visitorsFromWebsites_percent>0%</Referrers_visitorsFromWebsites_percent>
<nb_conversions>1</nb_conversions>
<nb_visits_converted>1</nb_visits_converted>
<revenue>2541</revenue>
diff --git a/plugins/Goals/API.php b/plugins/Goals/API.php
index 6ee19e66c1..b09519e37b 100644
--- a/plugins/Goals/API.php
+++ b/plugins/Goals/API.php
@@ -440,7 +440,7 @@ class API extends \Piwik\Plugin\API
* @param bool $showAllGoalSpecificMetrics whether to show all goal specific metrics when no goal is set
* @return DataTable
*/
- public function get($idSite, $period, $date, $segment = false, $idGoal = false, $columns = array(), $showAllGoalSpecificMetrics = false)
+ public function get($idSite, $period, $date, $segment = false, $idGoal = false, $columns = array(), $showAllGoalSpecificMetrics = false, $compare = false)
{
Piwik::checkUserHasViewAccess($idSite);
@@ -479,6 +479,19 @@ class API extends \Piwik\Plugin\API
}
}
+ // if we are comparing, this will be queried with format_metrics=0, but we will eventually need to format the metrics.
+ // unfortunately, we can't do that since the processed metric information is in the GetMetrics report. in this case,
+ // we queue the filter so it will eventually be formatted.
+ if (!empty($compare)) {
+ $getMetricsReport = ReportsProvider::factory('Goals', 'getMetrics');
+ $table->queueFilter(function (DataTable $t) use ($getMetricsReport) {
+ $t->setMetadata(Metrics\Formatter::PROCESSED_METRICS_FORMATTED_FLAG, false);
+
+ $formatter = new Metrics\Formatter();
+ $formatter->formatMetrics($t, $getMetricsReport, $metricsToFormat = null, $formatAll = true);
+ });
+ }
+
return $table;
}
diff --git a/plugins/ImageGraph/StaticGraph/Pie.php b/plugins/ImageGraph/StaticGraph/Pie.php
index 63d7bf0752..6bae617232 100644
--- a/plugins/ImageGraph/StaticGraph/Pie.php
+++ b/plugins/ImageGraph/StaticGraph/Pie.php
@@ -23,4 +23,9 @@ class Pie extends PieGraph
$this->pieConfig
);
}
+
+ public function supportsComparison()
+ {
+ return false;
+ }
}
diff --git a/plugins/Insights/tests/UI/expected-screenshots/Insights_initial.png b/plugins/Insights/tests/UI/expected-screenshots/Insights_initial.png
index f670d55409..458c9c05c6 100644
--- a/plugins/Insights/tests/UI/expected-screenshots/Insights_initial.png
+++ b/plugins/Insights/tests/UI/expected-screenshots/Insights_initial.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:2bc504a2c6e1ea9a920dd044b094ba81ab4b40f023e74d98b3ea7a6bfe9283a7
-size 152748
+oid sha256:f5be2ace3fdaaff48711714d4d3df88c062051871e8e2bf8e30f93e0159de774
+size 146699
diff --git a/plugins/Intl/Intl.php b/plugins/Intl/Intl.php
index b8e0030240..8f44e8d06a 100644
--- a/plugins/Intl/Intl.php
+++ b/plugins/Intl/Intl.php
@@ -10,5 +10,19 @@ namespace Piwik\Plugins\Intl;
class Intl extends \Piwik\Plugin
{
+ public function getListHooksRegistered()
+ {
+ return [
+ 'Translate.getClientSideTranslationKeys' => 'getClientSideTranslationKeys',
+ ];
+ }
+ public function getClientSideTranslationKeys(&$translationKeys)
+ {
+ $translationKeys[] = 'Intl_PeriodDay';
+ $translationKeys[] = 'Intl_PeriodMonth';
+ $translationKeys[] = 'Intl_PeriodWeek';
+ $translationKeys[] = 'Intl_PeriodYear';
+ $translationKeys[] = 'CoreHome_PeriodRange';
+ }
}
diff --git a/plugins/IntranetMeasurable/tests/System/expected/test__intranet__API.get_day.xml b/plugins/IntranetMeasurable/tests/System/expected/test__intranet__API.get_day.xml
index 6673fc4a55..779aa606bc 100644
--- a/plugins/IntranetMeasurable/tests/System/expected/test__intranet__API.get_day.xml
+++ b/plugins/IntranetMeasurable/tests/System/expected/test__intranet__API.get_day.xml
@@ -15,6 +15,22 @@
<bounce_rate_returning>0%</bounce_rate_returning>
<nb_actions_per_visit_returning>0</nb_actions_per_visit_returning>
<avg_time_on_site_returning>0</avg_time_on_site_returning>
+ <Referrers_visitorsFromSearchEngines>0</Referrers_visitorsFromSearchEngines>
+ <Referrers_visitorsFromSocialNetworks>0</Referrers_visitorsFromSocialNetworks>
+ <Referrers_visitorsFromDirectEntry>2</Referrers_visitorsFromDirectEntry>
+ <Referrers_visitorsFromWebsites>0</Referrers_visitorsFromWebsites>
+ <Referrers_visitorsFromCampaigns>0</Referrers_visitorsFromCampaigns>
+ <Referrers_distinctSearchEngines>0</Referrers_distinctSearchEngines>
+ <Referrers_distinctSocialNetworks>0</Referrers_distinctSocialNetworks>
+ <Referrers_distinctKeywords>0</Referrers_distinctKeywords>
+ <Referrers_distinctWebsites>0</Referrers_distinctWebsites>
+ <Referrers_distinctWebsitesUrls>0</Referrers_distinctWebsitesUrls>
+ <Referrers_distinctCampaigns>0</Referrers_distinctCampaigns>
+ <Referrers_visitorsFromDirectEntry_percent>100%</Referrers_visitorsFromDirectEntry_percent>
+ <Referrers_visitorsFromSearchEngines_percent>0%</Referrers_visitorsFromSearchEngines_percent>
+ <Referrers_visitorsFromCampaigns_percent>0%</Referrers_visitorsFromCampaigns_percent>
+ <Referrers_visitorsFromSocialNetworks_percent>0%</Referrers_visitorsFromSocialNetworks_percent>
+ <Referrers_visitorsFromWebsites_percent>0%</Referrers_visitorsFromWebsites_percent>
<nb_conversions>0</nb_conversions>
<nb_visits_converted>0</nb_visits_converted>
<revenue>0</revenue>
diff --git a/plugins/IntranetMeasurable/tests/System/expected/test__notIntranet__API.get_day.xml b/plugins/IntranetMeasurable/tests/System/expected/test__notIntranet__API.get_day.xml
index 416534aa46..ad303275b1 100644
--- a/plugins/IntranetMeasurable/tests/System/expected/test__notIntranet__API.get_day.xml
+++ b/plugins/IntranetMeasurable/tests/System/expected/test__notIntranet__API.get_day.xml
@@ -15,6 +15,22 @@
<bounce_rate_returning>0%</bounce_rate_returning>
<nb_actions_per_visit_returning>0</nb_actions_per_visit_returning>
<avg_time_on_site_returning>0</avg_time_on_site_returning>
+ <Referrers_visitorsFromSearchEngines>0</Referrers_visitorsFromSearchEngines>
+ <Referrers_visitorsFromSocialNetworks>0</Referrers_visitorsFromSocialNetworks>
+ <Referrers_visitorsFromDirectEntry>1</Referrers_visitorsFromDirectEntry>
+ <Referrers_visitorsFromWebsites>0</Referrers_visitorsFromWebsites>
+ <Referrers_visitorsFromCampaigns>0</Referrers_visitorsFromCampaigns>
+ <Referrers_distinctSearchEngines>0</Referrers_distinctSearchEngines>
+ <Referrers_distinctSocialNetworks>0</Referrers_distinctSocialNetworks>
+ <Referrers_distinctKeywords>0</Referrers_distinctKeywords>
+ <Referrers_distinctWebsites>0</Referrers_distinctWebsites>
+ <Referrers_distinctWebsitesUrls>0</Referrers_distinctWebsitesUrls>
+ <Referrers_distinctCampaigns>0</Referrers_distinctCampaigns>
+ <Referrers_visitorsFromDirectEntry_percent>100%</Referrers_visitorsFromDirectEntry_percent>
+ <Referrers_visitorsFromSearchEngines_percent>0%</Referrers_visitorsFromSearchEngines_percent>
+ <Referrers_visitorsFromCampaigns_percent>0%</Referrers_visitorsFromCampaigns_percent>
+ <Referrers_visitorsFromSocialNetworks_percent>0%</Referrers_visitorsFromSocialNetworks_percent>
+ <Referrers_visitorsFromWebsites_percent>0%</Referrers_visitorsFromWebsites_percent>
<nb_conversions>0</nb_conversions>
<nb_visits_converted>0</nb_visits_converted>
<revenue>0</revenue>
diff --git a/plugins/Live/Live.php b/plugins/Live/Live.php
index 29be212907..e1bf4f828f 100644
--- a/plugins/Live/Live.php
+++ b/plugins/Live/Live.php
@@ -34,9 +34,16 @@ class Live extends \Piwik\Plugin
'Live.renderVisitorDetails' => 'renderVisitorDetails',
'Live.renderVisitorIcons' => 'renderVisitorIcons',
'Template.jsGlobalVariables' => 'addJsGlobalVariables',
+ 'API.getPagesComparisonsDisabledFor' => 'getPagesComparisonsDisabledFor',
);
}
+ public function getPagesComparisonsDisabledFor(&$pages)
+ {
+ $pages[] = 'General_Visitors.Live_VisitorLog';
+ $pages[] = 'General_Visitors.General_RealTime';
+ }
+
public function addJsGlobalVariables(&$out)
{
$actionsToDisplayCollapsed = (int)StaticContainer::get('Live.pageViewActionsToDisplayCollapsed');
diff --git a/plugins/Live/javascripts/rowaction.js b/plugins/Live/javascripts/rowaction.js
index 061637eff4..ebaed67b0c 100644
--- a/plugins/Live/javascripts/rowaction.js
+++ b/plugins/Live/javascripts/rowaction.js
@@ -78,15 +78,22 @@
this.performAction(segment, tr, e);
};
- DataTable_RowActions_SegmentVisitorLog.prototype.performAction = function (segment, tr, e) {
+ DataTable_RowActions_SegmentVisitorLog.prototype.performAction = function (segment, tr, e, originalRow) {
var apiMethod = this.dataTable.param.module + '.' + this.dataTable.param.action;
var extraParams = {};
+
if (this.dataTable.param.date && this.dataTable.param.period) {
extraParams = {date: this.dataTable.param.date, period: this.dataTable.param.period};
}
+ var paramOverride = $(originalRow || tr).data('param-override');
+ if (typeof paramOverride !== 'object') {
+ paramOverride = {};
+ }
+ $.extend(extraParams, paramOverride);
+
$.each(this.dataTable.param, function (index, value) {
// we automatically add fields like idDimension, idGoal etc.
if (index !== 'idSite' && index.indexOf('id') === 0 && $.isNumeric(value)) {
@@ -133,7 +140,7 @@
},
isAvailableOnRow: function (dataTableParams, tr) {
- var value = getRawSegmentValueFromRow(tr)
+ var value = getRawSegmentValueFromRow(tr);
if ('undefined' === (typeof value)) {
return false;
}
diff --git a/plugins/Live/stylesheets/live.less b/plugins/Live/stylesheets/live.less
index eb9d64cdb3..9a0e4730a6 100644
--- a/plugins/Live/stylesheets/live.less
+++ b/plugins/Live/stylesheets/live.less
@@ -94,6 +94,12 @@
width: ~"calc(100% + 40px)";
}
+#visitsTotal table.dataTable {
+ td:first-child, th:first-child {
+ padding-left: 14px;
+ }
+}
+
.visitsLiveFooter img {
vertical-align: middle;
}
@@ -600,6 +606,11 @@ a.visitor-log-visitor-profile-link {
}
}
+.reporting-page #widgetLivewidget .theWidgetContent .visitsLiveFooter {
+ margin-left: -9px;
+ margin-right: -2px;
+}
+
.refresh-divider {
display: none;
}
diff --git a/plugins/Login/tests/Integration/PasswordResetterTest.php b/plugins/Login/tests/Integration/PasswordResetterTest.php
index 3d870f64fe..607316f91f 100644
--- a/plugins/Login/tests/Integration/PasswordResetterTest.php
+++ b/plugins/Login/tests/Integration/PasswordResetterTest.php
@@ -203,7 +203,7 @@ class PasswordResetterTest extends IntegrationTestCase
'observers.global' => [
['Mail.send', function (Mail $mail) {
$body = $mail->getBodyHtml(true);
- preg_match('/resetToken=3D([a-zA-Z0-9=\s]+)<\/p>/', $body, $matches);
+ preg_match('/resetToken[=\s]*3D([a-zA-Z0-9=\s]+)<\/p>/', $body, $matches);
if (!empty($matches[1])) {
$capturedToken = $matches[1];
$capturedToken = preg_replace('/=\s*/', '', $capturedToken);
diff --git a/plugins/Morpheus/images/compare.svg b/plugins/Morpheus/images/compare.svg
new file mode 100644
index 0000000000..efefc43677
--- /dev/null
+++ b/plugins/Morpheus/images/compare.svg
@@ -0,0 +1,6 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
+ <!--
+ Source: https://fonts.gstatic.com/s/i/materialicons/compare/v1/24px.svg?download=true (Apache license version 2.0.)
+ -->
+ <path d="M0 0h24v24H0z" fill="none"/><path d="M10 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h5v2h2V1h-2v2zm0 15H5l5-6v6zm9-15h-5v2h5v13l-5-6v9h5c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z"/>
+</svg> \ No newline at end of file
diff --git a/plugins/Morpheus/javascripts/ajaxHelper.js b/plugins/Morpheus/javascripts/ajaxHelper.js
index 9f1d944849..56531ed1e6 100644
--- a/plugins/Morpheus/javascripts/ajaxHelper.js
+++ b/plugins/Morpheus/javascripts/ajaxHelper.js
@@ -166,7 +166,15 @@ function ajaxHelper() {
params = broadcast.getValuesFromUrl(params);
}
+ var arrayParams = ['compareSegments', 'comparePeriods', 'compareDates'];
+
for (var key in params) {
+ if (arrayParams.indexOf(key) !== -1
+ && !params[key]
+ ) {
+ continue;
+ }
+
if(type.toLowerCase() == 'get') {
this.getParams[key] = params[key];
} else if(type.toLowerCase() == 'post') {
@@ -535,12 +543,9 @@ function ajaxHelper() {
* @private
*/
this._mixinDefaultGetParams = function (params) {
+ var piwikUrl = piwikHelper.getAngularDependency('piwikUrl');
- if (window.location.hash) {
- var segment = broadcast.getValueFromHash('segment', window.location.href.split('#')[1]);
- } else {
- var segment = broadcast.getValueFromUrl('segment');
- }
+ var segment = piwikUrl.getSearchParam('segment');
var defaultParams = {
idSite: piwik.idSite || broadcast.getValueFromUrl('idSite'),
diff --git a/plugins/Morpheus/stylesheets/base/colors.less b/plugins/Morpheus/stylesheets/base/colors.less
index 8c82888f03..9a8150c330 100644
--- a/plugins/Morpheus/stylesheets/base/colors.less
+++ b/plugins/Morpheus/stylesheets/base/colors.less
@@ -31,14 +31,46 @@
@color-blue-brandSocialVeryLight: #00aced;
@color-orange-brand: #ff9600;
-@graph-colors-data-series1: #3450A3;
-@graph-colors-data-series2: #43a047;
-@graph-colors-data-series3: #ff7f00;
-@graph-colors-data-series4: #d4291f;
-@graph-colors-data-series5: #6a3d9a;
-@graph-colors-data-series6: #b15928;
-@graph-colors-data-series7: #fdbf6f;
-@graph-colors-data-series8: #cab2d6;
+// see https://material.io/design/color/#tools-for-picking-colors for source for colors
+@graph-colors-data-series0: #0277BD; // light blue 50 series (800)
+@graph-series0-shade1-color: #40C4FF; // (A200)
+@graph-series0-shade2-color: #00B0FF; // (A400)
+@graph-series0-shade3-color: #0091EA; // (A700)
+
+@graph-colors-data-series1: #FF8F00; // amber 50 series (800)
+@graph-series1-shade1-color: #FFD740; // (A200)
+@graph-series1-shade2-color: #FFC400; // (A400)
+@graph-series1-shade3-color: #FFAB00; // (A700)
+
+@graph-colors-data-series2: #AD1457; // pink 50 series (800)
+@graph-series2-shade1-color: #FF4081; // (A200)
+@graph-series2-shade2-color: #F50057; // (A400)
+@graph-series2-shade3-color: #C51162; // (A700)
+
+@graph-colors-data-series3: #6A1B9A; // purple 50 series (800)
+@graph-series3-shade1-color: #E040FB; // (A200)
+@graph-series3-shade2-color: #D500F9; // (A400)
+@graph-series3-shade3-color: #AA00FF; // (A700)
+
+@graph-colors-data-series4: #558B2F; // light green 50 series (800)
+@graph-series4-shade1-color: #B2FF59; // (A200)
+@graph-series4-shade2-color: #76FF03; // (A400)
+@graph-series4-shade3-color: #64DD17; // (A700)
+
+@graph-colors-data-series5: #00838F; // cyan 50 series (800)
+@graph-series5-shade1-color: #18FFFF; // (A200)
+@graph-series5-shade2-color: #00E5FF; // (A400)
+@graph-series5-shade3-color: #00B8D4; // (A700)
+
+@graph-colors-data-series6: #283593; // indigo 50 series (800)
+@graph-series6-shade1-color: #536DFE; // (A200)
+@graph-series6-shade2-color: #3D5AFE; // (A400)
+@graph-series6-shade3-color: #304FFE; // (A700)
+
+@graph-colors-data-series7: #D84315; // deep orange 50 series (800)
+@graph-series7-shade1-color: #FF6E40; // (A200)
+@graph-series7-shade2-color: #FF3D00; // (A400)
+@graph-series7-shade3-color: #DD2C00; // (A700)
@default-box-shade: 0 2px 3px 0 rgba(0,0,0,0.16), 0 0px 3px 0 rgba(0,0,0,0.12);
diff --git a/plugins/Morpheus/stylesheets/main.less b/plugins/Morpheus/stylesheets/main.less
index b9175b225e..659a5af4ac 100644
--- a/plugins/Morpheus/stylesheets/main.less
+++ b/plugins/Morpheus/stylesheets/main.less
@@ -382,7 +382,7 @@ table.dataTable {
}
}
- tr {
+ tr:not(.subDataTableContainer) {
td {
border-bottom: 1px solid @color-silver-l95 !important;
border-color: @color-silver-l95 !important;
@@ -405,6 +405,17 @@ table.dataTable {
border-left: 0;
}
+ &:hover {
+ td:not(.cellSubDataTable):not(.parentComparisonRow) {
+ background-color: @color-silver-l95;
+ }
+ }
+
+ &.label + td.column {
+ // first column after label should have a bit less padding
+ padding-left: 10px;
+ }
+
&.label .label {
text-align: left;
}
@@ -476,23 +487,23 @@ table.dataTable {
div.dataTableVizHtmlTable:not(.dataTableActions),
div.dataTableVizAllColumns,
div.dataTableVizGoals {
- tr.subDataTable > td:first-child::before {
+ tr.subDataTable > td > span.label::before {
display: inline-block;
float: left;
top: 0;
width: 12px;
height: 12px;
- margin-left: -1px;
+ margin-left: -2px;
margin-top: 3px;
margin-right: 8px;
content: '';
}
- tr.subDataTable:not(.expanded) > td:first-child::before {
+ tr.subDataTable:not(.expanded) > td > span.label::before {
background-image: url(plugins/Morpheus/images/plus.png);
}
- tr.subDataTable.expanded > td:first-child::before {
+ tr.subDataTable.expanded > td > span.label::before {
background-image: url(plugins/Morpheus/images/minus.png);
}
}
@@ -515,10 +526,25 @@ div.dataTableVizHtmlTable:not(.dataTableActions),
}
}
+
.UserCountryMap-btn-zoom {
padding-left: 0;
}
+h6.sparkline-title {
+ margin-left: 2px;
+ text-transform: uppercase;
+ font-size: .8em;
+ font-weight: bold;
+ color: #999;
+ margin-bottom: 4px;
+
+ max-width: 95px;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ overflow-x: hidden;
+}
+
div.sparkline {
display: -ms-flexbox;
-ms-box-orient: horizontal;
@@ -527,10 +553,11 @@ div.sparkline {
display: -moz-flex;
display: -ms-flex;
display: flex;
- -webkit-justify-content: space-around;
- -moz-justify-content: space-around;
- -ms-justify-content: space-around;
- justify-content: space-around;
+ -webkit-justify-content: flex-start;
+ -moz-justify-content: flex-start;
+ -ms-justify-content: flex-start;
+ justify-content: flex-start;
+ align-items: center;
border-bottom: 0;
margin-bottom: 10px;
@@ -542,8 +569,20 @@ div.sparkline {
border-bottom: 1px dashed #c3c3c3;
}
}
+
+ .metric-group-title {
+ display: block;
+ font-size: .7em;
+ text-transform: uppercase;
+ color: #999;
+ }
+
+ .sparkline-metrics {
+ margin-bottom: 4px;
+ }
}
+
div.sparkline img {
-webkit-flex-shrink: 0;
-moz-flex-shrink: 0;
@@ -556,7 +595,6 @@ div.sparkline script + div {
margin: 1px 0 0 1px;
}
-
.widgetpreview-base li.widgetpreview-choosen {
background: @color-silver-l95;
position: relative;
diff --git a/plugins/Morpheus/stylesheets/ui/_charts.less b/plugins/Morpheus/stylesheets/ui/_charts.less
index f85008d8d2..917fe9bd14 100644
--- a/plugins/Morpheus/stylesheets/ui/_charts.less
+++ b/plugins/Morpheus/stylesheets/ui/_charts.less
@@ -1,3 +1,132 @@
+// comparison series colors & shades
+.comparison-series-color[data-name=series0] { // series0
+ color: @graph-colors-data-series0;
+}
+
+.comparison-series-color[data-name=series0-shade1] {
+ color: @graph-series0-shade1-color;
+}
+
+.comparison-series-color[data-name=series0-shade2] {
+ color: @graph-series0-shade2-color;
+}
+
+.comparison-series-color[data-name=series0-shade3] {
+ color: @graph-series0-shade3-color;
+}
+
+.comparison-series-color[data-name=series1] { // series1
+ color: @graph-colors-data-series1;
+}
+
+.comparison-series-color[data-name=series1-shade1] {
+ color: @graph-series1-shade1-color;
+}
+
+.comparison-series-color[data-name=series1-shade2] {
+ color: @graph-series1-shade2-color;
+}
+
+.comparison-series-color[data-name=series1-shade3] {
+ color: @graph-series1-shade3-color;
+}
+
+.comparison-series-color[data-name=series2] { // series2
+ color: @graph-colors-data-series2;
+}
+
+.comparison-series-color[data-name=series2-shade1] {
+ color: @graph-series2-shade1-color;
+}
+
+.comparison-series-color[data-name=series2-shade2] {
+ color: @graph-series2-shade2-color;
+}
+
+.comparison-series-color[data-name=series2-shade3] {
+ color: @graph-series2-shade3-color;
+}
+
+.comparison-series-color[data-name=series3] { // series3
+ color: @graph-colors-data-series3;
+}
+
+.comparison-series-color[data-name=series3-shade1] {
+ color: @graph-series3-shade1-color;
+}
+
+.comparison-series-color[data-name=series3-shade2] {
+ color: @graph-series3-shade2-color;
+}
+
+.comparison-series-color[data-name=series3-shade3] {
+ color: @graph-series3-shade3-color;
+}
+
+.comparison-series-color[data-name=series4] { // series4
+ color: @graph-colors-data-series4;
+}
+
+.comparison-series-color[data-name=series4-shade1] {
+ color: @graph-series4-shade1-color;
+}
+
+.comparison-series-color[data-name=series4-shade2] {
+ color: @graph-series4-shade2-color;
+}
+
+.comparison-series-color[data-name=series4-shade3] {
+ color: @graph-series4-shade3-color;
+}
+
+.comparison-series-color[data-name=series5] { // series5
+ color: @graph-colors-data-series5;
+}
+
+.comparison-series-color[data-name=series5-shade1] {
+ color: @graph-series5-shade1-color;
+}
+
+.comparison-series-color[data-name=series5-shade2] {
+ color: @graph-series5-shade2-color;
+}
+
+.comparison-series-color[data-name=series5-shade3] {
+ color: @graph-series5-shade3-color;
+}
+
+.comparison-series-color[data-name=series6] { // series6
+ color: @graph-colors-data-series6;
+}
+
+.comparison-series-color[data-name=series6-shade1] {
+ color: @graph-series6-shade1-color;
+}
+
+.comparison-series-color[data-name=series6-shade2] {
+ color: @graph-series6-shade2-color;
+}
+
+.comparison-series-color[data-name=series6-shade3] {
+ color: @graph-series6-shade3-color;
+}
+
+.comparison-series-color[data-name=series7] { // series7
+ color: @graph-colors-data-series7;
+}
+
+.comparison-series-color[data-name=series7-shade1] {
+ color: @graph-series7-shade1-color;
+}
+
+.comparison-series-color[data-name=series7-shade2] {
+ color: @graph-series7-shade2-color;
+}
+
+.comparison-series-color[data-name=series7-shade3] {
+ color: @graph-series7-shade3-color;
+}
+
// bar graph colors
.bar-graph-colors[data-name=grid-background] {
color: @theme-color-background-contrast;
@@ -12,35 +141,35 @@
}
.bar-graph-colors[data-name=series1] {
- color: @graph-colors-data-series1;
+ color: @graph-colors-data-series0;
}
.bar-graph-colors[data-name=series2] {
- color: @graph-colors-data-series2;
+ color: @graph-colors-data-series1;
}
.bar-graph-colors[data-name=series3] {
- color: @graph-colors-data-series3;
+ color: @graph-colors-data-series2;
}
.bar-graph-colors[data-name=series4] {
- color: @graph-colors-data-series4;
+ color: @graph-colors-data-series3;
}
.bar-graph-colors[data-name=series5] {
- color: @graph-colors-data-series5;
+ color: @graph-colors-data-series4;
}
-.bar-graph-colors[data-name=series6] {
- color: @graph-colors-data-series6;
+.bar-graph-colors[data-name=series5] {
+ color: @graph-colors-data-series5;
}
.bar-graph-colors[data-name=series7] {
- color: @graph-colors-data-series7;
+ color: @graph-colors-data-series6;
}
.bar-graph-colors[data-name=series8] {
- color: @graph-colors-data-series8;
+ color: @graph-colors-data-series7;
}
.bar-graph-colors[data-name=ticks] {
@@ -61,35 +190,35 @@
}
.pie-graph-colors[data-name=series1] {
- color: @graph-colors-data-series1;
+ color: @graph-colors-data-series0;
}
.pie-graph-colors[data-name=series2] {
- color: @graph-colors-data-series2;
+ color: @graph-colors-data-series1;
}
.pie-graph-colors[data-name=series3] {
- color: @graph-colors-data-series3;
+ color: @graph-colors-data-series2;
}
.pie-graph-colors[data-name=series4] {
- color: @graph-colors-data-series4;
+ color: @graph-colors-data-series3;
}
.pie-graph-colors[data-name=series5] {
- color: @graph-colors-data-series5;
+ color: @graph-colors-data-series4;
}
.pie-graph-colors[data-name=series6] {
- color: @graph-colors-data-series6;
+ color: @graph-colors-data-series5;
}
.pie-graph-colors[data-name=series7] {
- color: @graph-colors-data-series7;
+ color: @graph-colors-data-series6;
}
.pie-graph-colors[data-name=series8] {
- color: @graph-colors-data-series8;
+ color: @graph-colors-data-series7;
}
.pie-graph-colors[data-name=ticks] {
@@ -105,35 +234,35 @@
// evolution graph colors
.evolution-graph-colors[data-name=series1] {
- color: @graph-colors-data-series1;
+ color: @graph-colors-data-series0;
}
.evolution-graph-colors[data-name=series2] {
- color: @graph-colors-data-series2;
+ color: @graph-colors-data-series1;
}
.evolution-graph-colors[data-name=series3] {
- color: @graph-colors-data-series3;
+ color: @graph-colors-data-series2;
}
.evolution-graph-colors[data-name=series4] {
- color: @graph-colors-data-series4;
+ color: @graph-colors-data-series3;
}
.evolution-graph-colors[data-name=series5] {
- color: @graph-colors-data-series5;
+ color: @graph-colors-data-series4;
}
.evolution-graph-colors[data-name=series6] {
- color: @graph-colors-data-series6;
+ color: @graph-colors-data-series5;
}
.evolution-graph-colors[data-name=series7] {
- color: @graph-colors-data-series7;
+ color: @graph-colors-data-series6;
}
.evolution-graph-colors[data-name=series8] {
- color: @graph-colors-data-series8;
+ color: @graph-colors-data-series7;
}
.evolution-graph-colors[data-name=ticks] {
diff --git a/plugins/Morpheus/stylesheets/uibase/_periodSelect.less b/plugins/Morpheus/stylesheets/uibase/_periodSelect.less
index 023f278449..f14fc64fd2 100644
--- a/plugins/Morpheus/stylesheets/uibase/_periodSelect.less
+++ b/plugins/Morpheus/stylesheets/uibase/_periodSelect.less
@@ -56,10 +56,6 @@
padding: 0 0 4px 0;
}
-#periodMore {
- overflow: hidden;
-}
-
#periodString .period-date,
#periodString .period-range {
padding: 0 16px 0 0;
@@ -77,3 +73,16 @@
#periodString label.selected-period-label {
text-decoration: underline;
}
+
+#periodString .compareCheckbox {
+ transform: scale(.8);
+ margin-left: -29px;
+
+ .form-group {
+ margin: 15px 0 0;
+ }
+
+ label {
+ padding-left: 25px;
+ }
+}
diff --git a/plugins/Morpheus/templates/dashboard.twig b/plugins/Morpheus/templates/dashboard.twig
index cb7e7f5038..70419aedee 100644
--- a/plugins/Morpheus/templates/dashboard.twig
+++ b/plugins/Morpheus/templates/dashboard.twig
@@ -55,6 +55,8 @@
<div piwik-popover></div>
+ <piwik-comparisons></piwik-comparisons>
+
{% block content %}
{% endblock %}
diff --git a/plugins/MultiSites/MultiSites.php b/plugins/MultiSites/MultiSites.php
index 4b7f614e63..c4fbd355f9 100644
--- a/plugins/MultiSites/MultiSites.php
+++ b/plugins/MultiSites/MultiSites.php
@@ -21,10 +21,16 @@ class MultiSites extends \Piwik\Plugin
'AssetManager.getStylesheetFiles' => 'getStylesheetFiles',
'AssetManager.getJavaScriptFiles' => 'getJsFiles',
'Translate.getClientSideTranslationKeys' => 'getClientSideTranslationKeys',
- 'Metrics.getDefaultMetricTranslations' => 'addMetricTranslations'
+ 'Metrics.getDefaultMetricTranslations' => 'addMetricTranslations',
+ 'API.getPagesComparisonsDisabledFor' => 'getPagesComparisonsDisabledFor',
);
}
+ public function getPagesComparisonsDisabledFor(&$pages)
+ {
+ $pages[] = 'MultiSites.index';
+ }
+
public function addMetricTranslations(&$translations)
{
$appendix = " " . Piwik::translate('MultiSites_Evolution');
diff --git a/plugins/MultiSites/tests/UI/expected-screenshots/MultiSitesTest_all_websites.png b/plugins/MultiSites/tests/UI/expected-screenshots/MultiSitesTest_all_websites.png
index d1a157df0a..2f963cb2d6 100644
--- a/plugins/MultiSites/tests/UI/expected-screenshots/MultiSitesTest_all_websites.png
+++ b/plugins/MultiSites/tests/UI/expected-screenshots/MultiSitesTest_all_websites.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:d1e5b0449e989a4286f4e7fb496c210995bda8c2812ca4024d915bfdac17589b
-size 276562
+oid sha256:5b6962a43480f39fe6f47a7c4e9a5adb2ca394085aa67175be0010be6f321e1a
+size 276667
diff --git a/plugins/MultiSites/tests/UI/expected-screenshots/MultiSitesTest_all_websites_changed_sort_order.png b/plugins/MultiSites/tests/UI/expected-screenshots/MultiSitesTest_all_websites_changed_sort_order.png
index 7743f398a4..777f06010c 100644
--- a/plugins/MultiSites/tests/UI/expected-screenshots/MultiSitesTest_all_websites_changed_sort_order.png
+++ b/plugins/MultiSites/tests/UI/expected-screenshots/MultiSitesTest_all_websites_changed_sort_order.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:8033854fba0b193e69c7dd2d43f5c49782448a71ef8d729765ea2dc72d68df30
-size 55791
+oid sha256:f955e6c95975528ca310d8c01c7901e42f5179642d10afcdb19f91f5824dcfa5
+size 55795
diff --git a/plugins/MultiSites/tests/UI/expected-screenshots/MultiSitesTest_all_websites_page_1.png b/plugins/MultiSites/tests/UI/expected-screenshots/MultiSitesTest_all_websites_page_1.png
index 31cfab6200..b86a3d9d5c 100644
--- a/plugins/MultiSites/tests/UI/expected-screenshots/MultiSitesTest_all_websites_page_1.png
+++ b/plugins/MultiSites/tests/UI/expected-screenshots/MultiSitesTest_all_websites_page_1.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:9fcb7a08c93b58097b1d466e0de99d4e8e8187dd119ee917ecd6a17b20617c0d
-size 72399
+oid sha256:1fceeb08862017f3e8ee9e5da8f638a98516b0026a210b7e69063e9d85bfcbd3
+size 72411
diff --git a/plugins/MultiSites/tests/UI/expected-screenshots/MultiSitesTest_all_websites_range.png b/plugins/MultiSites/tests/UI/expected-screenshots/MultiSitesTest_all_websites_range.png
index 3f8ad7fa91..b8fb605c90 100644
--- a/plugins/MultiSites/tests/UI/expected-screenshots/MultiSitesTest_all_websites_range.png
+++ b/plugins/MultiSites/tests/UI/expected-screenshots/MultiSitesTest_all_websites_range.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:61985bb6c6236078211f5acb5b44999475983ff3dd5caa2ddd54f0dc1d650500
-size 240821
+oid sha256:a950fff37e36248fcca1f0c1df67249fce25f32574c43fcbfe29516c0148da63
+size 240812
diff --git a/plugins/MultiSites/tests/UI/expected-screenshots/MultiSitesTest_all_websites_search.png b/plugins/MultiSites/tests/UI/expected-screenshots/MultiSitesTest_all_websites_search.png
index eb335c1078..e17ef474a0 100644
--- a/plugins/MultiSites/tests/UI/expected-screenshots/MultiSitesTest_all_websites_search.png
+++ b/plugins/MultiSites/tests/UI/expected-screenshots/MultiSitesTest_all_websites_search.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:0d314c7b645eb175d69efff04e7b17db5cafbc9d0c56e19f9cbdb889dd453cc4
-size 55765
+oid sha256:9e324d4d241366a4fcd46f7a034326065cc99c653f92c1601f2754c1393f7670
+size 55766
diff --git a/plugins/Overlay/javascripts/rowaction.js b/plugins/Overlay/javascripts/rowaction.js
index f4e733433d..6b76071e24 100644
--- a/plugins/Overlay/javascripts/rowaction.js
+++ b/plugins/Overlay/javascripts/rowaction.js
@@ -18,10 +18,10 @@ DataTable_RowActions_Overlay.prototype = new DataTable_RowAction;
DataTable_RowActions_Overlay.registeredReports = [];
DataTable_RowActions_Overlay.registerReport = function (handler) {
DataTable_RowActions_Overlay.registeredReports.push(handler);
-}
+};
-DataTable_RowActions_Overlay.prototype.onClick = function (actionA, tr, e) {
+DataTable_RowActions_Overlay.prototype.onClick = function (actionA, tr, e, originalRow) {
if (!actionA.data('overlay-manipulated')) {
actionA.data('overlay-manipulated', 1);
@@ -48,6 +48,15 @@ DataTable_RowActions_Overlay.prototype.onClick = function (actionA, tr, e) {
}
}
+ var paramOverride = $(originalRow || tr).data('param-override');
+ if (typeof paramOverride === 'object' && paramOverride.segment) {
+ if (segment) {
+ segment += ';' + paramOverride.segment;
+ } else {
+ segment = paramOverride.segment;
+ }
+ }
+
if (link) {
var href = Overlay_Helper.getOverlayLink(this.dataTable.param.idSite, 'month', 'today', segment, link);
diff --git a/plugins/Overlay/tests/UI/expected-screenshots/Overlay_row_evolution.png b/plugins/Overlay/tests/UI/expected-screenshots/Overlay_row_evolution.png
index 0e5e4c914d..9dd85085bc 100644
--- a/plugins/Overlay/tests/UI/expected-screenshots/Overlay_row_evolution.png
+++ b/plugins/Overlay/tests/UI/expected-screenshots/Overlay_row_evolution.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:7057571fb17ba62fda1504aeb1aec0e3d2cf51c80ebc659ed8078dfcf0fc9d47
-size 102763
+oid sha256:ca43a135ec6a1900230b6d0c7d01c05252139d7ff4bfa04a281f7031d4d4c5af
+size 102772
diff --git a/plugins/PrivacyManager/tests/UI/PrivacyManager_spec.js b/plugins/PrivacyManager/tests/UI/PrivacyManager_spec.js
index 746006d906..a0c7447cb2 100644
--- a/plugins/PrivacyManager/tests/UI/PrivacyManager_spec.js
+++ b/plugins/PrivacyManager/tests/UI/PrivacyManager_spec.js
@@ -63,7 +63,7 @@ describe("PrivacyManager", function () {
async function deleteDataSubjects()
{
- await page.click('.deleteDataSubjects input');
+ await page.evaluate(() => $('.deleteDataSubjects input').click());
await page.waitFor(500); // wait for animation
}
@@ -267,7 +267,7 @@ describe("PrivacyManager", function () {
it('should ask for confirmation before deleting any visit', async function() {
await deleteDataSubjects();
- const modal = await page.$('.modal.open');
+ const modal = await page.waitFor('.modal.open', { visible: true });
expect(await modal.screenshot()).to.matchImage('gdpr_tools_delete_visit_unconfirmed');
});
diff --git a/plugins/PrivacyManager/tests/UI/expected-screenshots/PrivacyManager_gdpr_tools_delete_visit_cancelled.png b/plugins/PrivacyManager/tests/UI/expected-screenshots/PrivacyManager_gdpr_tools_delete_visit_cancelled.png
index 1f55f910fb..437ed20e1e 100644
--- a/plugins/PrivacyManager/tests/UI/expected-screenshots/PrivacyManager_gdpr_tools_delete_visit_cancelled.png
+++ b/plugins/PrivacyManager/tests/UI/expected-screenshots/PrivacyManager_gdpr_tools_delete_visit_cancelled.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:4f28961a08fcee83f90933ef4c6818bb8bc1962ecfbfa0caab54873575ff0e93
-size 579175
+oid sha256:c3d40d9a01a7e9d7bbb4e553c398c552b37e1a0d99687937742e59a48ba02c2a
+size 542961
diff --git a/plugins/PrivacyManager/tests/UI/expected-screenshots/PrivacyManager_gdpr_tools_delete_visit_unconfirmed.png b/plugins/PrivacyManager/tests/UI/expected-screenshots/PrivacyManager_gdpr_tools_delete_visit_unconfirmed.png
index d307935cac..f86a2bb290 100644
--- a/plugins/PrivacyManager/tests/UI/expected-screenshots/PrivacyManager_gdpr_tools_delete_visit_unconfirmed.png
+++ b/plugins/PrivacyManager/tests/UI/expected-screenshots/PrivacyManager_gdpr_tools_delete_visit_unconfirmed.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:10df037298b049fe7aa175340049bd22aededb592127eb339395e4315fbd44b1
+oid sha256:85f5a6e7f0bf43cad861da1fd9a1ae7ceccda110091b25b023ddc0408f065151
size 9192
diff --git a/plugins/PrivacyManager/tests/UI/expected-screenshots/PrivacyManager_gdpr_tools_enrich_segment_by_ip.png b/plugins/PrivacyManager/tests/UI/expected-screenshots/PrivacyManager_gdpr_tools_enrich_segment_by_ip.png
index 02a7343823..437ed20e1e 100644
--- a/plugins/PrivacyManager/tests/UI/expected-screenshots/PrivacyManager_gdpr_tools_enrich_segment_by_ip.png
+++ b/plugins/PrivacyManager/tests/UI/expected-screenshots/PrivacyManager_gdpr_tools_enrich_segment_by_ip.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:20431e23602615276b4a2ca75326761232bc4343b989ac2bfd61eabfb64f5a0b
-size 579646
+oid sha256:c3d40d9a01a7e9d7bbb4e553c398c552b37e1a0d99687937742e59a48ba02c2a
+size 542961
diff --git a/plugins/PrivacyManager/tests/UI/expected-screenshots/PrivacyManager_gdpr_tools_uncheck_one_visit.png b/plugins/PrivacyManager/tests/UI/expected-screenshots/PrivacyManager_gdpr_tools_uncheck_one_visit.png
index 1f55f910fb..1df77f8428 100644
--- a/plugins/PrivacyManager/tests/UI/expected-screenshots/PrivacyManager_gdpr_tools_uncheck_one_visit.png
+++ b/plugins/PrivacyManager/tests/UI/expected-screenshots/PrivacyManager_gdpr_tools_uncheck_one_visit.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:4f28961a08fcee83f90933ef4c6818bb8bc1962ecfbfa0caab54873575ff0e93
-size 579175
+oid sha256:980fb736468b6ddf3248ff249c7a5751fafeb3f71050ef78162174ca5e69af92
+size 543076
diff --git a/plugins/Referrers/API.php b/plugins/Referrers/API.php
index dadd589458..ab82cd91f0 100644
--- a/plugins/Referrers/API.php
+++ b/plugins/Referrers/API.php
@@ -14,9 +14,13 @@ use Piwik\API\ResponseBuilder;
use Piwik\Archive;
use Piwik\Common;
use Piwik\DataTable;
+use Piwik\DataTable\Filter\ColumnCallbackAddColumnPercentage;
use Piwik\Date;
+use Piwik\Metrics;
use Piwik\Piwik;
+use Piwik\Plugin\ReportsProvider;
use Piwik\Plugins\Referrers\DataTable\Filter\GroupDifferentSocialWritings;
+use Piwik\Plugins\Referrers\Reports\Get;
use Piwik\Site;
/**
@@ -30,6 +34,50 @@ use Piwik\Site;
*/
class API extends \Piwik\Plugin\API
{
+ public function get($idSite, $period, $date, $segment = false, $columns = false)
+ {
+ Piwik::checkUserHasViewAccess($idSite);
+
+ $dataTableReferrersType = $this->getReferrerType($idSite, $period, $date, $segment);
+ $dataTable = $this->createReferrerTypeTable($dataTableReferrersType);
+
+ $archive = Archive::build($idSite, $period, $date, $segment);
+
+ $numericArchives = $archive->getDataTableFromNumeric([
+ Archiver::METRIC_DISTINCT_SEARCH_ENGINE_RECORD_NAME,
+ Archiver::METRIC_DISTINCT_SOCIAL_NETWORK_RECORD_NAME,
+ Archiver::METRIC_DISTINCT_KEYWORD_RECORD_NAME,
+ Archiver::METRIC_DISTINCT_WEBSITE_RECORD_NAME,
+ Archiver::METRIC_DISTINCT_URLS_RECORD_NAME,
+ Archiver::METRIC_DISTINCT_CAMPAIGN_RECORD_NAME,
+ ]);
+ $this->mergeNumericArchives($dataTable, $numericArchives);
+
+ $totalVisits = array_sum($dataTableReferrersType->getColumn(Metrics::INDEX_NB_VISITS));
+
+ $percentColumns = [
+ 'Referrers_visitorsFromDirectEntry',
+ 'Referrers_visitorsFromSearchEngines',
+ 'Referrers_visitorsFromCampaigns',
+ 'Referrers_visitorsFromSocialNetworks',
+ 'Referrers_visitorsFromWebsites',
+ ];
+ foreach ($percentColumns as $column) {
+ $dataTable->filter(ColumnCallbackAddColumnPercentage::class, [
+ $column . '_percent',
+ $column,
+ $totalVisits
+ ]);
+ }
+
+ if (!empty($requestedColumns)) {
+ $requestedColumns = Piwik::getArrayFromApiParameter($columns);
+ $dataTable->filter(DataTable\Filter\ColumnDelete::class, [[], $requestedColumns]);
+ }
+
+ return $dataTable;
+ }
+
/**
* @param string $name
* @param int $idSite
@@ -669,4 +717,72 @@ class API extends \Piwik\Plugin\API
$urlsTable = null;
}
+ private function createReferrerTypeTable(DataTable\DataTableInterface $table)
+ {
+ if ($table instanceof DataTable) {
+ $nameToColumnId = array(
+ 'Referrers_visitorsFromSearchEngines' => Common::REFERRER_TYPE_SEARCH_ENGINE,
+ 'Referrers_visitorsFromSocialNetworks' => Common::REFERRER_TYPE_SOCIAL_NETWORK,
+ 'Referrers_visitorsFromDirectEntry' => Common::REFERRER_TYPE_DIRECT_ENTRY,
+ 'Referrers_visitorsFromWebsites' => Common::REFERRER_TYPE_WEBSITE,
+ 'Referrers_visitorsFromCampaigns' => Common::REFERRER_TYPE_CAMPAIGN,
+ );
+
+ $newRow = array();
+ foreach ($nameToColumnId as $nameVar => $columnId) {
+ $value = 0;
+ $row = $table->getRowFromLabel($columnId);
+ if ($row !== false) {
+ $value = $row->getColumn(Metrics::INDEX_NB_VISITS);
+ }
+ $newRow[$nameVar] = $value;
+ }
+
+ $result = new DataTable\Simple();
+ $result->addRowFromSimpleArray($newRow);
+ return $result;
+ } else if ($table instanceof DataTable\Map) {
+ $result = new DataTable\Map();
+ $result->setKeyName($table->getKeyName());
+ foreach ($table->getDataTables() as $label => $childTable) {
+ $referrerTypeTable = $this->createReferrerTypeTable($childTable);
+ $result->addTable($referrerTypeTable, $label);
+ }
+ } else {
+ throw new \Exception("Unexpected DataTable type: " . get_class($table)); // sanity check
+ }
+ return $result;
+ }
+
+ private function mergeNumericArchives(DataTable\DataTableInterface $table, DataTable\DataTableInterface $numericArchives = null)
+ {
+ if ($table instanceof DataTable) {
+ /** @var DataTable $numericArchives */
+ if (empty($numericArchives)) {
+ return;
+ }
+
+ $table->setAllTableMetadata($numericArchives->getAllTableMetadata());
+
+ if ($table->getRows() == 0) {
+ $table->addRow(new DataTable\Row());
+ }
+
+ if ($numericArchives->getRowsCount() == 0) {
+ return;
+ }
+
+ $row = $table->getFirstRow();
+ foreach ($numericArchives->getFirstRow() as $name => $value) {
+ $row->setColumn($name, $value);
+ }
+ } else if ($table instanceof DataTable\Map) {
+ foreach ($table->getDataTables() as $label => $childTable) {
+ $numericArchiveChildTable = $numericArchives->getTable($label);
+ $this->mergeNumericArchives($childTable, $numericArchiveChildTable);
+ }
+ } else {
+ throw new \Exception("Unexpected DataTable type: " . get_class($table)); // sanity check
+ }
+ }
}
diff --git a/plugins/Referrers/Controller.php b/plugins/Referrers/Controller.php
index bc2d25fb7e..66184b8816 100644
--- a/plugins/Referrers/Controller.php
+++ b/plugins/Referrers/Controller.php
@@ -12,6 +12,7 @@ use Piwik\API\Request;
use Piwik\Common;
use Piwik\DataTable\Filter\CalculateEvolutionFilter;
use Piwik\DataTable\Map;
+use Piwik\FrontController;
use Piwik\Metrics;
use Piwik\NumberFormatter;
use Piwik\Period\Range;
@@ -41,213 +42,12 @@ class Controller extends \Piwik\Plugin\Controller
public function getSparklines()
{
- $metrics = $this->getReferrersVisitorsByType();
- $distinctMetrics = $this->getDistinctReferrersMetrics();
+ $_GET['forceView'] = '1';
+ $_GET['viewDataTable'] = Sparklines::ID;
- $numberFormatter = NumberFormatter::getInstance();
-
- $totalVisits = array_sum($metrics);
- foreach ($metrics as $name => $value) {
-
- // calculate percent of total, if there were any visits
- if ($value != 0 && $totalVisits != 0) {
- $percentName = $name . 'Percent';
- $metrics[$percentName] = round(($value / $totalVisits) * 100, 0);
- }
- }
-
- // calculate evolution for visit metrics & distinct metrics
- list($lastPeriodDate, $ignore) = Range::getLastDate();
- if ($lastPeriodDate !== false) {
- $date = Common::getRequestVar('date');
- $period = Common::getRequestVar('period');
-
- $prettyDate = self::getPrettyDate($date, $period);
- $prettyLastPeriodDate = self::getPrettyDate($lastPeriodDate, $period);
-
- // visit metrics
- $previousValues = $this->getReferrersVisitorsByType($lastPeriodDate);
- $metrics = $this->addEvolutionPropertiesToView($prettyDate, $metrics, $prettyLastPeriodDate, $previousValues);
-
- // distinct metrics
- $previousValues = $this->getDistinctReferrersMetrics($lastPeriodDate);
- $distinctMetrics = $this->addEvolutionPropertiesToView($prettyDate, $distinctMetrics, $prettyLastPeriodDate, $previousValues);
- }
-
- /** @var Sparklines $view */
- $view = ViewDataTable\Factory::build(Sparklines::ID, $api = '', $controller = '', $force = true, $loadUserParams = false);
-
- // DIRECT ENTRY
- $metrics['visitorsFromDirectEntry'] = $numberFormatter->formatNumber($metrics['visitorsFromDirectEntry']);
- $values = array($metrics['visitorsFromDirectEntry']);
- $descriptions = array(Piwik::translate('Referrers_TypeDirectEntries'));
-
- if (!empty($metrics['visitorsFromDirectEntryPercent'])) {
- $metrics['visitorsFromDirectEntryPercent'] = $numberFormatter->formatPercent($metrics['visitorsFromDirectEntryPercent'], $precision = 1);
- $values[] = $metrics['visitorsFromDirectEntryPercent'];
- $descriptions[] = Piwik::translate('Referrers_XPercentOfVisits');
- }
-
- $directEntryParams = $this->getReferrerSparklineParams(Common::REFERRER_TYPE_DIRECT_ENTRY);
-
- $view->config->addSparkline($directEntryParams, $values, $descriptions, @$metrics['visitorsFromDirectEntryEvolution']);
-
-
- // WEBSITES
- $metrics['visitorsFromWebsites'] = $numberFormatter->formatNumber($metrics['visitorsFromWebsites']);
- $values = array($metrics['visitorsFromWebsites']);
- $descriptions = array(Piwik::translate('Referrers_TypeWebsites'));
-
- if (!empty($metrics['visitorsFromWebsitesPercent'])) {
- $metrics['visitorsFromWebsitesPercent'] = $numberFormatter->formatPercent($metrics['visitorsFromWebsitesPercent'], $precision = 1);
- $values[] = $metrics['visitorsFromWebsitesPercent'];
- $descriptions[] = Piwik::translate('Referrers_XPercentOfVisits');
- }
-
- $searchEngineParams = $this->getReferrerSparklineParams(Common::REFERRER_TYPE_WEBSITE);
-
- $view->config->addSparkline($searchEngineParams, $values, $descriptions, @$metrics['visitorsFromWebsitesEvolution']);
-
-
- // SEARCH ENGINES
- $metrics['visitorsFromSearchEngines'] = $numberFormatter->formatNumber($metrics['visitorsFromSearchEngines']);
- $values = array($metrics['visitorsFromSearchEngines']);
- $descriptions = array(Piwik::translate('Referrers_TypeSearchEngines'));
-
- if (!empty($metrics['visitorsFromSearchEnginesPercent'])) {
- $metrics['visitorsFromSearchEnginesPercent'] = $numberFormatter->formatPercent($metrics['visitorsFromSearchEnginesPercent'], $precision = 1);
- $values[] = $metrics['visitorsFromSearchEnginesPercent'];
- $descriptions[] = Piwik::translate('Referrers_XPercentOfVisits');
- }
- $searchEngineParams = $this->getReferrerSparklineParams(Common::REFERRER_TYPE_SEARCH_ENGINE);
-
- $view->config->addSparkline($searchEngineParams, $values, $descriptions, @$metrics['visitorsFromSearchEnginesEvolution']);
-
- // SOCIAL NETWORKS
- $metrics['visitorsFromSocialNetworks'] = $numberFormatter->formatNumber($metrics['visitorsFromSocialNetworks']);
- $values = array($metrics['visitorsFromSocialNetworks']);
- $descriptions = array(Piwik::translate('Referrers_TypeSocialNetworks'));
-
- if (!empty($metrics['visitorsFromSocialNetworksPercent'])) {
- $metrics['visitorsFromSocialNetworksPercent'] = $numberFormatter->formatPercent($metrics['visitorsFromSocialNetworksPercent'], $precision = 1);
- $values[] = $metrics['visitorsFromSocialNetworksPercent'];
- $descriptions[] = Piwik::translate('Referrers_XPercentOfVisits');
- }
- $socialNetworkParams = $this->getReferrerSparklineParams(Common::REFERRER_TYPE_SOCIAL_NETWORK);
-
- $view->config->addSparkline($socialNetworkParams, $values, $descriptions, @$metrics['visitorsFromSocialNetworksEvolution']);
-
-
- // CAMPAIGNS
- $metrics['visitorsFromCampaigns'] = $numberFormatter->formatNumber($metrics['visitorsFromCampaigns']);
- $values = array($metrics['visitorsFromCampaigns']);
- $descriptions = array(Piwik::translate('Referrers_TypeCampaigns'));
-
- if (!empty($metrics['visitorsFromCampaignsPercent'])) {
- $metrics['visitorsFromCampaignsPercent'] = $numberFormatter->formatPercent($metrics['visitorsFromCampaignsPercent'], $precision = 1);
- $values[] = $metrics['visitorsFromCampaignsPercent'];
- $descriptions[] = Piwik::translate('Referrers_XPercentOfVisits');
- }
-
- $searchEngineParams = $this->getReferrerSparklineParams(Common::REFERRER_TYPE_CAMPAIGN);
-
- $view->config->addSparkline($searchEngineParams, $values, $descriptions, @$metrics['visitorsFromCampaignsEvolution']);
-
-
- // DISTINCT SEARCH ENGINES
- $sparklineParams = $this->getDistinctSparklineUrlParams('getLastDistinctSearchEnginesGraph');
- $value = $distinctMetrics['numberDistinctSearchEngines'];
- $value = $numberFormatter->formatNumber($value);
- $description = Piwik::translate('Referrers_DistinctSearchEngines');
-
- $view->config->addSparkline($sparklineParams, $value, $description, @$distinctMetrics['numberDistinctSearchEnginesEvolution']);
-
-
- // DISTINCT SOCIAL NETWORKS
- $sparklineParams = $this->getDistinctSparklineUrlParams('getLastDistinctSocialNetworksGraph');
- $value = $distinctMetrics['numberDistinctSocialNetworks'];
- $value = $numberFormatter->formatNumber($value);
- $description = Piwik::translate('Referrers_DistinctSocialNetworks');
-
- $view->config->addSparkline($sparklineParams, $value, $description, @$distinctMetrics['numberDistinctSocialNetworksEvolution']);
-
-
- // DISTINCT WEBSITES
- $sparklineParams = $this->getDistinctSparklineUrlParams('getLastDistinctWebsitesGraph');
-
- $distinctMetrics['numberDistinctWebsites'] = $numberFormatter->formatNumber($distinctMetrics['numberDistinctWebsites']);
- $distinctMetrics['numberDistinctWebsitesUrls'] = $numberFormatter->formatNumber($distinctMetrics['numberDistinctWebsitesUrls']);
-
- $values = array($distinctMetrics['numberDistinctWebsites'], $distinctMetrics['numberDistinctWebsitesUrls']);
- $descriptions = array(Piwik::translate('Referrers_DistinctWebsites'), Piwik::translate('Referrers_UsingNDistinctUrls'));
-
- $view->config->addSparkline($sparklineParams, $values, $descriptions, @$distinctMetrics['numberDistinctWebsitesEvolution']);
-
-
- // DISTINCT KEYWORDS
- $sparklineParams = $this->getDistinctSparklineUrlParams('getLastDistinctKeywordsGraph');
- $value = $distinctMetrics['numberDistinctKeywords'];
- $value = $numberFormatter->formatNumber($value);
- $description = Piwik::translate('Referrers_DistinctKeywords');
-
- $view->config->addSparkline($sparklineParams, $value, $description, @$distinctMetrics['numberDistinctKeywordsEvolution']);
-
-
- // DISTINCT CAMPAIGNS
- $sparklineParams = $this->getDistinctSparklineUrlParams('getLastDistinctCampaignsGraph');
- $value = $distinctMetrics['numberDistinctCampaigns'];
- $value = $numberFormatter->formatNumber($value);
- $description = Piwik::translate('Referrers_DistinctCampaigns');
-
- $view->config->addSparkline($sparklineParams, $value, $description, @$distinctMetrics['numberDistinctCampaignsEvolution']);
-
- return $view->render();
- }
-
- private function getDistinctSparklineUrlParams($action)
- {
- return array('module' => $this->pluginName, 'action' => $action);
+ return FrontController::getInstance()->fetchDispatch('Referrers', 'get');
}
- protected function getReferrersVisitorsByType($date = false)
- {
- if ($date === false) {
- $date = Common::getRequestVar('date', false);
- }
-
- // we disable the queued filters because here we want to get the visits coming from search engines
- // if the filters were applied we would have to look up for a label looking like "Search Engines"
- // which is not good when we have translations
- $dataTableReferrersType = Request::processRequest(
- "Referrers.getReferrerType", array('disable_queued_filters' => '1', 'date' => $date));
-
- $nameToColumnId = array(
- 'visitorsFromSearchEngines' => Common::REFERRER_TYPE_SEARCH_ENGINE,
- 'visitorsFromSocialNetworks' => Common::REFERRER_TYPE_SOCIAL_NETWORK,
- 'visitorsFromDirectEntry' => Common::REFERRER_TYPE_DIRECT_ENTRY,
- 'visitorsFromWebsites' => Common::REFERRER_TYPE_WEBSITE,
- 'visitorsFromCampaigns' => Common::REFERRER_TYPE_CAMPAIGN,
- );
- $return = array();
- foreach ($nameToColumnId as $nameVar => $columnId) {
- $value = 0;
- $row = $dataTableReferrersType->getRowFromLabel($columnId);
- if ($row !== false) {
- $value = $row->getColumn(Metrics::INDEX_NB_VISITS);
- }
- $return[$nameVar] = $value;
- }
- return $return;
- }
-
- protected $referrerTypeToLabel = array(
- Common::REFERRER_TYPE_DIRECT_ENTRY => 'Referrers_DirectEntry',
- Common::REFERRER_TYPE_SEARCH_ENGINE => 'Referrers_SearchEngines',
- Common::REFERRER_TYPE_SOCIAL_NETWORK => 'Referrers_Socials',
- Common::REFERRER_TYPE_WEBSITE => 'Referrers_Websites',
- Common::REFERRER_TYPE_CAMPAIGN => 'Referrers_Campaigns',
- );
-
public function getEvolutionGraph($typeReferrer = false, array $columns = array(), array $defaultColumns = array())
{
$view = $this->getLastUnitGraph($this->pluginName, __FUNCTION__, 'Referrers.getReferrerType');
@@ -369,84 +169,4 @@ class Controller extends \Piwik\Plugin\Controller
return Piwik::translate($label);
}
- /**
- * Returns the URL for the sparkline of visits with a specific referrer type.
- *
- * @param int $referrerType The referrer type. Referrer types are defined in Common class.
- * @return string The URL that can be used to get a sparkline image.
- */
- private function getReferrerSparklineParams($referrerType)
- {
- $totalRow = $this->translator->translate('General_Total');
-
- return array(
- 'columns' => array('nb_visits'),
- 'rows' => array(self::getTranslatedReferrerTypeLabel($referrerType), $totalRow),
- 'typeReferrer' => $referrerType,
- 'module' => $this->pluginName,
- 'action' => 'getReferrerType'
- );
- }
-
- /**
- * Returns an array containing the number of distinct referrers for each
- * referrer type.
- *
- * @param bool|string $date The date to use when getting metrics. If false, the
- * date query param is used.
- * @return array The metrics.
- */
- private function getDistinctReferrersMetrics($date = false)
- {
- $propertyToAccessorMapping = array(
- 'numberDistinctSearchEngines' => 'getNumberOfDistinctSearchEngines',
- 'numberDistinctSocialNetworks' => 'getNumberOfDistinctSocialNetworks',
- 'numberDistinctKeywords' => 'getNumberOfDistinctKeywords',
- 'numberDistinctWebsites' => 'getNumberOfDistinctWebsites',
- 'numberDistinctWebsitesUrls' => 'getNumberOfDistinctWebsitesUrls',
- 'numberDistinctCampaigns' => 'getNumberOfDistinctCampaigns',
- );
-
- $result = array();
- foreach ($propertyToAccessorMapping as $property => $method) {
- $result[$property] = $this->getNumericValue('Referrers.' . $method, $date);
- }
- return $result;
- }
-
- /**
- * Utility method that calculates evolution values for a set of current & past values
- * and sets properties on a View w/ HTML that displays the evolution percents.
- *
- * @param string $date The date of the current values.
- * @param array $currentValues Array mapping view property names w/ present values.
- * @param string $lastPeriodDate The date of the period in the past.
- * @param array $previousValues Array mapping view property names w/ past values. Keys
- * in this array should be the same as keys in $currentValues.
- * @return array Added current values
- */
- private function addEvolutionPropertiesToView($date, $currentValues, $lastPeriodDate, $previousValues)
- {
- foreach ($previousValues as $name => $pastValue) {
- $currentValue = $currentValues[$name];
- $evolutionName = $name . 'Evolution';
-
- $currentValueFormatted = NumberFormatter::getInstance()->format($currentValue);
- $pastValueFormatted = NumberFormatter::getInstance()->format($pastValue);
-
- $currentValues[$evolutionName] = array(
- 'currentValue' => $currentValue,
- 'pastValue' => $pastValue,
- 'tooltip' => Piwik::translate('General_EvolutionSummaryGeneric', array(
- Piwik::translate('General_NVisits', $currentValueFormatted),
- $date,
- Piwik::translate('General_NVisits', $pastValueFormatted),
- $lastPeriodDate,
- CalculateEvolutionFilter::calculate($currentValue, $pastValue, $precision = 1)
- ))
- );
- }
-
- return $currentValues;
- }
}
diff --git a/plugins/Referrers/Referrers.php b/plugins/Referrers/Referrers.php
index 479323eeb4..5558d08918 100644
--- a/plugins/Referrers/Referrers.php
+++ b/plugins/Referrers/Referrers.php
@@ -10,6 +10,7 @@ namespace Piwik\Plugins\Referrers;
use Piwik\Common;
use Piwik\Piwik;
+use Piwik\Plugins\Referrers\Reports\Get;
use Piwik\Plugins\SitesManager\SiteUrls;
/**
@@ -33,9 +34,40 @@ class Referrers extends \Piwik\Plugin
'Tracker.setTrackerCacheGeneral' => 'setTrackerCacheGeneral',
'AssetManager.getJavaScriptFiles' => 'getJsFiles',
'AssetManager.getStylesheetFiles' => 'getStylesheetFiles',
+ 'API.getPagesComparisonsDisabledFor' => 'getPagesComparisonsDisabledFor',
+ 'Metrics.getDefaultMetricTranslations' => 'getDefaultMetricTranslations',
);
}
+ public function getDefaultMetricTranslations(&$translations)
+ {
+ $translations['Referrers_visitorsFromSearchEngines'] = Piwik::translate('Referrers_VisitorsFromSearchEngines');
+ $translations['Referrers_visitorsFromSearchEngines_percent'] = Piwik::translate('Referrers_PercentOfX', $translations['Referrers_visitorsFromSearchEngines']);
+
+ $translations['Referrers_visitorsFromSocialNetworks'] = Piwik::translate('Referrers_VisitorsFromSocialNetworks');
+ $translations['Referrers_visitorsFromSocialNetworks_percent'] = Piwik::translate('Referrers_PercentOfX', $translations['Referrers_visitorsFromSocialNetworks']);
+
+ $translations['Referrers_visitorsFromDirectEntry'] = Piwik::translate('Referrers_VisitorsFromDirectEntry');
+ $translations['Referrers_visitorsFromDirectEntry_percent'] = Piwik::translate('Referrers_PercentOfX', $translations['Referrers_visitorsFromDirectEntry']);
+
+ $translations['Referrers_visitorsFromWebsites'] = Piwik::translate('Referrers_VisitorsFromWebsites');
+ $translations['Referrers_visitorsFromWebsites_percent'] = Piwik::translate('Referrers_PercentOfX', $translations['Referrers_visitorsFromWebsites']);
+
+ $translations['Referrers_visitorsFromCampaigns'] = Piwik::translate('Referrers_VisitorsFromCampaigns');
+ $translations['Referrers_visitorsFromCampaigns_percent'] = Piwik::translate('Referrers_PercentOfX', $translations['Referrers_visitorsFromCampaigns']);
+
+ $translations[Archiver::METRIC_DISTINCT_SEARCH_ENGINE_RECORD_NAME] = ucfirst(Piwik::translate('Referrers_DistinctSearchEngines'));
+ $translations[Archiver::METRIC_DISTINCT_SOCIAL_NETWORK_RECORD_NAME] = ucfirst(Piwik::translate('Referrers_DistinctSocialNetworks'));
+ $translations[Archiver::METRIC_DISTINCT_WEBSITE_RECORD_NAME] = ucfirst(Piwik::translate('Referrers_DistinctWebsites'));
+ $translations[Archiver::METRIC_DISTINCT_KEYWORD_RECORD_NAME] = ucfirst(Piwik::translate('Referrers_DistinctKeywords'));
+ $translations[Archiver::METRIC_DISTINCT_CAMPAIGN_RECORD_NAME] = ucfirst(Piwik::translate('Referrers_DistinctCampaigns'));
+ }
+
+ public function getPagesComparisonsDisabledFor(&$pages)
+ {
+ $pages[] = 'Referrers_Referrers.Referrers_URLCampaignBuilder';
+ }
+
public function getStylesheetFiles(&$stylesheets)
{
$stylesheets[] = 'plugins/Referrers/angularjs/campaign-builder/campaign-builder.directive.less';
diff --git a/plugins/Referrers/Reports/Get.php b/plugins/Referrers/Reports/Get.php
new file mode 100644
index 0000000000..07c429cc6e
--- /dev/null
+++ b/plugins/Referrers/Reports/Get.php
@@ -0,0 +1,200 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link https://matomo.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+
+namespace Piwik\Plugins\Referrers\Reports;
+
+
+use Piwik\API\Request;
+use Piwik\Common;
+use Piwik\DataTable;
+use Piwik\DataTable\Filter\CalculateEvolutionFilter;
+use Piwik\DataTable\Filter\ColumnCallbackAddColumnPercentage;
+use Piwik\Date;
+use Piwik\NumberFormatter;
+use Piwik\Period;
+use Piwik\Period\Factory;
+use Piwik\Period\Month;
+use Piwik\Period\Range;
+use Piwik\Piwik;
+use Piwik\Plugin\ViewDataTable;
+use Piwik\Plugins\CoreVisualizations\Visualizations\Sparklines;
+use Piwik\Plugins\Referrers\Archiver;
+use Piwik\Plugins\Referrers\Controller;
+use Piwik\Report\ReportWidgetFactory;
+use Piwik\Widget\WidgetsList;
+
+class Get extends Base
+{
+ const TOTAL_DIRECT_ENTRIES_METRIC_NAME = 'Referrers_directEntries';
+
+ protected function init()
+ {
+ parent::init();
+
+ $this->name = Piwik::translate('Referrers_ReferrersOverview');
+ $this->documentation = '';
+ $this->processedMetrics = [
+ // none
+ ];
+ $this->metrics = [
+ 'Referrers_visitorsFromSearchEngines',
+ 'Referrers_visitorsFromSearchEngines_percent',
+ 'Referrers_visitorsFromSocialNetworks',
+ 'Referrers_visitorsFromSocialNetworks_percent',
+ 'Referrers_visitorsFromDirectEntry',
+ 'Referrers_visitorsFromDirectEntry_percent',
+ 'Referrers_visitorsFromWebsites',
+ 'Referrers_visitorsFromWebsites_percent',
+ 'Referrers_visitorsFromCampaigns',
+ 'Referrers_visitorsFromCampaigns_percent',
+ Archiver::METRIC_DISTINCT_SEARCH_ENGINE_RECORD_NAME,
+ Archiver::METRIC_DISTINCT_SOCIAL_NETWORK_RECORD_NAME,
+ Archiver::METRIC_DISTINCT_WEBSITE_RECORD_NAME,
+ Archiver::METRIC_DISTINCT_KEYWORD_RECORD_NAME,
+ Archiver::METRIC_DISTINCT_CAMPAIGN_RECORD_NAME,
+ ];
+ }
+
+ public function configureWidgets(WidgetsList $widgetsList, ReportWidgetFactory $factory)
+ {
+ // empty
+ }
+
+ public function configureView(ViewDataTable $view)
+ {
+ if ($view->isViewDataTableId(Sparklines::ID)
+ && $view instanceof Sparklines
+ ) {
+ $this->addSparklineColumns($view);
+ $view->config->addTranslations($this->getSparklineTranslations());
+
+ // add evolution values
+ list($lastPeriodDate, $ignore) = Range::getLastDate();
+ if ($lastPeriodDate !== false) {
+ $date = Common::getRequestVar('date');
+
+ /** @var DataTable $previousData */
+ $previousData = Request::processRequest('Referrers.get', ['date' => $lastPeriodDate]);
+ $previousDataRow = $previousData->getFirstRow();
+
+ $view->config->compute_evolution = function ($columns) use ($date, $lastPeriodDate, $previousDataRow) {
+ $value = reset($columns);
+ $columnName = key($columns);
+
+ if (!in_array($columnName, $this->metrics)) {
+ return;
+ }
+
+ $pastValue = $previousDataRow->getColumn($columnName);
+
+ $currentValueFormatted = NumberFormatter::getInstance()->format($value);
+ $pastValueFormatted = NumberFormatter::getInstance()->format($pastValue);
+
+ return [
+ 'currentValue' => $value,
+ 'pastValue' => $pastValue,
+ 'tooltip' => Piwik::translate('General_EvolutionSummaryGeneric', array(
+ Piwik::translate('General_NVisits', $currentValueFormatted),
+ $date,
+ Piwik::translate('General_NVisits', $pastValueFormatted),
+ $lastPeriodDate,
+ CalculateEvolutionFilter::calculate($value, $pastValue, $precision = 1)
+ )),
+ ];
+ };
+ }
+ }
+ }
+
+ /**
+ * Returns the pretty date representation
+ *
+ * @param $date string
+ * @param $period string
+ * @return string Pretty date
+ */
+ public static function getPrettyDate($date, $period)
+ {
+ return self::getCalendarPrettyDate(Factory::build($period, Date::factory($date)));
+ }
+
+ /**
+ * Returns a prettified date string for use in period selector widget.
+ *
+ * @param Period $period The period to return a pretty string for.
+ * @return string
+ * @api
+ */
+ public static function getCalendarPrettyDate($period)
+ {
+ if ($period instanceof Month) {
+ // show month name when period is for a month
+
+ return $period->getLocalizedLongString();
+ } else {
+ return $period->getPrettyString();
+ }
+ }
+
+ private function addSparklineColumns(Sparklines $view)
+ {
+ $directEntry = Controller::getTranslatedReferrerTypeLabel(Common::REFERRER_TYPE_DIRECT_ENTRY);
+ $directEntry = urlencode($directEntry);
+
+ $website = Controller::getTranslatedReferrerTypeLabel(Common::REFERRER_TYPE_WEBSITE);
+ $website = urlencode($website);
+
+ $searchEngine = Controller::getTranslatedReferrerTypeLabel(Common::REFERRER_TYPE_SEARCH_ENGINE);
+ $searchEngine = urlencode($searchEngine);
+
+ $campaigns = Controller::getTranslatedReferrerTypeLabel(Common::REFERRER_TYPE_CAMPAIGN);
+ $campaigns = urlencode($campaigns);
+
+ $socialNetworks = Controller::getTranslatedReferrerTypeLabel(Common::REFERRER_TYPE_SOCIAL_NETWORK);
+ $socialNetworks = urlencode($socialNetworks);
+
+ $total = Piwik::translate('General_Total');
+
+ $view->config->addSparklineMetric(['Referrers_visitorsFromDirectEntry', 'Referrers_visitorsFromDirectEntry_percent'], 10, ['rows' => $directEntry . ',' . $total]);
+ $view->config->addSparklineMetric(['Referrers_visitorsFromWebsites', 'Referrers_visitorsFromWebsites_percent'], 20, ['rows' => $website . ',' . $total]);
+ $view->config->addSparklineMetric(['Referrers_visitorsFromSearchEngines', 'Referrers_visitorsFromSearchEngines_percent'], 30, ['rows' => $searchEngine . ',' . $total]);
+ $view->config->addSparklineMetric(['Referrers_visitorsFromSocialNetworks', 'Referrers_visitorsFromSocialNetworks_percent'], 40, ['rows' => $socialNetworks . ',' . $total]);
+ $view->config->addSparklineMetric(['Referrers_visitorsFromCampaigns', 'Referrers_visitorsFromCampaigns_percent'], 50, ['rows' => $campaigns . ',' . $total]);
+ $view->config->addSparklineMetric([Archiver::METRIC_DISTINCT_SEARCH_ENGINE_RECORD_NAME], 50);
+ $view->config->addSparklineMetric([Archiver::METRIC_DISTINCT_SOCIAL_NETWORK_RECORD_NAME], 60);
+ $view->config->addSparklineMetric([Archiver::METRIC_DISTINCT_WEBSITE_RECORD_NAME], 70);
+ $view->config->addSparklineMetric([Archiver::METRIC_DISTINCT_KEYWORD_RECORD_NAME], 80);
+ $view->config->addSparklineMetric([Archiver::METRIC_DISTINCT_CAMPAIGN_RECORD_NAME], 90);
+ }
+
+ private function getSparklineTranslations()
+ {
+ $translations = [
+ 'Referrers_visitorsFromDirectEntry' => Piwik::translate('Referrers_TypeDirectEntries'),
+ 'Referrers_visitorsFromWebsites' => Piwik::translate('Referrers_TypeWebsites'),
+ 'Referrers_visitorsFromSearchEngines' => Piwik::translate('Referrers_TypeSearchEngines'),
+ 'Referrers_visitorsFromSocialNetworks' => Piwik::translate('Referrers_TypeSocialNetworks'),
+ 'Referrers_visitorsFromCampaigns' => Piwik::translate('Referrers_TypeCampaigns'),
+ ];
+
+ foreach ($translations as $name => $label) {
+ $translations[$name . '_percent'] = Piwik::translate('Referrers_XPercentOfVisits');
+ }
+
+ $translations = array_merge($translations, [
+ Archiver::METRIC_DISTINCT_SEARCH_ENGINE_RECORD_NAME => Piwik::translate('Referrers_DistinctSearchEngines'),
+ Archiver::METRIC_DISTINCT_SOCIAL_NETWORK_RECORD_NAME => Piwik::translate('Referrers_DistinctSocialNetworks'),
+ Archiver::METRIC_DISTINCT_WEBSITE_RECORD_NAME => Piwik::translate('Referrers_DistinctWebsites'),
+ Archiver::METRIC_DISTINCT_KEYWORD_RECORD_NAME => Piwik::translate('Referrers_DistinctKeywords'),
+ Archiver::METRIC_DISTINCT_CAMPAIGN_RECORD_NAME => Piwik::translate('Referrers_DistinctCampaigns'),
+ ]);
+
+ return $translations;
+ }
+} \ No newline at end of file
diff --git a/plugins/Referrers/lang/en.json b/plugins/Referrers/lang/en.json
index 53019f50ec..0c4d889928 100644
--- a/plugins/Referrers/lang/en.json
+++ b/plugins/Referrers/lang/en.json
@@ -26,6 +26,7 @@
"DistinctSearchEngines": "distinct search engines",
"DistinctSocialNetworks": "distinct social networks",
"DistinctWebsites": "distinct websites",
+ "DistinctWebsiteUrls": "distinct website URLs",
"EvolutionDocumentation": "This is an overview of the referrers that led visitors to your website.",
"EvolutionDocumentationMoreInfo": "For more information about the different channel types, see the documentation of the %s table.",
"Keywords": "Keywords",
@@ -68,6 +69,12 @@
"WidgetSocials": "List of social networks",
"WidgetTopKeywordsForPages": "Top Keywords for Page URL",
"XPercentOfVisits": "%s of visits",
- "Acquisition": "Acquisition"
+ "Acquisition": "Acquisition",
+ "VisitorsFromSearchEngines": "Visitors from Search Engines",
+ "PercentOfX": "Percent of %s",
+ "VisitorsFromSocialNetworks": "Visitors from Social Networks",
+ "VisitorsFromDirectEntry": "Visitors from Direct Entry",
+ "VisitorsFromWebsites": "Visitors from Websites",
+ "VisitorsFromCampaigns": "Visitors from Campaigns"
}
}
diff --git a/plugins/Referrers/tests/System/expected/test_phpSerialized__Referrers.getReferrerType_year.original b/plugins/Referrers/tests/System/expected/test_phpSerialized__Referrers.getReferrerType_year.original
index d1969c268a..96e76d807a 100644
--- a/plugins/Referrers/tests/System/expected/test_phpSerialized__Referrers.getReferrerType_year.original
+++ b/plugins/Referrers/tests/System/expected/test_phpSerialized__Referrers.getReferrerType_year.original
Binary files differ
diff --git a/plugins/Referrers/tests/Unit/DataTable/Filter/UrlsFromWebsiteIdTest.php b/plugins/Referrers/tests/Unit/DataTable/Filter/UrlsFromWebsiteIdTest.php
new file mode 100644
index 0000000000..47590690e2
--- /dev/null
+++ b/plugins/Referrers/tests/Unit/DataTable/Filter/UrlsFromWebsiteIdTest.php
@@ -0,0 +1,48 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+namespace Piwik\Plugins\Referrers\tests\Unit\DataTable\Filter;
+
+use Piwik\DataTable;
+use Piwik\Plugins\Referrers\DataTable\Filter\UrlsFromWebsiteId;
+
+require_once PIWIK_INCLUDE_PATH . '/plugins/Referrers/functions.php';
+
+class UrlsFromWebsiteIdTest extends \PHPUnit_Framework_TestCase
+{
+ public function test_filter_ignoresDomainsPortssAndPortsInRecord()
+ {
+ $table = new DataTable();
+ $table->addRowsFromSimpleArray([
+ ['label' => 'https://abc.com/x/y/z', 'nb_visits' => 1],
+ ['label' => 'https://def.com/x/y/z', 'nb_visits' => 1],
+ ['label' => 'https://def.com/x/y/z/', 'nb_visits' => 1],
+ ['label' => 'http://abc.com:3000/x/y/z', 'nb_visits' => 1],
+ ['label' => 'http://abc.com/', 'nb_visits' => 1],
+ ]);
+
+ $filter = new UrlsFromWebsiteId($table);
+ $filter->filter($table);
+
+ $expected = [
+ new DataTable\Row([
+ DataTable\Row::COLUMNS => ['label' => 'x/y/z', 'nb_visits' => 3],
+ DataTable\Row::METADATA => ['url' => ''],
+ ]),
+ new DataTable\Row([
+ DataTable\Row::COLUMNS => ['label' => 'x/y/z/', 'nb_visits' => 1],
+ DataTable\Row::METADATA => ['url' => ''],
+ ]),
+ new DataTable\Row([
+ DataTable\Row::COLUMNS => ['label' => 'index', 'nb_visits' => 1],
+ DataTable\Row::METADATA => ['url' => ''],
+ ]),
+ ];
+ $this->assertEquals($expected, array_values($table->getRows()));
+ }
+} \ No newline at end of file
diff --git a/plugins/SegmentEditor/SegmentEditor.php b/plugins/SegmentEditor/SegmentEditor.php
index c00e980c8e..7efc8dce91 100644
--- a/plugins/SegmentEditor/SegmentEditor.php
+++ b/plugins/SegmentEditor/SegmentEditor.php
@@ -11,6 +11,8 @@ namespace Piwik\Plugins\SegmentEditor;
use Piwik\API\Request;
use Piwik\ArchiveProcessor\PluginsArchiver;
use Piwik\ArchiveProcessor\Rules;
+use Piwik\Cache;
+use Piwik\CacheId;
use Piwik\Common;
use Piwik\Config;
use Piwik\Container\StaticContainer;
@@ -267,6 +269,7 @@ class SegmentEditor extends \Piwik\Plugin
$translationKeys[] = 'SegmentEditor_OperatorAND';
$translationKeys[] = 'SegmentEditor_OperatorOR';
$translationKeys[] = 'SegmentEditor_AddANDorORCondition';
+ $translationKeys[] = 'SegmentEditor_DefaultAllVisits';
$translationKeys[] = 'General_OperationEquals';
$translationKeys[] = 'General_OperationNotEquals';
$translationKeys[] = 'General_OperationAtMost';
@@ -279,5 +282,25 @@ class SegmentEditor extends \Piwik\Plugin
$translationKeys[] = 'General_OperationDoesNotContain';
$translationKeys[] = 'General_OperationStartsWith';
$translationKeys[] = 'General_OperationEndsWith';
+ $translationKeys[] = 'General_Unknown';
+ $translationKeys[] = 'SegmentEditor_ThisSegmentIsCompared';
+ $translationKeys[] = 'SegmentEditor_ThisSegmentIsSelectedAndCannotBeCompared';
+ $translationKeys[] = 'SegmentEditor_CompareThisSegment';
+ }
+
+ public static function getAllSegmentsForSite($idSite)
+ {
+ $cache = Cache::getTransientCache();
+ $cacheKey = CacheId::siteAware('SegmentEditor_getAll', [$idSite]);
+
+ $segments = $cache->fetch($cacheKey);
+ if (!is_array($segments)) {
+ $segments = Request::processRequest('SegmentEditor.getAll', ['idSite' => $idSite], $default = []);
+ usort($segments, function ($lhs, $rhs) {
+ return strcmp($lhs['name'], $rhs['name']);
+ });
+ $cache->save($cacheKey, $segments);
+ }
+ return $segments;
}
}
diff --git a/plugins/SegmentEditor/javascripts/Segmentation.js b/plugins/SegmentEditor/javascripts/Segmentation.js
index ffa2ea9aa2..afc9b02906 100644
--- a/plugins/SegmentEditor/javascripts/Segmentation.js
+++ b/plugins/SegmentEditor/javascripts/Segmentation.js
@@ -80,6 +80,20 @@ Segmentation = (function($) {
$(this.content).attr('title', title);
};
+ segmentation.prototype.markComparedSegments = function() {
+ var comparisonService = piwikHelper.getAngularDependency('piwikComparisonsService');
+ var comparedSegments = comparisonService.getSegmentComparisons().map(function (comparison) {
+ return comparison.params.segment;
+ });
+
+ $('div.segmentList ul li[data-definition]', this.target).removeClass('comparedSegment').filter(function () {
+ var definition = $(this).attr('data-definition');
+ return comparedSegments.indexOf(definition) !== -1 || comparedSegments.indexOf(decodeURIComponent(definition)) !== -1;
+ }).each(function () {
+ $(this).addClass('comparedSegment');
+ });
+ };
+
segmentation.prototype.markCurrentSegment = function(){
var current = this.getSegment();
@@ -150,7 +164,14 @@ Segmentation = (function($) {
(self.currentSegmentStr == "" ? " class='segmentSelected'" : "")
+ ' data-definition=""><span class="segname" tabindex="4">' + self.translations['SegmentEditor_DefaultAllVisits']
+ ' ' + self.translations['General_DefaultAppended']
- + '</span></li> ';
+ + '</span>';
+ var comparisonService = piwikHelper.getAngularDependency('piwikComparisonsService');
+ if (comparisonService.isComparisonEnabled()
+ || comparisonService.isComparisonEnabled() === null // may not be initialized since this code is outside of angular
+ ) {
+ listHtml += '<span class="compareSegment allVisitsCompareSegment" title="' + _pk_translate('SegmentEditor_CompareThisSegment') + '"></span>';
+ }
+ listHtml += '</li>';
var isVisibleToSuperUserNoticeAlreadyDisplayedOnce = false;
var isVisibleToSuperUserNoticeShouldBeClosed = false;
@@ -196,6 +217,11 @@ Segmentation = (function($) {
if(self.segmentAccess == "write") {
listHtml += '<span class="editSegment" title="'+ self.translations['General_Edit'].toLocaleLowerCase() +'"></span>';
}
+ if (comparisonService.isComparisonEnabled()
+ || comparisonService.isComparisonEnabled() === null // may not be initialized since this code is outside of angular
+ ) {
+ listHtml += '<span class="compareSegment" title="' + _pk_translate('SegmentEditor_CompareThisSegment') + '"></span>';
+ }
listHtml += '</li>';
}
@@ -392,6 +418,20 @@ Segmentation = (function($) {
e.preventDefault();
});
+ self.target.on('click', '.compareSegment', function (e) {
+ e.stopPropagation();
+ e.preventDefault();
+
+ var comparisonService = piwikHelper.getAngularDependency('piwikComparisonsService');
+ comparisonService.addSegmentComparison({
+ segment: $(e.target).closest('li').data('definition'),
+ });
+
+ self.markComparedSegments();
+
+ closeAllOpenLists();
+ });
+
self.target.on("click", ".segmentList li", function (e) {
if ($(e.currentTarget).hasClass("grayed") !== true) {
var idsegment = $(this).attr("data-idsegment");
@@ -764,6 +804,8 @@ Segmentation = (function($) {
}
this.initHtml = function() {
+ var self = this;
+
var html = getListHtml();
if(typeof self.content !== "undefined"){
@@ -775,6 +817,9 @@ Segmentation = (function($) {
// assign content to object attribute to make it easil accesible through all widget methods
this.markCurrentSegment();
+ setTimeout(function () {
+ self.markComparedSegments();
+ });
// Loading message
var segmentIsSet = this.getSegment().length;
@@ -794,6 +839,10 @@ Segmentation = (function($) {
if (self.getSegment() != segment) {
self.setSegment(segment);
self.initHtml();
+ } else {
+ setTimeout(function () {
+ self.markComparedSegments();
+ });
}
});
});
@@ -1044,6 +1093,8 @@ $(document).ready(function() {
$('body').on('mouseup', this.onMouseUp);
initTopControls();
+
+ piwikHelper.getAngularDependency('$rootScope').$emit('piwikSegmentationInited');
};
/**
diff --git a/plugins/SegmentEditor/lang/en.json b/plugins/SegmentEditor/lang/en.json
index 19446db9a6..8e3622801e 100644
--- a/plugins/SegmentEditor/lang/en.json
+++ b/plugins/SegmentEditor/lang/en.json
@@ -52,6 +52,9 @@
"CustomUnprocessedSegmentApiError5": "Please note that you can test whether your segment will work without having to wait for it to be processed by using the Live.getLastVisitsDetails API.",
"CustomUnprocessedSegmentApiError6": "When using this API method, you will see which users and actions were matched by your &segment= parameter.",
"CustomUnprocessedSegmentNoData": "To see data for this segment, you must create this segment manually in the Segment Editor, then wait a couple hours for preprocessing to complete.",
- "AddThisToMatomo": "Add this segment to Matomo"
+ "AddThisToMatomo": "Add this segment to Matomo",
+ "ThisSegmentIsCompared": "This segment is currently compared.",
+ "ThisSegmentIsSelectedAndCannotBeCompared": "This segment is currently selected so cannot be selected to compare.",
+ "CompareThisSegment": "Compare this segment with the selected segment and period."
}
} \ No newline at end of file
diff --git a/plugins/SegmentEditor/stylesheets/segmentation.less b/plugins/SegmentEditor/stylesheets/segmentation.less
index cd6b7632a7..a27e24fb0a 100644
--- a/plugins/SegmentEditor/stylesheets/segmentation.less
+++ b/plugins/SegmentEditor/stylesheets/segmentation.less
@@ -224,19 +224,46 @@ div.scrollable {
margin-bottom: 5px;
}
-.segmentationContainer ul.submenu > li span.editSegment {
- display: block;
- float: right;
- text-align: center;
- margin-right: 4px;
- font-weight: normal;
- background: url(plugins/SegmentEditor/images/edit_segment.png) no-repeat;
- width: 16px;
- height: 16px;
- .opacity(0.5);
+.segmentationContainer ul.submenu > li {
+ span.editSegment, span.compareSegment {
+ display: block;
+ float: right;
+ text-align: center;
+ margin-right: 4px;
+ font-weight: normal;
+ width: 16px;
+ height: 16px;
+ .opacity(0.5);
+
+ &:hover {
+ .opacity(1);
+ }
+ }
+
+ span.editSegment {
+ background: url(plugins/SegmentEditor/images/edit_segment.png) no-repeat;
+ }
+
+ span.compareSegment {
+ background: url(plugins/Morpheus/images/compare.svg) no-repeat;
+ background-size: cover;
+
+ &.allVisitsCompareSegment {
+ margin-right: 24px;
+ }
+ }
+
+ li.segmentSelected, li.comparedSegment {
+ span.compareSegment {
+ pointer-events: none;
+ opacity: 0.2;
+ }
+ }
+}
- &:hover {
- .opacity(1);
+html.comparisonsDisabled .segmentationContainer ul.submenu {
+ span.compareSegment {
+ display: none;
}
}
diff --git a/plugins/SegmentEditor/templates/_segmentSelector.twig b/plugins/SegmentEditor/templates/_segmentSelector.twig
index befd058c1c..528198ce5b 100644
--- a/plugins/SegmentEditor/templates/_segmentSelector.twig
+++ b/plugins/SegmentEditor/templates/_segmentSelector.twig
@@ -99,10 +99,10 @@
<input role="no" type="button" value="{{ 'General_No'|translate }}"/>
</div>
<div class="ui-confirm pleaseChangeBrowserAchivingDisabledSetting">
- <h2>{{ 'SegmentEditor_SegmentNotApplied'|translate(nameOfCurrentSegment)|raw }}</h2>
+ <h2>{{ 'SegmentEditor_SegmentNotApplied'|translate(nameOfCurrentSegment)|rawSafeDecoded|raw }}</h2>
{% set segmentSetting %}{{ 'SegmentEditor_AutoArchivePreProcessed'|translate }}{% endset %}
<p class="description">
- {{ 'SegmentEditor_SegmentNotAppliedMessage'|translate(nameOfCurrentSegment)|raw }}
+ {{ 'SegmentEditor_SegmentNotAppliedMessage'|translate(nameOfCurrentSegment)|rawSafeDecoded|raw }}
<br/>
{{ 'SegmentEditor_DataAvailableAtLaterDate'|translate }}
{% if isSuperUser %}
diff --git a/plugins/SegmentEditor/tests/UI/expected-screenshots/SegmentSelectorEditorTest_1_selector_open.png b/plugins/SegmentEditor/tests/UI/expected-screenshots/SegmentSelectorEditorTest_1_selector_open.png
index e8b3aee3d2..c7e28dd23a 100644
--- a/plugins/SegmentEditor/tests/UI/expected-screenshots/SegmentSelectorEditorTest_1_selector_open.png
+++ b/plugins/SegmentEditor/tests/UI/expected-screenshots/SegmentSelectorEditorTest_1_selector_open.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:bd0d007b56acbd89e249dfabff444366c203f8868a66e9abd300e11e72c89362
-size 14931
+oid sha256:94ed357dba4c01adf66d7ee817b58f48c5ee5e19773de488586e7cf1948f2c85
+size 15404
diff --git a/plugins/SegmentEditor/tests/UI/expected-screenshots/SegmentSelectorEditorTest_complex_segment.png b/plugins/SegmentEditor/tests/UI/expected-screenshots/SegmentSelectorEditorTest_complex_segment.png
index c5892ec6f6..cf0fa8d7fb 100644
--- a/plugins/SegmentEditor/tests/UI/expected-screenshots/SegmentSelectorEditorTest_complex_segment.png
+++ b/plugins/SegmentEditor/tests/UI/expected-screenshots/SegmentSelectorEditorTest_complex_segment.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:617aa6d391021381d4e8338f22df769cc0748a309fb863b93cd6ac8b598fbec6
-size 130614
+oid sha256:73d5555874c628f5e2db4e0dfebafabb4e9eb78a101f0fed06559ebe9a1c0994
+size 130776
diff --git a/plugins/SegmentEditor/tests/UI/expected-screenshots/SegmentSelectorEditorTest_deleted.png b/plugins/SegmentEditor/tests/UI/expected-screenshots/SegmentSelectorEditorTest_deleted.png
index e8b3aee3d2..c7e28dd23a 100644
--- a/plugins/SegmentEditor/tests/UI/expected-screenshots/SegmentSelectorEditorTest_deleted.png
+++ b/plugins/SegmentEditor/tests/UI/expected-screenshots/SegmentSelectorEditorTest_deleted.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:bd0d007b56acbd89e249dfabff444366c203f8868a66e9abd300e11e72c89362
-size 14931
+oid sha256:94ed357dba4c01adf66d7ee817b58f48c5ee5e19773de488586e7cf1948f2c85
+size 15404
diff --git a/plugins/SegmentEditor/tests/UI/expected-screenshots/SegmentSelectorEditorTest_saved.png b/plugins/SegmentEditor/tests/UI/expected-screenshots/SegmentSelectorEditorTest_saved.png
index 806a821fb6..1e7dddd9dd 100644
--- a/plugins/SegmentEditor/tests/UI/expected-screenshots/SegmentSelectorEditorTest_saved.png
+++ b/plugins/SegmentEditor/tests/UI/expected-screenshots/SegmentSelectorEditorTest_saved.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:0370a2bb4439ad2eff9cab35eb342332ef57c02123432f525305c188c3751cc8
-size 16550
+oid sha256:8ddc2c7da46342c738ab78be5cf55a85b7afe1dc3232e16d159b3602f62a67bc
+size 17090
diff --git a/plugins/SegmentEditor/tests/UI/expected-screenshots/SegmentSelectorEditorTest_updated.png b/plugins/SegmentEditor/tests/UI/expected-screenshots/SegmentSelectorEditorTest_updated.png
index c954156536..856bc1dd54 100644
--- a/plugins/SegmentEditor/tests/UI/expected-screenshots/SegmentSelectorEditorTest_updated.png
+++ b/plugins/SegmentEditor/tests/UI/expected-screenshots/SegmentSelectorEditorTest_updated.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:8fe41b99e492db062653d6b96db653cad0354c7c99f8c9f65205b5a91dd8ff0f
-size 16619
+oid sha256:2726e5cac039297dd41cc00ab853df5d216fb05aa9f34638e6e731b26b5e04db
+size 17434
diff --git a/plugins/SegmentEditor/tests/UI/expected-screenshots/UnprocessedSegmentTest_custom_segment.png b/plugins/SegmentEditor/tests/UI/expected-screenshots/UnprocessedSegmentTest_custom_segment.png
index 69c60e77f0..ba7c566010 100644
--- a/plugins/SegmentEditor/tests/UI/expected-screenshots/UnprocessedSegmentTest_custom_segment.png
+++ b/plugins/SegmentEditor/tests/UI/expected-screenshots/UnprocessedSegmentTest_custom_segment.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:dc5cc15887932ad1c9b9c1eba10e5f7cf30f091fa12d4ec62d1ffbf24b11785b
-size 57284
+oid sha256:bd5e092fb0f2d3086c0d9457b469200105151b6f5bfc6862dd0ae64a248e7fa9
+size 57503
diff --git a/plugins/SegmentEditor/tests/UI/expected-screenshots/UnprocessedSegmentTest_unprocessed_segment.png b/plugins/SegmentEditor/tests/UI/expected-screenshots/UnprocessedSegmentTest_unprocessed_segment.png
index 2f9485ca16..ba7c566010 100644
--- a/plugins/SegmentEditor/tests/UI/expected-screenshots/UnprocessedSegmentTest_unprocessed_segment.png
+++ b/plugins/SegmentEditor/tests/UI/expected-screenshots/UnprocessedSegmentTest_unprocessed_segment.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:94ad972d91de1a1932bf09f7744d571595eb54b4c7ddca71eef53d918451368a
-size 98802
+oid sha256:bd5e092fb0f2d3086c0d9457b469200105151b6f5bfc6862dd0ae64a248e7fa9
+size 57503
diff --git a/plugins/Transitions/Transitions.php b/plugins/Transitions/Transitions.php
index 8649bf110f..7d6248fdc0 100644
--- a/plugins/Transitions/Transitions.php
+++ b/plugins/Transitions/Transitions.php
@@ -21,10 +21,16 @@ class Transitions extends \Piwik\Plugin
return array(
'AssetManager.getStylesheetFiles' => 'getStylesheetFiles',
'AssetManager.getJavaScriptFiles' => 'getJsFiles',
- 'Translate.getClientSideTranslationKeys' => 'getClientSideTranslationKeys'
+ 'Translate.getClientSideTranslationKeys' => 'getClientSideTranslationKeys',
+ 'API.getPagesComparisonsDisabledFor' => 'getPagesComparisonsDisabledFor',
);
}
+ public function getPagesComparisonsDisabledFor(&$pages)
+ {
+ $pages[] = "General_Actions.Transitions_Transitions";
+ }
+
public function getStylesheetFiles(&$stylesheets)
{
$stylesheets[] = 'plugins/Transitions/stylesheets/transitions.less';
diff --git a/plugins/Transitions/javascripts/transitions.js b/plugins/Transitions/javascripts/transitions.js
index 2862263ff6..b67245203f 100644
--- a/plugins/Transitions/javascripts/transitions.js
+++ b/plugins/Transitions/javascripts/transitions.js
@@ -71,16 +71,24 @@ DataTable_RowActions_Transitions.prototype.performAction = function (label, tr,
};
DataTable_RowActions_Transitions.prototype.doOpenPopover = function (link) {
- var posSegment = (link+'').indexOf(':segment:');
- var segment = null;
+ var ALLOWED_OVERRIDE_PARAMS = ['segment', 'date', 'period', 'idSite'];
- // handle and remove ':segment:$SEGMENT' from link
- if (posSegment && posSegment > 0) {
- segment = link.substring(posSegment + (':segment:'.length));
- link = link.substring(0, posSegment);
+ var parts = link.split(':');
+
+ var overrideParams = {};
+
+ var i = 0;
+ while (i < parts.length) {
+ var paramName = decodeURIComponent(parts[i]);
+ if (ALLOWED_OVERRIDE_PARAMS.indexOf(paramName) === -1) {
+ i += 1;
+ continue;
+ }
+
+ overrideParams[paramName] = decodeURIComponent(parts[i + 1]);
+ parts.splice(i, 2);
}
- var parts = link.split(':');
if (parts.length < 2) {
return;
}
@@ -90,7 +98,7 @@ DataTable_RowActions_Transitions.prototype.doOpenPopover = function (link) {
var actionName = parts.join(':');
if (this.transitions === null) {
- this.transitions = new Piwik_Transitions(actionType, actionName, this, segment);
+ this.transitions = new Piwik_Transitions(actionType, actionName, this, overrideParams);
} else {
this.transitions.reset(actionType, actionName, segment);
}
@@ -154,8 +162,8 @@ DataTable_RowActions_Registry.register({
// TRANSITIONS IMPLEMENTATION
//
-function Piwik_Transitions(actionType, actionName, rowAction, segment) {
- this.reset(actionType, actionName, segment);
+function Piwik_Transitions(actionType, actionName, rowAction, overrideParams) {
+ this.reset(actionType, actionName, overrideParams);
this.rowAction = rowAction;
this.ajax = new Piwik_Transitions_Ajax();
@@ -165,10 +173,10 @@ function Piwik_Transitions(actionType, actionName, rowAction, segment) {
this.rightGroups = ['followingPages', 'followingSiteSearches', 'downloads', 'outlinks'];
}
-Piwik_Transitions.prototype.reset = function (actionType, actionName, segment) {
+Piwik_Transitions.prototype.reset = function (actionType, actionName, overrideParams) {
this.actionType = actionType;
this.actionName = actionName;
- this.segment = segment;
+ this.overrideParams = overrideParams;
this.popover = null;
this.canvas = null;
@@ -230,7 +238,7 @@ Piwik_Transitions.prototype.showPopover = function (showEmbeddedInReport) {
}
// load the data
- self.model.loadData(self.actionType, self.actionName, self.segment, function () {
+ self.model.loadData(self.actionType, self.actionName, self.overrideParams, function () {
if (typeof Piwik_Transitions.popoverHtml == 'undefined') {
// html not there yet
callbackForHtml = bothLoaded;
@@ -1316,7 +1324,7 @@ Piwik_Transitions_Model.prototype.htmlLoaded = function () {
};
};
-Piwik_Transitions_Model.prototype.loadData = function (actionType, actionName, segment, callback) {
+Piwik_Transitions_Model.prototype.loadData = function (actionType, actionName, overrideParams, callback) {
var self = this;
this.pageviews = 0;
@@ -1362,8 +1370,8 @@ Piwik_Transitions_Model.prototype.loadData = function (actionType, actionName, s
actionName: actionName,
expanded: 1
};
- if (segment) {
- params.segment = segment;
+ if (overrideParams) {
+ $.extend(params, overrideParams);
}
this.ajax.callApi('Transitions.getTransitionsForAction', params,
diff --git a/plugins/TwoFactorAuth/tests/UI/expected-screenshots/TwoFactorAuth_logme_verified.png b/plugins/TwoFactorAuth/tests/UI/expected-screenshots/TwoFactorAuth_logme_verified.png
index 1dbc4ef8fc..efa7e9b284 100644
--- a/plugins/TwoFactorAuth/tests/UI/expected-screenshots/TwoFactorAuth_logme_verified.png
+++ b/plugins/TwoFactorAuth/tests/UI/expected-screenshots/TwoFactorAuth_logme_verified.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:18ce0e44ecb7a3b5bdac1b31cb4ba2f282930e6cd81a0e895c2a93a8d4e6e58b
-size 184145
+oid sha256:c0861497c831b0efdd3571b0b712291bf496bde58112cb7d7edea1cdf5071bbd
+size 184082
diff --git a/plugins/TwoFactorAuth/tests/UI/expected-screenshots/TwoFactorAuth_twofa_forced_step4.png b/plugins/TwoFactorAuth/tests/UI/expected-screenshots/TwoFactorAuth_twofa_forced_step4.png
index 0ce7a55ca0..f7c196018d 100644
--- a/plugins/TwoFactorAuth/tests/UI/expected-screenshots/TwoFactorAuth_twofa_forced_step4.png
+++ b/plugins/TwoFactorAuth/tests/UI/expected-screenshots/TwoFactorAuth_twofa_forced_step4.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:a2f06d7f09046ad8d4d1648d5069b16bc3a0d62314111cd8dea5dfa27575c7fd
-size 183162
+oid sha256:2da09422405154f3d93f1833795649913af4499eb2016664fa9a5874fa851c77
+size 183095
diff --git a/plugins/UserCountryMap/UserCountryMap.php b/plugins/UserCountryMap/UserCountryMap.php
index b2b47e2a72..392df7291a 100644
--- a/plugins/UserCountryMap/UserCountryMap.php
+++ b/plugins/UserCountryMap/UserCountryMap.php
@@ -31,11 +31,17 @@ class UserCountryMap extends \Piwik\Plugin
$hooks = array(
'AssetManager.getJavaScriptFiles' => 'getJsFiles',
'AssetManager.getStylesheetFiles' => 'getStylesheetFiles',
- 'Translate.getClientSideTranslationKeys' => 'getClientSideTranslationKeys'
+ 'Translate.getClientSideTranslationKeys' => 'getClientSideTranslationKeys',
+ 'API.getPagesComparisonsDisabledFor' => 'getPagesComparisonsDisabledFor',
);
return $hooks;
}
+ public function getPagesComparisonsDisabledFor(&$pages)
+ {
+ $pages[] = 'General_Visitors.UserCountryMap_RealTimeMap';
+ }
+
public function getJsFiles(&$jsFiles)
{
$jsFiles[] = "libs/bower_components/visibilityjs/lib/visibility.core.js";
diff --git a/plugins/Widgetize/stylesheets/widgetize.less b/plugins/Widgetize/stylesheets/widgetize.less
index d4bc1d43a2..447c7dc7fe 100644
--- a/plugins/Widgetize/stylesheets/widgetize.less
+++ b/plugins/Widgetize/stylesheets/widgetize.less
@@ -47,4 +47,10 @@ body.widgetized {
#pageFooter {
margin-bottom: 0;
}
+
+ table.dataTable {
+ table-layout: fixed;
+ width: auto;
+ min-width: 100%;
+ }
} \ No newline at end of file