From 1fffbb7f28709e0f3c1a0014f5d41bfb24c23df5 Mon Sep 17 00:00:00 2001 From: blavenie <benoit.lavenier@e-is.pro> Date: Sat, 27 May 2017 00:50:12 +0200 Subject: [PATCH] [enh] Add graph on a wallet --- www/index.html | 3 + www/js/services/bma-services.js | 15 +- www/js/services/currency-services.js | 48 +- www/plugins/es/i18n/locale-fr-FR.json | 2 + www/plugins/graph/i18n/locale-fr-FR.json | 17 + .../js/controllers/account-controllers.js | 476 +++++++++++++++ .../js/controllers/blockchain-controllers.js | 228 +------- .../js/controllers/common-controllers.js | 232 ++++++++ .../js/controllers/currency-controllers.js | 147 +---- www/plugins/graph/js/plugin.js | 4 +- www/plugins/graph/js/services.js | 1 + .../graph/js/services/color-services.js | 138 +++++ .../graph/js/services/data-services.js | 543 +++++++++++++----- .../templates/account/graph_balance.html | 24 + .../account/graph_certifications.html | 12 + .../graph/templates/account/view_stats.html | 39 ++ .../blockchain/graph_block_issuers.html | 4 +- .../templates/blockchain/graph_tx_count.html | 24 +- .../templates/common/graph_range_bar.html | 20 + .../popover_range_actions.html} | 12 +- .../currency/graph_members_count.html | 2 +- .../currency/graph_monetary_mass.html | 2 +- www/templates/network/view_peer.html | 4 +- 23 files changed, 1508 insertions(+), 489 deletions(-) create mode 100644 www/plugins/graph/js/controllers/account-controllers.js create mode 100644 www/plugins/graph/js/controllers/common-controllers.js create mode 100644 www/plugins/graph/js/services/color-services.js create mode 100644 www/plugins/graph/templates/account/graph_balance.html create mode 100644 www/plugins/graph/templates/account/graph_certifications.html create mode 100644 www/plugins/graph/templates/account/view_stats.html create mode 100644 www/plugins/graph/templates/common/graph_range_bar.html rename www/plugins/graph/templates/{blockchain/popover_tx_actions.html => common/popover_range_actions.html} (64%) diff --git a/www/index.html b/www/index.html index 4596ffbcc..fe6a31274 100644 --- a/www/index.html +++ b/www/index.html @@ -159,9 +159,12 @@ <script src="dist/dist_js/plugins/graph/js/plugin.js"></script> <script src="dist/dist_js/plugins/graph/js/services.js"></script> <script src="dist/dist_js/plugins/graph/js/services/data-services.js"></script> + <script src="dist/dist_js/plugins/graph/js/services/color-services.js"></script> + <script src="dist/dist_js/plugins/graph/js/controllers/common-controllers.js"></script> <script src="dist/dist_js/plugins/graph/js/controllers/blockchain-controllers.js"></script> <script src="dist/dist_js/plugins/graph/js/controllers/network-controllers.js"></script> <script src="dist/dist_js/plugins/graph/js/controllers/currency-controllers.js"></script> + <script src="dist/dist_js/plugins/graph/js/controllers/account-controllers.js"></script> <!--endRemoveIf(no-plugin)--> diff --git a/www/js/services/bma-services.js b/www/js/services/bma-services.js index dffc4a471..572d24a06 100644 --- a/www/js/services/bma-services.js +++ b/www/js/services/bma-services.js @@ -615,7 +615,7 @@ angular.module('cesium.bma.services', ['ngResource', 'ngApi', 'cesium.http.servi } }) .catch(function(err){ - if (err && err.ucode === errorCodes.HTTP_LIMITATION) { + if (err && err.ucode === exports.errorCodes.HTTP_LIMITATION) { resolve(result); } else { @@ -625,6 +625,19 @@ angular.module('cesium.bma.services', ['ngResource', 'ngApi', 'cesium.http.servi }); }; + exports.raw.getHttpWithRetryIfLimitation = function(exec) { + return exec() + .catch(function(err){ + // When too many request, retry in 3s + if (err && err.ucode == exports.errorCodes.HTTP_LIMITATION) { + return $timeout(function() { + // retry + return exports.raw.getHttpWithRetryIfLimitation(exec); + }, exports.constants.LIMIT_REQUEST_DELAY); + } + }); + }; + exports.blockchain.lastUd = function() { return exports.blockchain.stats.ud() .then(function(res) { diff --git a/www/js/services/currency-services.js b/www/js/services/currency-services.js index 086b898ba..97ed4d399 100644 --- a/www/js/services/currency-services.js +++ b/www/js/services/currency-services.js @@ -7,6 +7,15 @@ angular.module('cesium.currency.services', ['ngResource', 'ngApi', 'cesium.bma.s factory = function(id) { var + constants = { + // Avoid to many call on well known currencies + WELL_KNOWN_CURRENCIES: { + g1: { + firstBlockTime: 1488987127 + } + } + }, + data = { loaded: false, currencies: null, @@ -33,7 +42,7 @@ angular.module('cesium.currency.services', ['ngResource', 'ngApi', 'cesium.bma.s // Load currency from default node var promise = BMA.blockchain.parameters() .then(function(res){ - data.currencies.push({ + var currency = { name: res.currency, peer: { host: BMA.host, @@ -41,21 +50,47 @@ angular.module('cesium.currency.services', ['ngResource', 'ngApi', 'cesium.bma.s server: BMA.server }, parameters: res - }); - + }; + // Add to data + data.currencies.push(currency); + + // Well known currencies + if (constants.WELL_KNOWN_CURRENCIES[res.currency]){ + angular.merge(currency, constants.WELL_KNOWN_CURRENCIES[res.currency]); + } + + // Load some default values + else { + return BMA.blockchain.block({block:0}) + .then(function(json) { + // Need by graph plugin + currency.firstBlockTime = json.medianTime; + }) + .catch(function(err) { + // Special case, when currency not started yet + if (err && err.ucode === BMA.errorCodes.BLOCK_NOT_FOUND) { + currency.firstBlockTime = 0; + return; + } + throw err; + }) + ; + } + }) + .then(function() { // API extension point return api.data.raisePromise.load(data); }) .then(function() { console.debug('[currency] Loaded in ' + (new Date().getTime() - now) + 'ms'); data.loaded = true; - delete data.cache.loadJobPromise; + delete data.cache.loadPromise; return data; }) .catch(function(err) { data.loaded = false; data.currencies = []; - delete data.cache.loadJobPromise; + delete data.cache.loadPromise; throw err; }); @@ -83,8 +118,7 @@ angular.module('cesium.currency.services', ['ngResource', 'ngApi', 'cesium.bma.s .then(function(data){ return _.findWhere(data.currencies, {name: name}); }); - } - ; + }; // Register extension points api.registerEvent('data', 'load'); diff --git a/www/plugins/es/i18n/locale-fr-FR.json b/www/plugins/es/i18n/locale-fr-FR.json index ff507e3bf..8db450447 100644 --- a/www/plugins/es/i18n/locale-fr-FR.json +++ b/www/plugins/es/i18n/locale-fr-FR.json @@ -470,6 +470,8 @@ "NODE_BMA_UP": "Le noeud <b>{{params[0]}}:{{params[1]}}</b> est à nouveau accessible.", "MEMBER_JOIN": "Vous êtes maintenant <b>membre</b> de la monnaie <b>{{params[0]}}</b> !", "MEMBER_LEAVE": "Vous n'êtes <b>plus membre</b> de la monnaie <b>{{params[0]}}</b>!", + "MEMBER_EXCLUDE": "Vous n'êtes <b>plus membre</b> de la monnaie <b>{{params[0]}}</b>, faute de non renouvellement ou par manque de certifications.", + "MEMBER_REVOKE": "La révocation de votre compte a été effectuée. Il ne pourra plus être un compte membre de la monnaie <b>{{params[0]}}</b>.", "MEMBER_ACTIVE": "Votre renouvellement d'adhésion à la monnaie <b>{{params[0]}}</b> a été <b>pris en compte</b>.", "TX_SENT": "Votre <b>paiement</b> à <span ng-class=\"{'gray': !notification.uid, 'positive':notification.uid}\" ><i class=\"icon\" ng-class=\"{'ion-person': notification.uid, 'ion-key': !notification.uid}\"></i> {{name||uid||params[1]}}</span> a été effectué.", "TX_SENT_MULTI": "Votre <b>paiement</b> à <b>{{params[1]}}</b> a été effectué.", diff --git a/www/plugins/graph/i18n/locale-fr-FR.json b/www/plugins/graph/i18n/locale-fr-FR.json index 7e781d9af..1d2b236c9 100644 --- a/www/plugins/graph/i18n/locale-fr-FR.json +++ b/www/plugins/graph/i18n/locale-fr-FR.json @@ -11,6 +11,23 @@ "BTN_SHOW_STATS": "Voir les statistiques", "BTN_SHOW_DETAILED_STATS": "Statistiques détaillées" }, + "ACCOUNT": { + "TITLE": "Statistiques", + "BALANCE_DIVIDER": "Situation du compte", + "BALANCE_TITLE": "Balance - {{pubkey|formatPubkey}}", + "TX_RECEIVED_LABEL": "Recettes", + "TX_SENT_LABEL": "Dépenses", + "TX_ACCUMULATION_LABEL": "Bilan des transactions", + "UD_LABEL": "DU", + "UD_ACCUMULATION_LABEL": "Bilan des DU", + "BALANCE_LABEL": "Solde", + "WOT_DIVIDER": "Toile de confiance", + "CERTIFICATION_TITLE": "Nombre de certifications - {{pubkey|formatPubkey}}", + "RECEIVED_CERT_LABEL": "Total reçues", + "RECEIVED_CERT_DELTA_LABEL": "Variation reçues", + "GIVEN_CERT_LABEL": "Total envoyées", + "GIVEN_CERT_DELTA_LABEL": "Variation envoyées" + }, "BLOCKCHAIN": { "TITLE": "Statistiques", "BLOCKS_ISSUERS_DIVIDER": "Analyse de la répartition du calcul", diff --git a/www/plugins/graph/js/controllers/account-controllers.js b/www/plugins/graph/js/controllers/account-controllers.js new file mode 100644 index 000000000..1542daf6d --- /dev/null +++ b/www/plugins/graph/js/controllers/account-controllers.js @@ -0,0 +1,476 @@ + +angular.module('cesium.graph.account.controllers', ['chart.js', 'cesium.graph.services']) + + .config(function($stateProvider, PluginServiceProvider, csConfig) { + 'ngInject'; + + var enable = csConfig.plugins && csConfig.plugins.es; + if (enable) { + + /*PluginServiceProvider + .extendState('app.view_wallet', { + points: { + 'general': { + templateUrl: "plugins/graph/templates/network/view_wallet_extend.html", + controller: 'GpWalletExtendCtrl' + } + } + }) + + // TODO add wot extend + ;*/ + + $stateProvider + .state('app.view_wallet_stats', { + url: "/wallet/stats?t&stepUnit", + views: { + 'menuContent': { + templateUrl: "plugins/graph/templates/account/view_stats.html" + } + }, + data: { + auth: true + } + }) + + .state('app.wot_identity_stats', { + url: "/wot/:pubkey/stats?t&stepUnit", + views: { + 'menuContent': { + templateUrl: "plugins/graph/templates/account/view_stats.html" + } + } + }); + } + }) + + .controller('GpWalletExtendCtrl', GpWalletExtendController) + + .controller('GpAccountBalanceCtrl', GpAccountBalanceController) + + .controller('GpAccountCertificationCtrl', GpAccountCertificationController) + +; + +function GpWalletExtendController($scope, PluginService, esSettings) { + 'ngInject'; + + $scope.extensionPoint = PluginService.extensions.points.current.get(); + $scope.enable = esSettings.isEnable(); + + esSettings.api.state.on.changed($scope, function(enable) { + $scope.enable = enable; + }); +} + + +function GpAccountBalanceController($scope, $controller, $q, $state, $filter, $translate, gpData, gpColor, csWallet) { + 'ngInject'; + + // Initialize the super class and extend it. + angular.extend(this, $controller('GpCurrencyAbstractCtrl', {$scope: $scope})); + + $scope.init = function(e, state) { + + if (state && state.stateParams && state.stateParams.pubkey) { // Currency parameter + $scope.formData.pubkey = state.stateParams.pubkey; + } + else if(csWallet.isLogin()) { + $scope.formData.pubkey = csWallet.data.pubkey; + } + + // for DEV only + //$scope.formData.pubkey = '38MEAZN68Pz1DTvT3tqgxx4yQP6snJCQhPqEFxbDk4aE'; + }; + + $scope.load = function(updateTimePct) { + + updateTimePct = angular.isDefined(updateTimePct) ? updateTimePct : true; + + return $q.all([ + + $translate('GRAPH.ACCOUNT.BALANCE_TITLE', $scope.formData), + + // translate i18n keys + $translate(['GRAPH.ACCOUNT.UD_LABEL', + 'GRAPH.ACCOUNT.TX_RECEIVED_LABEL', + 'GRAPH.ACCOUNT.TX_SENT_LABEL', + 'GRAPH.ACCOUNT.UD_ACCUMULATION_LABEL', + 'GRAPH.ACCOUNT.TX_ACCUMULATION_LABEL', + 'GRAPH.ACCOUNT.BALANCE_LABEL', + 'COMMON.DATE_PATTERN', + 'COMMON.DATE_SHORT_PATTERN', + 'COMMON.DATE_MONTH_YEAR_PATTERN']), + + // get data + gpData.blockchain.movement($scope.formData.currency, angular.copy($scope.formData)) + ]) + .then(function(result) { + var title = result[0]; + var translations = result[1]; + result = result[2]; + + if (!result || !result.times) return; // no data + $scope.times = result.times; + + var formatInteger = $filter('formatInteger'); + var formatAmount = $filter('formatDecimal'); + $scope.currencySymbol = $filter('currencySymbolNoHtml')($scope.currency, $scope.formData.useRelative); + + // Data + $scope.data = [ + result.ud, + result.received, + result.sent, + result.udSum, + result.txSum, + result.balance + ]; + + var displayFormats = { + hour: translations['COMMON.DATE_PATTERN'], + day: translations['COMMON.DATE_SHORT_PATTERN'], + month: translations['COMMON.DATE_MONTH_YEAR_PATTERN'] + }; + var displayFormat = displayFormats[$scope.formData.rangeDuration]; + // Labels + $scope.labels = result.times.reduce(function(res, time) { + return res.concat(moment.unix(time).local().format(displayFormat)); + }, []); + + // Colors + $scope.colors = gpColor.scale.fix(result.times.length); + + // Update range with received values + $scope.updateRange(result.times[0], result.times[result.times.length-1], updateTimePct); + + // Options + $scope.options = { + responsive: true, + maintainAspectRatio: true, + title: { + display: true, + text: title + }, + scales: { + yAxes: [ + { + id: 'y-axis-left', + type: 'linear', + position: 'left', + ticks: { + beginAtZero:true, + callback: function(value) { + return formatInteger(value); + } + } + }, + { + id: 'y-axis-right', + type: 'linear', + position: 'right', + stacked: true, + display: false, + gridLines: { + show: false + }, + ticks: { + beginAtZero:true, + callback: function(value) { + return formatInteger(value); + } + } + } + ] + }, + legend: { + display: true + }, + tooltips: { + enabled: true, + mode: 'index', + callbacks: { + label: function(tooltipItems, data) { + // Should add a '+' before value ? + var addPlus = (tooltipItems.datasetIndex < 2) + && tooltipItems.yLabel > 0; + return data.datasets[tooltipItems.datasetIndex].label + + ': ' + + (tooltipItems.yLabel === 0 ? '0' : + ((addPlus ? '+' : '') + formatAmount(tooltipItems.yLabel) + ' ' + $scope.currencySymbol)); + } + } + } + }; + + $scope.datasetOverride = [ + { + yAxisID: 'y-axis-right', + type: 'bar', + label: translations['GRAPH.ACCOUNT.UD_LABEL'], + backgroundColor: gpColor.rgba.energized(0.3), + hoverBackgroundColor: gpColor.rgba.energized(0.5), + borderWidth: 1 + }, + { + yAxisID: 'y-axis-right', + type: 'bar', + label: translations['GRAPH.ACCOUNT.TX_RECEIVED_LABEL'], + backgroundColor: gpColor.rgba.positive(0.3), + hoverBackgroundColor: gpColor.rgba.positive(0.5), + borderWidth: 1 + }, + { + yAxisID: 'y-axis-right', + type: 'bar', + label: translations['GRAPH.ACCOUNT.TX_SENT_LABEL'], + backgroundColor: gpColor.rgba.gray(0.3), + hoverBackgroundColor: gpColor.rgba.gray(0.5), + borderWidth: 1 + }, + { + yAxisID: 'y-axis-left', + type: 'line', + label: translations['GRAPH.ACCOUNT.UD_ACCUMULATION_LABEL'], + fill: false, + borderColor: gpColor.rgba.energized(0.5), + borderWidth: 2, + backgroundColor: gpColor.rgba.energized(0.7), + pointBackgroundColor: gpColor.rgba.energized(0.5), + pointBorderColor: gpColor.rgba.white(), + pointHoverBackgroundColor: gpColor.rgba.energized(1), + pointHoverBorderColor: 'rgba(0,0,0,0)', + pointRadius: 3, + lineTension: 0.1 + }, + { + yAxisID: 'y-axis-left', + type: 'line', + label: translations['GRAPH.ACCOUNT.TX_ACCUMULATION_LABEL'], + fill: false, + borderColor: gpColor.rgba.positive(0.5), + borderWidth: 2, + backgroundColor: gpColor.rgba.positive(0.7), + pointBackgroundColor: gpColor.rgba.positive(0.5), + pointBorderColor: gpColor.rgba.white(), + pointHoverBackgroundColor: gpColor.rgba.positive(1), + pointHoverBorderColor: 'rgba(0,0,0,0)', + pointRadius: 3, + lineTension: 0.1 + }, + { + yAxisID: 'y-axis-left', + type: 'line', + label: translations['GRAPH.ACCOUNT.BALANCE_LABEL'], + fill: 'origin', + borderColor: gpColor.rgba.calm(0.5), + borderWidth: 2, + pointBackgroundColor: gpColor.rgba.calm(0.5), + pointBorderColor: gpColor.rgba.white(), + pointHoverBackgroundColor: gpColor.rgba.calm(1), + pointHoverBorderColor: 'rgba(0,0,0,0)', + pointRadius: 3, + lineTension: 0.1 + } + ]; + }); + }; + + $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 query = '_exists_:transactions AND medianTime:>={0} AND medianTime:<{1}'.format(from, to); + if ($scope.formData.pubkey) { + query += ' AND (transactions.issuers:' + $scope.formData.pubkey + ' OR transactions.outputs:*' + $scope.formData.pubkey + ')'; + } + $state.go('app.blockchain_search', {q: query}); + }; +} + + +/** + * Graph that display received/sent certification + */ +function GpAccountCertificationController($scope, $controller, $q, $state, $filter, $translate, gpData, gpColor, csWallet) { + 'ngInject'; + + // Initialize the super class and extend it. + angular.extend(this, $controller('GpCurrencyAbstractCtrl', {$scope: $scope})); + + $scope.init = function(e, state) { + if (state && state.stateParams && state.stateParams.pubkey) { // Currency parameter + $scope.formData.pubkey = state.stateParams.pubkey; + } + else if(csWallet.isLogin()) { + $scope.formData.pubkey = csWallet.data.pubkey; + } + + // for DEV only + //$scope.formData.pubkey = '38MEAZN68Pz1DTvT3tqgxx4yQP6snJCQhPqEFxbDk4aE'; + }; + + $scope.load = function(updateTimePct) { + + var formData = $scope.formData; + + return $q.all([ + + $translate('GRAPH.ACCOUNT.CERTIFICATION_TITLE', formData), + + // translate i18n keys + $translate(['GRAPH.ACCOUNT.GIVEN_CERT_LABEL', + 'GRAPH.ACCOUNT.RECEIVED_CERT_LABEL', + 'GRAPH.ACCOUNT.GIVEN_CERT_DELTA_LABEL', + 'GRAPH.ACCOUNT.RECEIVED_CERT_DELTA_LABEL', + 'COMMON.DATE_PATTERN', + 'COMMON.DATE_SHORT_PATTERN', + 'COMMON.DATE_MONTH_YEAR_PATTERN']), + + // get data + gpData.wot.certifications(formData) + ]) + .then(function(result) { + + var title = result[0]; + var translations = result[1]; + result = result[2]; + + if (!result || !result.times) return; // no data + $scope.times = result.times; + + var formatInteger = $filter('formatInteger'); + + // Data + $scope.data = [ + result.deltaReceived, + result.received, + result.deltaGiven, + result.given + ]; + + // Labels + $scope.labels = result.labels; + + var displayFormats = { + hour: translations['COMMON.DATE_PATTERN'], + day: translations['COMMON.DATE_SHORT_PATTERN'], + month: translations['COMMON.DATE_MONTH_YEAR_PATTERN'] + }; + var displayFormat = displayFormats[$scope.formData.rangeDuration]; + // Labels + $scope.labels = result.times.reduce(function(res, time) { + return res.concat(moment.unix(time).local().format(displayFormat)); + }, []); + + // Colors + $scope.colors = gpColor.scale.fix(result.times.length); + + // Update options with received values + $scope.updateRange(result.times[0], result.times[result.times.length-1], updateTimePct); + + // Options + $scope.options = { + responsive: true, + maintainAspectRatio: true, + title: { + display: true, + text: title + }, + scales: { + yAxes: [ + { + id: 'y-axis-left', + type: 'linear', + position: 'left' + }, + { + id: 'y-axis-hide', + type: 'linear', + display: false, + position: 'right' + } + ] + }, + legend: { + display: true + }, + tooltips: { + enabled: true, + mode: 'index', + callbacks: { + label: function(tooltipItems, data) { + // Should add a '+' before value ? + var addPlus = (tooltipItems.datasetIndex === 0 || tooltipItems.datasetIndex === 2) && tooltipItems.yLabel > 0; + return data.datasets[tooltipItems.datasetIndex].label + + ': ' + + (addPlus ? '+' : '') + + formatInteger(tooltipItems.yLabel); + } + } + } + }; + + $scope.datasetOverride = [ + { + yAxisID: 'y-axis-left', + type: 'bar', + label: translations['GRAPH.ACCOUNT.RECEIVED_CERT_DELTA_LABEL'], + borderColor: gpColor.rgba.positive(0.6), + borderWidth: 1, + backgroundColor: gpColor.rgba.positive(0.4), + hoverBackgroundColor: gpColor.rgba.positive(0.6) + }, + { + yAxisID: 'y-axis-left', + type: 'line', + label: translations['GRAPH.ACCOUNT.RECEIVED_CERT_LABEL'], + fill: false, + borderColor: gpColor.rgba.positive(0.5), + borderWidth: 2, + backgroundColor: gpColor.rgba.positive(1), + pointBackgroundColor: gpColor.rgba.positive(0.5), + pointBorderColor: gpColor.rgba.white(), + pointHoverBackgroundColor: gpColor.rgba.positive(1), + pointHoverBorderColor: 'rgba(0,0,0,0)', + pointRadius: 3 + }, + { + yAxisID: 'y-axis-left', + type: 'bar', + label: translations['GRAPH.ACCOUNT.GIVEN_CERT_DELTA_LABEL'], + borderColor: gpColor.rgba.assertive(0.6), + borderWidth: 1, + backgroundColor: gpColor.rgba.assertive(0.4), + hoverBackgroundColor: gpColor.rgba.assertive(0.6) + }, + { + yAxisID: 'y-axis-left', + type: 'line', + label: translations['GRAPH.ACCOUNT.GIVEN_CERT_LABEL'], + fill: false, + borderColor: gpColor.rgba.assertive(0.4), + borderWidth: 2, + backgroundColor: gpColor.rgba.assertive(1), + pointBackgroundColor: gpColor.rgba.assertive(0.4), + pointBorderColor: gpColor.rgba.white(), + pointHoverBackgroundColor: gpColor.rgba.assertive(1), + pointHoverBorderColor: 'rgba(0,0,0,0)', + pointRadius: 3, + lineTension: 0.1 + } + ]; + }); + }; + + $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 query = '_exists_:transactions AND medianTime:>={0} AND medianTime:<{1}'.format(from, to); + if ($scope.formData.pubkey) { + query += ' AND (transactions.issuers:' + $scope.formData.pubkey + ' OR transactions.outputs:*' + $scope.formData.pubkey + ')'; + } + $state.go('app.blockchain_search', {q: query}); + }; +} diff --git a/www/plugins/graph/js/controllers/blockchain-controllers.js b/www/plugins/graph/js/controllers/blockchain-controllers.js index a7f0ba1a9..a9b770aa7 100644 --- a/www/plugins/graph/js/controllers/blockchain-controllers.js +++ b/www/plugins/graph/js/controllers/blockchain-controllers.js @@ -31,61 +31,21 @@ angular.module('cesium.graph.blockchain.controllers', ['chart.js', 'cesium.servi ; -function GpBlockchainTxCountController($scope, $q, $state, $filter, $translate, $ionicPopover, csCurrency, BMA, esHttp, gpData) { +function GpBlockchainTxCountController($scope, $controller, $q, $state, $filter, $translate, gpData, gpColor) { 'ngInject'; - $scope.loading = true; - $scope.height=undefined; - $scope.width=undefined; - $scope.formData = { - timePct: 100, - useRelative: false /*csSettings.data.useRelative*/ - }; - - // Default TX range duration - $scope.txOptions = { - rangeDuration: 'day' - }; - - $scope.enter = function(e, state) { - if ($scope.loading) { - - if (state && state.stateParams && state.stateParams.currency) { // Currency parameter - $scope.currency = state.stateParams.currency; - } - - // Make sure there is currency, or load it not - if (!$scope.currency) { - return csCurrency.default() - .then(function(currency) { - $scope.currency = currency ? currency.name : null; - return $scope.enter(e, state); - }); - } - - $scope.load() - .then(function() { - $scope.loading = false; - }); - } - }; - $scope.$on('$ionicParentView.enter', $scope.enter); + // Initialize the super class and extend it. + angular.extend(this, $controller('GpCurrencyAbstractCtrl', {$scope: $scope})); $scope.load = function(updateTimePct) { - updateTimePct = angular.isDefined(updateTimePct) ? updateTimePct : true; - - var truncDate = function(time) { - return moment.unix(time).utc().startOf($scope.txOptions.rangeDuration).unix(); - }; - - var txOptions = $scope.txOptions; + var formData = $scope.formData; return $q.all([ - $translate($scope.txOptions.issuer? + $translate($scope.formData.issuer? 'GRAPH.BLOCKCHAIN.TX_AMOUNT_PUBKEY_TITLE': - 'GRAPH.BLOCKCHAIN.TX_AMOUNT_TITLE', txOptions), + 'GRAPH.BLOCKCHAIN.TX_AMOUNT_TITLE', formData), // translate i18n keys $translate(['GRAPH.BLOCKCHAIN.TX_AMOUNT_LABEL', @@ -95,38 +55,25 @@ function GpBlockchainTxCountController($scope, $q, $state, $filter, $translate, 'COMMON.DATE_SHORT_PATTERN', 'COMMON.DATE_MONTH_YEAR_PATTERN']), - // get block #0 - $scope.firstBlockTime ? - $q.when({medianTime: $scope.firstBlockTime}) : - BMA.blockchain.block({block: 0}) - .catch(function(err) { - if (err && err.ucode == BMA.errorCodes.BLOCK_NOT_FOUND) { - return {medianTime: esHttp.date.now()}; - } - }), - // get data - gpData.blockchain.txCount($scope.currency, txOptions) + gpData.blockchain.txCount($scope.formData.currency, formData) ]) .then(function(result) { var title = result[0]; var translations = result[1]; - $scope.firstBlockTime = $scope.firstBlockTime || result[2].medianTime; - $scope.formData.firstBlockTime = $scope.formData.firstBlockTime || truncDate($scope.firstBlockTime); - $scope.formData.currencyAge = truncDate(esHttp.date.now()) - $scope.formData.firstBlockTime; - result = result[3]; + result = result[2]; if (!result || !result.times) return; // no data $scope.times = result.times; var formatInteger = $filter('formatInteger'); var formatAmount = $filter('formatDecimal'); - $scope.currencySymbol = $filter('currencySymbolNoHtml')($scope.currency, false/*$scope.formData.useRelative*/); + $scope.currencySymbol = $filter('currencySymbolNoHtml')($scope.formData.currency, $scope.formData.useRelative); // Data - if ($scope.txOptions.rangeDuration != 'hour') { + if ($scope.formData.rangeDuration != 'hour') { $scope.data = [ result.amount, result.count, @@ -148,27 +95,17 @@ function GpBlockchainTxCountController($scope, $q, $state, $filter, $translate, day: translations['COMMON.DATE_SHORT_PATTERN'], month: translations['COMMON.DATE_MONTH_YEAR_PATTERN'] }; - var displayFormat = displayFormats[$scope.txOptions.rangeDuration]; + var displayFormat = displayFormats[$scope.formData.rangeDuration]; // Labels $scope.labels = result.times.reduce(function(res, time) { return res.concat(moment.unix(time).local().format(displayFormat)); }, []); // Colors - $scope.colors = result.times.reduce(function(res) { - return res.concat('rgba(17,193,243,0.5)'); - }, []); + $scope.colors = gpColor.scale.fix(result.times.length); - // Update options with received values - $scope.txOptions.startTime = result.times[0]; - $scope.txOptions.endTime = result.times[result.times.length-1]; - $scope.formData.timeWindow = $scope.formData.timeWindow || $scope.txOptions.endTime - $scope.txOptions.startTime; - $scope.formData.rangeDuration = $scope.formData.rangeDuration || $scope.formData.timeWindow / result.times.length; - - if (updateTimePct) { - $scope.formData.timePct = Math.ceil(($scope.txOptions.startTime - $scope.formData.firstBlockTime) * 100 / - ($scope.formData.currencyAge - $scope.formData.timeWindow)); - } + // Update range options with received values + $scope.updateRange(result.times[0], result.times[result.times.length-1], updateTimePct); // Options $scope.options = { @@ -233,18 +170,18 @@ function GpBlockchainTxCountController($scope, $q, $state, $filter, $translate, yAxisID: 'y-axis-amount', type: 'bar', label: translations['GRAPH.BLOCKCHAIN.TX_AMOUNT_LABEL'], - hoverBackgroundColor: 'rgba(17,193,243,0.6)' + hoverBackgroundColor: gpColor.rgba.calm(0.6) }, { yAxisID: 'y-axis-count', type: 'line', label: translations['GRAPH.BLOCKCHAIN.TX_COUNT_LABEL'], fill: false, - borderColor: 'rgba(150,150,150,0.5)', + borderColor: gpColor.rgba.gray(0.5), borderWidth: 2, - pointBackgroundColor: 'rgba(150,150,150,0.5)', - pointBorderColor: 'rgba(255,255,255,1)', - pointHoverBackgroundColor: 'rgba(150,150,150,1)', + pointBackgroundColor: gpColor.rgba.gray(0.5), + pointBorderColor: gpColor.rgba.white(), + pointHoverBackgroundColor: gpColor.rgba.gray(1), pointHoverBorderColor: 'rgba(0,0,0,0)', pointRadius: 3 }, @@ -264,122 +201,25 @@ function GpBlockchainTxCountController($scope, $q, $state, $filter, $translate, }); }; - $scope.setSize = function(height, width, maintainAspectRatio) { - $scope.height = height; - $scope.width = width; - $scope.maintainAspectRatio = angular.isDefined(maintainAspectRatio) ? maintainAspectRatio : $scope.maintainAspectRatio; - }; - - $scope.showTxRange = function(data, e, item) { - if (!item) return + $scope.onChartClick = function(data, e, item) { + if (!item) return; var from = $scope.times[item._index]; - var to = moment.unix(from).utc().add(1, $scope.txOptions.rangeDuration).unix(); + var to = moment.unix(from).utc().add(1, $scope.formData.rangeDuration).unix(); var query = '_exists_:transactions AND medianTime:>={0} AND medianTime:<{1}'.format(from, to); - if ($scope.txOptions.issuer) { - query += ' AND issuer:' + $scope.txOptions.issuer; + if ($scope.formData.issuer) { + query += ' AND issuer:' + $scope.formData.issuer; } $state.go('app.blockchain_search', {q: query}); }; - $scope.setTxRangeDuration = function(txRangeDuration) { - $scope.hideActionsPopover(); - if ($scope.txOptions && txRangeDuration == $scope.txOptions.rangeDuration) return; - - $scope.txOptions.rangeDuration = txRangeDuration; - - // Restore default values - delete $scope.txOptions.startTime; - delete $scope.txOptions.endTime; - $scope.formData = { - timePct: 100 - }; - - // Reload TX data - $scope.load(); - }; - - $scope.loadPreviousTx = function() { - $scope.txOptions.startTime -= $scope.times.length * $scope.formData.rangeDuration; - if ($scope.txOptions.startTime < $scope.firstBlockTime) { - $scope.txOptions.startTime = $scope.firstBlockTime; - } - $scope.txOptions.endTime = $scope.txOptions.startTime + $scope.times.length * $scope.formData.rangeDuration; - // Reload TX data - $scope.load(); - }; - - $scope.loadNextTx = function() { - $scope.txOptions.startTime += $scope.times.length * $scope.formData.rangeDuration; - if ($scope.txOptions.startTime > $scope.firstBlockTime + $scope.formData.currencyAge - $scope.formData.timeWindow) { - $scope.txOptions.startTime = $scope.firstBlockTime + $scope.formData.currencyAge - $scope.formData.timeWindow; - } - $scope.txOptions.endTime = $scope.txOptions.startTime + $scope.times.length * $scope.formData.rangeDuration; - // Reload TX data - $scope.load(); - }; - - $scope.onTxTimeChanged = function() { - $scope.txOptions.startTime = $scope.firstBlockTime + (parseFloat($scope.formData.timePct) / 100) * ($scope.formData.currencyAge - $scope.formData.timeWindow) ; - $scope.txOptions.endTime = $scope.txOptions.startTime + $scope.times.length * $scope.formData.rangeDuration; - - // Reload TX data - $scope.load(false); - }; - - /* -- Popover -- */ - - $scope.showTxActionsPopover = function(event) { - $scope.hideActionsPopover(); - $ionicPopover.fromTemplateUrl('plugins/graph/templates/blockchain/popover_tx_actions.html', { - scope: $scope - }).then(function(popover) { - $scope.actionsPopover = popover; - //Cleanup the popover when we're done with it! - $scope.$on('$destroy', function() { - $scope.actionsPopover.remove(); - }); - $scope.actionsPopover.show(event); - }); - }; - - $scope.hideActionsPopover = function() { - if ($scope.actionsPopover) { - $scope.actionsPopover.hide(); - } - }; - } -function GpBlockchainIssuersController($scope, $q, $state, $translate, csCurrency, gpData) { +function GpBlockchainIssuersController($scope, $controller, $q, $state, $translate, gpColor, gpData) { 'ngInject'; - $scope.loading = true; - $scope.height = undefined; - $scope.width = undefined; - - $scope.enter = function(e, state) { - if ($scope.loading) { - - if (state && state.stateParams && state.stateParams.currency) { // Currency parameter - $scope.currency = state.stateParams.currency; - } - - // Make sure there is currency, or load it not - if (!$scope.currency) { - return csCurrency.default() - .then(function(currency) { - $scope.currency = currency ? currency.name : null; - return $scope.enter(e, state); - }); - } - - $scope.load() - .then(function() { - $scope.loading = false; - }); - } - }; - $scope.$on('$ionicParentView.enter', $scope.enter); + + // Initialize the super class and extend it. + angular.extend(this, $controller('GpCurrencyAbstractCtrl', {$scope: $scope})); $scope.load = function() { return $q.all([ @@ -387,7 +227,7 @@ function GpBlockchainIssuersController($scope, $q, $state, $translate, csCurrenc 'GRAPH.BLOCKCHAIN.BLOCKS_ISSUERS_TITLE', 'GRAPH.BLOCKCHAIN.BLOCKS_ISSUERS_LABEL' ]), - gpData.blockchain.countByIssuer($scope.currency) + gpData.blockchain.countByIssuer($scope.formData.currency) ]) .then(function(result) { var translations = result[0]; @@ -423,18 +263,12 @@ function GpBlockchainIssuersController($scope, $q, $state, $translate, csCurrenc }; // Colors - $scope.colors = gpData.util.colors.custom(result.data.length); + $scope.colors = gpColor.scale.custom(result.data.length); }); }; - $scope.setSize = function(height, width, maintainAspectRatio) { - $scope.height = height; - $scope.width = width; - $scope.maintainAspectRatio = angular.isDefined(maintainAspectRatio) ? maintainAspectRatio : $scope.maintainAspectRatio; - }; - - $scope.showBlockIssuer = function(data, e, item) { + $scope.onChartClick = function(data, e, item) { if (!item) return; var issuer = $scope.issuers[item._index]; $state.go('app.wot_identity', issuer); diff --git a/www/plugins/graph/js/controllers/common-controllers.js b/www/plugins/graph/js/controllers/common-controllers.js new file mode 100644 index 000000000..9615beb5d --- /dev/null +++ b/www/plugins/graph/js/controllers/common-controllers.js @@ -0,0 +1,232 @@ + +angular.module('cesium.graph.common.controllers', ['cesium.services']) + + .controller('GpCurrencyAbstractCtrl', GpCurrencyAbstractController) +; + +function GpCurrencyAbstractController($scope, $filter, $ionicPopover, $ionicHistory, $state, csSettings, csCurrency, esHttp) { + 'ngInject'; + + $scope.loading = true; + $scope.formData = $scope.formData || { + useRelative: csSettings.data.useRelative, + timePct: 100, + rangeDuration: 'day', + firstBlockTime: 0 + }; + $scope.formData.useRelative = angular.isDefined($scope.formData.useRelative) ? + $scope.formData.useRelative : csSettings.data.useRelative; + $scope.scale = 'linear'; + $scope.height = undefined; + $scope.width = undefined; + $scope.maintainAspectRatio = true; + $scope.times = []; + + function _truncDate(time) { + return moment.unix(time).utc().startOf($scope.formData.rangeDuration).unix(); + } + + $scope.enter = function (e, state) { + if ($scope.loading) { + + if (state && state.stateParams) { + // remember state, to be able to refresh location + $scope.stateName = state && state.stateName; + $scope.stateParams = angular.copy(state && state.stateParams||{}); + + if (!$scope.formData.currency && state && state.stateParams && state.stateParams.currency) { // Currency parameter + $scope.formData.currency = state.stateParams.currency; + } + if (state.stateParams.timePct) { + $scope.formData.timePct = state.stateParams.timePct; + } + if (state.stateParams.group) { + $scope.formData.rangeDuration = state.stateParams.group; + } + } + + $scope.init(e, state); + + // Make sure there is currency, or load it not + if (!$scope.formData.currency) { + return csCurrency.default() + .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(esHttp.date.now()) - $scope.formData.firstBlockTime; + return $scope.enter(e, state); + }); + } + + $scope.load() + .then(function () { + $scope.loading = false; + }); + } + }; + $scope.$on('$csExtension.enter', $scope.enter); + $scope.$on('$ionicParentView.enter', $scope.enter); + + $scope.updateLocation = function() { + $ionicHistory.nextViewOptions({ + disableAnimate: true, + disableBack: true, + historyRoot: true + }); + + $scope.stateParams = $scope.stateParams || {}; + $scope.stateParams.t = $scope.formData.timePct < 100 || $scope.formData.timePct >= 0 ? $scope.formData.timePct : undefined; + $scope.stateParams.stepUnit = $scope.formData.rangeDuration != 'day' ? $scope.formData.rangeDuration : undefined; + + $state.go($scope.stateName, $scope.stateParams, { + reload: false, + inherit: true, + notify: false} + ); + }; + + // Allow to fixe size, form a template (e.g. in a 'ng-init' tag) + $scope.setSize = function(height, width, maintainAspectRatio) { + $scope.height = height; + $scope.width = width; + $scope.maintainAspectRatio = angular.isDefined(maintainAspectRatio) ? maintainAspectRatio : $scope.maintainAspectRatio; + }; + + // When parent view execute a refresh action + $scope.$on('csView.action.refresh', function(event, context) { + if (!context || context == 'currency') { + return $scope.load(); + } + }); + + $scope.init = function(stateParams) { + // Should be override by subclasses + }; + + $scope.load = function() { + // Should be override by subclasses + }; + + $scope.setScale = function(scale) { + $scope.hideActionsPopover(); + $scope.scale = scale; + + var format = $filter('formatInteger'); + + _.forEach($scope.options.scales.yAxes, function(yAxe) { + yAxe.type = scale; + yAxe.ticks = yAxe.ticks || {}; + if (scale == 'linear') { + yAxe.ticks.beginAtZero = true; + delete yAxe.ticks.min; + yAxe.ticks.callback = function(value) { + return format(value); + }; + } + else { + yAxe.ticks.min = 0; + delete yAxe.ticks.beginAtZero; + delete yAxe.ticks.callback; + yAxe.ticks.callback = function(value, index) { + if (!value) return; + if (Math.log10(value)%1 === 0 || Math.log10(value/3)%1 === 0) { + return format(value); + } + return ''; + }; + } + }); + }; + + $scope.setRangeDuration = function(rangeDuration) { + $scope.hideActionsPopover(); + if ($scope.formData && rangeDuration == $scope.formData.rangeDuration) return; + + $scope.formData.rangeDuration = rangeDuration; + + // Restore default values + delete $scope.formData.startTime; + delete $scope.formData.endTime; + delete $scope.formData.rangeDurationSec; + //$scope.formData.timePct = 100; + + // Reload data + $scope.load(); + // Update location + $scope.updateLocation(); + }; + + $scope.goPreviousRange = function() { + $scope.formData.startTime -= $scope.times.length * $scope.formData.rangeDurationSec; + if ($scope.formData.startTime < $scope.formData.firstBlockTime) { + $scope.formData.startTime = $scope.formData.firstBlockTime; + } + $scope.formData.endTime = $scope.formData.startTime + $scope.times.length * $scope.formData.rangeDurationSec; + + // Reload data + $scope.load(); + // Update location + $scope.updateLocation(); + }; + + $scope.goNextRange = function() { + $scope.formData.startTime += $scope.times.length * $scope.formData.rangeDurationSec; + if ($scope.formData.startTime > $scope.formData.firstBlockTime + $scope.formData.currencyAge - $scope.formData.timeWindow) { + $scope.formData.startTime = $scope.formData.firstBlockTime + $scope.formData.currencyAge - $scope.formData.timeWindow; + } + $scope.formData.endTime = $scope.formData.startTime + $scope.times.length * $scope.formData.rangeDurationSec; + // Reload data + $scope.load(); + // Update location + $scope.updateLocation(); + }; + + $scope.onRangeChanged = function() { + $scope.formData.startTime = $scope.formData.firstBlockTime + (parseFloat($scope.formData.timePct) / 100) * ($scope.formData.currencyAge - $scope.formData.timeWindow) ; + $scope.formData.endTime = $scope.formData.startTime + $scope.times.length * $scope.formData.rangeDurationSec; + + // Reload data + $scope.load(false); + // Update location + $scope.updateLocation(); + }; + + $scope.updateRange = function(startTime, endTime, updateTimePct) { + updateTimePct = angular.isDefined(updateTimePct) ? updateTimePct : true; + + $scope.formData.startTime = startTime; + $scope.formData.endTime = endTime; + $scope.formData.timeWindow = $scope.formData.timeWindow || $scope.formData.endTime - $scope.formData.startTime; + $scope.formData.rangeDurationSec = $scope.formData.rangeDurationSec || $scope.formData.timeWindow / ($scope.times.length-1); + + if (updateTimePct) { + $scope.formData.timePct = Math.ceil(($scope.formData.startTime - $scope.formData.firstBlockTime) * 100 / + ($scope.formData.currencyAge - $scope.formData.timeWindow)); + } + }; + + /* -- Popover -- */ + + $scope.showActionsPopover = function(event) { + $scope.hideActionsPopover(); + $ionicPopover.fromTemplateUrl('plugins/graph/templates/common/popover_range_actions.html', { + scope: $scope + }).then(function(popover) { + $scope.actionsPopover = popover; + //Cleanup the popover when we're done with it! + $scope.$on('$destroy', function() { + $scope.actionsPopover.remove(); + }); + $scope.actionsPopover.show(event); + }); + }; + + $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 056588024..aaf200e9b 100644 --- a/www/plugins/graph/js/controllers/currency-controllers.js +++ b/www/plugins/graph/js/controllers/currency-controllers.js @@ -1,5 +1,5 @@ -angular.module('cesium.graph.currency.controllers', ['chart.js', 'cesium.graph.services', 'cesium.graph.blockchain.controllers']) +angular.module('cesium.graph.currency.controllers', ['chart.js', 'cesium.graph.services', 'cesium.graph.common.controllers']) .config(function($stateProvider, PluginServiceProvider, csConfig) { 'ngInject'; @@ -82,8 +82,6 @@ angular.module('cesium.graph.currency.controllers', ['chart.js', 'cesium.graph.s .controller('GpCurrencyViewExtendCtrl', GpCurrencyViewExtendController) - .controller('GpCurrencyAbstractCtrl', GpCurrencyAbstractController) - .controller('GpCurrencyMonetaryMassCtrl', GpCurrencyMonetaryMassController) .controller('GpCurrencyDUCtrl', GpCurrencyDUController) @@ -103,63 +101,7 @@ function GpCurrencyViewExtendController($scope, PluginService, UIUtils, esSettin }); } - - -function GpCurrencyAbstractController($scope, csCurrency) { - 'ngInject'; - - $scope.loading = true; - $scope.formData = $scope.formData || {}; - $scope.height = undefined; - $scope.width = undefined; - $scope.maintainAspectRatio = true; - - $scope.enter = function (e, state) { - if ($scope.loading) { - - if (!$scope.formData.currency && state && state.stateParams && state.stateParams.currency) { // Currency parameter - $scope.formData.currency = state.stateParams.currency; - } - - // Make sure there is currency, or load it not - if (!$scope.formData.currency) { - return csCurrency.default() - .then(function (currency) { - $scope.formData.currency = currency ? currency.name : null; - return $scope.enter(e, state); - }); - } - - $scope.load() - .then(function () { - $scope.loading = false; - }); - } - }; - $scope.$on('$csExtension.enter', $scope.enter); - $scope.$on('$ionicParentView.enter', $scope.enter); - - // Allow to fixe size, form a template (e.g. in a 'ng-init' tag) - $scope.setSize = function(height, width, maintainAspectRatio) { - $scope.height = height; - $scope.width = width; - $scope.maintainAspectRatio = angular.isDefined(maintainAspectRatio) ? maintainAspectRatio : $scope.maintainAspectRatio; - }; - - // When parent view execute a refresh action - $scope.$on('csView.action.refresh', function(event, context) { - if (!context || context == 'currency') { - return $scope.load(); - } - }); - - $scope.load = function() { - // Should be override by subclasses - }; - -} - -function GpCurrencyMonetaryMassController($scope, $controller, $q, $state, $translate, $ionicPopover, gpData, $filter, csSettings) { +function GpCurrencyMonetaryMassController($scope, $controller, $q, $state, $translate, $ionicPopover, gpColor, gpData, $filter, csSettings) { 'ngInject'; // Initialize the super class and extend it. @@ -168,7 +110,6 @@ function GpCurrencyMonetaryMassController($scope, $controller, $q, $state, $tran $scope.formData.useRelative = angular.isDefined($scope.formData.useRelative) ? $scope.formData.useRelative : csSettings.data.useRelative; - $scope.scale = 'linear'; $scope.displayShareAxis = true; $scope.onUseRelativeChanged = function() { @@ -198,10 +139,11 @@ function GpCurrencyMonetaryMassController($scope, $controller, $q, $state, $tran .then(function(result) { var translations = result[0]; result = result[1]; - if (!result || !result.blocks) return; + if (!result || !result.times) return; + $scope.times = result.times; // Choose a date formatter, depending on the blocks period - var blocksPeriod = result.blocks[result.blocks.length-1].medianTime - result.blocks[0].medianTime; + var blocksPeriod = result.times[result.times.length-1] - result.times[0]; var formatDate; if (blocksPeriod < 15778800/* less than 6 months*/) { formatDate = $filter('formatDateShort'); @@ -247,14 +189,12 @@ function GpCurrencyMonetaryMassController($scope, $controller, $q, $state, $tran $scope.data = data; // Labels - $scope.labels = result.labels.reduce(function(res, time) { + $scope.labels = result.times.reduce(function(res, time) { return res.concat(formatDate(time)); }, []); // Colors - $scope.colors = result.blocks.reduce(function(res) { - return res.concat('rgba(17,193,243,0.5)'); - }, []); + $scope.colors = gpColor.scale.fix(result.times.length); // Options $scope.options = { @@ -264,6 +204,9 @@ function GpCurrencyMonetaryMassController($scope, $controller, $q, $state, $tran display: true, text: translations['GRAPH.CURRENCY.MONETARY_MASS_TITLE'] }, + legend: { + display: $scope.displayShareAxis + }, scales: { yAxes: [ { @@ -293,9 +236,10 @@ function GpCurrencyMonetaryMassController($scope, $controller, $q, $state, $tran $scope.datasetOverride = [ { yAxisID: 'y-axis-mass', - type: 'bar', + type: 'line', label: translations['GRAPH.CURRENCY.MONETARY_MASS_LABEL'], - hoverBackgroundColor: 'rgba(17,193,243,0.6)' + hoverBackgroundColor: gpColor.rgba.calm(0.6), + borderWidth: 1 }, { yAxisID: 'y-axis-mn', @@ -305,6 +249,7 @@ function GpCurrencyMonetaryMassController($scope, $controller, $q, $state, $tran showLine: true, borderColor: 'rgba(255,201,0,1)', borderWidth: 2, + backgroundColor: 'rgba(255,201,0,1)', pointBackgroundColor: 'rgba(255,201,0,1)', pointBorderColor: 'rgba(255,255,255,1)', pointHoverBackgroundColor: 'rgba(255,201,0,1)', @@ -320,44 +265,12 @@ function GpCurrencyMonetaryMassController($scope, $controller, $q, $state, $tran }; - $scope.showBlock = function(data, e, item) { + $scope.onChartClick = function(data, e, item) { if (!item) return; var number = $scope.blocks[item._index]; $state.go('app.view_block', {number: number}); }; - $scope.setScale = function(scale) { - $scope.hideActionsPopover(); - $scope.scale = scale; - - var format = $filter('formatInteger'); - - _.forEach($scope.options.scales.yAxes, function(yAxe) { - yAxe.type = scale; - yAxe.ticks = yAxe.ticks || {}; - if (scale == 'linear') { - yAxe.ticks.beginAtZero = true; - delete yAxe.ticks.min; - yAxe.ticks.callback = function(value) { - return format(value); - }; - } - else { - yAxe.ticks.min = 0; - delete yAxe.ticks.beginAtZero; - delete yAxe.ticks.callback; - yAxe.ticks.callback = function(value, index) { - if (!value) return; - //console.log(value + '->' + Math.log10(value)%1); - if (Math.log10(value)%1 === 0 || Math.log10(value/3)%1 === 0) { - return format(value); - } - return ''; - }; - } - }); - }; - /* -- Popover -- */ $scope.showActionsPopover = function(event) { @@ -403,10 +316,11 @@ function GpCurrencyDUController($scope, $q, $controller, $translate, gpData, $fi .then(function(result) { var translations = result[0]; result = result[1]; - if (!result || !result.blocks) return; + if (!result || !result.times) return; + $scope.times = result.times; // Choose a date formatter, depending on the blocks period - var blocksPeriod = result.blocks[result.blocks.length-1].medianTime - result.blocks[0].medianTime; + var blocksPeriod = result.times[result.times.length-1] - result.times[0]; var dateFilter; if (blocksPeriod < 15778800/* less than 6 months*/) { dateFilter = $filter('formatDateShort'); @@ -426,7 +340,7 @@ function GpCurrencyDUController($scope, $q, $controller, $translate, gpData, $fi ]; // Labels - $scope.labels = result.labels.reduce(function(res, time) { + $scope.labels = result.times.reduce(function(res, time) { return res.concat(dateFilter(time)); }, []); @@ -485,7 +399,7 @@ function GpCurrencyDUController($scope, $q, $controller, $translate, gpData, $fi } -function GpCurrencyMembersCountController($scope, $controller, $q, $state, $translate, gpData, $filter) { +function GpCurrencyMembersCountController($scope, $controller, $q, $state, $translate, gpColor, gpData, $filter) { 'ngInject'; // Initialize the super class and extend it. @@ -506,10 +420,12 @@ function GpCurrencyMembersCountController($scope, $controller, $q, $state, $tran .then(function(result) { var translations = result[0]; result = result[1]; - if (!result || !result.blocks) return; + + if (!result || !result.times) return; + $scope.times = result.times; // Choose a date formatter, depending on the blocks period - var blocksPeriod = result.blocks[result.blocks.length-1].medianTime - result.blocks[0].medianTime; + var blocksPeriod = result.times[result.blocks.length-1] - result.times[0].medianTime; var dateFormat; if (blocksPeriod < 15778800/* less than 6 months*/) { dateFormat = $filter('formatDateShort'); @@ -519,7 +435,7 @@ function GpCurrencyMembersCountController($scope, $controller, $q, $state, $tran } // Format time - $scope.labels = result.labels.reduce(function(res, time) { + $scope.labels = result.times.reduce(function(res, time) { return res.concat(dateFormat(time)); }, []); @@ -558,24 +474,17 @@ function GpCurrencyMembersCountController($scope, $controller, $q, $state, $tran ]; // Colors - $scope.colors = result.blocks.reduce(function(res) { - return res.concat('rgba(17,193,243,0.5)'); - }, []); - - // Keep times (need for click) - $scope.blockTimes = result.blocks.reduce(function(res, block) { - return res.concat(block.medianTime); - }, []); + $scope.colors = gpColor.scale.fix(result.blocks.length); }); }; - $scope.showBlock = function(data, e, item) { + $scope.onChartClick = function(data, e, item) { if (!item) return; if (!item._index) { $state.go('app.view_block', {number: 0}); return; } - var from = $scope.blockTimes[item._index-1]; + var from = $scope.times[item._index-1]; var to = moment.unix(from).utc().add(1, 'day').unix(); $state.go('app.blockchain_search', { q: '(_exists_:joiners OR _exists_:leavers OR _exists_:revoked OR _exists_:excluded) AND medianTime:>{0} AND medianTime:<={1}'.format(from, to) diff --git a/www/plugins/graph/js/plugin.js b/www/plugins/graph/js/plugin.js index e2d4965ea..baa6ca013 100644 --- a/www/plugins/graph/js/plugin.js +++ b/www/plugins/graph/js/plugin.js @@ -3,8 +3,10 @@ angular.module('cesium.graph.plugin', [ // Services 'cesium.graph.services', // Controllers + 'cesium.graph.common.controllers', 'cesium.graph.blockchain.controllers', 'cesium.graph.network.controllers', - 'cesium.graph.currency.controllers' + 'cesium.graph.currency.controllers', + 'cesium.graph.account.controllers' ]) ; diff --git a/www/plugins/graph/js/services.js b/www/plugins/graph/js/services.js index 6cae46c69..ac325d0c5 100644 --- a/www/plugins/graph/js/services.js +++ b/www/plugins/graph/js/services.js @@ -1,6 +1,7 @@ angular.module('cesium.graph.services', [ // Services + 'cesium.graph.color.services', 'cesium.graph.data.services' ]) ; diff --git a/www/plugins/graph/js/services/color-services.js b/www/plugins/graph/js/services/color-services.js new file mode 100644 index 000000000..d123e545a --- /dev/null +++ b/www/plugins/graph/js/services/color-services.js @@ -0,0 +1,138 @@ +angular.module('cesium.graph.color.services', []) + + .factory('gpColor', function($rootScope) { + 'ngInject'; + + var + constants = { + css2Rgb: { + 'white': [255, 255, 255], + 'assertive': [239, 71, 58], // ok + 'calm': [17, 193, 243], // ok + 'positive': [56, 126, 245], // ok + 'balanced': [51, 205, 95], // ok + 'energized': [255, 201, 0], // ok + 'royal': [136, 106, 234], // ok + 'gray': [150, 150, 150], // ok + 'stable': [248, 248, 248] // ok + } + }, + exports = { + scale: {} + }; + + + /** + * Compute colors scale + * @param count + * @param opacity + * @param startColor + * @param startState + * @returns {Array} + */ + exports.scale.custom = function (count, opacity, startColor, startState) { + + function _state2side(state) { + switch (state) { + case 0: + return 0; + case 1: + return -1; + case 2: + return 0; + case 3: + return 1; + } + } + + // From [0,1] + opacity = opacity>0 && opacity|| '0.55'; + + var defaultStateSize = Math.round(count / 2.5/*=4 states max*/); + + // Start color [r,v,b] + var color = startColor ? angular.copy(startColor) : [255, 0, 0]; // Red + + // Colors state: 0=keep, 1=decrease, 2=keep, 3=increase + var states = startState ? angular.copy(startState) : [0, 2, 3]; // R=keep, V=keep, B=increase + + var steps = startColor ? [ + Math.round(255 / defaultStateSize), + Math.round(255 / defaultStateSize), + Math.round(255 / defaultStateSize) + ] : [ + Math.round((color[0] - 50) / defaultStateSize), + Math.round((255 - color[1]) / defaultStateSize), + Math.round((255 - color[2]) / defaultStateSize) + ]; + + + // Compute start sides (1=increase, 0=flat, -1=decrease) + var sides = [ + _state2side(states[0]), + _state2side(states[1]), + _state2side(states[2])]; + + // Use to detect when need to change a 'flat' state (when state = 0 or 2) + var stateCounters = [0, 0, 0]; + + var result = []; + for (var i = 0; i < count; i++) { + for (var j = 0; j < 3; j++) { + color[j] += sides[j] * steps[j]; + stateCounters[j]++; + // color has reach a limit + if (((color[j] <= 0 || color[j] >= 255) && sides[j] !== 0) || + (sides[j] === 0 && stateCounters[j] == defaultStateSize)) { + // Max sure not overflow limit + if (color[j] <= 0) { + color[j] = 0; + } + else if (color[j] >= 255) { + color[j] = 255; + } + // Go to the next state, in [0..3] + states[j] = (states[j] + 1) % 4; + + // Update side from this new state + sides[j] = _state2side(states[j]); + + // Reset state counter + stateCounters[j] = 0; + } + } + + // Add the color to result + result.push('rgba(' + color[0] + ',' + color[1] + ',' + color[2] + ',' + opacity + ')'); + + } + return result; + }; + + exports.scale.default = function () { + return exports.scale.custom(25); + }; + + /** + * Create a array with the given color + **/ + exports.scale.fix = function (length, color) { + return Array.apply(null, Array(length||25)) + .map(String.prototype.valueOf, color||exports.rgba.calm(0.5)); + }; + + // Create a function to generate a rgba string, from + exports.rgba = _.mapObject(constants.css2Rgb, function(rgbArray){ + var prefix = 'rgba(' + rgbArray.join(',') + ','; + return function(opacity){ + if (!opacity || opacity < 0) { + return 'rgb(' + rgbArray.join(',') + ')'; + } + return prefix + opacity + ')'; + }; + }); + + return exports; + }) + +; diff --git a/www/plugins/graph/js/services/data-services.js b/www/plugins/graph/js/services/data-services.js index ddd2d7733..198d30b25 100644 --- a/www/plugins/graph/js/services/data-services.js +++ b/www/plugins/graph/js/services/data-services.js @@ -1,119 +1,72 @@ angular.module('cesium.graph.data.services', ['cesium.wot.services', 'cesium.es.http.services']) - .factory('gpData', function($rootScope, $q, $timeout, esHttp, BMA, csWot, csCache) { + .factory('gpData', function($rootScope, $q, $timeout, esHttp, BMA, csWot, csCache, csCurrency) { 'ngInject'; var currencyCache = csCache.get('gpData-currency-', csCache.constants.SHORT), exports = { node: {}, + wot: {}, blockchain: {}, - util: { - colors: {} - }, raw: { block: { search: esHttp.post('/:currency/block/_search') }, blockstat: { - search: esHttp.post('/:currency/blockstat/_search?pretty') + search: esHttp.post('/:currency/blockstat/_search') + }, + movement: { + search: esHttp.post('/:currency/movement/_search') + }, + user: { + event: esHttp.post('/user/event/_search?pretty') } }, regex: { } }; + function onCurrencyLoad(data, deferred) { + deferred = deferred || $q.defer(); + var currency = data.currencies[data.currencies.length-1]; - /** - * Compute colors scale - * @param count - * @param opacity - * @param startColor - * @param startState - * @returns {Array} - */ - exports.util.colors.custom = function(count, opacity, startColor, startState) { - - function _state2side(state) { - switch(state) { - case 0: - return 0; - case 1: - return -1; - case 2: - return 0; - case 3: - return 1; + if (currency.firstBlockTime) { + deferred.resolve(); + return deferred.promise; } + // Fill first block time value + BMA.blockchain.block({block: 0}) + .then(function(block) { + currency.firstBlockTime = block.medianTime; + deferred.resolve(); + }) + .catch(function(err) { + if (err && err.ucode == BMA.errorCodes.BLOCK_NOT_FOUND) { + currency.firstBlockTime = esHttp.date.now(); + deferred.resolve(); + } + deferred.reject(err); + }); + return deferred.promise; } - // From [0,1] - opacity = opacity || '0.55'; - - var defaultStateSize = Math.round(count / 2.5/*=4 states max*/); - - // Start color [r,v,b] - var color = startColor ? angular.copy(startColor) : [255,0,0]; // Red - - // Colors state: 0=keep, 1=decrease, 2=keep, 3=increase - var states = startState ? angular.copy(startState) : [0,2,3]; // R=keep, V=keep, B=increase - - var steps = startColor ? [ - Math.round(255 / defaultStateSize), - Math.round(255 / defaultStateSize), - Math.round(255 / defaultStateSize) - ] : [ - Math.round((color[0]-50) / defaultStateSize), - Math.round((255-color[1]) / defaultStateSize), - Math.round((255-color[2]) / defaultStateSize) - ]; - - - // Compute start sides (1=increase, 0=flat, -1=decrease) - var sides = [ - _state2side(states[0]), - _state2side(states[1]), - _state2side(states[2])]; - - // Use to detect when need to change a 'flat' state (when state = 0 or 2) - var stateCounters = [0,0,0]; - - var result = []; - for (var i = 0; i<count; i++) { - for (var j=0; j<3;j++) { - color[j] += sides[j] * steps[j]; - stateCounters[j]++; - // color has reach a limit - if (((color[j] <= 0 || color[j] >= 255) && sides[j] !== 0) || - (sides[j] === 0 && stateCounters[j] == defaultStateSize)) { - // Max sure not overflow limit - if (color[j] <= 0) { - color[j] = 0; - } - else if (color[j] >= 255) { - color[j] = 255; - } - // Go to the next state, in [0..3] - states[j] = (states[j] + 1) % 4; - - // Update side from this new state - sides[j] = _state2side(states[j]); - // Reset state counter - stateCounters[j] = 0; - } - } + function _powBase(amount, base) { + return base <= 0 ? amount : amount * Math.pow(10, base); + } - // Add the color to result - result.push('rgba(' + color[0] + ',' + color[1] + ',' + color[2] + ',' + opacity+')'); - - } - return result; - }; + function _initRangeOptions(options) { + options = options || {}; + options.maxRangeSize = options.maxRangeSize || 30; + options.defaultTotalRangeCount = options.defaultTotalRangeCount || options.maxRangeSize*2; - exports.util.colors.default = function() { - return exports.util.colors.custom(25); - }; + options.rangeDuration = options.rangeDuration || 'day'; + options.endTime = options.endTime || moment().utc().add(1, options.rangeDuration).unix(); + options.startTime = options.startTime || + moment.unix(options.endTime).utc().subtract(options.defaultTotalRangeCount, options.rangeDuration).unix(); + return options; + } /** * Graph: "blocks count by issuer" @@ -188,10 +141,6 @@ angular.module('cesium.graph.data.services', ['cesium.wot.services', 'cesium.es. } } - function _powBase(amount, base) { - return base <= 0 ? amount : amount * Math.pow(10, base); - } - var request = { query: { filtered: { @@ -255,7 +204,7 @@ angular.module('cesium.graph.data.services', ['cesium.wot.services', 'cesium.es. } } - result.labels = result.blocks.reduce(function(res, block){ + result.times = result.blocks.reduce(function(res, block){ return res.concat(block.medianTime); }, []); @@ -275,20 +224,14 @@ angular.module('cesium.graph.data.services', ['cesium.wot.services', 'cesium.es. */ exports.blockchain.txCount = function(currency, options) { - var maxRangeSize = 30; - var defaultTotalRangeCount = maxRangeSize*2; - - options = options || {}; - options.rangeDuration = options.rangeDuration || 'day'; - options.endTime = options.endTime || moment.unix(esHttp.date.now()).utc().add(1, options.rangeDuration).unix(); - options.startTime = options.startTime || - moment.unix(options.endTime).utc().subtract(defaultTotalRangeCount, options.rangeDuration).unix(); + options = _initRangeOptions(options); var jobs = []; var from = moment.unix(options.startTime).utc().startOf(options.rangeDuration); + var to = moment.unix(options.endTime).utc(); var ranges = []; - while(from.unix() < options.endTime) { + while(from.isBefore(to)) { ranges.push({ from: from.unix(), @@ -296,7 +239,7 @@ angular.module('cesium.graph.data.services', ['cesium.wot.services', 'cesium.es. }); // Do not exceed max range count - if (ranges.length == maxRangeSize) { + if (ranges.length == options.maxRangeSize) { var request = { size: 0, aggs: { @@ -329,8 +272,11 @@ angular.module('cesium.graph.data.services', ['cesium.wot.services', 'cesium.es. // prepare next loop ranges = []; - if (jobs.length < 10) { - + if (jobs.length == 10) { + console.error('Too many parallel jobs!'); + from = moment.unix(options.endTime).utc(); // stop while + } + else { jobs.push( exports.raw.blockstat.search(request, {currency: currency}) .then(function (res) { @@ -349,11 +295,6 @@ angular.module('cesium.graph.data.services', ['cesium.wot.services', 'cesium.es. }) ); } - else { - console.error('Too many call of txCount request ! '); - from = moment.unix(options.endTime).utc(); - } - } } @@ -366,23 +307,15 @@ angular.module('cesium.graph.data.services', ['cesium.wot.services', 'cesium.es. res = _.sortBy(res, 'from'); - var result = {}; - result.count = res.reduce(function(res, hit){ - return res.concat(hit.count); - }, []); - result.avgByBlock = res.reduce(function(res, hit){ - return res.concat(hit.avgByBlock); - }, []); - result.maxByBlock = res.reduce(function(res, hit){ - return res.concat(hit.maxByBlock); - }, []); - result.amount = res.reduce(function(res, hit){ - return res.concat(hit.amount/100); - }, []); - result.times = res.reduce(function(res, hit){ - return res.concat(hit.from); - }, []); - return result; + return { + count: _.pluck(res, 'count'), + avgByBlock: _.pluck(res, 'avgByBlock'), + maxByBlock: _.pluck(res, 'maxByBlock'), + amount: res.reduce(function(res, hit){ + return res.concat(hit.amount/100); + }, []), + times: _.pluck(res, 'from') + }; }); }; @@ -391,9 +324,7 @@ angular.module('cesium.graph.data.services', ['cesium.wot.services', 'cesium.es. * @param currency * @returns {*} */ - exports.node.blockCount = function(currency, pubkey, options) { - - options = options || {}; + exports.node.blockCount = function(currency, pubkey) { var request = { size: 0, @@ -406,6 +337,356 @@ angular.module('cesium.graph.data.services', ['cesium.wot.services', 'cesium.es. }); }; + + exports.raw.movement.getByRange = function(currency, pubkey, ranges) { + if (!pubkey) { + throw new Error('Missing \'pubkey\' argument!'); + } + var request = { + size: 0, + query: { + bool: { + should: [ + {term: {recipient: pubkey}}, + {term: {issuer: pubkey}} + ] + } + }, + aggs: { + tx: { + range: { + field: "medianTime", + ranges: ranges + }, + aggs: { + received: { + filter: {term: {recipient: pubkey}}, + aggs: { + received_stats: { + stats: { + field: "amount" + } + } + } + }, + sent: { + filter: {term: {issuer: pubkey}}, + aggs: { + sent_stats: { + stats: { + field: "amount" + } + } + } + } + } + + } + } + }; + + return exports.raw.movement.search(request, {currency: currency}) + .then(function(res) { + var aggs = res.aggregations; + if (!aggs.tx || !aggs.tx.buckets || !aggs.tx.buckets.length) return; + return (aggs.tx.buckets || []).reduce(function (res, agg) { + var sent = agg.sent.sent_stats; + var received = agg.received.received_stats; + return res.concat({ + from: agg.from, + to: agg.to, + sent: -sent.sum / 100, + received: received.sum / 100, + delta: (received.sum - sent.sum) / 100, + count: received.count + sent.count + }); + }, []); + }); + }; + + exports.raw.movement.getUds = function(currency, ranges, fromMapping) { + var request = { + size: 0, + query: { + bool: { + should: [ + {exists: {field: 'dividend'}} + ] + } + }, + aggs: { + ud: { + range: { + field: 'medianTime', + ranges: ranges + }, + aggs: { + ud_stats: { + stats: { + field: 'dividend' + } + }, + unitbase_stats: { + stats: { + field: 'unitbase' + } + } + } + } + } + }; + + return exports.raw.block.search(request, {currency: currency}) + .then(function(res) { + var aggs = res.aggregations; + if (!aggs.ud || !aggs.ud.buckets || !aggs.ud.buckets.length) return; + return (aggs.ud.buckets || []).reduce(function (res, agg) { + var from = fromMapping[agg.from]; + res[from] = _powBase(agg.ud_stats.sum, agg.unitbase_stats.min) / 100; + return res; + }, {}); + }); + }; + + /** + * Graph: "tx count" + * @param currency + * @returns {*} + */ + exports.blockchain.movement = function(currency, options) { + + options = _initRangeOptions(options); + options.withUD = angular.isDefined(options.withUD) ? options.withUD : true; + + var jobs = []; + + // If need and missing: load membership periods + if (options.withUD && !options.memberships) { + return exports.wot.memberships(options) + .then(function(res) { + options.memberships = res || []; + return exports.blockchain.movement(currency, options); + }); + } + + var from = moment.unix(options.startTime).utc().startOf(options.rangeDuration); + var to = moment.unix(options.endTime).utc(); + + // Add a range to get TX before startTime + var ranges = [{ + from: 0, + to: from.unix() + }]; + + var memberships = angular.copy(options.memberships).reverse(); + var membership = memberships.pop(); + while (membership && (membership.leaveTime && membership.leaveTime < from.unix())) { + membership = memberships.pop(); + } + + var udRanges = []; + var udFromMapping = {}; + + while(from.isBefore(to)) { + + var range = { + from: from.unix(), + to: from.add(1, options.rangeDuration).unix() + }; + ranges.push(range); + + var member = membership && membership.joinTime < range.to; + if (member) { + var udRange = { + from: Math.max(membership.joinTime, range.from), + to: Math.min(membership.leaveTime, range.to) + }; + udRanges.push(udRange); + udFromMapping[udRange.from] = range.from; + while (membership && (membership.leaveTime && membership.leaveTime < range.to)) { + membership = memberships.pop(); + } + } + + // Do not exceed max range count + if ((!jobs.length && ranges.length == options.maxRangeSize+1) || (jobs.length && ranges.length == options.maxRangeSize)) { + + if (udRanges.length) { + jobs.push($q.all([ + exports.raw.movement.getUds(currency, udRanges, udFromMapping), + exports.raw.movement.getByRange(currency, options.pubkey, ranges) + ]) + .then(function(res){ + var udsMap = res[0]; + res[1].forEach(function(hit){ + hit.ud = udsMap[hit.from]||0; + }); + return res[1]; + })); + } + else { + jobs.push(exports.raw.movement.getByRange(currency, options.pubkey, ranges)); + } + + // reset ranges for the next loop + ranges = []; + } + } // loop + + return $q.all(jobs) + .then(function(res) { + // concat all results + res = res.reduce(function(res, hits){ + if (!hits || !hits.length) return res; + return res.concat(hits); + }, []); + + // Sort by 'from' field + res = _.sortBy(res, 'from'); + + // First item should be history (tx before startTime) + var history = res.splice(0,1)[0]; + var txSum = history.delta||0; + var udSum = history.ud||0; + var balance = history.delta+history.ud||0; + + return { + times: _.pluck(res, 'from'), + sent: _.pluck(res, 'sent'), + received: _.pluck(res, 'received'), + delta: _.pluck(res, 'delta'), + ud: _.pluck(res, 'ud'), + txSum: res.reduce(function(res, hit){ + txSum += hit.delta; + return res.concat(txSum); + }, []), + udSum: res.reduce(function(res, hit){ + udSum += hit.ud||0; + return res.concat(udSum); + }, []), + balance: res.reduce(function(res, hit){ + balance += hit.delta+hit.ud||0; + return res.concat(balance); + }, []), + count: _.pluck(res, 'count') + }; + }); + }; + + + /** + * Graph: "tx count" + * @param currency + * @returns {*} + */ + exports.wot.certifications = function(options) { + + options = _initRangeOptions(options); + + return csWot.load(options.pubkey) + .then(function(idty) { + var res = {}; + _.forEach(idty.given_cert||[], function(cert){ + var truncTime = moment.unix(cert.time).utc().startOf(options.rangeDuration).unix(); + res[truncTime] = res[truncTime] || {time:truncTime,given:0,received:0}; + res[truncTime].given++; + }); + _.forEach(idty.received_cert||[], function(cert){ + var truncTime = moment.unix(cert.time).utc().startOf(options.rangeDuration).unix(); + res[truncTime] = res[truncTime] || {time:truncTime,given:0,received:0}; + res[truncTime].received++; + }); + + // Sort by time + res = _.sortBy(_.values(res), 'time'); + + // create final result + var result = { + times: _.pluck(res, 'time'), + deltaGiven: _.pluck(res, 'given'), + deltaReceived: _.pluck(res, 'received') + }; + var sum = 0; + result.given = result.deltaGiven.reduce(function(res, delta) { + sum += delta; + return res.concat(sum); + }, []); + sum = 0; + result.received = result.deltaReceived.reduce(function(res, delta) { + sum += delta; + return res.concat(sum); + }, []); + return result; + + }); + }; + + + exports.wot.memberships = function(options) { + + options = options || {}; + + // Get user events on membership state + var request = { + "size": 1000, + "query": { + "bool": { + "filter": [ + {"term": {"recipient" : options.pubkey }}, + {"terms": {"code" : ["MEMBER_JOIN","MEMBER_ACTIVE","MEMBER_LEAVE","MEMBER_EXCLUDE","MEMBER_REVOKE"] }} + ] + } + }, + "sort" : [ + { "time" : {"order" : "asc"}} + ], + _source: ["code", "time"] + }; + + return exports.raw.user.event(request) + + .then(function(res) { + if (!res.hits || !res.hits.total) return; + + // Compute member periods + var lastJoinTime; + var result = res.hits.hits.reduce(function(res, hit){ + var isMember = hit._source.code == 'MEMBER_JOIN' || hit._source.code == 'MEMBER_ACTIVE'; + // If join + if (isMember && !lastJoinTime) { + lastJoinTime = hit._source.time; + } + // If leave + else if (!isMember && lastJoinTime) { + // Add an entry + res = res.concat({ + joinTime: lastJoinTime, + leaveTime: hit._source.time + }); + lastJoinTime = 0; // reset + } + return res; + }, []); + + if (lastJoinTime) { + // Add last entry if need + result.push({ + joinTime: lastJoinTime, + leaveTime: moment().utc().unix() + }); + } + + return result; + }); + }; + + + // register listener +// csCurrency.api.data.on.load($rootScope, onCurrencyLoad, this); + return exports; }) + + + ; diff --git a/www/plugins/graph/templates/account/graph_balance.html b/www/plugins/graph/templates/account/graph_balance.html new file mode 100644 index 000000000..7939969f6 --- /dev/null +++ b/www/plugins/graph/templates/account/graph_balance.html @@ -0,0 +1,24 @@ + + <!-- 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 pull-right" + ng-click="showActionsPopover($event)"> + <i class="icon ion-navicon-round"></i> + </button> + </div> + + <div class="padding-left padding-right"> + <canvas id="account-balance" class="chart-bar" + height="{{height}}" width="{{width}}" + chart-data="data" + chart-dataset-override="datasetOverride" + chart-colors="colors" + chart-options="options" + chart-labels="labels" + chart-click="onChartClick"> + </canvas> + </div> + + <ng-include src="'plugins/graph/templates/common/graph_range_bar.html'"></ng-include> diff --git a/www/plugins/graph/templates/account/graph_certifications.html b/www/plugins/graph/templates/account/graph_certifications.html new file mode 100644 index 000000000..6e4816e12 --- /dev/null +++ b/www/plugins/graph/templates/account/graph_certifications.html @@ -0,0 +1,12 @@ + + <div class="padding-left padding-right"> + <canvas id="account-certifications" class="chart-bar" + height="{{height}}" width="{{width}}" + chart-data="data" + chart-dataset-override="datasetOverride" + chart-colors="colors" + chart-options="options" + chart-labels="labels" + chart-click="onChartClick"> + </canvas> + </div> diff --git a/www/plugins/graph/templates/account/view_stats.html b/www/plugins/graph/templates/account/view_stats.html new file mode 100644 index 000000000..7ded7f9e7 --- /dev/null +++ b/www/plugins/graph/templates/account/view_stats.html @@ -0,0 +1,39 @@ +<ion-view left-buttons="leftButtons" + cache-view="false"> + <ion-nav-title> + {{'GRAPH.ACCOUNT.TITLE' | translate}}{{id}} + </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-if="!loading"> + + <!-- - - - - Balance - - - - --> + <div class="item item-divider" translate> + GRAPH.ACCOUNT.BALANCE_DIVIDER + </div> + + <div class="item no-padding-xs" + ng-include="'plugins/graph/templates/account/graph_balance.html'" + ng-controller="GpAccountBalanceCtrl" + ng-init="setSize(350, 1000)"> + </div> + + <!-- - - - - Balance - - - - --> + <div class="item item-divider" translate> + GRAPH.ACCOUNT.WOT_DIVIDER + </div> + + <div class="item no-padding-xs" + ng-include="'plugins/graph/templates/account/graph_certifications.html'" + ng-controller="GpAccountCertificationCtrl" + ng-init="setSize(350, 1000)"> + </div> + + </ion-content> + +</ion-view> diff --git a/www/plugins/graph/templates/blockchain/graph_block_issuers.html b/www/plugins/graph/templates/blockchain/graph_block_issuers.html index 81ef7618a..c97178f57 100644 --- a/www/plugins/graph/templates/blockchain/graph_block_issuers.html +++ b/www/plugins/graph/templates/blockchain/graph_block_issuers.html @@ -9,7 +9,7 @@ chart-labels="labels" chart-colors="colors" chart-options="barOptions" - chart-click="showBlockIssuer"> + chart-click="onChartClick"> </canvas> </div> @@ -19,7 +19,7 @@ chart-data="data" chart-labels="labels" chart-colors="colors" - chart-click="showBlockIssuer"> + chart-click="onChartClick"> </canvas> <div class="gray padding-top text-center"> diff --git a/www/plugins/graph/templates/blockchain/graph_tx_count.html b/www/plugins/graph/templates/blockchain/graph_tx_count.html index 93ea3734b..def80eccc 100644 --- a/www/plugins/graph/templates/blockchain/graph_tx_count.html +++ b/www/plugins/graph/templates/blockchain/graph_tx_count.html @@ -4,7 +4,7 @@ style="top: 33px; margin-top:-33px; position: relative;"> <button class="button button-stable button-clear no-padding-xs pull-right" - ng-click="showTxActionsPopover($event)"> + ng-click="showActionsPopover($event)"> <i class="icon ion-navicon-round"></i> </button> </div> @@ -17,26 +17,8 @@ chart-colors="colors" chart-options="options" chart-labels="labels" - chart-click="showTxRange"> + chart-click="onChartClick"> </canvas> </div> - <div class="range range-positive no-padding-left no-padding-right"> - <a - class="button button-stable button-clear no-padding pull-left" - ng-click="loadPreviousTx()"> - <i class="icon ion-chevron-left"></i> - </a> - <input type="range" - ng-model="formData.timePct" - name="timePct" - min="0" max="100" - value="{{formData.timePct}}" - ng-change="onTxTimeChanged();" - ng-model-options="{ debounce: 250 }"> - <a - class="button button-stable button-clear no-padding pull-right" - ng-click="loadNextTx($event)"> - <i class="icon ion-chevron-right"></i> - </a> - </div> + <ng-include src="'plugins/graph/templates/common/graph_range_bar.html'"></ng-include> diff --git a/www/plugins/graph/templates/common/graph_range_bar.html b/www/plugins/graph/templates/common/graph_range_bar.html new file mode 100644 index 000000000..3a7c75a06 --- /dev/null +++ b/www/plugins/graph/templates/common/graph_range_bar.html @@ -0,0 +1,20 @@ + + <div class="range range-positive no-padding-left no-padding-right"> + <a + class="button button-stable button-clear no-padding pull-left" + ng-click="goPreviousRange($event)"> + <i class="icon ion-chevron-left"></i> + </a> + <input type="range" + ng-model="formData.timePct" + name="timePct" + min="0" max="100" + value="{{formData.timePct}}" + ng-change="onRangeChanged();" + ng-model-options="{ debounce: 250 }"> + <a + class="button button-stable button-clear no-padding pull-right" + ng-click="goNextRange($event)"> + <i class="icon ion-chevron-right"></i> + </a> + </div> diff --git a/www/plugins/graph/templates/blockchain/popover_tx_actions.html b/www/plugins/graph/templates/common/popover_range_actions.html similarity index 64% rename from www/plugins/graph/templates/blockchain/popover_tx_actions.html rename to www/plugins/graph/templates/common/popover_range_actions.html index 970a901d9..2211f9359 100644 --- a/www/plugins/graph/templates/blockchain/popover_tx_actions.html +++ b/www/plugins/graph/templates/common/popover_range_actions.html @@ -7,23 +7,23 @@ <!-- duration: hour --> <a class="item item-icon-right ink" - ng-click="setTxRangeDuration('hour')"> + ng-click="setRangeDuration('hour')"> <span ng-bind-html="'GRAPH.BLOCKCHAIN.TX_RANGE_DURATION.HOUR' | translate"></span> - <i class="icon ion-ios-checkmark-empty" ng-show="txOptions.rangeDuration=='hour'"></i> + <i class="icon ion-ios-checkmark-empty" ng-show="formData.rangeDuration=='hour'"></i> </a> <!-- duration: day --> <a class="item item-icon-right ink" - ng-click="setTxRangeDuration('day')"> + ng-click="setRangeDuration('day')"> <span ng-bind-html="'GRAPH.BLOCKCHAIN.TX_RANGE_DURATION.DAY' | translate"></span> - <i class="icon ion-ios-checkmark-empty" ng-show="txOptions.rangeDuration=='day'"></i> + <i class="icon ion-ios-checkmark-empty" ng-show="formData.rangeDuration=='day'"></i> </a> <!-- duration: month --> <a class="item item-icon-right ink" - ng-click="setTxRangeDuration('month')"> + ng-click="setRangeDuration('month')"> <span ng-bind-html="'GRAPH.BLOCKCHAIN.TX_RANGE_DURATION.MONTH' | translate"></span> - <i class="icon ion-ios-checkmark-empty" ng-show="txOptions.rangeDuration=='month'"></i> + <i class="icon ion-ios-checkmark-empty" ng-show="formData.rangeDuration=='month'"></i> </a> </div> diff --git a/www/plugins/graph/templates/currency/graph_members_count.html b/www/plugins/graph/templates/currency/graph_members_count.html index 2b26b0a4c..0b261fb47 100644 --- a/www/plugins/graph/templates/currency/graph_members_count.html +++ b/www/plugins/graph/templates/currency/graph_members_count.html @@ -6,5 +6,5 @@ chart-colors="colors" chart-options="options" chart-dataset-override="datasetOverride" - chart-click="showBlock"> + chart-click="onChartClick"> </canvas> diff --git a/www/plugins/graph/templates/currency/graph_monetary_mass.html b/www/plugins/graph/templates/currency/graph_monetary_mass.html index 2b34a02ac..894e82b84 100644 --- a/www/plugins/graph/templates/currency/graph_monetary_mass.html +++ b/www/plugins/graph/templates/currency/graph_monetary_mass.html @@ -17,5 +17,5 @@ chart-colors="colors" chart-dataset-override="datasetOverride" chart-options="options" - chart-click="showBlock"> + chart-click="onChartClick"> </canvas> diff --git a/www/templates/network/view_peer.html b/www/templates/network/view_peer.html index c8b1ec992..2f52296f5 100644 --- a/www/templates/network/view_peer.html +++ b/www/templates/network/view_peer.html @@ -3,9 +3,9 @@ <span translate>PEER.VIEW.TITLE</span> </ion-nav-title> - <ion-content class="has-header padding" scroll="true"> + <ion-content class="has-header" scroll="true"> - <div class="row"> + <div class="row no-padding"> <div class="col col-20 hidden-xs hidden-sm"> </div> -- GitLab