diff --git a/src/app/components/require.config.js b/src/app/components/require.config.js
index e987b4cbf7329..64d03a8ab28dc 100644
--- a/src/app/components/require.config.js
+++ b/src/app/components/require.config.js
@@ -41,6 +41,7 @@ require.config({
modernizr: '../vendor/modernizr-2.6.1',
+ numeral: '../vendor/numeral',
elasticjs: '../vendor/elasticjs/elastic-angular-client',
},
shim: {
diff --git a/src/app/panels/stats/editor.html b/src/app/panels/stats/editor.html
new file mode 100644
index 0000000000000..28a1739e862ad
--- /dev/null
+++ b/src/app/panels/stats/editor.html
@@ -0,0 +1,28 @@
+
+
Details
+
+
+
+
+
+
+
+
+
+
+
+
+
Formating
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/app/panels/stats/module.html b/src/app/panels/stats/module.html
new file mode 100644
index 0000000000000..0e26c199c6a5f
--- /dev/null
+++ b/src/app/panels/stats/module.html
@@ -0,0 +1,11 @@
+
+
{{data.value}} {{panel.unit}}
+
+
+
+ {{item.label}} |
+ {{item.value}} {{panel.unit}} |
+
+
+
+
diff --git a/src/app/panels/stats/module.js b/src/app/panels/stats/module.js
new file mode 100644
index 0000000000000..8bad6ca693d04
--- /dev/null
+++ b/src/app/panels/stats/module.js
@@ -0,0 +1,179 @@
+/*
+
+ ## Stats Module
+
+ ### Parameters
+ * format :: The format of the value returned. (Default: number)
+ * style :: The font size of the main number to be displayed.
+ * mode :: The aggergate value to use for display
+ * spyable :: Dislay the 'eye' icon that show the last elasticsearch query
+
+*/
+define([
+ 'angular',
+ 'app',
+ 'underscore',
+ 'jquery',
+ 'kbn',
+ 'numeral'
+], function (
+ angular,
+ app,
+ _,
+ $,
+ kbn,
+ numeral
+) {
+
+ 'use strict';
+
+ var module = angular.module('kibana.panels.stats', []);
+ app.useModule(module);
+
+ module.controller('stats', function ($scope, querySrv, dashboard, filterSrv) {
+
+ $scope.panelMeta = {
+ modals : [
+ {
+ description: "Inspect",
+ icon: "icon-info-sign",
+ partial: "app/partials/inspector.html",
+ show: $scope.panel.spyable
+ }
+ ],
+ editorTabs : [
+ {title:'Queries', src:'app/partials/querySelect.html'}
+ ],
+ status: 'Beta',
+ description: 'A statatics panel for displaying aggergations using the Elastic Search statistical facet query.'
+ };
+
+
+ var defaults = {
+ queries : {
+ mode : 'all',
+ ids : []
+ },
+ style : { "font-size": '24pt'},
+ format: 'number',
+ mode: 'count',
+ display_breakdown: 'yes',
+ spyable : true
+ };
+
+ _.defaults($scope.panel, defaults);
+
+ $scope.init = function () {
+ $scope.ready = false;
+ $scope.$on('refresh', function () {
+ $scope.get_data();
+ });
+ $scope.get_data();
+ };
+
+ $scope.get_data = function () {
+ if(dashboard.indices.length === 0) {
+ return;
+ }
+
+ $scope.panelMeta.loading = true;
+
+ var request,
+ results,
+ boolQuery,
+ queries;
+
+ request = $scope.ejs.Request().indices(dashboard.indices);
+
+ $scope.panel.queries.ids = querySrv.idsByMode($scope.panel.queries);
+ queries = querySrv.getQueryObjs($scope.panel.queries.ids);
+
+
+ // This could probably be changed to a BoolFilter
+ boolQuery = $scope.ejs.BoolQuery();
+ _.each(queries,function(q) {
+ boolQuery = boolQuery.should(querySrv.toEjsObj(q));
+ });
+
+ request = request
+ .facet($scope.ejs.StatisticalFacet('stats')
+ .field($scope.panel.field)
+ .facetFilter($scope.ejs.QueryFilter(
+ $scope.ejs.FilteredQuery(
+ boolQuery,
+ filterSrv.getBoolFilter(filterSrv.ids)
+ )))).size(0);
+
+ _.each(queries, function (q) {
+ var alias = q.alias || q.query;
+ var query = $scope.ejs.BoolQuery();
+ query.should(querySrv.toEjsObj(q));
+ request.facet($scope.ejs.StatisticalFacet('stats_'+alias)
+ .field($scope.panel.field)
+ .facetFilter($scope.ejs.QueryFilter(
+ $scope.ejs.FilteredQuery(
+ query,
+ filterSrv.getBoolFilter(filterSrv.ids)
+ )
+ ))
+ );
+ });
+
+ // Populate the inspector panel
+ $scope.inspector = angular.toJson(JSON.parse(request.toString()),true);
+
+ results = request.doSearch();
+
+ var format = function (format, value) {
+ switch (format) {
+ case 'money':
+ value = numeral(value).format('$0,0.00');
+ break;
+ case 'bytes':
+ value = numeral(value).format('0.00b');
+ break;
+ case 'float':
+ value = numeral(value).format('0.000');
+ break;
+ default:
+ value = numeral(value).format('0,0');
+ }
+ return value;
+ };
+
+ results.then(function(results) {
+ $scope.panelMeta.loading = false;
+ var value = results.facets.stats[$scope.panel.mode];
+
+ var rows = queries.map(function (q) {
+ var alias = q.alias || q.query;
+ var obj = _.clone(q);
+ obj.label = alias;
+ obj.value = format($scope.panel.format,results.facets['stats_'+alias][$scope.panel.mode]);
+ return obj;
+ });
+
+ $scope.data = {
+ value: format($scope.panel.format, value),
+ rows: rows
+ };
+
+ $scope.$emit('render');
+ });
+ };
+
+ $scope.set_refresh = function (state) {
+ $scope.refresh = state;
+ };
+
+ $scope.close_edit = function() {
+ if($scope.refresh) {
+ $scope.get_data();
+ }
+ $scope.refresh = false;
+ $scope.$emit('render');
+ };
+
+ });
+
+});
diff --git a/src/config.js b/src/config.js
index 80c0c8129a728..6b14e2ff1c49b 100644
--- a/src/config.js
+++ b/src/config.js
@@ -49,6 +49,7 @@ function (Settings) {
'bettermap',
'query',
'terms',
+ 'stats',
'sparklines'
]
});
diff --git a/src/vendor/numeral.js b/src/vendor/numeral.js
new file mode 100644
index 0000000000000..20b514018b067
--- /dev/null
+++ b/src/vendor/numeral.js
@@ -0,0 +1,565 @@
+/*!
+ * numeral.js
+ * version : 1.5.2
+ * author : Adam Draper
+ * license : MIT
+ * http://adamwdraper.github.com/Numeral-js/
+ */
+
+(function () {
+
+ /************************************
+ Constants
+ ************************************/
+
+ var numeral,
+ VERSION = '1.5.2',
+ // internal storage for language config files
+ languages = {},
+ currentLanguage = 'en',
+ zeroFormat = null,
+ defaultFormat = '0,0',
+ // check for nodeJS
+ hasModule = (typeof module !== 'undefined' && module.exports);
+
+
+ /************************************
+ Constructors
+ ************************************/
+
+
+ // Numeral prototype object
+ function Numeral (number) {
+ this._value = number;
+ }
+
+ /**
+ * Implementation of toFixed() that treats floats more like decimals
+ *
+ * Fixes binary rounding issues (eg. (0.615).toFixed(2) === '0.61') that present
+ * problems for accounting- and finance-related software.
+ */
+ function toFixed (value, precision, roundingFunction, optionals) {
+ var power = Math.pow(10, precision),
+ optionalsRegExp,
+ output;
+
+ //roundingFunction = (roundingFunction !== undefined ? roundingFunction : Math.round);
+ // Multiply up by precision, round accurately, then divide and use native toFixed():
+ output = (roundingFunction(value * power) / power).toFixed(precision);
+
+ if (optionals) {
+ optionalsRegExp = new RegExp('0{1,' + optionals + '}$');
+ output = output.replace(optionalsRegExp, '');
+ }
+
+ return output;
+ }
+
+ /************************************
+ Formatting
+ ************************************/
+
+ // determine what type of formatting we need to do
+ function formatNumeral (n, format, roundingFunction) {
+ var output;
+
+ // figure out what kind of format we are dealing with
+ if (format.indexOf('$') > -1) { // currency!!!!!
+ output = formatCurrency(n, format, roundingFunction);
+ } else if (format.indexOf('%') > -1) { // percentage
+ output = formatPercentage(n, format, roundingFunction);
+ } else if (format.indexOf(':') > -1) { // time
+ output = formatTime(n, format);
+ } else { // plain ol' numbers or bytes
+ output = formatNumber(n._value, format, roundingFunction);
+ }
+
+ // return string
+ return output;
+ }
+
+ // revert to number
+ function unformatNumeral (n, string) {
+ var stringOriginal = string,
+ thousandRegExp,
+ millionRegExp,
+ billionRegExp,
+ trillionRegExp,
+ suffixes = ['KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'],
+ bytesMultiplier = false,
+ power;
+
+ if (string.indexOf(':') > -1) {
+ n._value = unformatTime(string);
+ } else {
+ if (string === zeroFormat) {
+ n._value = 0;
+ } else {
+ if (languages[currentLanguage].delimiters.decimal !== '.') {
+ string = string.replace(/\./g,'').replace(languages[currentLanguage].delimiters.decimal, '.');
+ }
+
+ // see if abbreviations are there so that we can multiply to the correct number
+ thousandRegExp = new RegExp('[^a-zA-Z]' + languages[currentLanguage].abbreviations.thousand + '(?:\\)|(\\' + languages[currentLanguage].currency.symbol + ')?(?:\\))?)?$');
+ millionRegExp = new RegExp('[^a-zA-Z]' + languages[currentLanguage].abbreviations.million + '(?:\\)|(\\' + languages[currentLanguage].currency.symbol + ')?(?:\\))?)?$');
+ billionRegExp = new RegExp('[^a-zA-Z]' + languages[currentLanguage].abbreviations.billion + '(?:\\)|(\\' + languages[currentLanguage].currency.symbol + ')?(?:\\))?)?$');
+ trillionRegExp = new RegExp('[^a-zA-Z]' + languages[currentLanguage].abbreviations.trillion + '(?:\\)|(\\' + languages[currentLanguage].currency.symbol + ')?(?:\\))?)?$');
+
+ // see if bytes are there so that we can multiply to the correct number
+ for (power = 0; power <= suffixes.length; power++) {
+ bytesMultiplier = (string.indexOf(suffixes[power]) > -1) ? Math.pow(1024, power + 1) : false;
+
+ if (bytesMultiplier) {
+ break;
+ }
+ }
+
+ // do some math to create our number
+ n._value = ((bytesMultiplier) ? bytesMultiplier : 1) * ((stringOriginal.match(thousandRegExp)) ? Math.pow(10, 3) : 1) * ((stringOriginal.match(millionRegExp)) ? Math.pow(10, 6) : 1) * ((stringOriginal.match(billionRegExp)) ? Math.pow(10, 9) : 1) * ((stringOriginal.match(trillionRegExp)) ? Math.pow(10, 12) : 1) * ((string.indexOf('%') > -1) ? 0.01 : 1) * (((string.split('-').length + Math.min(string.split('(').length-1, string.split(')').length-1)) % 2)? 1: -1) * Number(string.replace(/[^0-9\.]+/g, ''));
+
+ // round if we are talking about bytes
+ n._value = (bytesMultiplier) ? Math.ceil(n._value) : n._value;
+ }
+ }
+ return n._value;
+ }
+
+ function formatCurrency (n, format, roundingFunction) {
+ var prependSymbol = format.indexOf('$') <= 1 ? true : false,
+ space = '',
+ output;
+
+ // check for space before or after currency
+ if (format.indexOf(' $') > -1) {
+ space = ' ';
+ format = format.replace(' $', '');
+ } else if (format.indexOf('$ ') > -1) {
+ space = ' ';
+ format = format.replace('$ ', '');
+ } else {
+ format = format.replace('$', '');
+ }
+
+ // format the number
+ output = formatNumber(n._value, format, roundingFunction);
+
+ // position the symbol
+ if (prependSymbol) {
+ if (output.indexOf('(') > -1 || output.indexOf('-') > -1) {
+ output = output.split('');
+ output.splice(1, 0, languages[currentLanguage].currency.symbol + space);
+ output = output.join('');
+ } else {
+ output = languages[currentLanguage].currency.symbol + space + output;
+ }
+ } else {
+ if (output.indexOf(')') > -1) {
+ output = output.split('');
+ output.splice(-1, 0, space + languages[currentLanguage].currency.symbol);
+ output = output.join('');
+ } else {
+ output = output + space + languages[currentLanguage].currency.symbol;
+ }
+ }
+
+ return output;
+ }
+
+ function formatPercentage (n, format, roundingFunction) {
+ var space = '',
+ output,
+ value = n._value * 100;
+
+ // check for space before %
+ if (format.indexOf(' %') > -1) {
+ space = ' ';
+ format = format.replace(' %', '');
+ } else {
+ format = format.replace('%', '');
+ }
+
+ output = formatNumber(value, format, roundingFunction);
+
+ if (output.indexOf(')') > -1 ) {
+ output = output.split('');
+ output.splice(-1, 0, space + '%');
+ output = output.join('');
+ } else {
+ output = output + space + '%';
+ }
+
+ return output;
+ }
+
+ function formatTime (n) {
+ var hours = Math.floor(n._value/60/60),
+ minutes = Math.floor((n._value - (hours * 60 * 60))/60),
+ seconds = Math.round(n._value - (hours * 60 * 60) - (minutes * 60));
+ return hours + ':' + ((minutes < 10) ? '0' + minutes : minutes) + ':' + ((seconds < 10) ? '0' + seconds : seconds);
+ }
+
+ function unformatTime (string) {
+ var timeArray = string.split(':'),
+ seconds = 0;
+ // turn hours and minutes into seconds and add them all up
+ if (timeArray.length === 3) {
+ // hours
+ seconds = seconds + (Number(timeArray[0]) * 60 * 60);
+ // minutes
+ seconds = seconds + (Number(timeArray[1]) * 60);
+ // seconds
+ seconds = seconds + Number(timeArray[2]);
+ } else if (timeArray.length === 2) {
+ // minutes
+ seconds = seconds + (Number(timeArray[0]) * 60);
+ // seconds
+ seconds = seconds + Number(timeArray[1]);
+ }
+ return Number(seconds);
+ }
+
+ function formatNumber (value, format, roundingFunction) {
+ var negP = false,
+ signed = false,
+ optDec = false,
+ abbr = '',
+ bytes = '',
+ ord = '',
+ abs = Math.abs(value),
+ suffixes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'],
+ min,
+ max,
+ power,
+ w,
+ precision,
+ thousands,
+ d = '',
+ neg = false;
+
+ // check if number is zero and a custom zero format has been set
+ if (value === 0 && zeroFormat !== null) {
+ return zeroFormat;
+ } else {
+ // see if we should use parentheses for negative number or if we should prefix with a sign
+ // if both are present we default to parentheses
+ if (format.indexOf('(') > -1) {
+ negP = true;
+ format = format.slice(1, -1);
+ } else if (format.indexOf('+') > -1) {
+ signed = true;
+ format = format.replace(/\+/g, '');
+ }
+
+ // see if abbreviation is wanted
+ if (format.indexOf('a') > -1) {
+ // check for space before abbreviation
+ if (format.indexOf(' a') > -1) {
+ abbr = ' ';
+ format = format.replace(' a', '');
+ } else {
+ format = format.replace('a', '');
+ }
+
+ if (abs >= Math.pow(10, 12)) {
+ // trillion
+ abbr = abbr + languages[currentLanguage].abbreviations.trillion;
+ value = value / Math.pow(10, 12);
+ } else if (abs < Math.pow(10, 12) && abs >= Math.pow(10, 9)) {
+ // billion
+ abbr = abbr + languages[currentLanguage].abbreviations.billion;
+ value = value / Math.pow(10, 9);
+ } else if (abs < Math.pow(10, 9) && abs >= Math.pow(10, 6)) {
+ // million
+ abbr = abbr + languages[currentLanguage].abbreviations.million;
+ value = value / Math.pow(10, 6);
+ } else if (abs < Math.pow(10, 6) && abs >= Math.pow(10, 3)) {
+ // thousand
+ abbr = abbr + languages[currentLanguage].abbreviations.thousand;
+ value = value / Math.pow(10, 3);
+ }
+ }
+
+ // see if we are formatting bytes
+ if (format.indexOf('b') > -1) {
+ // check for space before
+ if (format.indexOf(' b') > -1) {
+ bytes = ' ';
+ format = format.replace(' b', '');
+ } else {
+ format = format.replace('b', '');
+ }
+
+ for (power = 0; power <= suffixes.length; power++) {
+ min = Math.pow(1024, power);
+ max = Math.pow(1024, power+1);
+
+ if (value >= min && value < max) {
+ bytes = bytes + suffixes[power];
+ if (min > 0) {
+ value = value / min;
+ }
+ break;
+ }
+ }
+ }
+
+ // see if ordinal is wanted
+ if (format.indexOf('o') > -1) {
+ // check for space before
+ if (format.indexOf(' o') > -1) {
+ ord = ' ';
+ format = format.replace(' o', '');
+ } else {
+ format = format.replace('o', '');
+ }
+
+ ord = ord + languages[currentLanguage].ordinal(value);
+ }
+
+ if (format.indexOf('[.]') > -1) {
+ optDec = true;
+ format = format.replace('[.]', '.');
+ }
+
+ w = value.toString().split('.')[0];
+ precision = format.split('.')[1];
+ thousands = format.indexOf(',');
+
+ if (precision) {
+ if (precision.indexOf('[') > -1) {
+ precision = precision.replace(']', '');
+ precision = precision.split('[');
+ d = toFixed(value, (precision[0].length + precision[1].length), roundingFunction, precision[1].length);
+ } else {
+ d = toFixed(value, precision.length, roundingFunction);
+ }
+
+ w = d.split('.')[0];
+
+ if (d.split('.')[1].length) {
+ d = languages[currentLanguage].delimiters.decimal + d.split('.')[1];
+ } else {
+ d = '';
+ }
+
+ if (optDec && Number(d.slice(1)) === 0) {
+ d = '';
+ }
+ } else {
+ w = toFixed(value, null, roundingFunction);
+ }
+
+ // format number
+ if (w.indexOf('-') > -1) {
+ w = w.slice(1);
+ neg = true;
+ }
+
+ if (thousands > -1) {
+ w = w.toString().replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1' + languages[currentLanguage].delimiters.thousands);
+ }
+
+ if (format.indexOf('.') === 0) {
+ w = '';
+ }
+
+ return ((negP && neg) ? '(' : '') + ((!negP && neg) ? '-' : '') + ((!neg && signed) ? '+' : '') + w + d + ((ord) ? ord : '') + ((abbr) ? abbr : '') + ((bytes) ? bytes : '') + ((negP && neg) ? ')' : '');
+ }
+ }
+
+ /************************************
+ Top Level Functions
+ ************************************/
+
+ numeral = function (input) {
+ if (numeral.isNumeral(input)) {
+ input = input.value();
+ } else if (input === 0 || typeof input === 'undefined') {
+ input = 0;
+ } else if (!Number(input)) {
+ input = numeral.fn.unformat(input);
+ }
+
+ return new Numeral(Number(input));
+ };
+
+ // version number
+ numeral.version = VERSION;
+
+ // compare numeral object
+ numeral.isNumeral = function (obj) {
+ return obj instanceof Numeral;
+ };
+
+ // This function will load languages and then set the global language. If
+ // no arguments are passed in, it will simply return the current global
+ // language key.
+ numeral.language = function (key, values) {
+ if (!key) {
+ return currentLanguage;
+ }
+
+ if (key && !values) {
+ if(!languages[key]) {
+ throw new Error('Unknown language : ' + key);
+ }
+ currentLanguage = key;
+ }
+
+ if (values || !languages[key]) {
+ loadLanguage(key, values);
+ }
+
+ return numeral;
+ };
+
+ // This function provides access to the loaded language data. If
+ // no arguments are passed in, it will simply return the current
+ // global language object.
+ numeral.languageData = function (key) {
+ if (!key) {
+ return languages[currentLanguage];
+ }
+
+ if (!languages[key]) {
+ throw new Error('Unknown language : ' + key);
+ }
+
+ return languages[key];
+ };
+
+ numeral.language('en', {
+ delimiters: {
+ thousands: ',',
+ decimal: '.'
+ },
+ abbreviations: {
+ thousand: 'k',
+ million: 'm',
+ billion: 'b',
+ trillion: 't'
+ },
+ ordinal: function (number) {
+ var b = number % 10;
+ return (~~ (number % 100 / 10) === 1) ? 'th' :
+ (b === 1) ? 'st' :
+ (b === 2) ? 'nd' :
+ (b === 3) ? 'rd' : 'th';
+ },
+ currency: {
+ symbol: '$'
+ }
+ });
+
+ numeral.zeroFormat = function (format) {
+ zeroFormat = typeof(format) === 'string' ? format : null;
+ };
+
+ numeral.defaultFormat = function (format) {
+ defaultFormat = typeof(format) === 'string' ? format : '0.0';
+ };
+
+ /************************************
+ Helpers
+ ************************************/
+
+ function loadLanguage(key, values) {
+ languages[key] = values;
+ }
+
+
+ /************************************
+ Numeral Prototype
+ ************************************/
+
+
+ numeral.fn = Numeral.prototype = {
+
+ clone : function () {
+ return numeral(this);
+ },
+
+ format : function (inputString, roundingFunction) {
+ return formatNumeral(this,
+ inputString ? inputString : defaultFormat,
+ (roundingFunction !== undefined) ? roundingFunction : Math.round
+ );
+ },
+
+ unformat : function (inputString) {
+ if (Object.prototype.toString.call(inputString) === '[object Number]') {
+ return inputString;
+ }
+ return unformatNumeral(this, inputString ? inputString : defaultFormat);
+ },
+
+ value : function () {
+ return this._value;
+ },
+
+ valueOf : function () {
+ return this._value;
+ },
+
+ set : function (value) {
+ this._value = Number(value);
+ return this;
+ },
+
+ add : function (value) {
+ this._value = this._value + Number(value);
+ return this;
+ },
+
+ subtract : function (value) {
+ this._value = this._value - Number(value);
+ return this;
+ },
+
+ multiply : function (value) {
+ this._value = this._value * Number(value);
+ return this;
+ },
+
+ divide : function (value) {
+ this._value = this._value / Number(value);
+ return this;
+ },
+
+ difference : function (value) {
+ var difference = this._value - Number(value);
+
+ if (difference < 0) {
+ difference = -difference;
+ }
+
+ return difference;
+ }
+
+ };
+
+ /************************************
+ Exposing Numeral
+ ************************************/
+
+ // CommonJS module is defined
+ if (hasModule) {
+ module.exports = numeral;
+ }
+
+ /*global ender:false */
+ if (typeof ender === 'undefined') {
+ // here, `this` means `window` in the browser, or `global` on the server
+ // add `numeral` as a global object via a string identifier,
+ // for Closure Compiler 'advanced' mode
+ this['numeral'] = numeral;
+ }
+
+ /*global define:false */
+ if (typeof define === 'function' && define.amd) {
+ define([], function () {
+ return numeral;
+ });
+ }
+}).call(this);