From 173c663dd99699069b3f720804a1bf666ad3e855 Mon Sep 17 00:00:00 2001
From: Benoit Lavenier <benoit.lavenier@e-is.pro>
Date: Thu, 31 Dec 2020 19:20:15 +0100
Subject: [PATCH] [enh] Allow to change period on blockchain stats charts (24h
 by default) [enh] Add network stats charts (endpoints per API) [enh] Add
 currency stats charts (memberships and pending memberships)

---
 www/js/entities/peer.js                       |   2 +-
 .../es/js/controllers/document-controllers.js |  16 +-
 www/plugins/graph/css/style.css               |   6 +-
 www/plugins/graph/i18n/locale-en-GB.json      |  33 ++-
 www/plugins/graph/i18n/locale-en.json         |  33 ++-
 www/plugins/graph/i18n/locale-fr-FR.json      |  25 +-
 .../js/controllers/blockchain-controllers.js  |  10 +-
 .../js/controllers/common-controllers.js      | 104 ++++++--
 .../js/controllers/currency-controllers.js    | 230 ++++++++++++-----
 .../js/controllers/docstats-controllers.js    |   3 +
 .../js/controllers/network-controllers.js     | 143 +++++++++++
 .../graph/js/services/data-services.js        | 232 ++++++++++++------
 .../blockchain/graph_block_issuers.html       |  10 +
 .../templates/blockchain/view_stats.html      |   5 +-
 .../common/popover_period_actions.html        |  47 ++++
 .../common/popover_range_actions.html         |  10 +-
 .../currency/tabs/tab_network_stats.html      |   4 +-
 .../currency/tabs/tab_wot_stats.html          |  17 +-
 .../currency/view_currency_extend.html        |   4 +-
 .../templates/currency/view_stats_lg.html     |  19 +-
 .../templates/network/view_peer_stats.html    |   1 -
 .../graph/templates/network/view_stats.html   |  26 ++
 22 files changed, 783 insertions(+), 197 deletions(-)
 create mode 100644 www/plugins/graph/templates/common/popover_period_actions.html
 create mode 100644 www/plugins/graph/templates/network/view_stats.html

diff --git a/www/js/entities/peer.js b/www/js/entities/peer.js
index 6672ba834..91e7b6d13 100644
--- a/www/js/entities/peer.js
+++ b/www/js/entities/peer.js
@@ -176,5 +176,5 @@ Peer.prototype.isBma = function() {
 };
 
 Peer.prototype.hasBma = function() {
-  return this.hasEndpoint('(BASIC_MERKLE_API|BMAS|BMATOR)');
+  return this.hasEndpoint('(BASIC_MERKLED_API|BMAS|BMATOR)');
 };
diff --git a/www/plugins/es/js/controllers/document-controllers.js b/www/plugins/es/js/controllers/document-controllers.js
index 3920f7207..54dc059ed 100644
--- a/www/plugins/es/js/controllers/document-controllers.js
+++ b/www/plugins/es/js/controllers/document-controllers.js
@@ -6,7 +6,7 @@ angular.module('cesium.es.document.controllers', ['cesium.es.services'])
     $stateProvider
 
       .state('app.document_search', {
-        url: "/data/search/:index/:type?q",
+        url: "/data/search/:index/:type?q&sort",
         views: {
           'menuContent': {
             templateUrl: "plugins/es/templates/document/lookup.html",
@@ -47,8 +47,10 @@ function ESDocumentLookupController($scope, $ionicPopover, $location, $timeout,
   $scope.helptipPrefix = 'helptip-document';
   $scope.compactMode = angular.isDefined($scope.compactMode) ? $scope.compactMode : true;
   $scope._source = $scope._source || ["issuer", "hash", "time", "creationTime", "title", "message", "recipient",
-    // Movement field:
-    "medianTime", "amount", "currency", "reference"
+    // Movement fields:
+    "medianTime", "amount", "currency", "reference",
+    // Pending fields:
+    "pubkey", "uid", "blockNumber"
   ];
   $scope.showHeaders = angular.isDefined($scope.showHeaders) ? $scope.showHeaders : true;
 
@@ -62,6 +64,14 @@ function ESDocumentLookupController($scope, $ionicPopover, $location, $timeout,
       $scope.entered = true;
       $scope.search.index = state.stateParams && state.stateParams.index || $scope.search.index;
       $scope.search.type = state.stateParams && state.stateParams.type || $scope.search.type;
+      $scope.search.text = state.stateParams && state.stateParams.q || $scope.search.text;
+      $scope.search.sort = state.stateParams && state.stateParams.sort || $scope.search.sort;
+      $scope.search.last = !$scope.search.text;
+      $scope.load();
+    }
+
+    // Reload only if params changed (.e.g if comes from a graph click)
+    else if (state.stateParams && state.stateParams.q && $scope.search.text !== state.stateParams.q) {
       $scope.search.text = state.stateParams && state.stateParams.q || $scope.search.text;
       $scope.search.last = !$scope.search.text;
       $scope.load();
diff --git a/www/plugins/graph/css/style.css b/www/plugins/graph/css/style.css
index 0d97c8161..7c0f14cff 100644
--- a/www/plugins/graph/css/style.css
+++ b/www/plugins/graph/css/style.css
@@ -3,7 +3,11 @@
    Graph currency popover
 **********/
 
-.popover-graph-currency {
+.popover-graph-range {
   height: 300px !important;
   max-width: 250px !important;
 }
+.popover-graph-period {
+  height: 420px !important;
+  max-width: 250px !important;
+}
diff --git a/www/plugins/graph/i18n/locale-en-GB.json b/www/plugins/graph/i18n/locale-en-GB.json
index d972514c8..8a8bdfc7d 100644
--- a/www/plugins/graph/i18n/locale-en-GB.json
+++ b/www/plugins/graph/i18n/locale-en-GB.json
@@ -15,6 +15,15 @@
         "HOUR": "Group by <b>hour</b>",
         "DAY": "Group by <b>day</b>",
         "MONTH": "Group by <b>month</b>"
+      },
+      "MAX_AGE": {
+        "DAY": "For 24h",
+        "WEEK": "For a week",
+        "MONTH": "For a month",
+        "QUARTER": "For 3 months",
+        "SEMESTER": "For 6 months",
+        "YEAR": "For a year",
+        "FOREVER": "Forever"
       }
     },
     "ACCOUNT": {
@@ -49,7 +58,11 @@
       "MONETARY_MASS_SHARE_LABEL": "Average per member",
       "UD_TITLE": "Evolution of the universal dividend",
       "MEMBERS_COUNT_TITLE": "Evolution of the number of members",
-      "MEMBERS_COUNT_LABEL": "Number of members"
+      "MEMBERS_COUNT_LABEL": "Number of members",
+      "MEMBERS_DELTA_TITLE": "variation in the number of members",
+      "IS_MEMBER_DELTA_LABEL": "Validated memberships",
+      "WAS_MEMBER_DELTA_LABEL": "Membership losses",
+      "PENDING_DELTA_LABEL": "Membership requests"
     },
     "PEER": {
       "VIEW": {
@@ -86,6 +99,24 @@
         "TITLE": "Other documents",
         "HISTORY_DELETE": "Deletion of documents"
       }
+    },
+    "SYNCHRO": {
+      "TITLE": "Synchronization statistics",
+      "COUNT": {
+        "TITLE": "Synchronized volume",
+        "INSERTS": "Insertions",
+        "UPDATES": "Updates",
+        "DELETES": "Deletions"
+      },
+      "PEER": {
+        "TITLE": "Requested peers",
+        "ES_USER_API": "Peers with with user data",
+        "ES_SUBSCRIPTION_API": "Peers with subscription"
+      },
+      "PERFORMANCE": {
+        "TITLE": "Execution performance",
+        "DURATION": "Execution time (ms)"
+      }
     }
   }
 }
diff --git a/www/plugins/graph/i18n/locale-en.json b/www/plugins/graph/i18n/locale-en.json
index d972514c8..8a8bdfc7d 100644
--- a/www/plugins/graph/i18n/locale-en.json
+++ b/www/plugins/graph/i18n/locale-en.json
@@ -15,6 +15,15 @@
         "HOUR": "Group by <b>hour</b>",
         "DAY": "Group by <b>day</b>",
         "MONTH": "Group by <b>month</b>"
+      },
+      "MAX_AGE": {
+        "DAY": "For 24h",
+        "WEEK": "For a week",
+        "MONTH": "For a month",
+        "QUARTER": "For 3 months",
+        "SEMESTER": "For 6 months",
+        "YEAR": "For a year",
+        "FOREVER": "Forever"
       }
     },
     "ACCOUNT": {
@@ -49,7 +58,11 @@
       "MONETARY_MASS_SHARE_LABEL": "Average per member",
       "UD_TITLE": "Evolution of the universal dividend",
       "MEMBERS_COUNT_TITLE": "Evolution of the number of members",
-      "MEMBERS_COUNT_LABEL": "Number of members"
+      "MEMBERS_COUNT_LABEL": "Number of members",
+      "MEMBERS_DELTA_TITLE": "variation in the number of members",
+      "IS_MEMBER_DELTA_LABEL": "Validated memberships",
+      "WAS_MEMBER_DELTA_LABEL": "Membership losses",
+      "PENDING_DELTA_LABEL": "Membership requests"
     },
     "PEER": {
       "VIEW": {
@@ -86,6 +99,24 @@
         "TITLE": "Other documents",
         "HISTORY_DELETE": "Deletion of documents"
       }
+    },
+    "SYNCHRO": {
+      "TITLE": "Synchronization statistics",
+      "COUNT": {
+        "TITLE": "Synchronized volume",
+        "INSERTS": "Insertions",
+        "UPDATES": "Updates",
+        "DELETES": "Deletions"
+      },
+      "PEER": {
+        "TITLE": "Requested peers",
+        "ES_USER_API": "Peers with with user data",
+        "ES_SUBSCRIPTION_API": "Peers with subscription"
+      },
+      "PERFORMANCE": {
+        "TITLE": "Execution performance",
+        "DURATION": "Execution time (ms)"
+      }
     }
   }
 }
