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/CoreVisualizations
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/CoreVisualizations')
-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
6 files changed, 433 insertions, 1 deletions
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