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

github.com/matomo-org/matomo.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authordiosmosis <diosmosis@users.noreply.github.com>2018-08-03 01:57:13 +0300
committerGitHub <noreply@github.com>2018-08-03 01:57:13 +0300
commitcb1d83db863938ace3ebdafd072dfd32e434fded (patch)
tree32d8e98d500b2167dcd9d04d92edfec21da9f5e9 /plugins
parent59e6f48c9d9112b7335e078f05d405264b46f0c5 (diff)
Add reusable widget to display single metric w/ sparkline & evolution percent (+ other changes) (#13101)
* Add empty metric for single metric view. * Add new isReusable property to widget metadata & if set to true, do not grey out the widget in the dashboard manager, even if the widget is used in the dashboard. * Initial working version of single metric view. * Get single metric view widget to work and look correctly (no series picker). * Add series picker to single metric widget and add filter_last_period_evolution parameter. * Persist metric change through dashboard widget parameter saving. * Loading state for single metric view. * Make new evolution param work on processed reports + tweak component implementation. * Tweak CSS and make sure angular components are compiled in widget preview. * Make component work with widget preview and avoid unnecessary widget reloads when multiple widgets of the same type are shown. * Generalize JS lastN range period computing and use to create standalone sparkline angular component and get rid of need for "past-period" argument to single metric view. * Add format_metrics: "1" to API.get method. * Add escaping to _angularComponent.twig. * hacky fix for formatting revenue columns * Format past data values & allow evolution to be calculated for processed metrics. * filter evolution changes * Fix issue in subtable recursion for processed metric computation & metric formatting + add new processed metric compute hooks to fix bug in evolution calculation on subtables. * remove isReusable property. * attempting to change strategy * simpler solution that does not require backend changes * remove unneeded code + fix issue w/ formatted metrics * remove some more unneeded code * write UI test * add new screenshots * Add all goals to single metric view picker. * move category * fix test * fixing more tests * Fixing some UI tests. * Update more screenshots. * update two more screenshots
Diffstat (limited to 'plugins')
-rw-r--r--plugins/API/ProcessedReport.php1
-rw-r--r--plugins/API/lang/en.json3
-rw-r--r--plugins/CoreHome/Categories/GenericCategory.php17
-rw-r--r--plugins/CoreHome/Columns/Metrics/EvolutionMetric.php58
-rw-r--r--plugins/CoreHome/CoreHome.php3
-rw-r--r--plugins/CoreHome/angularjs/common/services/periods.js64
-rw-r--r--plugins/CoreHome/angularjs/common/services/piwik-api.js7
-rw-r--r--plugins/CoreHome/angularjs/sparkline/sparkline.component.js62
-rw-r--r--plugins/CoreHome/angularjs/sparkline/sparkline.component.less8
-rw-r--r--plugins/CoreHome/templates/_angularComponent.twig5
-rw-r--r--plugins/CoreHome/tests/UI/SingleMetricView_spec.js43
-rw-r--r--plugins/CoreHome/tests/UI/expected-screenshots/SingleMetricView_formatted_metric.png3
-rw-r--r--plugins/CoreHome/tests/UI/expected-screenshots/SingleMetricView_loaded.png3
-rw-r--r--plugins/CoreHome/tests/UI/expected-screenshots/SingleMetricView_range.png3
-rw-r--r--plugins/CoreVisualizations/CoreVisualizations.php4
-rw-r--r--plugins/CoreVisualizations/Widgets/SingleMetricView.php83
-rw-r--r--plugins/CoreVisualizations/angularjs/series-picker/series-picker.component.less4
-rw-r--r--plugins/CoreVisualizations/angularjs/single-metric-view/single-metric-view.component.html20
-rw-r--r--plugins/CoreVisualizations/angularjs/single-metric-view/single-metric-view.component.js262
-rw-r--r--plugins/CoreVisualizations/angularjs/single-metric-view/single-metric-view.component.less61
-rw-r--r--plugins/Dashboard/javascripts/dashboardObject.js19
-rw-r--r--plugins/Dashboard/javascripts/dashboardWidget.js4
-rw-r--r--plugins/Dashboard/javascripts/widgetMenu.js7
-rw-r--r--plugins/Dashboard/tests/UI/Dashboard_spec.js1
-rw-r--r--plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_create_new.png4
-rw-r--r--plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_expanded.png4
-rw-r--r--plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_removed.png4
-rw-r--r--plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_widget_list_shown.png4
-rw-r--r--plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_widget_preview.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_reset.png4
-rw-r--r--plugins/Goals/API.php2
-rw-r--r--plugins/Morpheus/javascripts/piwikHelper.js15
34 files changed, 735 insertions, 59 deletions
diff --git a/plugins/API/ProcessedReport.php b/plugins/API/ProcessedReport.php
index 6caac672c3..ec4bfc986f 100644
--- a/plugins/API/ProcessedReport.php
+++ b/plugins/API/ProcessedReport.php
@@ -23,6 +23,7 @@ use Piwik\Metrics;
use Piwik\Metrics\Formatter;
use Piwik\Period;
use Piwik\Piwik;
+use Piwik\Plugin\Metric;
use Piwik\Plugin\ReportsProvider;
use Piwik\Site;
use Piwik\Timer;
diff --git a/plugins/API/lang/en.json b/plugins/API/lang/en.json
index 7b37f23921..72dd837fae 100644
--- a/plugins/API/lang/en.json
+++ b/plugins/API/lang/en.json
@@ -11,6 +11,7 @@
"UserAuthentication": "User authentication",
"UsingTokenAuth": "If you want to %1$s request data within a script, a crontab, etc. %2$s you need to add the parameter %3$s to the API calls URLs that require authentication.",
"Glossary": "Glossary",
- "LearnAboutCommonlyUsedTerms2": "Learn about the commonly used terms to make the most of Matomo Analytics."
+ "LearnAboutCommonlyUsedTerms2": "Learn about the commonly used terms to make the most of Matomo Analytics.",
+ "EvolutionMetricName": "%s Evolution"
}
} \ No newline at end of file
diff --git a/plugins/CoreHome/Categories/GenericCategory.php b/plugins/CoreHome/Categories/GenericCategory.php
new file mode 100644
index 0000000000..13727cc51c
--- /dev/null
+++ b/plugins/CoreHome/Categories/GenericCategory.php
@@ -0,0 +1,17 @@
+<?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\CoreHome\Categories;
+
+use Piwik\Category\Category;
+
+class GenericCategory extends Category
+{
+ protected $id = 'General_Generic';
+ protected $order = 70;
+}
diff --git a/plugins/CoreHome/Columns/Metrics/EvolutionMetric.php b/plugins/CoreHome/Columns/Metrics/EvolutionMetric.php
index 537e42a520..2df4e93171 100644
--- a/plugins/CoreHome/Columns/Metrics/EvolutionMetric.php
+++ b/plugins/CoreHome/Columns/Metrics/EvolutionMetric.php
@@ -10,6 +10,7 @@ namespace Piwik\Plugins\CoreHome\Columns\Metrics;
use Piwik\DataTable;
use Piwik\DataTable\Row;
+use Piwik\Metrics;
use Piwik\Metrics\Formatter;
use Piwik\Piwik;
use Piwik\Plugin\Metric;
@@ -46,15 +47,23 @@ class EvolutionMetric extends ProcessedMetric
private $pastData;
/**
+ * The list of labels leading to the current subtable being processed. Used to get the proper subtable in
+ * $pastData.
+ *
+ * @var string[]
+ */
+ private $labelPath = [];
+
+ /**
* Constructor.
*
* @param string|Metric $wrapped The metric used to calculate the evolution.
- * @param DataTable $pastData The data in the past to use when calculating evolutions.
+ * @param DataTable|null $pastData The data in the past to use when calculating evolutions.
* @param string|false $evolutionMetricName The name of the evolution processed metric. Defaults to
* $wrapped's name with `'_evolution'` appended.
* @param int $quotientPrecision The percent's quotient precision.
*/
- public function __construct($wrapped, DataTable $pastData, $evolutionMetricName = false, $quotientPrecision = 0)
+ public function __construct($wrapped, DataTable $pastData = null, $evolutionMetricName = false, $quotientPrecision = 0)
{
$this->wrapped = $wrapped;
$this->pastData = $pastData;
@@ -75,7 +84,13 @@ class EvolutionMetric extends ProcessedMetric
public function getTranslatedName()
{
- return $this->wrapped instanceof Metric ? $this->wrapped->getTranslatedName() : $this->getName();
+ if ($this->wrapped instanceof Metric) {
+ $metricName = $this->wrapped->getTranslatedName();
+ } else {
+ $defaultMetricTranslations = Metrics::getDefaultMetricTranslations();
+ $metricName = isset($defaultMetricTranslations[$this->wrapped]) ? $defaultMetricTranslations[$this->wrapped] : $this->wrapped;
+ }
+ return Piwik::translate('CoreHome_EvolutionMetricName', [$metricName]);
}
public function compute(Row $row)
@@ -108,6 +123,16 @@ class EvolutionMetric extends ProcessedMetric
return array($this->getWrappedName());
}
+ public function beforeComputeSubtable(Row $row)
+ {
+ $this->labelPath[] = $row->getColumn('label');
+ }
+
+ public function afterComputeSubtable(Row $row)
+ {
+ array_pop($this->labelPath);
+ }
+
protected function getWrappedName()
{
return $this->wrapped instanceof Metric ? $this->wrapped->getName() : $this->wrapped;
@@ -118,6 +143,31 @@ class EvolutionMetric extends ProcessedMetric
*/
public function getPastRowFromCurrent(Row $row)
{
- return $this->pastData->getRowFromLabel($row->getColumn('label'));
+ $pastData = $this->getPastDataTable();
+ if (empty($pastData)) {
+ return null;
+ }
+
+ $label = $row->getColumn('label');
+ return $label ? $pastData->getRowFromLabel($label) : $pastData->getFirstRow();
+ }
+
+ private function getPastDataTable()
+ {
+ $result = $this->pastData;
+ foreach ($this->labelPath as $label) {
+ $row = $result->getRowFromLabel($label);
+ if (empty($row)) {
+ return null;
+ }
+
+ $subtable = $row->getSubtable();
+ if (empty($subtable)) {
+ return null;
+ }
+
+ $result = $subtable;
+ }
+ return $result;
}
} \ No newline at end of file
diff --git a/plugins/CoreHome/CoreHome.php b/plugins/CoreHome/CoreHome.php
index 04fc917ec7..3905bdf78b 100644
--- a/plugins/CoreHome/CoreHome.php
+++ b/plugins/CoreHome/CoreHome.php
@@ -117,6 +117,7 @@ class CoreHome extends \Piwik\Plugin
$stylesheets[] = "plugins/CoreHome/angularjs/period-date-picker/period-date-picker.component.less";
$stylesheets[] = "plugins/CoreHome/angularjs/period-selector/period-selector.directive.less";
$stylesheets[] = "plugins/CoreHome/angularjs/multipairfield/multipairfield.directive.less";
+ $stylesheets[] = "plugins/CoreHome/angularjs/sparkline/sparkline.component.less";
$stylesheets[] = "plugins/CoreHome/angularjs/field-array/field-array.directive.less";
}
@@ -207,6 +208,7 @@ class CoreHome extends \Piwik\Plugin
$jsFiles[] = "plugins/CoreHome/angularjs/activity-indicator/activityindicator.directive.js";
$jsFiles[] = "plugins/CoreHome/angularjs/progressbar/progressbar.directive.js";
$jsFiles[] = "plugins/CoreHome/angularjs/alert/alert.directive.js";
+ $jsFiles[] = "plugins/CoreHome/angularjs/sparkline/sparkline.component.js";
$jsFiles[] = "plugins/CoreHome/angularjs/siteselector/siteselector-model.service.js";
$jsFiles[] = "plugins/CoreHome/angularjs/siteselector/siteselector.controller.js";
@@ -441,6 +443,5 @@ class CoreHome extends \Piwik\Plugin
$translationKeys[] = 'CoreHome_PageDownShortcutDescription';
$translationKeys[] = 'CoreHome_MacPageUp';
$translationKeys[] = 'CoreHome_MacPageDown';
-
}
}
diff --git a/plugins/CoreHome/angularjs/common/services/periods.js b/plugins/CoreHome/angularjs/common/services/periods.js
index d1b50917bd..1c02bca2df 100644
--- a/plugins/CoreHome/angularjs/common/services/periods.js
+++ b/plugins/CoreHome/angularjs/common/services/periods.js
@@ -167,40 +167,64 @@
addCustomPeriod('year', YearPeriod);
// range period
- function RangePeriod(startDate, endDate) {
+ function RangePeriod(startDate, endDate, childPeriodType) {
this.startDate = startDate;
this.endDate = endDate;
+ this.childPeriodType = childPeriodType;
}
- RangePeriod.parse = function parseRangePeriod(strDate) {
- var dates = [];
+ RangePeriod.parse = function parseRangePeriod(strDate, childPeriodType) {
+ childPeriodType = childPeriodType || 'day';
if (/^previous/.test(strDate)) {
- dates = getLastNRange(strDate.substring(8), 1);
+ var endDate = RangePeriod.getLastNRange(childPeriodType, 2).startDate;
+ return RangePeriod.getLastNRange(childPeriodType, strDate.substring(8), endDate);
} else if (/^last/.test(strDate)) {
- dates = getLastNRange(strDate.substring(4), 0);
+ return RangePeriod.getLastNRange(childPeriodType, strDate.substring(4));
} else {
var parts = strDate.split(',');
- dates[0] = parseDate(parts[0]);
- dates[1] = parseDate(parts[1]);
+ return new RangePeriod(parseDate(parts[0]), parseDate(parts[1]), childPeriodType)
}
+ };
- return new RangePeriod(dates[0], dates[1]);
-
- function getLastNRange(strAmount, extraDaysStart) {
- var nAmount = Math.max(parseInt(strAmount) - 1, 0);
- if (isNaN(nAmount)) {
- throw new Error('Invalid range date: ' + strDate);
- }
+ /**
+ * Returns a range representing the last N childPeriodType periods, including the current one.
+ *
+ * @param childPeriodType
+ * @param strAmount
+ * @param endDate
+ * @returns {RangePeriod}
+ */
+ RangePeriod.getLastNRange = function (childPeriodType, strAmount, endDate) {
+ var nAmount = Math.max(parseInt(strAmount) - 1, 0);
+ if (isNaN(nAmount)) {
+ throw new Error('Invalid range date: ' + strDate);
+ }
- var endDate = getToday();
- endDate.setDate(endDate.getDate() - extraDaysStart);
+ endDate = endDate ? parseDate(endDate) : getToday();
- var startDate = new Date(endDate.getTime());
+ var startDate = new Date(endDate.getTime());
+ if (childPeriodType === 'day') {
startDate.setDate(startDate.getDate() - nAmount);
+ } else if (childPeriodType === 'week') {
+ startDate.setDate(startDate.getDate() - (nAmount * 7));
+ } else if (childPeriodType === 'month') {
+ startDate.setMonth(startDate.getMonth() - nAmount);
+ } else if (childPeriodType === 'year') {
+ startDate.setFullYear(startDate.getFullYear() - nAmount);
+ } else {
+ throw new Error("Unknown period type '" + childPeriodType + "'.");
+ }
+
+ if (childPeriodType !== 'day') {
+ var startPeriod = periods[childPeriodType].parse(startDate);
+ var endPeriod = periods[childPeriodType].parse(endDate);
- return [startDate, endDate];
+ startDate = startPeriod.getDateRange()[0];
+ endDate = endPeriod.getDateRange()[1];
}
+
+ return new RangePeriod(startDate, endDate, childPeriodType);
};
RangePeriod.getDisplayText = function () {
@@ -268,6 +292,10 @@
}
function parseDate(strDate) {
+ if (strDate instanceof Date) {
+ return strDate;
+ }
+
if (strDate === 'today'
|| strDate === 'now'
) {
diff --git a/plugins/CoreHome/angularjs/common/services/piwik-api.js b/plugins/CoreHome/angularjs/common/services/piwik-api.js
index 54c07dc0d8..0f77fd7dbb 100644
--- a/plugins/CoreHome/angularjs/common/services/piwik-api.js
+++ b/plugins/CoreHome/angularjs/common/services/piwik-api.js
@@ -137,7 +137,7 @@ var hasBlockedContent = false;
method: 'POST',
url: url,
responseType: requestFormat,
- params: _mixinDefaultGetParams(getParams),
+ params: mixinDefaultGetParams(getParams),
data: $.param(getPostParams(postParams)),
timeout: requestPromise,
headers: headers
@@ -201,7 +201,7 @@ var hasBlockedContent = false;
* @return {object}
* @private
*/
- function _mixinDefaultGetParams (getParamsToMixin) {
+ 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,
@@ -338,7 +338,8 @@ var hasBlockedContent = false;
* @deprecated
*/
abort: abort,
- abortAll: abortAll
+ abortAll: abortAll,
+ mixinDefaultGetParams: mixinDefaultGetParams
};
}
})(); \ No newline at end of file
diff --git a/plugins/CoreHome/angularjs/sparkline/sparkline.component.js b/plugins/CoreHome/angularjs/sparkline/sparkline.component.js
new file mode 100644
index 0000000000..4bf959875c
--- /dev/null
+++ b/plugins/CoreHome/angularjs/sparkline/sparkline.component.js
@@ -0,0 +1,62 @@
+/*!
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+/**
+ * Displays a sparkline. 'module', 'action' and 'date' are required elements of the
+ * params attribute.
+ *
+ * Usage:
+ * <piwik-sparkline params="{'module': 'API', 'action': 'get', 'date': '...'}"></piwik-sparkline>
+ */
+(function () {
+ angular.module('piwikApp').component('piwikSparkline', {
+ template: '<img />',
+ bindings: {
+ params: '<'
+ },
+ controller: SparklineController
+ });
+
+ SparklineController.$inject = ['$element', '$httpParamSerializer', 'piwikApi', 'piwik', 'piwikPeriods'];
+
+ function SparklineController($element, $httpParamSerializer, piwikApi, piwik, piwikPeriods) {
+ var vm = this;
+ vm.$onChanges = $onChanges;
+
+ function $onChanges() {
+ // done manually due to 'random' query param. since it changes the URL on each digest, depending on angular
+ // results in an infinite digest
+ $element.find('img').attr('src', getSparklineUrl());
+ }
+
+ function getSparklineUrl() {
+ var defaultParams = {
+ forceView: '1',
+ viewDataTable: 'sparkline',
+ widget: $element.closest('[widgetId]').length ? '1' : '0',
+ showtitle: '1',
+ colors: JSON.stringify(piwik.getSparklineColors()),
+ random: Date.now(),
+ date: getDefaultDate()
+ };
+
+ var urlParams = piwikApi.mixinDefaultGetParams($element.extend(defaultParams, vm.params));
+ return '?' + $httpParamSerializer(urlParams);
+ }
+
+ function getDefaultDate() {
+ if (piwik.period === 'range') {
+ return piwik.startDateString + ',' + piwik.endDateString;
+ }
+
+ var dateRange = piwikPeriods.get('range').getLastNRange(piwik.period, 30, piwik.currentDateString).getDateRange();
+ var startDateStr = $.datepicker.formatDate('yy-mm-dd', dateRange[0]);
+ var endDateStr = $.datepicker.formatDate('yy-mm-dd', dateRange[1]);
+ return startDateStr + ',' + endDateStr;
+ }
+ }
+})();
diff --git a/plugins/CoreHome/angularjs/sparkline/sparkline.component.less b/plugins/CoreHome/angularjs/sparkline/sparkline.component.less
new file mode 100644
index 0000000000..a31d68547e
--- /dev/null
+++ b/plugins/CoreHome/angularjs/sparkline/sparkline.component.less
@@ -0,0 +1,8 @@
+[piwik-sparkline] {
+ display: inline-block;
+
+ img {
+ width: 100px;
+ height: 25px;
+ }
+} \ No newline at end of file
diff --git a/plugins/CoreHome/templates/_angularComponent.twig b/plugins/CoreHome/templates/_angularComponent.twig
new file mode 100644
index 0000000000..2ac6cb7f90
--- /dev/null
+++ b/plugins/CoreHome/templates/_angularComponent.twig
@@ -0,0 +1,5 @@
+<{{ componentName|e('html') }}
+ {% for key, value in componentParameters %}
+ {{ key|e('html') }}="{{ value|e('html_attr') }}"
+ {% endfor %}
+/> \ No newline at end of file
diff --git a/plugins/CoreHome/tests/UI/SingleMetricView_spec.js b/plugins/CoreHome/tests/UI/SingleMetricView_spec.js
new file mode 100644
index 0000000000..4305c95ae8
--- /dev/null
+++ b/plugins/CoreHome/tests/UI/SingleMetricView_spec.js
@@ -0,0 +1,43 @@
+/*!
+ * Piwik - free/libre analytics platform
+ *
+ * Dashboard screenshot tests.
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+describe('SingleMetricView', function () {
+ this.timeout(0);
+
+ var url = "?module=Widgetize&action=iframe&idSite=1&period=year&date=2012-08-09&moduleToWidgetize=Dashboard&"
+ + "actionToWidgetize=index&idDashboard=5";
+ var rangeUrl = "?module=Widgetize&action=iframe&idSite=1&period=range&date=2012-08-07,2012-08-10&moduleToWidgetize=Dashboard&"
+ + "actionToWidgetize=index&idDashboard=5";
+
+ it('should load correctly', function (done) {
+ expect.screenshot("loaded").to.be.captureSelector('#widgetCoreVisualizationssingleMetricViewcolumn', function (page) {
+ page.load(url, 5000);
+ page.click('.dashboard-manager a.title');
+
+ page.mouseMove('.widgetpreview-categorylist>li:contains(Live!)'); // have to mouse move twice... otherwise Live! will just be highlighted
+ page.mouseMove('.widgetpreview-categorylist > li:contains(Generic)');
+
+ page.mouseMove('.widgetpreview-widgetlist li:contains(Metric)');
+ page.click('.widgetpreview-widgetlist li:contains(Metric)');
+ }, done);
+ });
+
+ it('should handle formatted metrics properly', function (done) {
+ expect.screenshot("formatted_metric").to.be.captureSelector('#widgetCoreVisualizationssingleMetricViewcolumn', function (page) {
+ page.mouseMove('#widgetCoreVisualizationssingleMetricViewcolumn .single-metric-view-picker');
+ page.click('.jqplot-seriespicker-popover label:contains(Revenue)');
+ }, done);
+ });
+
+ it('should handle range periods correctly', function (done) {
+ expect.screenshot("range").to.be.captureSelector('#widgetCoreVisualizationssingleMetricViewcolumn', function (page) {
+ page.load(rangeUrl, 8000);
+ }, done);
+ });
+});
diff --git a/plugins/CoreHome/tests/UI/expected-screenshots/SingleMetricView_formatted_metric.png b/plugins/CoreHome/tests/UI/expected-screenshots/SingleMetricView_formatted_metric.png
new file mode 100644
index 0000000000..358cebf25c
--- /dev/null
+++ b/plugins/CoreHome/tests/UI/expected-screenshots/SingleMetricView_formatted_metric.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:313dada73a2be00e60b92b87103ff6034469ca4f5791a748e89c0c7416731c6e
+size 8060
diff --git a/plugins/CoreHome/tests/UI/expected-screenshots/SingleMetricView_loaded.png b/plugins/CoreHome/tests/UI/expected-screenshots/SingleMetricView_loaded.png
new file mode 100644
index 0000000000..2c5ba94ffd
--- /dev/null
+++ b/plugins/CoreHome/tests/UI/expected-screenshots/SingleMetricView_loaded.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:63f8be060d07ca212107f46af079331be10d41d217200b7866b190be73810b08
+size 5584
diff --git a/plugins/CoreHome/tests/UI/expected-screenshots/SingleMetricView_range.png b/plugins/CoreHome/tests/UI/expected-screenshots/SingleMetricView_range.png
new file mode 100644
index 0000000000..a8b656864e
--- /dev/null
+++ b/plugins/CoreHome/tests/UI/expected-screenshots/SingleMetricView_range.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d67e7660eef5bee9b595401e3500b48fbd36ddaede374d7bc3f59cb331ae56e7
+size 2952
diff --git a/plugins/CoreVisualizations/CoreVisualizations.php b/plugins/CoreVisualizations/CoreVisualizations.php
index 5c633706e3..48117517a4 100644
--- a/plugins/CoreVisualizations/CoreVisualizations.php
+++ b/plugins/CoreVisualizations/CoreVisualizations.php
@@ -40,6 +40,7 @@ class CoreVisualizations extends \Piwik\Plugin
public function getStylesheetFiles(&$stylesheets)
{
$stylesheets[] = "plugins/CoreVisualizations/angularjs/series-picker/series-picker.component.less";
+ $stylesheets[] = "plugins/CoreVisualizations/angularjs/single-metric-view/single-metric-view.component.less";
$stylesheets[] = "plugins/CoreVisualizations/stylesheets/dataTableVisualizations.less";
$stylesheets[] = "plugins/CoreVisualizations/stylesheets/jqplot.css";
@@ -48,7 +49,7 @@ class CoreVisualizations extends \Piwik\Plugin
public function getJsFiles(&$jsFiles)
{
$jsFiles[] = "plugins/CoreVisualizations/angularjs/series-picker/series-picker.component.js";
- $jsFiles[] = "plugins/CoreVisualizations/angularjs/series-picker/series-picker.component.js";
+ $jsFiles[] = "plugins/CoreVisualizations/angularjs/single-metric-view/single-metric-view.component.js";
$jsFiles[] = "plugins/CoreVisualizations/javascripts/seriesPicker.js";
$jsFiles[] = "plugins/CoreVisualizations/javascripts/jqplot.js";
@@ -65,5 +66,6 @@ class CoreVisualizations extends \Piwik\Plugin
$translationKeys[] = 'General_SaveImageOnYourComputer';
$translationKeys[] = 'General_ExportAsImage';
$translationKeys[] = 'General_NoDataForGraph';
+ $translationKeys[] = 'General_EvolutionSummaryGeneric';
}
}
diff --git a/plugins/CoreVisualizations/Widgets/SingleMetricView.php b/plugins/CoreVisualizations/Widgets/SingleMetricView.php
new file mode 100644
index 0000000000..b9ea39840e
--- /dev/null
+++ b/plugins/CoreVisualizations/Widgets/SingleMetricView.php
@@ -0,0 +1,83 @@
+<?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\CoreVisualizations\Widgets;
+
+use Piwik\API\Request;
+use Piwik\Common;
+use Piwik\View;
+use Piwik\Widget\WidgetConfig;
+use Piwik\Plugin\Manager as PluginManager;
+use Piwik\Plugins\Goals\API as GoalsAPI;
+
+class SingleMetricView extends \Piwik\Widget\Widget
+{
+ public static function configure(WidgetConfig $config)
+ {
+ parent::configure($config);
+
+ $column = Common::getRequestVar('column', '', 'string');
+
+ $config->addParameters(['column' => $column]);
+ $config->setCategoryId('General_Generic');
+ $config->setName('General_Metric');
+ $config->setIsWidgetizable();
+ }
+
+ public function render()
+ {
+ $column = Common::getRequestVar('column', 'nb_visits', 'string');
+
+ $goalMetrics = [];
+ $goals = [];
+
+ $idSite = Common::getRequestVar('idSite');
+ $idGoal = Common::getRequestVar('idGoal', false);
+
+ $reportMetadata = Request::processRequest('API.getMetadata', [
+ 'idSites' => $idSite,
+ 'apiModule' => 'API',
+ 'apiAction' => 'get',
+ ]);
+ $reportMetadata = reset($reportMetadata);
+
+ $metricTranslations = array_merge($reportMetadata['metrics'], $reportMetadata['processedMetrics']);
+ $metricDocumentations = $reportMetadata['metricsDocumentation'];
+
+ if (PluginManager::getInstance()->isPluginActivated('Goals')) {
+ $reportMetadata = Request::processRequest('API.getMetadata', [
+ 'idSites' => $idSite,
+ 'apiModule' => 'Goals',
+ 'apiAction' => 'get',
+ ]);
+ $reportMetadata = reset($reportMetadata);
+
+ $goalMetrics = array_merge(
+ array_keys($reportMetadata['metrics']),
+ array_keys($reportMetadata['processedMetrics'])
+ );
+ $metricDocumentations = array_merge($metricDocumentations, $reportMetadata['metricsDocumentation']);
+
+ $goals = GoalsAPI::getInstance()->getGoals($idSite);
+ }
+
+ $view = new View("@CoreHome/_angularComponent.twig");
+ $view->componentName = 'piwik-single-metric-view';
+ $view->componentParameters = [
+ 'metric' => json_encode($column),
+ 'id-goal' => $idGoal === false ? 'undefined' : $idGoal,
+ 'goal-metrics' => json_encode($goalMetrics),
+ 'goals' => json_encode($goals),
+ 'metric-translations' => json_encode($metricTranslations),
+ 'metric-documentations' => json_encode($metricDocumentations),
+ ];
+
+ return $view->render();
+ }
+} \ No newline at end of file
diff --git a/plugins/CoreVisualizations/angularjs/series-picker/series-picker.component.less b/plugins/CoreVisualizations/angularjs/series-picker/series-picker.component.less
index e950b1eefb..9d4fdc8de9 100644
--- a/plugins/CoreVisualizations/angularjs/series-picker/series-picker.component.less
+++ b/plugins/CoreVisualizations/angularjs/series-picker/series-picker.component.less
@@ -6,6 +6,10 @@ piwik-series-picker {
opacity: .55;
}
+ &.open { // while open, make sure we're above other series picker icons
+ z-index: 1000;
+ }
+
> a {
display: inline-block;
opacity: 0;
diff --git a/plugins/CoreVisualizations/angularjs/single-metric-view/single-metric-view.component.html b/plugins/CoreVisualizations/angularjs/single-metric-view/single-metric-view.component.html
new file mode 100644
index 0000000000..7d7b0c2a9f
--- /dev/null
+++ b/plugins/CoreVisualizations/angularjs/single-metric-view/single-metric-view.component.html
@@ -0,0 +1,20 @@
+<div class="singleMetricView" ng-class="{'loading': $ctrl.isLoading}">
+ <piwik-sparkline
+ class="metric-sparkline"
+ params="{module: 'API', action: 'get', columns: $ctrl.metric}"
+ >
+ </piwik-sparkline>
+ <div class="metric-value">
+ <span title="{{ $ctrl.metricDocumentation }}">
+ <strong>{{ $ctrl.metricValue }}</strong> {{ ($ctrl.metricTranslation || '').toLowerCase() }}
+ </span>
+ <span class="metricEvolution"
+ ng-if="$ctrl.pastValue !== null"
+ title="{{ 'General_EvolutionSummaryGeneric'|translate:$ctrl.metricValue:$ctrl.getCurrentPeriod():$ctrl.pastValue:$ctrl.pastPeriod:$ctrl.metricChangePercent }}"
+ >
+ <span ng-class="{'positive-evolution': $ctrl.metricValueUnformatted > $ctrl.pastValueUnformatted, 'negative-evolution': $ctrl.metricValueUnformatted < $ctrl.pastValueUnformatted}">
+ {{ $ctrl.metricChangePercent }}
+ </span>
+ </span>
+ </div>
+</div> \ No newline at end of file
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
new file mode 100644
index 0000000000..d18e9d88ed
--- /dev/null
+++ b/plugins/CoreVisualizations/angularjs/single-metric-view/single-metric-view.component.js
@@ -0,0 +1,262 @@
+/*!
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+/**
+ * Usage:
+ * <piwik-single-metric-view>
+ */
+(function () {
+ angular.module('piwikApp').component('piwikSingleMetricView', {
+ templateUrl: 'plugins/CoreVisualizations/angularjs/single-metric-view/single-metric-view.component.html?cb=' + piwik.cacheBuster,
+ bindings: {
+ metric: '<',
+ idGoal: '<',
+ metricTranslations: '<',
+ metricDocumentations: '<',
+ goals: '<',
+ goalMetrics: '<'
+ },
+ controller: SingleMetricViewController
+ });
+
+ SingleMetricViewController.$inject = ['piwik', 'piwikApi', '$element', '$httpParamSerializer', '$compile', '$scope', 'piwikPeriods', '$q'];
+
+ function SingleMetricViewController(piwik, piwikApi, $element, $httpParamSerializer, $compile, $scope, piwikPeriods, $q) {
+ var seriesPickerScope;
+
+ var vm = this;
+ vm.metricValue = null;
+ vm.isLoading = false;
+ vm.metricTranslation = null;
+ vm.metricDocumentation = null;
+ vm.selectableColumns = [];
+ vm.responses = null;
+ vm.$onInit = $onInit;
+ vm.$onChanges = $onChanges;
+ vm.$onDestroy = $onDestroy;
+ vm.getCurrentPeriod = getCurrentPeriod;
+ vm.getMetricTranslation = getMetricTranslation;
+ vm.setMetric = setMetric;
+
+ function $onInit() {
+ vm.selectedColumns = [vm.metric];
+ if (piwik.period !== 'range') {
+ vm.pastPeriod = getPastPeriodStr();
+ }
+
+ setSelectableColumns();
+
+ createSeriesPicker();
+
+ $element.closest('.widgetContent')
+ .on('widget:destroy', function() { $scope.$parent.$destroy(); })
+ .on('widget:reload', function() { $scope.$parent.$destroy(); });
+ }
+
+ function $onChanges(changes) {
+ if (changes.metric && changes.metric.previousValue !== changes.metric.currentValue) {
+ onMetricChanged();
+ }
+ }
+
+ function $onDestroy() {
+ $element.closest('.widgetContent').off('widget:destroy').off('widget:reload');
+ destroySeriesPicker();
+ }
+
+ function fetchData() {
+ if (vm.responses && vm.responses.length && typeof vm.idGoal === 'undefined') {
+ return $q.resolve();
+ }
+
+ vm.isLoading = true;
+
+ var promises = [];
+
+ var apiModule = 'API';
+ var apiAction = 'get';
+
+ var extraParams = {};
+ if (vm.idGoal) {
+ extraParams.idGoal = vm.idGoal;
+ // the conversion rate added by the AddColumnsProcessedMetrics filter conflicts w/ the goals one, so don't run it
+ extraParams.filter_add_columns_when_show_all_columns = 0;
+
+ apiModule = 'Goals';
+ apiAction = 'get';
+ }
+
+ // first request for formatted data
+ promises.push(piwikApi.fetch($.extend({
+ method: apiModule + '.' + apiAction,
+ format_metrics: 'all'
+ }, extraParams)));
+
+ if (piwik.period !== 'range') {
+ // second request for unformatted data so we can calculate evolution
+ promises.push(piwikApi.fetch($.extend({
+ method: apiModule + '.' + apiAction,
+ format_metrics: '0'
+ }, extraParams)));
+
+ // third request for past data (unformatted)
+ promises.push(piwikApi.fetch($.extend({
+ method: apiModule + '.' + apiAction,
+ date: getLastPeriodDate(),
+ format_metrics: '0',
+ }, extraParams)));
+
+ // fourth request for past data (formatted for tooltip display)
+ promises.push(piwikApi.fetch($.extend({
+ method: apiModule + '.' + apiAction,
+ date: getLastPeriodDate(),
+ format_metrics: 'all',
+ }, extraParams)));
+ }
+
+ return $q.all(promises).then(function (responses) {
+ vm.responses = responses;
+ vm.isLoading = false;
+ });
+ }
+
+ function recalculateValues() {
+ // update display based on processed report metadata
+ setWidgetTitle();
+ vm.metricDocumentation = getMetricDocumentation();
+
+ // update data
+ var currentData = vm.responses[0];
+ vm.metricValue = currentData[vm.metric] || 0;
+
+ if (vm.responses[1]) {
+ vm.metricValueUnformatted = vm.responses[1][vm.metric];
+
+ var pastData = vm.responses[2];
+ vm.pastValueUnformatted = pastData[vm.metric] || 0;
+
+ var evolution = piwik.helper.calculateEvolution(vm.metricValueUnformatted, vm.pastValueUnformatted);
+ vm.metricChangePercent = (evolution * 100).toFixed(2) + ' %';
+
+ var pastDataFormatted = vm.responses[3];
+ vm.pastValue = pastDataFormatted[vm.metric] || 0;
+ } else {
+ vm.pastValue = null;
+ vm.metricChangePercent = null;
+ }
+
+ // don't change the metric translation until data is fetched to avoid loading state confusion
+ vm.metricTranslation = getMetricTranslation();
+ }
+
+ function getLastPeriodDate() {
+ var RangePeriod = piwikPeriods.get('range');
+ var result = RangePeriod.getLastNRange(piwik.period, 2, piwik.currentDateString).startDate;
+ return $.datepicker.formatDate('yy-mm-dd', result);
+ }
+
+ function setWidgetTitle() {
+ var title = vm.getMetricTranslation();
+ if (vm.idGoal) {
+ var goalName = vm.goals[vm.idGoal].name;
+ title = goalName + ' - ' + title;
+ }
+
+ $element.closest('div.widget').find('.widgetTop > .widgetName > span').text(title);
+ }
+
+ function getCurrentPeriod() {
+ if (piwik.startDateString === piwik.endDateString) {
+ return piwik.endDateString;
+ }
+ return piwik.startDateString + ', ' + piwik.endDateString;
+ }
+
+ function createSeriesPicker() {
+ vm.selectedColumns = [vm.idGoal ? ('goal' + vm.idGoal + '_' + vm.metric) : vm.metric];
+
+ var $widgetName = $element.closest('div.widget').find('.widgetTop > .widgetName');
+
+ var $seriesPicker = $('<piwik-series-picker class="single-metric-view-picker" multiselect="false" ' +
+ 'selectable-columns="$ctrl.selectableColumns" selectable-rows="[]" selected-columns="$ctrl.selectedColumns" ' +
+ 'selected-rows="[]" on-select="$ctrl.setMetric(columns[0])" />');
+
+ seriesPickerScope = $scope.$new();
+ $compile($seriesPicker)(seriesPickerScope);
+
+ $widgetName.append($seriesPicker);
+ }
+
+ function destroySeriesPicker() {
+ $element.closest('div.widget').find('.single-metric-view-picker').remove();
+
+ seriesPickerScope.$destroy();
+ seriesPickerScope = null;
+ }
+
+ function getMetricDocumentation() {
+ if (!vm.metricDocumentations || !vm.metricDocumentations[vm.metric]) {
+ return '';
+ }
+
+ return vm.metricDocumentations[vm.metric];
+ }
+
+ function getMetricTranslation() {
+ if (!vm.metricTranslations || !vm.metricTranslations[vm.metric]) {
+ return '';
+ }
+
+ return vm.metricTranslations[vm.metric];
+ }
+
+ function setSelectableColumns() {
+ var result = [];
+ Object.keys(vm.metricTranslations).forEach(function (column) {
+ result.push({ column: column, translation: vm.metricTranslations[column] });
+ });
+
+ Object.keys(vm.goals).forEach(function (idgoal) {
+ var goal = vm.goals[idgoal];
+ vm.goalMetrics.forEach(function (column) {
+ result.push({
+ column: 'goal' + goal.idgoal + '_' + column,
+ translation: goal.name + ' - ' + vm.metricTranslations[column]
+ });
+ });
+ });
+
+ vm.selectableColumns = result;
+ }
+
+ function onMetricChanged() {
+ fetchData().then(recalculateValues);
+
+ // notify widget of parameter change so it is replaced
+ $element.closest('[widgetId]').trigger('setParameters', { column: vm.metric, idGoal: vm.idGoal });
+ }
+
+ function setMetric(newColumn) {
+ var m = newColumn.match(/^goal([0-9])_(.*)/);
+ if (m) {
+ vm.idGoal = m[1];
+ newColumn = m[2];
+ } else {
+ vm.idGoal = undefined;
+ }
+
+ vm.metric = newColumn;
+ onMetricChanged();
+ }
+
+ 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]);
+ }
+ }
+})();
diff --git a/plugins/CoreVisualizations/angularjs/single-metric-view/single-metric-view.component.less b/plugins/CoreVisualizations/angularjs/single-metric-view/single-metric-view.component.less
new file mode 100644
index 0000000000..f091b530e2
--- /dev/null
+++ b/plugins/CoreVisualizations/angularjs/single-metric-view/single-metric-view.component.less
@@ -0,0 +1,61 @@
+.singleMetricView {
+ margin: 5px 12px 10px;
+ display: inline-block;
+
+ &.loading {
+ opacity: 0.5;
+ }
+
+ .metric-value {
+ display: inline-block;
+ font-size: 14px;
+ line-height: 25px;
+ vertical-align: top;
+ margin-left: 3px;
+ }
+
+ .metric-sparkline {
+ display: inline-block;
+ vertical-align: top;
+ img {
+ width: 100px;
+ height: 25px;
+ }
+ }
+
+ .metricEvolution {
+ font-weight: bold;
+ font-size: 12px;
+
+ > span {
+ display: inline-block;
+
+ &:not(.positive-evolution):not(.negative-evolution) {
+ margin-left: 8px;
+ }
+ }
+
+ .positive-evolution::before {
+ content: "";
+ background: url(plugins/MultiSites/images/arrow_up.png) no-repeat center center;
+ display: inline-block;
+ height: 7px;
+ width: 12px;
+ }
+ .negative-evolution::before {
+ content: "";
+ background: url(plugins/MultiSites/images/arrow_down.png) no-repeat center center;
+ display: inline-block;
+ height: 7px;
+ width: 12px;
+ }
+ }
+}
+
+[piwik-single-metric-view] {
+ display: block;
+}
+
+.single-metric-view-picker {
+ margin-left: 6px;
+} \ No newline at end of file
diff --git a/plugins/Dashboard/javascripts/dashboardObject.js b/plugins/Dashboard/javascripts/dashboardObject.js
index c72f53a8c9..cb603be3a6 100644
--- a/plugins/Dashboard/javascripts/dashboardObject.js
+++ b/plugins/Dashboard/javascripts/dashboardObject.js
@@ -160,7 +160,6 @@
*/
addWidget: function (uniqueId, columnNumber, widgetParameters, addWidgetOnTop, isHidden) {
addWidgetTemplate(uniqueId, columnNumber, widgetParameters, addWidgetOnTop, isHidden);
- reloadWidget(uniqueId);
saveLayout();
},
@@ -445,10 +444,14 @@
/**
* Reloads the widget with the given uniqueId
*
- * @param {String} uniqueId
+ * @param {String|jQuery} $widget
*/
- function reloadWidget(uniqueId) {
- $('[widgetId="' + uniqueId + '"]', dashboardElement).dashboardWidget('reload', false, true);
+ function reloadWidget($widget) {
+ if (typeof widget === 'string') {
+ $widget = $('[widgetId="' + uniqueId + '"]', dashboardElement);
+ }
+
+ $widget.dashboardWidget('reload', false, true);
}
/**
@@ -469,15 +472,15 @@
return;
}
- var widgetContent = '<div class="sortable" widgetId="' + uniqueId + '"></div>';
+ var $widgetContent = $('<div class="sortable" widgetId="' + uniqueId + '"></div>');
if (addWidgetOnTop) {
- $('> .col:nth-child(' + columnNumber + ')', dashboardElement).prepend(widgetContent);
+ $('> .col:nth-child(' + columnNumber + ')', dashboardElement).prepend($widgetContent);
} else {
- $('> .col:nth-child(' + columnNumber + ')', dashboardElement).append(widgetContent);
+ $('> .col:nth-child(' + columnNumber + ')', dashboardElement).append($widgetContent);
}
- $('[widgetId="' + uniqueId + '"]', dashboardElement).dashboardWidget({
+ return $widgetContent.dashboardWidget({
uniqueId: uniqueId,
widgetParameters: widgetParameters,
onChange: function () {
diff --git a/plugins/Dashboard/javascripts/dashboardWidget.js b/plugins/Dashboard/javascripts/dashboardWidget.js
index 6756b1d291..22c0538471 100644
--- a/plugins/Dashboard/javascripts/dashboardWidget.js
+++ b/plugins/Dashboard/javascripts/dashboardWidget.js
@@ -117,6 +117,8 @@
var self = this, currentWidget = this.element;
+ $('.widgetContent', currentWidget).trigger('widget:reload');
+
function onWidgetLoadedReplaceElementWithContent(loadedContent) {
var $widgetContent = $('.widgetContent', currentWidget);
@@ -133,7 +135,7 @@
if (currentWidget.parents('body').length) {
// there might be race conditions, eg widget might be just refreshed while whole dashboard is also
// removed from DOM
- piwikHelper.compileAngularComponents($widgetContent);
+ piwikHelper.compileAngularComponents($widgetContent, { forceNewScope: true });
}
$widgetContent.removeClass('loading');
$widgetContent.trigger('widget:create', [self]);
diff --git a/plugins/Dashboard/javascripts/widgetMenu.js b/plugins/Dashboard/javascripts/widgetMenu.js
index 465e8001f5..c847edeb70 100644
--- a/plugins/Dashboard/javascripts/widgetMenu.js
+++ b/plugins/Dashboard/javascripts/widgetMenu.js
@@ -327,9 +327,9 @@ widgetsHelper.loadWidgetAjax = function (widgetUniqueId, widgetParameters, onWid
for (var j = 0; j < widgets.length; j++) {
var widgetName = widgets[j]["name"];
var widgetUniqueId = widgets[j]["uniqueId"];
- // var widgetParameters = widgets[j]["parameters"];
+ var widgetCategoryId = widgets[j].category ? widgets[j].category.id : null;
var widgetClass = '';
- if (!settings.isWidgetAvailable(widgetUniqueId)) {
+ if (!settings.isWidgetAvailable(widgetUniqueId) && widgetCategoryId !== 'General_Generic') {
widgetClass += ' ' + settings.unavailableClass;
}
@@ -408,7 +408,7 @@ widgetsHelper.loadWidgetAjax = function (widgetUniqueId, widgetParameters, onWid
var emptyWidgetHtml = require('piwik/UI/Dashboard').WidgetFactory.make(
widgetUniqueId,
- $('<div/>')
+ $('<span/>')
.attr('title', _pk_translate("Dashboard_AddPreviewedWidget"))
.text(_pk_translate('Dashboard_WidgetPreview'))
);
@@ -418,6 +418,7 @@ widgetsHelper.loadWidgetAjax = function (widgetUniqueId, widgetParameters, onWid
var widgetElement = $(document.getElementById(widgetUniqueId));
// document.getElementById needed for widgets with uniqueid like widgetOpens+Contact+Form
$('.widgetContent', widgetElement).html($(response));
+ piwikHelper.compileAngularComponents($('.widgetContent', widgetElement), { forceNewScope: true });
$('.widgetContent', widgetElement).trigger('widget:create');
settings.onPreviewLoaded(widgetUniqueId, widgetElement);
$('.' + settings.widgetpreviewClass + ' .widgetTop', widgetPreview).on('click', function () {
diff --git a/plugins/Dashboard/tests/UI/Dashboard_spec.js b/plugins/Dashboard/tests/UI/Dashboard_spec.js
index c929f6783e..79b7eb997d 100644
--- a/plugins/Dashboard/tests/UI/Dashboard_spec.js
+++ b/plugins/Dashboard/tests/UI/Dashboard_spec.js
@@ -52,7 +52,6 @@ describe("Dashboard", function () {
[]
];
- // TODO: should probably include an async lib
testEnvironment.callController("Dashboard.saveLayout", {name: 'D4', layout: JSON.stringify(layout), idDashboard: 5, idSite: 2}, function () {
// reset default widget selection
testEnvironment.callController("Dashboard.saveLayoutAsDefault", {layout: 0}, function () {
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 58490a4567..6d8cb7adeb 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:4cc5ac318571f225047a6ea8099a36a3eddf84160490455e4be0cb2f1b1fedfa
-size 47474
+oid sha256:4eed2ccd11a0fd8867a9f83721d674cdcc80d3b3fafaa3e28e237201480e0e65
+size 47578
diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_expanded.png b/plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_expanded.png
index 5f26293303..39a1d09d71 100644
--- a/plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_expanded.png
+++ b/plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_expanded.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:4fd2c5c78884be0cfd6c7bf620dc2f7aa5f82bea9de34a65fae33ef0ddadc5d8
-size 46904
+oid sha256:f581b26a963cf0b0318ef6277122a3c433803bf9a47c419085f491d27a9f0d0c
+size 47830
diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_removed.png b/plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_removed.png
index 4910980871..28a728eb9a 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:0ba89b3a3e64b84fd716d852a1b7c87b13328f7b8722ec67b0ff3f23a63a108c
-size 429615
+oid sha256:6b3381ae6c02a78d3d04192425e289e2cc6d06c5422f2536b48326ac43ffaff7
+size 415347
diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_widget_list_shown.png b/plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_widget_list_shown.png
index e015fafe22..800e666235 100644
--- a/plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_widget_list_shown.png
+++ b/plugins/Dashboard/tests/UI/expected-screenshots/DashboardManager_widget_list_shown.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:2692f5b18e1643264b616159b4a8043fd4b33cfee9abeb89bfcaef19290677b7
-size 57021
+oid sha256:0189c5b0b15e78e0d575180b721e873fc17c441bf5a3418a2afa5068fd206e20
+size 57925
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 40665bf6a7..e8caefddbd 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:22a795cbcbbe4600add04a010b32d8bf6fc4ae353bc31e643b474a2e68ba327e
-size 68744
+oid sha256:76d374477ef2b3898b21b764bab479bb0f297efa1d068b28d2fe7eaf43c8903f
+size 69442
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 6d51d33409..48f26c8a2d 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:e24de3ee34581a370f0e7b91e079d25ca411c0897964525551156259862bf3d5
-size 624303
+oid sha256:6cd315980242867325c54b263aa94c3fbd453ae93f23873c3f1d1e8bded128eb
+size 616084
diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_removed.png b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_removed.png
index 5d8f810e97..159b8b5b30 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:4148a72a37f310fce630d18b5dd1dea6012afc27751bd8f954db37c432917e70
-size 624066
+oid sha256:cb7a5493c40df8442fee1b9119ef4a08b1bb9c5cb61e508d72edd46534e3c07b
+size 615793
diff --git a/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_reset.png b/plugins/Dashboard/tests/UI/expected-screenshots/Dashboard_reset.png
index 7b237f9bb3..58769a7eb8 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:587356c3aa5d29f3ec2e719ae9eaabf1c9db176ffa7322f2af4e81d03a8a6066
-size 252483
+oid sha256:391fc9177fd7effde0c8c66d83d2570f5d84cbb0d58687f883843f26d9b905d4
+size 252398
diff --git a/plugins/Goals/API.php b/plugins/Goals/API.php
index 5d091aa8a4..072f1b9b8e 100644
--- a/plugins/Goals/API.php
+++ b/plugins/Goals/API.php
@@ -427,7 +427,7 @@ class API extends \Piwik\Plugin\API
'date' => $date,
'idGoal' => $idGoal,
'columns' => $columns,
- 'format_metrics' => 'bc'
+ 'format_metrics' => Common::getRequestVar('format_metrics', 'bc'),
));
$tableSegmented->filter('Piwik\Plugins\Goals\DataTable\Filter\AppendNameToColumnNames',
diff --git a/plugins/Morpheus/javascripts/piwikHelper.js b/plugins/Morpheus/javascripts/piwikHelper.js
index dc8b27f50b..685c967d46 100644
--- a/plugins/Morpheus/javascripts/piwikHelper.js
+++ b/plugins/Morpheus/javascripts/piwikHelper.js
@@ -165,7 +165,7 @@ var piwikHelper = {
var scope = null;
if (options.scope) {
scope = options.scope;
- } else {
+ } else if (!options.forceNewScope) { // TODO: docs
scope = angular.element($element).scope();
}
if (!scope) {
@@ -552,6 +552,19 @@ var piwikHelper = {
piwikHelper.shortcuts[key] = description;
Mousetrap.bind(key, callback);
+ },
+
+ calculateEvolution: function (currentValue, pastValue) {
+ var dividend = currentValue - pastValue;
+ var divisor = pastValue;
+
+ if (dividend == 0) {
+ return 0;
+ } else if (divisor == 0) {
+ return 1;
+ } else {
+ return Math.round((dividend / divisor) * 1000) / 1000;
+ }
}
};