diff --git a/www/plugins/graph/i18n/locale-fr-FR.json b/www/plugins/graph/i18n/locale-fr-FR.json
index b79de1bc7..47289779b 100644
--- a/www/plugins/graph/i18n/locale-fr-FR.json
+++ b/www/plugins/graph/i18n/locale-fr-FR.json
@@ -15,6 +15,15 @@
         "HOUR": "Heure",
         "DAY": "Jour",
         "MONTH": "Mois"
+      },
+      "MAX_AGE": {
+        "DAY": "Depuis 24h",
+        "WEEK": "Depuis une semaine",
+        "MONTH": "Depuis un mois",
+        "QUARTER": "Depuis 3 mois",
+        "SEMESTER": "Depuis 6 mois",
+        "YEAR": "Depuis un an",
+        "FOREVER": "Depuis toujours"
       }
     },
     "ACCOUNT": {
@@ -50,7 +59,6 @@
       "TX_COUNT_TITLE": "Nombre de transactions écrites",
       "TX_COUNT_LABEL": "Nombre de transactions",
       "TX_AVG_BY_BLOCK": "Nombre moyen de transactions / bloc"
-
     },
     "CURRENCY": {
       "MONETARY_MASS_TITLE": "Evolution de la masse monétaire",
@@ -58,7 +66,11 @@
       "MONETARY_MASS_SHARE_LABEL": "Moyenne par membre",
       "UD_TITLE": "Evolution du dividende universel",
       "MEMBERS_COUNT_TITLE": "Evolution du nombre de membres",
-      "MEMBERS_COUNT_LABEL": "Nombre de membres"
+      "MEMBERS_COUNT_LABEL": "Nombre de membres",
+      "MEMBERS_DELTA_TITLE": "Variation du nombre de membres",
+      "IS_MEMBER_DELTA_LABEL": "Prises en compte d'adhésion",
+      "WAS_MEMBER_DELTA_LABEL": "Pertes d'adhésion",
+      "PENDING_DELTA_LABEL": "Demandes d'adhésion"
     },
     "PEER": {
       "VIEW": {
@@ -106,13 +118,18 @@
       },
       "PEER": {
         "TITLE": "Noeuds requêtés",
-        "ES_USER_API": "Noeuds données utilisateurs",
-        "ES_SUBSCRIPTION_API": "Noeuds services en ligne"
+        "ES_USER_API": "Noeuds avec données utilisateurs",
+        "ES_SUBSCRIPTION_API": "Noeuds avec services en ligne"
       },
       "PERFORMANCE": {
         "TITLE": "Performances d'exécution",
         "DURATION": "Temps d'exécution (ms)"
       }
+    },
+    "NETWORK": {
+      "TITLE": "Statistiques réseau",
+      "ENDPOINT_COUNT_TITLE": "Nombre de points d'accès",
+      "ENDPOINT_DELTA_TITLE": "Variation du nombre de points d'accès"
     }
   }
 }
diff --git a/www/plugins/graph/js/controllers/blockchain-controllers.js b/www/plugins/graph/js/controllers/blockchain-controllers.js
index d15f2650f..4488543b4 100644
--- a/www/plugins/graph/js/controllers/blockchain-controllers.js
+++ b/www/plugins/graph/js/controllers/blockchain-controllers.js
@@ -86,12 +86,11 @@ function GpBlockchainTxCountController($scope, $controller, $q, $state, $filter,
         if (!result || !result.times) return; // no data
         $scope.times = result.times;
 
-        var formatInteger = $filter('formatInteger');
         var formatAmount =  $filter('formatDecimal');
         $scope.currencySymbol = $filter('currencySymbolNoHtml')($scope.formData.currency, $scope.formData.useRelative);
 
         // Data
-        if ($scope.formData.rangeDuration != 'hour') {
+        if ($scope.formData.rangeDuration !== 'hour') {
           $scope.data = [
             result.amount,
             result.count
@@ -207,13 +206,18 @@ function GpBlockchainIssuersController($scope, $controller, $q, $state, $transla
   // Initialize the super class and extend it.
   angular.extend(this, $controller('GpCurrencyAbstractCtrl', {$scope: $scope}));
 
+  // Change defaults
+  $scope.formData.maxAge = 'day';
+  $scope.computeStartTimeByAge();
+
   $scope.load = function() {
+
     return $q.all([
       $translate([
         'GRAPH.BLOCKCHAIN.BLOCKS_ISSUERS_TITLE',
         'GRAPH.BLOCKCHAIN.BLOCKS_ISSUERS_LABEL'
       ]),
-      gpData.blockchain.countByIssuer($scope.formData.currency)
+      gpData.blockchain.countByIssuer($scope.formData.currency, {startTime: $scope.formData.startTime})
     ])
       .then(function(result) {
         var translations =  result[0];
diff --git a/www/plugins/graph/js/controllers/common-controllers.js b/www/plugins/graph/js/controllers/common-controllers.js
index 042f35b18..2d6a0e1fd 100644
--- a/www/plugins/graph/js/controllers/common-controllers.js
+++ b/www/plugins/graph/js/controllers/common-controllers.js
@@ -12,13 +12,12 @@ function GpCurrencyAbstractController($scope, $filter, $ionicPopover, $ionicHist
     useRelative: csSettings.data.useRelative,
     timePct: 100,
     rangeDuration: 'day',
+    maxAge: undefined, // forever
     firstBlockTime: 0,
     scale: 'linear',
     hide: [],
     beginAtZero: true
   };
-  $scope.formData.useRelative = false; /*angular.isDefined($scope.formData.useRelative) ?
-    $scope.formData.useRelative : csSettings.data.useRelative;*/
   $scope.scale = 'linear';
   $scope.height = undefined;
   $scope.width = undefined;
@@ -32,6 +31,21 @@ function GpCurrencyAbstractController($scope, $filter, $ionicPopover, $ionicHist
   $scope.enter = function (e, state) {
     if ($scope.loading) {
 
+      // Make sure there is currency, or load it not
+      if (!$scope.formData.currency) {
+        return csCurrency.get()
+          .then(function (currency) {
+            $scope.formData.currency = currency ? currency.name : null;
+            $scope.formData.firstBlockTime = currency ? _truncDate(currency.firstBlockTime) : 0;
+            if (!$scope.formData.firstBlockTime){
+              console.warn('[graph] currency.firstBlockTime not loaded ! Should have been loaded by currrency service!');
+            }
+            $scope.formData.currencyAge = _truncDate(moment().utc().unix()) - $scope.formData.firstBlockTime;
+
+            return $scope.enter(e, state); // Loop
+          });
+      }
+
       if (state && state.stateParams) {
         // remember state, to be able to refresh location
         $scope.stateName = state && state.stateName;
@@ -63,21 +77,6 @@ function GpCurrencyAbstractController($scope, $filter, $ionicPopover, $ionicHist
       // Should be override by subclasses
       $scope.init(e, state);
 
-      // Make sure there is currency, or load it not
-      if (!$scope.formData.currency) {
-        return csCurrency.get()
-          .then(function (currency) {
-            $scope.formData.currency = currency ? currency.name : null;
-            $scope.formData.firstBlockTime = currency ? _truncDate(currency.firstBlockTime) : 0;
-            if (!$scope.formData.firstBlockTime){
-              console.warn('[graph] currency.firstBlockTime not loaded ! Should have been loaded by currrency service!');
-            }
-            $scope.formData.currencyAge = _truncDate(moment().utc().unix()) - $scope.formData.firstBlockTime;
-
-            return $scope.enter(e, state);
-          });
-      }
-
       $scope.load()  // Should be override by subclasses
         .then(function () {
           // Update scale
@@ -103,9 +102,9 @@ function GpCurrencyAbstractController($scope, $filter, $ionicPopover, $ionicHist
 
     $scope.stateParams = $scope.stateParams || {};
     $scope.stateParams.t = ($scope.formData.timePct >= 0 && $scope.formData.timePct < 100) ? $scope.formData.timePct : undefined;
-    $scope.stateParams.stepUnit = $scope.formData.rangeDuration != 'day' ? $scope.formData.rangeDuration : undefined;
+    $scope.stateParams.stepUnit = $scope.formData.rangeDuration !== 'day' ? $scope.formData.rangeDuration : undefined;
     $scope.stateParams.hide = $scope.formData.hide && $scope.formData.hide.length ? $scope.formData.hide.join(',') : undefined;
-    $scope.stateParams.scale = $scope.formData.scale != 'linear' ?$scope.formData.scale : undefined;
+    $scope.stateParams.scale = $scope.formData.scale !== 'linear' ?$scope.formData.scale : undefined;
 
     $state.go($scope.stateName, $scope.stateParams, {
       reload: false,
@@ -123,7 +122,7 @@ function GpCurrencyAbstractController($scope, $filter, $ionicPopover, $ionicHist
 
   // When parent view execute a refresh action
   $scope.$on('csView.action.refresh', function(event, context) {
-    if (!context || context == 'currency') {
+    if (!context || context === 'currency') {
       return $scope.load();
     }
   });
@@ -152,7 +151,7 @@ function GpCurrencyAbstractController($scope, $filter, $ionicPopover, $ionicHist
     _.forEach($scope.options.scales.yAxes, function(yAxe, index) {
       yAxe.type = scale;
       yAxe.ticks = yAxe.ticks || {};
-      if (scale == 'linear') {
+      if (scale === 'linear') {
         yAxe.ticks.beginAtZero = angular.isDefined($scope.formData.beginAtZero) ? $scope.formData.beginAtZero : true;
         delete yAxe.ticks.min;
         yAxe.ticks.callback = function(value) {
@@ -176,7 +175,7 @@ function GpCurrencyAbstractController($scope, $filter, $ionicPopover, $ionicHist
 
   $scope.setRangeDuration = function(rangeDuration) {
     $scope.hideActionsPopover();
-    if ($scope.formData && rangeDuration == $scope.formData.rangeDuration) return;
+    if ($scope.formData && rangeDuration === $scope.formData.rangeDuration) return;
 
     $scope.formData.rangeDuration = rangeDuration;
 
@@ -192,6 +191,55 @@ function GpCurrencyAbstractController($scope, $filter, $ionicPopover, $ionicHist
     $scope.updateLocation();
   };
 
+  $scope.setMaxAge = function(maxAge) {
+    $scope.hideActionsPopover();
+    if ($scope.formData && maxAge === $scope.formData.maxAge) return;
+
+    $scope.formData.maxAge = maxAge;
+
+    // Restore default values
+    delete $scope.formData.startTime;
+    delete $scope.formData.endTime;
+    delete $scope.formData.rangeDurationSec;
+
+    $scope.computeStartTimeByAge(); // Compute formData.startTime
+
+    // Reload data
+    $scope.load();
+    // Update location
+    $scope.updateLocation();
+  };
+
+  $scope.computeStartTimeByAge = function() {
+    if (!$scope.formData.maxAge) {
+      delete $scope.formData.startTime; // Forever
+    }
+    else {
+      var ageInSecond = 60 * 60; // one hour
+      switch ($scope.formData.maxAge) {
+        case 'day':
+          ageInSecond *= 24;
+          break;
+        case 'week':
+          ageInSecond *= 24 * 7;
+          break;
+        case 'month':
+          ageInSecond *= 24 * 365.25/12;
+          break;
+        case 'quarter':
+          ageInSecond *= 24 * 365.25/4;
+          break;
+        case 'semester':
+          ageInSecond *= 24 * 365.25/2;
+          break;
+        case 'year':
+          ageInSecond *= 24 * 365.25;
+          break;
+      }
+      $scope.formData.startTime = moment.utc().unix() - ageInSecond;
+    }
+  }
+
   $scope.updateHiddenDataset = function(datasetOverride) {
     datasetOverride = datasetOverride || $scope.datasetOverride || {};
 
@@ -208,7 +256,7 @@ function GpCurrencyAbstractController($scope, $filter, $ionicPopover, $ionicHist
         var yAxisDatasetCount = _.filter(datasetOverride, function(dataset) {
           return dataset.yAxisID === yAxisID;
         }).length;
-        if (yAxisDatasetCount == 1) {
+        if (yAxisDatasetCount === 1) {
           yAxe.display = false;
         }
       }
@@ -314,9 +362,10 @@ function GpCurrencyAbstractController($scope, $filter, $ionicPopover, $ionicHist
 
   /* -- Popover -- */
 
-  $scope.showActionsPopover = function(event) {
+  $scope.showActionsPopover = function(event, options) {
+    var templateUrl = options && options.templateUrl || 'plugins/graph/templates/common/popover_range_actions.html';
     UIUtils.popover.show(event, {
-      templateUrl: 'plugins/graph/templates/common/popover_range_actions.html',
+      templateUrl: templateUrl,
       scope: $scope,
       autoremove: true,
       afterShow: function(popover) {
@@ -325,6 +374,11 @@ function GpCurrencyAbstractController($scope, $filter, $ionicPopover, $ionicHist
     });
   };
 
+  $scope.showPeriodPopover = function(event) {
+    return $scope.showActionsPopover(event, {templateUrl: 'plugins/graph/templates/common/popover_period_actions.html'});
+  };
+
+
   $scope.hideActionsPopover = function() {
     if ($scope.actionsPopover) {
       $scope.actionsPopover.hide();
diff --git a/www/plugins/graph/js/controllers/currency-controllers.js b/www/plugins/graph/js/controllers/currency-controllers.js
index caaaadff7..4311f863c 100644
--- a/www/plugins/graph/js/controllers/currency-controllers.js
+++ b/www/plugins/graph/js/controllers/currency-controllers.js
@@ -39,7 +39,7 @@ angular.module('cesium.graph.currency.controllers', ['chart.js', 'cesium.graph.s
         }
       })
       .state('app.currency_stats_lg', {
-        url: "/currency/stats/lg?hide&scale",
+        url: "/currency/stats/lg?hide&scale&stepUnit&t",
         views: {
           'menuContent': {
             templateUrl: "plugins/graph/templates/currency/view_stats_lg.html"
@@ -86,6 +86,8 @@ angular.module('cesium.graph.currency.controllers', ['chart.js', 'cesium.graph.s
   .controller('GpCurrencyDUCtrl', GpCurrencyDUController)
 
   .controller('GpCurrencyMembersCountCtrl', GpCurrencyMembersCountController)
+
+  .controller('GpCurrencyPendingCountCtrl', GpCurrencyPendingCountController)
 ;
 
 function GpCurrencyViewExtendController($scope, PluginService, UIUtils, esSettings) {
@@ -134,7 +136,9 @@ function GpCurrencyMonetaryMassController($scope, $controller, $q, $state, $tran
     return $q.all([
       $translate(['GRAPH.CURRENCY.MONETARY_MASS_TITLE',
         'GRAPH.CURRENCY.MONETARY_MASS_LABEL',
-        'GRAPH.CURRENCY.MONETARY_MASS_SHARE_LABEL']),
+        'GRAPH.CURRENCY.MONETARY_MASS_SHARE_LABEL',
+        'COMMON.DATE_SHORT_PATTERN',
+        'COMMON.DATE_MONTH_YEAR_PATTERN']),
       gpData.blockchain.withDividend($scope.formData.currency, {
         from: from,
         size: size
@@ -146,56 +150,51 @@ function GpCurrencyMonetaryMassController($scope, $controller, $q, $state, $tran
         if (!result || !result.times) return;
         $scope.times = result.times;
 
-        // Choose a date formatter, depending on the blocks period
+        // Compute the date pattern, depending on the blocks period
         var blocksPeriod = result.times[result.times.length-1] - result.times[0];
-        var formatDate;
-        if (blocksPeriod < 31557600/* less than 1 year */) {
-          formatDate = $filter('medianDateShort');
-        }
-        else {
-          formatDate = $filter('formatDateMonth'); //see #683
-        }
+        var datePattern = (blocksPeriod < 31557600/* less than 1 year */) ?
+          translations['COMMON.DATE_SHORT_PATTERN'] : translations['COMMON.DATE_MONTH_YEAR_PATTERN'];
 
         var formatAmount =  $filter('formatDecimal');
         $scope.currencySymbol = $filter('currencySymbolNoHtml')($scope.formData.currency, $scope.formData.useRelative);
 
         // Data: relative
         var data = [];
-        if($scope.formData.useRelative) {
+        if ($scope.formData.useRelative) {
 
           // M/N
           data.push(
-            result.blocks.reduce(function(res, block) {
-              return res.concat(truncAmount(block.monetaryMass / block.dividend / block.membersCount));
-            }, []));
+            _.map(result.blocks, function(block) {
+              return truncAmount(block.monetaryMass / block.dividend / block.membersCount);
+            }));
 
           // Mass
           data.push(
-            result.blocks.reduce(function(res, block) {
-              return res.concat(truncAmount(block.monetaryMass / block.dividend));
-            }, []));
+            _.map(result.blocks, function(block) {
+              return truncAmount(block.monetaryMass / block.dividend);
+            }));
         }
 
         // Data: quantitative
         else {
           // M/N
           data.push(
-            result.blocks.reduce(function(res, block) {
-              return res.concat(truncAmount(block.monetaryMass / block.membersCount / 100));
-            }, []));
+            _.map(result.blocks, function(block) {
+              return truncAmount(block.monetaryMass / block.membersCount / 100);
+            }));
 
           // Mass
           data.push(
-            result.blocks.reduce(function(res, block) {
-              return res.concat(block.monetaryMass / 100);
-            }, []));
+            _.map(result.blocks, function(block) {
+              return block.monetaryMass / 100;
+            }));
         }
         $scope.data = data;
 
         // Labels
-        $scope.labels = result.times.reduce(function(res, time) {
-          return res.concat(formatDate(time));
-        }, []);
+        $scope.labels = _.map(result.times, function(time) {
+          return moment.unix(time).local().format(datePattern);
+        });
 
         // Colors
         $scope.colors = gpColor.scale.fix(result.times.length);
@@ -327,7 +326,9 @@ function GpCurrencyDUController($scope, $q, $controller, $translate, gpColor, gp
     return $q.all([
       $translate([
         'GRAPH.CURRENCY.UD_TITLE',
-        'COMMON.UNIVERSAL_DIVIDEND']),
+        'COMMON.UNIVERSAL_DIVIDEND',
+        'COMMON.DATE_SHORT_PATTERN',
+        'COMMON.DATE_MONTH_YEAR_PATTERN']),
       gpData.blockchain.withDividend($scope.formData.currency, {
         from: from,
         size: size
@@ -341,28 +342,23 @@ function GpCurrencyDUController($scope, $q, $controller, $translate, gpColor, gp
 
         // Choose a date formatter, depending on the blocks period
         var blocksPeriod = result.times[result.times.length-1] - result.times[0];
-        var dateFilter;
-        if (blocksPeriod < 31557600/* less than 1 year */) {
-          dateFilter = $filter('medianDateShort');
-        }
-        else {
-          dateFilter = $filter('formatDateMonth');
-        }
+        var datePattern = (blocksPeriod < 31557600/* less than 1 year */) ?
+          translations['COMMON.DATE_SHORT_PATTERN'] : translations['COMMON.DATE_MONTH_YEAR_PATTERN'];
 
         var formatAmount =  $filter('formatDecimal');
         $scope.currencySymbol = $filter('currencySymbolNoHtml')($scope.formData.currency, false);
 
         // Data
         $scope.data = [
-          result.blocks.reduce(function(res, block) {
-            return res.concat(block.dividend / 100);
-          }, [])
+          _.map(result.blocks, function(block) {
+            return block.dividend / 100;
+          })
         ];
 
         // Labels
-        $scope.labels = result.times.reduce(function(res, time) {
-          return res.concat(dateFilter(time));
-        }, []);
+        $scope.labels = _.map(result.times, function(time) {
+          return moment.unix(time).local().format(datePattern);
+        });
 
         // Colors
         $scope.colors = result.blocks.reduce(function(res) {
@@ -453,7 +449,11 @@ function GpCurrencyMembersCountController($scope, $controller, $q, $state, $tran
     size = size || 10000;
 
     return $q.all([
-      $translate(['GRAPH.CURRENCY.MEMBERS_COUNT_TITLE', 'GRAPH.CURRENCY.MEMBERS_COUNT_LABEL']),
+      $translate(['GRAPH.CURRENCY.MEMBERS_COUNT_TITLE',
+        'GRAPH.CURRENCY.MEMBERS_COUNT_LABEL',
+        'COMMON.DATE_SHORT_PATTERN',
+        'COMMON.DATE_MONTH_YEAR_PATTERN'
+      ]),
       gpData.blockchain.withDividend($scope.formData.currency, {
         from: from,
         size: size,
@@ -469,18 +469,18 @@ function GpCurrencyMembersCountController($scope, $controller, $q, $state, $tran
 
         // Choose a date formatter, depending on the blocks period
         var blocksPeriod = result.times[result.blocks.length-1] - result.times[0];
-        var dateFilter;
-        if (blocksPeriod < 31557600/* less than 1 year*/) {
-          dateFilter = $filter('medianDateShort');
-        }
-        else {
-          dateFilter = $filter('formatDateMonth');
-        }
+        var datePattern = (blocksPeriod < 31557600/* less than 1 year */) ?
+          translations['COMMON.DATE_SHORT_PATTERN'] : translations['COMMON.DATE_MONTH_YEAR_PATTERN'];
 
-        // Format time
-        $scope.labels = result.times.reduce(function(res, time) {
-          return res.concat(dateFilter(time));
-        }, []);
+        // Data
+        $scope.data = [
+          _.pluck(result.blocks, 'membersCount')
+        ];
+
+        // Labels
+        $scope.labels = _.map(result.times, function(time) {
+          return moment.unix(time).local().format(datePattern);
+        });
 
         // Members count graph: -------------------------
         $scope.options = {
@@ -512,12 +512,6 @@ function GpCurrencyMembersCountController($scope, $controller, $q, $state, $tran
             pointHoverRadius: 3
           }];
 
-        // Data
-        $scope.data = [
-          result.blocks.reduce(function(res, block) {
-            return res.concat(block.membersCount);
-          }, [])
-        ];
 
         // Colors
         $scope.colors = gpColor.scale.fix(result.blocks.length);
@@ -539,3 +533,123 @@ function GpCurrencyMembersCountController($scope, $controller, $q, $state, $tran
 
 
 }
+
+
+function GpCurrencyPendingCountController($scope, $controller, $q, $state, $translate, gpColor, esHttp) {
+  'ngInject';
+
+  // Initialize the super class and extend it.
+  angular.extend(this, $controller('GpDocStatsCtrl', {$scope: $scope}));
+
+  $scope.chartIdPrefix = 'currency-chart-pending-';
+
+  $scope.init = function(e, state) {
+    var currency = $scope.formData.currency;
+    if (!currency) throw Error('Missing formData.currency!');
+
+    $scope.formData.index = currency;
+    $scope.formData.types = ['member', 'pending'];
+
+    $scope.charts = [
+
+      // Pending delta
+      {
+        id: currency + '_member_delta',
+        title: 'GRAPH.CURRENCY.MEMBERS_DELTA_TITLE',
+        series: [
+          {
+            key: currency + '_is_member_delta',
+            label: 'GRAPH.CURRENCY.IS_MEMBER_DELTA_LABEL',
+            type: 'bar',
+            yAxisID: 'y-axis-delta',
+            color: gpColor.rgba.calm(),
+            pointHoverBackgroundColor: gpColor.rgba.calm(),
+          },
+          {
+            key: currency + '_was_member_delta',
+            label: 'GRAPH.CURRENCY.WAS_MEMBER_DELTA_LABEL',
+            type: 'bar',
+            yAxisID: 'y-axis-delta',
+            color: gpColor.rgba.assertive(0.7),
+            pointHoverBackgroundColor: gpColor.rgba.assertive(),
+          },
+          {
+            key: currency + '_pending_delta',
+            label: 'GRAPH.CURRENCY.PENDING_DELTA_LABEL',
+            type: 'line',
+            yAxisID: 'y-axis-delta',
+            color: gpColor.rgba.gray(0.5),
+            pointHoverBackgroundColor: gpColor.rgba.gray()
+          }
+        ]
+      }];
+
+    $scope.formData.queryNames = $scope.charts.reduce(function(res, chart){
+      return chart.series.reduce(function(res, serie) {
+        var queryName = serie.key.replace(/_delta$/, '');
+        return res.concat(queryName);
+      }, res);
+    }, []);
+  };
+
+  var inheritedLoad = $scope.load;
+  $scope.load = function() {
+
+    // Call inherited load
+    return inheritedLoad()
+      .then(function() {
+        var chart = $scope.charts[0];
+
+        // Compute wasMember serie, using the is_member
+        if (chart.data[1]) {
+          chart.data[1] = _.map(chart.data[0], function(value) {
+            return value < 0 ? value : undefined;
+          });
+        }
+        chart.data[0] = _.map(chart.data[0], function(value) {
+          return value >= 0 ? value : undefined;
+        });
+
+        $scope.chart = chart;
+      });
+  };
+
+  $scope.onChartClick = function(data, e, item) {
+    if (!item) return;
+
+    var from = $scope.times[item._index];
+    var to = moment.unix(from).utc().add(1, $scope.formData.rangeDuration).unix();
+
+    var blockRequest = esHttp.get('/{0}/block/_search?pretty'.format($scope.formData.currency));
+    // Get block min/max
+    return $q.all([
+      // Get first (min) block
+      blockRequest({
+        q: 'time:>={0} AND time:<={1}'.format(from, to),
+        sort: 'number:asc',
+        size: 1,
+        _source: ['number']
+      }),
+
+      // Get last (max) block
+      blockRequest({
+        q: 'time:>={0} AND time:<={1}'.format(from, to),
+        sort: 'number:desc',
+        size: 1,
+        _source: ['number']
+      })
+    ])
+      .then(function(res) {
+        var minBlockHit = res[0] && res[0].hits && res[0].hits.hits && res[0].hits.hits[0];
+        var maxBlockHit = res[1] && res[1].hits && res[1].hits.hits && res[1].hits.hits[0];
+        var minBlockNumber = minBlockHit ? minBlockHit._source.number : undefined;
+        var maxBlockNumber = maxBlockHit ? maxBlockHit._source.number : undefined;
+        return $state.go('app.document_search', {
+          index: $scope.formData.currency,
+          type: 'pending',
+          q: 'blockNumber:>={0} AND blockNumber:<{1}'.format(minBlockNumber, maxBlockNumber)
+        });
+      })
+
+  };
+}
diff --git a/www/plugins/graph/js/controllers/docstats-controllers.js b/www/plugins/graph/js/controllers/docstats-controllers.js
index 7fe42911d..a9f43470f 100644
--- a/www/plugins/graph/js/controllers/docstats-controllers.js
+++ b/www/plugins/graph/js/controllers/docstats-controllers.js
@@ -262,6 +262,9 @@ function GpDocStatsController($scope, $state, $controller, $q, $translate, gpCol
   };
 
   $scope.load = function(updateTimePct) {
+    updateTimePct = angular.isDefined(updateTimePct) ? updateTimePct : true;
+
+    console.debug("[graph] Loading docstats data...");
 
     return $q.all([
       // Get i18n keys (chart title, series labels, date patterns)
diff --git a/www/plugins/graph/js/controllers/network-controllers.js b/www/plugins/graph/js/controllers/network-controllers.js
index 93cef43c1..94152b139 100644
--- a/www/plugins/graph/js/controllers/network-controllers.js
+++ b/www/plugins/graph/js/controllers/network-controllers.js
@@ -54,12 +54,25 @@ angular.module('cesium.graph.network.controllers', ['chart.js', 'cesium.graph.se
               controller: 'GpBlockchainTxCountCtrl'
             }
           }
+        })
+
+      .state('app.network_stats', {
+          url: "/network/stats?stepUnit&t&hide&scale",
+          views: {
+            'menuContent': {
+              templateUrl: "plugins/graph/templates/network/view_stats.html",
+              controller: 'GpNetworkStatsCtrl'
+            }
+          }
         });
     }
   })
 
   .controller('GpPeerViewExtendCtrl', GpPeerViewExtendController)
 
+  .controller('GpNetworkStatsCtrl', GpNetworkStatsController)
+
+
 ;
 
 function GpPeerViewExtendController($scope, $timeout, PluginService, esSettings, csCurrency, gpData) {
@@ -117,3 +130,133 @@ function GpPeerViewExtendController($scope, $timeout, PluginService, esSettings,
       });
   };
 }
+
+
+function GpNetworkStatsController($scope, $controller, $q, $state, $translate, gpColor, esHttp) {
+  'ngInject';
+
+  // Initialize the super class and extend it.
+  angular.extend(this, $controller('GpDocStatsCtrl', {$scope: $scope}));
+
+  $scope.chartIdPrefix = 'network-chart-stats-';
+  $scope.apis = [
+    {
+      name: 'BASIC_MERKLED_API',
+      color: gpColor.rgba.calm()
+    },
+    {
+      name: 'BMAS',
+      color: gpColor.rgba.calm()
+    },
+    {
+      name: 'BMATOR',
+      color: gpColor.rgba.calm()
+    },
+    {
+      name: 'WS2P',
+      color: gpColor.rgba.balanced()
+    },
+    {
+      name: 'GVA',
+      color: gpColor.rgba.energized()
+    },
+    {
+      name: 'GVASUB',
+      color: gpColor.rgba.energized()
+    }
+  ];
+
+  var inheritedInit = $scope.init;
+  $scope.init = function(e, state) {
+    var currency = $scope.formData.currency;
+    if (!currency) throw Error('Missing formData.currency!');
+
+    inheritedInit(e, state);
+
+    if (state && state.stateParams) {
+
+    }
+
+    $scope.formData.index = currency;
+    $scope.formData.types = ['peer'];
+
+    $scope.charts = [
+
+      // Count by api
+      {
+        id: currency + '_peer',
+        title: 'GRAPH.NETWORK.ENDPOINT_COUNT_TITLE',
+        series: _.map($scope.apis, function (api) {
+          return {
+            key: currency + '_peer_' + api.name.toLowerCase(),
+            label: api.name,
+            color: api.color,
+            pointHoverBackgroundColor: api.color,
+          };
+        })
+      },
+
+      // Delta by api
+      {
+        id: currency + '_peer_delta',
+        title: 'GRAPH.NETWORK.ENDPOINT_DELTA_TITLE',
+        series: _.map($scope.apis, function (api) {
+          return {
+            key: currency + '_peer_' + api.name.toLowerCase() + '_delta',
+            label: api.name,
+            type: 'line',
+            yAxisID: 'y-axis-delta',
+            color: api.color,
+            pointHoverBackgroundColor: api.color,
+          };
+        })
+      }
+    ];
+
+    $scope.formData.queryNames = $scope.charts.reduce(function(res, chart){
+      return chart.series.reduce(function(res, serie) {
+        var queryName = serie.key.replace(/_delta$/, '');
+        return res.concat(queryName);
+      }, res);
+    }, []);
+  };
+
+  $scope.onChartClick = function(data, e, item) {
+    if (!item) return;
+
+    var from = $scope.times[item._index];
+    var to = moment.unix(from).utc().add(1, $scope.formData.rangeDuration).unix();
+
+    var blockRequest = esHttp.get('/{0}/block/_search?pretty'.format($scope.formData.currency));
+    // Get block min/max
+    return $q.all([
+      // Get first (min) block
+      blockRequest({
+        q: 'time:>={0} AND time:<={1}'.format(from, to),
+        sort: 'number:asc',
+        size: 1,
+        _source: ['number']
+      }),
+
+      // Get last (max) block
+      blockRequest({
+        q: 'time:>={0} AND time:<={1}'.format(from, to),
+        sort: 'number:desc',
+        size: 1,
+        _source: ['number']
+      })
+    ])
+      .then(function(res) {
+        var minBlockHit = res[0] && res[0].hits && res[0].hits.hits && res[0].hits.hits[0];
+        var maxBlockHit = res[1] && res[1].hits && res[1].hits.hits && res[1].hits.hits[0];
+        var minBlockNumber = minBlockHit ? minBlockHit._source.number : undefined;
+        var maxBlockNumber = maxBlockHit ? maxBlockHit._source.number : undefined;
+        return $state.go('app.document_search', {
+          index: $scope.formData.currency,
+          type: 'peer',
+          q: 'blockNumber:>={0} AND blockNumber:<{1}'.format(minBlockNumber, maxBlockNumber)
+        });
+      })
+
+  };
+}
diff --git a/www/plugins/graph/js/services/data-services.js b/www/plugins/graph/js/services/data-services.js
index 8c9ba109c..84be0b0c1 100644
--- a/www/plugins/graph/js/services/data-services.js
+++ b/www/plugins/graph/js/services/data-services.js
@@ -58,13 +58,13 @@ angular.module('cesium.graph.data.services', ['cesium.wot.services', 'cesium.es.
     }
 
     function _initRangeOptions(options) {
-      options = options || {};
+      options = angular.copy(options);
       options.maxRangeSize = options.maxRangeSize || 30;
       options.defaultTotalRangeCount = options.defaultTotalRangeCount || options.maxRangeSize*2;
 
       options.rangeDuration = options.rangeDuration || 'day';
-      options.endTime = options.endTime || moment().utc().add(1, options.rangeDuration).unix();
-      options.startTime = options.startTime ||
+      options.endTime = angular.isDefined(options.endTime) ? options.endTime : moment().utc().add(1, options.rangeDuration).unix();
+      options.startTime = angular.isDefined(options.startTime) ? options.startTime :
         moment.unix(options.endTime).utc().subtract(options.defaultTotalRangeCount, options.rangeDuration).unix();
       // Make to sure startTime is never before the currency starts - fix #483
       if (options.firstBlockTime && options.startTime < options.firstBlockTime) {
@@ -73,12 +73,48 @@ angular.module('cesium.graph.data.services', ['cesium.wot.services', 'cesium.es.
       return options;
     }
 
+    function _getManyRanges(options) {
+
+      var from = moment.unix(options.startTime).utc().startOf(options.rangeDuration);
+      var to = moment.unix(options.endTime).utc().startOf(options.rangeDuration);
+      var result = [];
+      var ranges = [];
+      while (from.isBefore(to)) {
+
+        ranges.push({
+          from: from.unix(),
+          to: from.add(1, options.rangeDuration).unix()
+        });
+
+        // Flush if max range count, or just before loop condition end (fix #483)
+        var flush = (ranges.length === options.maxRangeSize) || !from.isBefore(to);
+        if (flush) {
+          result.push(ranges);
+          ranges = [];
+        }
+      }
+
+      if (ranges.length) {
+        result.push(ranges);
+      }
+
+      return result;
+    }
+
     /**
      * Graph: "blocks count by issuer"
      * @param currency
      * @returns {*}
      */
-    exports.blockchain.countByIssuer = function(currency) {
+    exports.blockchain.countByIssuer = function(currency, options) {
+      options = options || {};
+
+      var filters = [];
+      if (options.startTime > 0) {
+        // Round to hour, to be able to use cache
+        var startTime = Math.floor(options.startTime / 60 / 60 ) * 60 * 60;
+        filters.push({range: {time: {gte: startTime}}});
+      }
 
       var request = {
         size: 0,
@@ -92,7 +128,17 @@ angular.module('cesium.graph.data.services', ['cesium.wot.services', 'cesium.es.
         }
       };
 
-      return exports.raw.block.search(request, {currency: currency})
+      if (filters.length) {
+        request.query = {bool: {}};
+        request.query.bool.filter = filters;
+      }
+
+      var params = {
+        currency: currency,
+        request_cache: angular.isDefined(options.cache) ? options.cache : true // enable by default
+      };
+
+      return exports.raw.block.search(request, params)
         .then(function(res) {
           var aggs = res.aggregations;
           if (!aggs.blocksByIssuer || !aggs.blocksByIssuer.buckets || !aggs.blocksByIssuer.buckets.length) return;
@@ -695,7 +741,6 @@ angular.module('cesium.graph.data.services', ['cesium.wot.services', 'cesium.es.
 
       options = _initRangeOptions(options);
 
-      var searchRequest = exports.raw.docstat.search;
       if (options.server) {
         var serverParts = options.server.split(':');
         var host = serverParts[0];
@@ -703,96 +748,123 @@ angular.module('cesium.graph.data.services', ['cesium.wot.services', 'cesium.es.
         searchRequest = rawLightInstance(host, port, options.useSsl).docstat.search;
       }
 
-      var jobs = [];
-
-      var from = moment.unix(options.startTime).utc().startOf(options.rangeDuration);
-      var to = moment.unix(options.endTime).utc().startOf(options.rangeDuration);
-      var ranges = [];
-
-      var processSearchResult = function (res) {
-        var aggs = res.aggregations;
-        return (aggs.range && aggs.range.buckets || []).reduce(function (res, agg) {
-          var item = {
-            from: agg.from,
-            to: agg.to
-          };
-          _.forEach(agg.index && agg.index.buckets || [], function (agg) {
-            var index = agg.key;
-            _.forEach(agg.type && agg.type.buckets || [], function (agg) {
-              var key = (index + '_' + agg.key);
-              item[key] = agg.max.value;
-              if (!indices[key]) indices[key] = true;
-            });
-          });
-          return res.concat(item);
-        }, []);
+      var filters = [];
+      if (options.index) {
+        console.debug('[graph] filter on index:', options.index);
+        filters.push({term : { index: options.index}});
+      }
+      if (options.types) {
+        console.debug('[graph] filter on types:', options.types);
+        filters.push({terms : { type: options.types}});
+      }
+      var request = {
+        size: 0,
+        aggs: {
+          range: {
+            range: {
+              field: "time",
+              ranges: [] // Will be replace
+            }
+          }
+        }
       };
 
-      while(from.isBefore(to)) {
 
-        ranges.push({
-          from: from.unix(),
-          to: from.add(1, options.rangeDuration).unix()
-        });
-
-        // Flush if max range count, or just before loop condition end (fix #483)
-        var flush = (ranges.length === options.maxRangeSize) || !from.isBefore(to);
-        if (flush) {
-          var request = {
-            size: 0,
+      if (options.queryNames) {
+        console.debug('[graph] filter on queryNames:', options.queryNames);
+        filters.push({terms : { queryName: options.queryNames }});
+        request.aggs.range.aggs = {
+          queryName: {
+            terms: {
+              field: "queryName",
+              size: 0
+            },
             aggs: {
-              range: {
-                range: {
-                  field: "time",
-                  ranges: ranges
+              max: {
+                max: {
+                  field: "count"
+                }
+              }
+            }
+          }
+        };
+      }
+      else {
+        request.aggs.range.aggs = {
+          index: {
+            terms: {
+              field: "index",
+              size: 0
+            },
+            aggs: {
+              type: {
+                terms: {
+                  field: "type",
+                  size: 0
                 },
                 aggs: {
-                  index : {
-                    terms: {
-                      field: "index",
-                      size: 0
-                    },
-                    aggs: {
-                      type: {
-                        terms: {
-                          field: "type",
-                          size: 0
-                        },
-                        aggs: {
-                          max: {
-                            max: {
-                              field : "count"
-                            }
-                          }
-                        }
-                      }
+                  max: {
+                    max: {
+                      field: "count"
                     }
                   }
                 }
               }
             }
+          }
+        };
+      }
 
-          };
+      // Add filter on request
+      if (filters.length > 0) {
+        request.query = request.query || {};
+        request.query.bool = request.query.bool || {};
+        request.query.bool.filter =  filters;
+      }
 
-          // prepare next loop
-          ranges = [];
-          var indices = {};
-          var params = {
-            request_cache: angular.isDefined(options.cache) ? options.cache : true // enable by default
-          };
+      var params = {
+        request_cache: angular.isDefined(options.cache) ? options.cache : true // enable by default
+      };
 
-          if (jobs.length === 10) {
-            console.error('Too many parallel jobs!');
-            from = moment.unix(options.endTime).utc(); // stop while
+      var indices = {};
+      var processSearchResult = function (res) {
+        var aggs = res.aggregations;
+        return _.map(aggs.range && aggs.range.buckets || [], function (agg) {
+          var item = { from: agg.from, to: agg.to };
+          if (agg.queryName) {
+            _.forEach(agg.queryName && agg.queryName.buckets || [], function (agg) {
+              var key = agg.key;
+              item[key] = agg.max.value;
+              if (!indices[key]) indices[key] = true;
+            });
           }
-          else {
-            jobs.push(
-              searchRequest(request, params)
-                  .then(processSearchResult)
-            );
+          else{
+            _.forEach(agg.index && agg.index.buckets || [], function (agg) {
+              var key = agg.key;
+              if (agg.max) {
+                item[key] = agg.max.value;
+                if (!indices[key]) indices[key] = true;
+              } else {
+                _.forEach(agg.type && agg.type.buckets || [], function (agg) {
+                  var typeKey = key + '_' + agg.key;
+                  item[typeKey] = agg.max.value;
+                  if (!indices[typeKey]) indices[typeKey] = true;
+                });
+              }
+            });
           }
-        }
-      } // loop
+          return item;
+        });
+      };
+
+      var jobs = _.map(_getManyRanges(options), function(ranges) {
+        var req = angular.copy(request);
+        req.aggs.range.range.ranges = ranges;
+
+        // Execute request
+        return exports.raw.docstat.search(req, params)
+          .then(processSearchResult);
+      });
 
       return $q.all(jobs)
         .then(function(res) {
diff --git a/www/plugins/graph/templates/blockchain/graph_block_issuers.html b/www/plugins/graph/templates/blockchain/graph_block_issuers.html
index c97178f57..13811de9e 100644
--- a/www/plugins/graph/templates/blockchain/graph_block_issuers.html
+++ b/www/plugins/graph/templates/blockchain/graph_block_issuers.html
@@ -3,6 +3,16 @@
 
       <!-- bar -->
       <div class="col col-75">
+        <!-- graphs button bar -->
+        <div class="button-bar-inline "
+             style="top: 33px; margin-top:-33px; position: relative;">
+          <button
+            class="button button-stable button-clear no-padding-xs no-padding-sm pull-right"
+            ng-click="showPeriodPopover($event)">
+            <i class="icon ion-navicon-round"></i>
+          </button>
+        </div>
+
         <canvas id="bar" class="chart-bar"
                 height="{{height}}" width="{{width}}"
                 chart-data="data"
diff --git a/www/plugins/graph/templates/blockchain/view_stats.html b/www/plugins/graph/templates/blockchain/view_stats.html
index a3393e1be..3f979b725 100644
--- a/www/plugins/graph/templates/blockchain/view_stats.html
+++ b/www/plugins/graph/templates/blockchain/view_stats.html
@@ -35,7 +35,10 @@
       <!-- Blocks issuer -->
       <ng-controller ng-controller="GpBlockchainIssuersCtrl">
 
-        <div class="item item-divider" ng-if="!loading" translate>GRAPH.BLOCKCHAIN.BLOCKS_ISSUERS_DIVIDER</div>
+        <div class="item item-divider" ng-if="!loading">
+          <span translate>GRAPH.BLOCKCHAIN.BLOCKS_ISSUERS_DIVIDER</span>
+          <span ng-if="formData.maxAge">({{'GRAPH.COMMON.MAX_AGE.' + formData.maxAge | uppercase | translate | lowercase }})</span>
+        </div>
 
         <div class="item no-padding-xs no-padding-sm"
              ng-if="!loading"
diff --git a/www/plugins/graph/templates/common/popover_period_actions.html b/www/plugins/graph/templates/common/popover_period_actions.html
new file mode 100644
index 000000000..3496b64f8
--- /dev/null
+++ b/www/plugins/graph/templates/common/popover_period_actions.html
@@ -0,0 +1,47 @@
+<ion-popover-view class="has-header popover-graph-period">
+  <ion-header-bar>
+    <h1 class="title" translate>COMMON.POPOVER_ACTIONS_TITLE</h1>
+  </ion-header-bar>
+  <ion-content scroll="false">
+    <div class="list item-text-wrap">
+
+      <!-- each durations -->
+      <a class="item item-icon-left ink"
+         ng-click="setMaxAge('day')">
+        <i class="icon ion-ios-checkmark-empty" ng-show="formData.maxAge==='day'"></i>
+        <span translate>GRAPH.COMMON.MAX_AGE.DAY</span>
+      </a>
+      <a class="item item-icon-left ink"
+         ng-click="setMaxAge('week')">
+        <i class="icon ion-ios-checkmark-empty" ng-show="formData.maxAge==='week'"></i>
+        <span translate>GRAPH.COMMON.MAX_AGE.WEEK</span>
+      </a>
+      <a class="item item-icon-left ink"
+         ng-click="setMaxAge('month')">
+        <i class="icon ion-ios-checkmark-empty" ng-show="formData.maxAge==='month'"></i>
+        <span translate>GRAPH.COMMON.MAX_AGE.MONTH</span>
+      </a>
+      <a class="item item-icon-left ink"
+         ng-click="setMaxAge('quarter')">
+        <i class="icon ion-ios-checkmark-empty" ng-show="formData.maxAge==='quarter'"></i>
+        <span translate>GRAPH.COMMON.MAX_AGE.QUARTER</span>
+      </a>
+      <a class="item item-icon-left ink"
+         ng-click="setMaxAge('semester')">
+        <i class="icon ion-ios-checkmark-empty" ng-show="formData.maxAge==='semester'"></i>
+        <span translate>GRAPH.COMMON.MAX_AGE.SEMESTER</span>
+      </a>
+      <a class="item item-icon-left ink"
+         ng-click="setMaxAge('year')">
+        <i class="icon ion-ios-checkmark-empty" ng-show="formData.maxAge==='year'"></i>
+        <span translate>GRAPH.COMMON.MAX_AGE.YEAR</span>
+      </a>
+      <a class="item item-icon-left ink"
+         ng-click="setMaxAge()">
+        <i class="icon ion-ios-checkmark-empty" ng-show="!formData.maxAge"></i>
+        <span translate>GRAPH.COMMON.MAX_AGE.FOREVER</span>
+      </a>
+
+    </div>
+  </ion-content>
+</ion-popover-view>
diff --git a/www/plugins/graph/templates/common/popover_range_actions.html b/www/plugins/graph/templates/common/popover_range_actions.html
index cf8893b84..5b977aaa4 100644
--- a/www/plugins/graph/templates/common/popover_range_actions.html
+++ b/www/plugins/graph/templates/common/popover_range_actions.html
@@ -1,4 +1,4 @@
-<ion-popover-view class="has-header popover-graph-currency">
+<ion-popover-view class="has-header popover-graph-range">
   <ion-header-bar>
     <h1 class="title" translate>COMMON.POPOVER_ACTIONS_TITLE</h1>
   </ion-header-bar>
@@ -8,7 +8,7 @@
       <!-- scale -->
       <a class="item item-icon-left ink"
          ng-click="toggleScale()">
-        <i class="icon ion-ios-checkmark-empty" ng-show="formData.scale=='logarithmic'"></i>
+        <i class="icon ion-ios-checkmark-empty" ng-show="formData.scale==='logarithmic'"></i>
         <span ng-bind-html="'GRAPH.COMMON.LOGARITHMIC_SCALE' | translate"></span>
       </a>
 
@@ -20,21 +20,21 @@
       <!-- duration: hour -->
       <a class="item item-icon-left ink"
          ng-click="setRangeDuration('hour')">
-        <i class="icon ion-ios-checkmark-empty" ng-show="formData.rangeDuration=='hour'"></i>
+        <i class="icon ion-ios-checkmark-empty" ng-show="formData.rangeDuration==='hour'"></i>
         <span ng-bind-html="'GRAPH.COMMON.RANGE_DURATION.HOUR' | translate"></span>
       </a>
 
       <!-- duration: day -->
       <a class="item item-icon-left ink"
          ng-click="setRangeDuration('day')">
-        <i class="icon ion-ios-checkmark-empty" ng-show="formData.rangeDuration=='day'"></i>
+        <i class="icon ion-ios-checkmark-empty" ng-show="formData.rangeDuration==='day'"></i>
         <span ng-bind-html="'GRAPH.COMMON.RANGE_DURATION.DAY' | translate"></span>
       </a>
 
       <!-- duration: month -->
       <a class="item item-icon-left ink"
          ng-click="setRangeDuration('month')">
-        <i class="icon ion-ios-checkmark-empty" ng-show="formData.rangeDuration=='month'"></i>
+        <i class="icon ion-ios-checkmark-empty" ng-show="formData.rangeDuration==='month'"></i>
         <span ng-bind-html="'GRAPH.COMMON.RANGE_DURATION.MONTH' | translate"></span>
       </a>
 
diff --git a/www/plugins/graph/templates/currency/tabs/tab_network_stats.html b/www/plugins/graph/templates/currency/tabs/tab_network_stats.html
index ec8f5c0d6..184d0bc9f 100644
--- a/www/plugins/graph/templates/currency/tabs/tab_network_stats.html
+++ b/www/plugins/graph/templates/currency/tabs/tab_network_stats.html
@@ -3,8 +3,8 @@
 
     <div class="list">
       <div class="item"
-         ng-include="::'plugins/graph/templates/blockchain/graph_block_issuers.html'"
-         ng-controller="GpBlockchainIssuersCtrl"
+           ng-controller="GpBlockchainIssuersCtrl"
+           ng-include="::'plugins/graph/templates/blockchain/graph_block_issuers.html'"
            ng-init="setSize(500,700,true)">
       </div>
     </div>
diff --git a/www/plugins/graph/templates/currency/tabs/tab_wot_stats.html b/www/plugins/graph/templates/currency/tabs/tab_wot_stats.html
index 336648d43..d9c3f0aa9 100644
--- a/www/plugins/graph/templates/currency/tabs/tab_wot_stats.html
+++ b/www/plugins/graph/templates/currency/tabs/tab_wot_stats.html
@@ -5,10 +5,19 @@
     </div>
 
     <div class="list no-padding">
-      <div class="item no-padding-top"
-           ng-include="::'plugins/graph/templates/currency/graph_members_count.html'"
-           ng-init="setSize(600,700,false)">
-      </div>
+      <ng-controller ng-controller="GpCurrencyMembersCountCtrl" >
+        <div class="item no-padding-top"
+             ng-include="::'plugins/graph/templates/currency/graph_members_count.html'"
+             ng-init="setSize(600,700,false)">
+        </div>
+      </ng-controller>
+
+      <ng-controller ng-controller="GpCurrencyPendingCountCtrl" >
+        <div class="item"
+             ng-include="::'plugins/graph/templates/docstats/graph.html'"
+             ng-init="setSize(600,700,false)">
+        </div>
+      </ng-controller>
     </div>
   </ion-content>
 </ion-view>
diff --git a/www/plugins/graph/templates/currency/view_currency_extend.html b/www/plugins/graph/templates/currency/view_currency_extend.html
index 556ed85c2..51c1ce92a 100644
--- a/www/plugins/graph/templates/currency/view_currency_extend.html
+++ b/www/plugins/graph/templates/currency/view_currency_extend.html
@@ -1,5 +1,5 @@
 
-<!-- section actual parameters -->
+<!-- section parameters -->
 <ng-if ng-if=":state:enable && extensionPoint === 'parameters-actual'" >
 
   <ng-if ng-if="!smallscreen">
@@ -50,7 +50,7 @@
   </div>
 </ng-if>
 
-<!-- section Wot -->
+<!-- section Network -->
 <ng-if ng-if=":state:enable && extensionPoint === 'network-actual'" >
 
   <div class="item padding-left padding-right no-padding-xs no-padding-sm"
diff --git a/www/plugins/graph/templates/currency/view_stats_lg.html b/www/plugins/graph/templates/currency/view_stats_lg.html
index bf1c64ca4..af97abeba 100644
--- a/www/plugins/graph/templates/currency/view_stats_lg.html
+++ b/www/plugins/graph/templates/currency/view_stats_lg.html
@@ -45,12 +45,21 @@
 
       <!-- Member count  -->
       <ng-controller ng-controller="GpCurrencyMembersCountCtrl" >
-      <div class="item no-padding-xs"
-           ng-if="!loading"
-           ng-include="::'plugins/graph/templates/currency/graph_members_count.html'"
-           ng-init="setSize(250, 1000)">
-      </div>
+        <div class="item no-padding-xs"
+             ng-if="!loading"
+             ng-include="::'plugins/graph/templates/currency/graph_members_count.html'"
+             ng-init="setSize(250, 1000)">
+        </div>
+      </ng-controller>
 
+      <!-- Pending count  -->
+      <ng-controller ng-controller="GpCurrencyPendingCountCtrl" >
+        <div class="item no-padding-xs"
+             ng-if="chart"
+             ng-include="::'plugins/graph/templates/docstats/graph.html'"
+             ng-init="setSize(250, 1000)">
+        </div>
+      </ng-controller>
 
     </div>
 
diff --git a/www/plugins/graph/templates/network/view_peer_stats.html b/www/plugins/graph/templates/network/view_peer_stats.html
index 92845974d..b68d2654c 100644
--- a/www/plugins/graph/templates/network/view_peer_stats.html
+++ b/www/plugins/graph/templates/network/view_peer_stats.html
@@ -12,7 +12,6 @@
 
     <div class="list" ng-if="!loading">
 
-      <!--  - - - - TX divider - - - - -->
       <div class="item item-divider hidden-xs hidden-sm" translate>
         GRAPH.BLOCKCHAIN.TX_DIVIDER
       </div>
diff --git a/www/plugins/graph/templates/network/view_stats.html b/www/plugins/graph/templates/network/view_stats.html
new file mode 100644
index 000000000..ab547029d
--- /dev/null
+++ b/www/plugins/graph/templates/network/view_stats.html
@@ -0,0 +1,26 @@
+<ion-view left-buttons="leftButtons"
+          cache-view="false">
+  <ion-nav-title>
+    {{'GRAPH.NETWORK.TITLE' | translate}}
+  </ion-nav-title>
+
+  <ion-content scroll="true" class="no-padding" >
+
+    <div class="center padding" ng-if="loading">
+      <ion-spinner icon="android"></ion-spinner>
+    </div>
+
+    <div class="list" ng-show="!loading">
+
+      <!-- charts  -->
+      <div class="item no-padding-xs"
+           ng-repeat="chart in charts"
+           ng-include="::'plugins/graph/templates/docstats/graph.html'"
+           ng-init="setSize(250, 1000)">
+      </div>
+
+    </div>
+
+  </ion-content>
+
+</ion-view>
-- 
GitLab