diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 000000000..4ce83cf3d
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,30 @@
+sudo: required
+dist: trusty
+addons:
+ apt:
+ sources:
+ - google-chrome
+ packages:
+ - google-chrome-stable
+language: node_js
+node_js:
+ - "6.10.3"
+
+before_install:
+ # setting the path for phantom.js 2.0.0
+ - export PATH=/usr/local/phantomjs-2.0.0/bin:$PATH
+ # starting a GUI to run tests, per https://docs.travis-ci.com/user/gui-and-headless-browsers/#Using-xvfb-to-Run-Tests-That-Require-a-GUI
+ - export DISPLAY=:99.0
+ - sh -e /etc/init.d/xvfb start
+ - "npm config set spin false"
+ - "npm install -g npm@^2"
+install:
+ - mkdir travis-phantomjs
+ - wget https://s3.amazonaws.com/travis-phantomjs/phantomjs-2.0.0-ubuntu-12.04.tar.bz2 -O $PWD/travis-phantomjs/phantomjs-2.0.0-ubuntu-12.04.tar.bz2
+ - tar -xvf $PWD/travis-phantomjs/phantomjs-2.0.0-ubuntu-12.04.tar.bz2 -C $PWD/travis-phantomjs
+ - export PATH=$PWD/travis-phantomjs:$PATH
+ - npm install -g bower
+ - npm install
+ - bower install
+script:
+ - npm run test-single-run
diff --git a/README.md b/README.md
index 06b61a0bc..48840fe59 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,7 @@
# Banana
+![](https://travis-ci.org/aadel/banana.svg?branch=develop)
+
The Banana project was forked from [Kibana](https://github.com/elastic/kibana), and works with all kinds of time series
(and non-time series) data stored in [Apache Solr](https://lucene.apache.org/solr/). It uses Kibana's powerful dashboard
configuration capabilities, ports key panels to work with Solr, and provides significant additional capabilities,
@@ -17,22 +19,19 @@ Pull the repo from the `release` branch for production deployment; version x.y.z
`fusion` branch is used for Lucidworks Fusion release. The code base and features are the same as `develop`. The main difference
is in the configuration.
-## Banana 1.6.26
+## Banana 1.7.0
This release includes the following bug fixes and improvement:
-1. Enhance heatmap
- * Add axis and axis labels
- * Add axis grid and ticks
- * Add gradient legend and ranges
- * Fix heatmap transpose icon
- * Enhance positioning and padding of panel elements
- * Fix bettermap tooltip and hint text
-1. Enhance hits panel
- * Add panel horizontal and vertical direction option
- * Fix metrics text and label overlap and margins
-1. Fix bettermap render issue when resized
-1. Fix jshint warnings
+1. Added new panels:
+ * Significant Terms panel
+ * Time Series panel
+ * Graph panel
+1. Adding multi-query operator with a default OR operator
+1. Added Travis CI integration
+1. Using collections API instead of cores API to populate collections list
+1. Allow users to change URL setting of banana-int collection
+1. Fixed bug when loading saved dashboards from a remote Solr server
## Older Release Notes
diff --git a/bower.json b/bower.json
index 24ee4686b..78dccae24 100644
--- a/bower.json
+++ b/bower.json
@@ -1,5 +1,5 @@
{
- "name": "Banana",
+ "name": "banana",
"description": "Banana for Solr Data Visualization",
"homepage": "https://github.com/LucidWorks/banana/wiki",
"license": "Apache License",
@@ -17,7 +17,9 @@
"requirejs": "2.1.8",
"modernizr": "2.6.1",
"moment": "2.1.0",
- "underscore": "1.5.1"
+ "underscore": "1.5.1",
+ "vis": "4.21.0",
+ "x2js": "abdolence/x2js#1.2.0"
},
"resolutions": {
"angular": "1.0.8",
diff --git a/package.json b/package.json
index ec74571f0..01fa9ae9f 100644
--- a/package.json
+++ b/package.json
@@ -1,5 +1,5 @@
{
- "version": "1.6.26",
+ "version": "1.7.0",
"name": "banana-fusion",
"description": "Banana for Solr - A Port of Kibana",
"repository": "https://github.com/LucidWorks/banana",
@@ -10,19 +10,21 @@
"company": "Lucidworks, Inc."
},
"devDependencies": {
+ "babel-cli": "^6.26.0",
+ "babel-preset-env": "^1.6.1",
"bower": "^1.3.1",
"grunt": "^0.4.5",
"grunt-angular-templates": "~0.3.12",
"grunt-contrib": "^0.10.2",
"grunt-contrib-clean": "~0.5.0",
- "grunt-contrib-compress": "~0.5.2",
- "grunt-contrib-copy": "~0.4.1",
- "grunt-contrib-cssmin": "~0.6.1",
- "grunt-contrib-htmlmin": "~0.1.3",
- "grunt-contrib-jshint": "^0.10.0",
- "grunt-contrib-less": "~0.7.0",
+ "grunt-contrib-compress": "~0.7.0",
+ "grunt-contrib-copy": "~0.5.0",
+ "grunt-contrib-cssmin": "~0.9.0",
+ "grunt-contrib-htmlmin": "~0.2.0",
+ "grunt-contrib-jshint": "^0.9.0",
+ "grunt-contrib-less": "~0.11.0",
"grunt-contrib-requirejs": "~0.4.1",
- "grunt-contrib-uglify": "^0.11.0",
+ "grunt-contrib-uglify": "^0.4.0",
"grunt-git-describe": "~2.3.2",
"grunt-ng-annotate": "0.3.0",
"grunt-ngmin": "0.0.3",
diff --git a/src/app/components/require.config.js b/src/app/components/require.config.js
index 9c4f8ed8b..b3b1a793d 100755
--- a/src/app/components/require.config.js
+++ b/src/app/components/require.config.js
@@ -44,6 +44,8 @@ require.config({
elasticjs: '../vendor/elasticjs/elastic-angular-client',
solrjs: '../vendor/solrjs/solr-angular-client',
d3: '../vendor/d3',
+ vis: '../bower_components/vis/dist/vis',
+ x2js: '../bower_components/abdmob/x2js/xml2json',
'd3-sankey': '../vendor/d3-sankey',
'd3-array': '../vendor/d3-array',
'd3-collection': '../vendor/d3-collection',
diff --git a/src/app/components/settings.js b/src/app/components/settings.js
index aec605f57..9ad1f3a89 100755
--- a/src/app/components/settings.js
+++ b/src/app/components/settings.js
@@ -17,7 +17,7 @@ function (_) {
USE_ADMIN_CORES: true,
panel_names: [],
banana_index: "system_banana",
- // uncomment the following line to specify the URL of banana-int
+ // Uncomment the following line to specify the URL of Solr server that will be used to store and load saved dashboards.
// banana_server: "http://localhost:8983/solr/",
// Lucidworks Fusion settings
diff --git a/src/app/panels/bar/module.js b/src/app/panels/bar/module.js
index feaae148f..d9869a2ad 100755
--- a/src/app/panels/bar/module.js
+++ b/src/app/panels/bar/module.js
@@ -87,7 +87,7 @@ define([
var facet = '&facet=true&facet.field=' + $scope.panel.field + '&facet.limit=' + $scope.panel.size;
// Set the panel's query
- $scope.panel.queries.query = querySrv.getORquery() + wt_json + rows_limit + fq + facet;
+ $scope.panel.queries.query = querySrv.getOPQuery() + wt_json + rows_limit + fq + facet;
// Set the additional custom query
if ($scope.panel.queries.custom != null) {
diff --git a/src/app/panels/bettermap/module.js b/src/app/panels/bettermap/module.js
index b61873740..6cec171d9 100755
--- a/src/app/panels/bettermap/module.js
+++ b/src/app/panels/bettermap/module.js
@@ -168,7 +168,7 @@ function (angular, app, _, L, localRequire) {
}
// Set the panel's query
- $scope.panel.queries.query = querySrv.getORquery() + wt_json + rows_limit + fq + sorting;
+ $scope.panel.queries.query = querySrv.getOPQuery() + wt_json + rows_limit + fq + sorting;
// Set the additional custom query
if ($scope.panel.queries.custom != null) {
diff --git a/src/app/panels/facet/module.js b/src/app/panels/facet/module.js
index 3d30ff0ef..9b93ea499 100755
--- a/src/app/panels/facet/module.js
+++ b/src/app/panels/facet/module.js
@@ -149,7 +149,7 @@ define([
}
// Set the panel's query
- $scope.panel.queries.basic_query = querySrv.getORquery() + fq + facet + facet_fields;
+ $scope.panel.queries.basic_query = querySrv.getOPQuery() + fq + facet + facet_fields;
$scope.panel.queries.query = $scope.panel.queries.basic_query + wt_json;
// Set the additional custom query
diff --git a/src/app/panels/force/module.js b/src/app/panels/force/module.js
index bbb4b2e2f..831951771 100755
--- a/src/app/panels/force/module.js
+++ b/src/app/panels/force/module.js
@@ -171,7 +171,7 @@ define([
}).join("&");
// f.effective_date_fiscal_facet.facet.limit=3&f.institution_facet.facet.limit=10';
- $scope.panel.queries.query = querySrv.getORquery() + fq + wt_json + facet + facet_pivot + facet_limits + rows;
+ $scope.panel.queries.query = querySrv.getOPQuery() + fq + wt_json + facet + facet_pivot + facet_limits + rows;
if (DEBUG) {
console.log($scope.panel.queries.query);
}
diff --git a/src/app/panels/fullTextSearch/module.js b/src/app/panels/fullTextSearch/module.js
index c9b3ccf34..82f9cb216 100755
--- a/src/app/panels/fullTextSearch/module.js
+++ b/src/app/panels/fullTextSearch/module.js
@@ -284,7 +284,7 @@ define([
// Set the panel's query
//var query = $scope.panel.searchQuery == null ? querySrv.getQuery(0) : 'q=' + $scope.panel.searchQuery
- $scope.panel.queries.basic_query = querySrv.getORquery() + fq + facet + facet_fields + sorting;
+ $scope.panel.queries.basic_query = querySrv.getOPQuery() + fq + facet + facet_fields + sorting;
$scope.panel.queries.query = $scope.panel.queries.basic_query + wt_json + rows_limit + highlight;
// Set the additional custom query
diff --git a/src/app/panels/graph/editor.html b/src/app/panels/graph/editor.html
new file mode 100755
index 000000000..c95ceb9bc
--- /dev/null
+++ b/src/app/panels/graph/editor.html
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/app/panels/graph/module.html b/src/app/panels/graph/module.html
new file mode 100755
index 000000000..03dcc99b5
--- /dev/null
+++ b/src/app/panels/graph/module.html
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/src/app/panels/significantTerms/module.html b/src/app/panels/significantTerms/module.html
new file mode 100755
index 000000000..e8243cfea
--- /dev/null
+++ b/src/app/panels/significantTerms/module.html
@@ -0,0 +1,70 @@
+
+
+
+
+
+
+
+
Term
+
Score
+
Foreground
+
Background
+
Action
+
+
+
+
{{term.label}}
+
{{term.data[0][1]}}
+
{{term.data[0][2]}}
+
{{term.data[0][3]}}
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/app/panels/significantTerms/module.js b/src/app/panels/significantTerms/module.js
new file mode 100755
index 000000000..1170a4914
--- /dev/null
+++ b/src/app/panels/significantTerms/module.js
@@ -0,0 +1,314 @@
+/*
+ ## Significant Terms
+
+ ### Parameters
+ * size :: top N
+*/
+define([
+ 'angular',
+ 'app',
+ 'underscore',
+ 'jquery',
+ 'kbn'
+],
+function (angular, app, _, $, kbn) {
+ 'use strict';
+
+ var module = angular.module('kibana.panels.significantTerms', []);
+ app.useModule(module);
+
+ module.controller('significantTerms', function($scope, $timeout, timer, querySrv, dashboard, filterSrv) {
+ $scope.panelMeta = {
+ modals : [
+ {
+ description: "Inspect",
+ icon: "icon-info-sign",
+ partial: "app/partials/inspector.html",
+ show: $scope.panel.spyable
+ }
+ ],
+ exportfile: false,
+ editorTabs : [
+ {title:'Queries', src:'app/partials/querySelect.html'}
+ ],
+ status : "Stable",
+ description : "Displays the results of significant terms as a table."
+ };
+
+ // Set and populate defaults
+ var _d = {
+ queries : {
+ mode : 'all',
+ ids : [],
+ query : '*:*',
+ custom : ''
+ },
+ mode : 'count', // mode to tell which number will be used to plot the chart.
+ field : '',
+ stats_field : '',
+ decimal_points : 0, // The number of digits after the decimal point
+ exclude : [],
+ missing : false,
+ other : false,
+ size : 10,
+ sortBy : 'count',
+ order : 'descending',
+ style : { "font-size": '10pt'},
+ donut : false,
+ tilt : false,
+ labels : true,
+ logAxis : false,
+ arrangement : 'horizontal',
+ chart : 'bar',
+ counter_pos : 'above',
+ exportSize : 10000,
+ lastColor : '',
+ spyable : true,
+ show_queries:true,
+ error : '',
+ chartColors : querySrv.colors,
+ refresh: {
+ enable: false,
+ interval: 2
+ }
+ };
+ _.defaults($scope.panel,_d);
+
+ $scope.init = function () {
+ $scope.hits = 0;
+ $scope.panel.mindoc_freq = 5;
+ $scope.panel.maxdoc_freq = 0.3;
+ $scope.panel.minterm_len = 4;
+
+ // Start refresh timer if enabled
+ if ($scope.panel.refresh.enable) {
+ $scope.set_timer($scope.panel.refresh.interval);
+ }
+
+ $scope.$on('refresh',function(){
+ $scope.get_data();
+ });
+
+ $scope.get_data();
+ };
+
+ $scope.testMultivalued = function() {
+ if($scope.panel.field && $scope.fields.typeList[$scope.panel.field] && $scope.fields.typeList[$scope.panel.field].schema.indexOf("M") > -1) {
+ $scope.panel.error = "Can't proceed with Multivalued field";
+ return;
+ }
+
+ if($scope.panel.stats_field && $scope.fields.typeList[$scope.panel.stats_field].schema.indexOf("M") > -1) {
+ $scope.panel.error = "Can't proceed with Multivalued field";
+ return;
+ }
+ };
+
+ $scope.build_expression = function() {
+
+ var fq = '';
+ if (filterSrv.getSolrFq()) {
+ fq = ',' + filterSrv.getSolrFq(false, ',');
+ }
+
+ var expression = 'expr=significantTerms(' + dashboard.current.solr.core_name + ','
+ + querySrv.getOPQuery() + fq + ',field=' + $scope.panel.field
+ + ',limit=' + $scope.panel.size + ',minDocFreq=' + $scope.panel.mindoc_freq
+ + ',maxDocFreq=' + $scope.panel.maxdoc_freq + ',minTermLength='
+ + $scope.panel.minterm_len + ')';
+
+ return expression;
+ };
+
+ $scope.set_timer = function(refresh_interval) {
+ $scope.panel.refresh.interval = refresh_interval;
+ if (_.isNumber($scope.panel.refresh.interval)) {
+ timer.cancel($scope.refresh_timer);
+ $scope.realtime();
+ } else {
+ timer.cancel($scope.refresh_timer);
+ }
+ };
+
+ $scope.realtime = function() {
+ if ($scope.panel.refresh.enable) {
+ timer.cancel($scope.refresh_timer);
+
+ $scope.refresh_timer = timer.register($timeout(function() {
+ $scope.realtime();
+ $scope.get_data();
+ }, $scope.panel.refresh.interval*1000));
+ } else {
+ timer.cancel($scope.refresh_timer);
+ }
+ };
+
+ $scope.get_data = function() {
+ // Make sure we have everything for the request to complete
+ if(dashboard.indices.length === 0) {
+ return;
+ }
+
+ delete $scope.panel.error;
+ $scope.panelMeta.loading = true;
+ var request, results;
+
+ $scope.sjs.client.server(dashboard.current.solr.server + dashboard.current.solr.core_name);
+
+ request = $scope.sjs.Request().indices(dashboard.indices);
+ $scope.panel.queries.ids = querySrv.idsByMode($scope.panel.queries);
+
+ // Populate the inspector panel
+ $scope.inspector = angular.toJson(JSON.parse(request.toString()),true);
+
+ var query = this.build_expression('json', false);
+
+ // Set the panel's query
+ $scope.panel.queries.query = query;
+
+ request.setQuery(query);
+
+ results = request.streamExpression();
+
+ // Populate scope when we have results
+ results.then(function(results) {
+ // Check for error and abort if found
+ if(!(_.isUndefined(results.error))) {
+ $scope.panel.error = $scope.parse_error(results.error.msg);
+ $scope.data = [];
+ $scope.panelMeta.loading = false;
+ $scope.$emit('render');
+ return;
+ }
+
+ // Function for validating HTML color by assign it to a dummy
+ // and let the browser do the work of validation.
+ var isValidHTMLColor = function(color) {
+ // clear attr first, before comparison
+ $('#colorTest').removeAttr('style');
+ var valid = $('#colorTest').css('color');
+ $('#colorTest').css('color', color);
+
+ if (valid === $('#colorTest').css('color')) {
+ return false;
+ } else {
+ return true;
+ }
+ };
+
+ // Function for customizing chart color by using field values as colors.
+ var addSliceColor = function(slice,color) {
+ if ($scope.panel.useColorFromField && isValidHTMLColor(color)) {
+ slice.color = color;
+ }
+ return slice;
+ };
+
+ var sum = 0;
+ var k = 0;
+ var missing =0;
+ $scope.panelMeta.loading = false;
+ $scope.hits = results['result-set'].docs.length;
+ $scope.data = [];
+
+ if ($scope.panel.mode === 'count') {
+ // In count mode, the y-axis min should be zero because count value cannot be negative.
+ $scope.yaxis_min = 0;
+ _.each(results['result-set'].docs, function(v) {
+ if (v.EOF) return;
+
+ var term = v.term;
+ if (term === null) {
+ missing = count;
+ } else {
+ // if count = 0, do not add it to the chart, just skip it
+ if (v.score === 0) { return; }
+ var slice = { label : term, data : [[k, v.score, v.foreground, v.background]], actions: true};
+ slice = addSliceColor(slice,term);
+ $scope.data.push(slice);
+ }
+ });
+ }
+ // Sort the results
+ $scope.data = _.sortBy($scope.data, function(d) {
+ return $scope.panel.sortBy === 'index' ? d.label : d.data[0][1];
+ });
+ if ($scope.panel.order === 'descending') {
+ $scope.data.reverse();
+ }
+
+ // Slice it according to panel.size, and then set the x-axis values with k.
+ $scope.data = $scope.data.slice(0,$scope.panel.size);
+ _.each($scope.data, function(v) {
+ v.data[0][0] = k;
+ k++;
+ });
+
+ if ($scope.panel.field && $scope.fields.typeList[$scope.panel.field] && $scope.fields.typeList[$scope.panel.field].schema.indexOf("T") > -1) {
+ $scope.hits = sum;
+ }
+
+ $scope.data.push({label:'Missing field',
+ // data:[[k,results.facets.terms.missing]],meta:"missing",color:'#aaa',opacity:0});
+ // TODO: Hard coded to 0 for now. Solr faceting does not provide 'missing' value.
+ data:[[k,missing]],meta:"missing",color:'#aaa',opacity:0});
+ $scope.data.push({label:'Other values',
+ // data:[[k+1,results.facets.terms.other]],meta:"other",color:'#444'});
+ // TODO: Hard coded to 0 for now. Solr faceting does not provide 'other' value.
+ data:[[k+1,$scope.hits-sum]],meta:"other",color:'#444'});
+
+ $scope.$emit('render');
+ });
+ };
+
+ $scope.build_search = function(term,negate) {
+ if(_.isUndefined(term.meta)) {
+ filterSrv.set({type:'terms',field:$scope.panel.field,value:term.label,
+ mandate:(negate ? 'mustNot':'must')});
+ } else if(term.meta === 'missing') {
+ filterSrv.set({type:'exists',field:$scope.panel.field,
+ mandate:(negate ? 'must':'mustNot')});
+ } else {
+ return;
+ }
+ dashboard.refresh();
+ };
+
+ $scope.set_refresh = function (state) {
+ $scope.refresh = state;
+ // if 'count' mode is selected, set decimal_points to zero automatically.
+ if ($scope.panel.mode === 'count') {
+ $scope.panel.decimal_points = 0;
+ }
+ };
+
+ $scope.close_edit = function() {
+ // Start refresh timer if enabled
+ if ($scope.panel.refresh.enable) {
+ $scope.set_timer($scope.panel.refresh.interval);
+ }
+
+ if ($scope.refresh) {
+ // $scope.testMultivalued();
+ $scope.get_data();
+ }
+ $scope.refresh = false;
+ $scope.$emit('render');
+ };
+
+ $scope.showMeta = function(term) {
+ if(_.isUndefined(term.meta)) {
+ return true;
+ }
+ if(term.meta === 'other' && !$scope.panel.other) {
+ return false;
+ }
+ if(term.meta === 'missing' && !$scope.panel.missing) {
+ return false;
+ }
+ return true;
+ };
+
+ });
+
+});
diff --git a/src/app/panels/sunburst/module.js b/src/app/panels/sunburst/module.js
index 84da95d33..6f6fd72cf 100644
--- a/src/app/panels/sunburst/module.js
+++ b/src/app/panels/sunburst/module.js
@@ -102,7 +102,7 @@ define([
var facet_limits = '&facet.limit=' + $scope.panel.facet_limit;
// Set the panel's query
- $scope.panel.queries.query = querySrv.getORquery() + fq + wt_json + facet + facet_pivot + facet_limits + rows;
+ $scope.panel.queries.query = querySrv.getOPQuery() + fq + wt_json + facet + facet_pivot + facet_limits + rows;
// Set the additional custom query
if ($scope.panel.queries.custom != null) {
diff --git a/src/app/panels/table/module.js b/src/app/panels/table/module.js
index cb917b105..bd5183172 100755
--- a/src/app/panels/table/module.js
+++ b/src/app/panels/table/module.js
@@ -310,7 +310,7 @@ function (angular, app, _, kbn, moment) {
}
// Set the panel's query
- $scope.panel.queries.basic_query = querySrv.getORquery() + fq + sorting;
+ $scope.panel.queries.basic_query = querySrv.getOPQuery() + fq + sorting;
$scope.panel.queries.query = $scope.panel.queries.basic_query + wt_json + rows_limit;
// Set the additional custom query
diff --git a/src/app/panels/tagcloud/module.js b/src/app/panels/tagcloud/module.js
index fd8208f30..9d15f5a08 100755
--- a/src/app/panels/tagcloud/module.js
+++ b/src/app/panels/tagcloud/module.js
@@ -93,7 +93,7 @@ define([
var facet = '&facet=true&facet.field=' + $scope.panel.field + '&facet.limit=' + $scope.panel.size;
// Set the panel's query
- $scope.panel.queries.query = querySrv.getORquery() + wt_json + rows_limit + fq + facet;
+ $scope.panel.queries.query = querySrv.getOPQuery() + wt_json + rows_limit + fq + facet;
// Set the additional custom query
if ($scope.panel.queries.custom != null) {
diff --git a/src/app/panels/terms/module.js b/src/app/panels/terms/module.js
index a111eb575..74926fc70 100755
--- a/src/app/panels/terms/module.js
+++ b/src/app/panels/terms/module.js
@@ -149,7 +149,7 @@ function (angular, app, _, $, kbn) {
}
}
- return querySrv.getORquery() + wt_json + rows_limit + fq + exclude_filter + facet + ($scope.panel.queries.custom != null ? $scope.panel.queries.custom : '');
+ return querySrv.getOPQuery() + wt_json + rows_limit + fq + exclude_filter + facet + ($scope.panel.queries.custom != null ? $scope.panel.queries.custom : '');
};
$scope.exportfile = function(filetype) {
diff --git a/src/app/panels/timeseries/editor.html b/src/app/panels/timeseries/editor.html
new file mode 100755
index 000000000..8c05c9c74
--- /dev/null
+++ b/src/app/panels/timeseries/editor.html
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/app/panels/timeseries/interval.js b/src/app/panels/timeseries/interval.js
new file mode 100644
index 000000000..673371fcf
--- /dev/null
+++ b/src/app/panels/timeseries/interval.js
@@ -0,0 +1,57 @@
+define([
+ 'kbn'
+],
+function (kbn) {
+ 'use strict';
+
+ /**
+ * manages the interval logic
+ * @param {[type]} interval_string An interval string in the format '1m', '1y', etc
+ */
+ function Interval(interval_string) {
+ this.string = interval_string;
+
+ var info = kbn.describe_interval(interval_string);
+ this.type = info.type;
+ this.ms = info.sec * 1000 * info.count;
+
+ // does the length of the interval change based on the current time?
+ if (this.type === 'y' || this.type === 'M') {
+ // we will just modify this time object rather that create a new one constantly
+ this.get = this.get_complex;
+ this.date = new Date(0);
+ } else {
+ this.get = this.get_simple;
+ }
+ }
+
+ Interval.prototype = {
+ toString: function () {
+ return this.string;
+ },
+ after: function(current_ms) {
+ return this.get(current_ms, 1);
+ },
+ before: function (current_ms) {
+ return this.get(current_ms, -1);
+ },
+ get_complex: function (current, delta) {
+ this.date.setTime(current);
+ switch(this.type) {
+ case 'M':
+ this.date.setUTCMonth(this.date.getUTCMonth() + delta);
+ break;
+ case 'y':
+ this.date.setUTCFullYear(this.date.getUTCFullYear() + delta);
+ break;
+ }
+ return this.date.getTime();
+ },
+ get_simple: function (current, delta) {
+ return current + (delta * this.ms);
+ }
+ };
+
+ return Interval;
+
+});
\ No newline at end of file
diff --git a/src/app/panels/timeseries/module.html b/src/app/panels/timeseries/module.html
new file mode 100755
index 000000000..ca4211759
--- /dev/null
+++ b/src/app/panels/timeseries/module.html
@@ -0,0 +1,94 @@
+
\ No newline at end of file
diff --git a/src/app/panels/timeseries/module.js b/src/app/panels/timeseries/module.js
new file mode 100755
index 000000000..4fa57185f
--- /dev/null
+++ b/src/app/panels/timeseries/module.js
@@ -0,0 +1,653 @@
+/*
+
+ ## Timeseries
+
+ ### Parameters
+ * auto_int :: Auto calculate data point interval?
+ * resolution :: If auto_int is enables, shoot for this many data points, rounding to
+ sane intervals
+ * interval :: Datapoint interval in elasticsearch date math format (eg 1d, 1w, 1y, 5y)
+ * fill :: Only applies to line charts. Level of area shading from 0-10
+ * linewidth :: Only applies to line charts. How thick the line should be in pixels
+ While the editor only exposes 0-10, this can be any numeric value.
+ Set to 0 and you'll get something like a scatter plot
+ * timezone :: This isn't totally functional yet. Currently only supports browser and utc.
+ browser will adjust the x-axis labels to match the timezone of the user's
+ browser
+ * spyable :: Dislay the 'eye' icon that show the last elasticsearch query
+ * zoomlinks :: Show the zoom links?
+ * bars :: Show bars in the chart
+ * stack :: Stack multiple queries. This generally a crappy way to represent things.
+ You probably should just use a line chart without stacking
+ * points :: Should circles at the data points on the chart
+ * lines :: Line chart? Sweet.
+ * legend :: Show the legend?
+ * x-axis :: Show x-axis labels and grid lines
+ * y-axis :: Show y-axis labels and grid lines
+ * interactive :: Allow drag to select time range
+
+*/
+define([
+ 'angular',
+ 'app',
+ 'jquery',
+ 'underscore',
+ 'kbn',
+ 'moment',
+ './timeSeries',
+
+ 'jquery.flot',
+ 'jquery.flot.pie',
+ 'jquery.flot.selection',
+ 'jquery.flot.time',
+ 'jquery.flot.stack',
+ 'jquery.flot.stackpercent',
+ 'jquery.flot.axislabels'
+ ],
+ function (angular, app, $, _, kbn, moment, timeSeries) {
+ 'use strict';
+
+ var module = angular.module('kibana.panels.timeseries', []);
+ app.useModule(module);
+
+ module.controller('timeseries', function ($scope, $q, $timeout, timer, 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: "Stable",
+ description: "A bucketed time series chart of the current query, including all applied time and non-time filters. When count metric is used, counts are plotted and no field is required. When other metrics are used, corresponding aggregates of the specified field over time are plotted."
+ };
+
+ // Set and populate defaults
+ var _d = {
+ queries: {
+ mode: 'all',
+ ids: [],
+ query: '*:*',
+ custom: ''
+ },
+ max_rows: 100000, // maximum number of rows returned from Solr (also use this for group.limit to simplify UI setting)
+ value_field: null,
+ group_field: null,
+ sum_value: false,
+ auto_int: true,
+ resolution: 100,
+ interval: '5m',
+ intervals: ['auto', '1s', '1m', '5m', '10m', '30m', '1h', '3h', '12h', '1d', '1w', '1M', '1y'],
+ fill: 0,
+ linewidth: 3,
+ timezone: 'browser', // browser, utc or a standard timezone
+ spyable: true,
+ zoomlinks: true,
+ bars: true,
+ stack: true,
+ points: false,
+ lines: false,
+ lines_smooth: false, // Enable 'smooth line' mode by removing zero values from the plot.
+ legend: true,
+ 'x-axis': true,
+ 'y-axis': true,
+ percentage: false,
+ interactive: true,
+ options: true,
+ show_queries: true,
+ tooltip: {
+ value_type: 'cumulative',
+ query_as_alias: false
+ },
+ refresh: {
+ enable: false,
+ interval: 2
+ }
+ };
+
+ _.defaults($scope.panel, _d);
+
+ $scope.init = function () {
+ // Hide view options by default
+ $scope.options = false;
+
+ // Start refresh timer if enabled
+ if ($scope.panel.refresh.enable) {
+ $scope.set_timer($scope.panel.refresh.interval);
+ }
+
+ $scope.$on('refresh', function () {
+ $scope.get_data();
+ });
+
+ $scope.get_data();
+ };
+
+ $scope.set_interval = function (interval) {
+ if (interval !== 'auto') {
+ $scope.panel.auto_int = false;
+ $scope.panel.interval = interval;
+ } else {
+ $scope.panel.auto_int = true;
+ }
+ };
+
+ $scope.interval_label = function (interval) {
+ return $scope.panel.auto_int && interval === $scope.panel.interval ? interval + " (auto)" : interval;
+ };
+
+ /**
+ * The time range effecting the panel
+ * @return {[type]} [description]
+ */
+ $scope.get_time_range = function () {
+ var range = $scope.range = filterSrv.timeRange('min');
+ return range;
+ };
+
+ $scope.get_interval = function () {
+ var interval = $scope.panel.interval,
+ range;
+ if ($scope.panel.auto_int) {
+ range = $scope.get_time_range();
+ if (range) {
+ interval = kbn.secondsToHms(
+ kbn.calculate_interval(range.from, range.to, $scope.panel.resolution, 0) / 1000
+ );
+ }
+ }
+ $scope.panel.interval = interval || '10m';
+ return $scope.panel.interval;
+ };
+
+ $scope.metric_changed = function () {
+ if ($scope.panel.metric === 'count')
+ $scope.panel.field = '*';
+ };
+
+ $scope.build_expression = function (q) {
+ var expression = 'expr=timeseries(' + dashboard.current.solr.core_name + ',' + q + ')';
+
+ return expression;
+ };
+
+ /**
+ * Fetches data for ORed queries using Solr timeseries streaming expressions
+ *
+ * @param {number} segment The segment count, (0 based)
+ * @param {number} query_id The id of the query, generated on the first run and passed back when
+ * this call is made recursively for more segments
+ */
+ $scope.get_data = function (segment, query_id) {
+ if (_.isUndefined(segment)) {
+ segment = 0;
+ }
+ delete $scope.panel.error;
+
+ // Make sure we have everything for the request to complete
+ if (dashboard.indices.length === 0) {
+ return;
+ }
+ var _range = $scope.get_time_range();
+ var _interval = $scope.get_interval(_range);
+
+ if ($scope.panel.auto_int) {
+ $scope.panel.interval = kbn.secondsToHms(
+ kbn.calculate_interval(_range.from, _range.to, $scope.panel.resolution, 0) / 1000);
+ }
+
+ $scope.panelMeta.loading = true;
+
+ // Solr
+ $scope.sjs.client.server(dashboard.current.solr.server + dashboard.current.solr.core_name);
+
+ var request = $scope.sjs.Request().indices(dashboard.indices[segment]);
+ $scope.panel.queries.ids = querySrv.idsByMode($scope.panel.queries);
+
+
+ $scope.panel.queries.query = "";
+
+ // Populate the inspector panel
+ $scope.populate_modal(request);
+
+ // Build Solr query
+ var fq = '';
+ if (filterSrv.getSolrFq()) {
+ fq = ',' + filterSrv.getSolrFq(false, ',');
+ }
+ var time_field = filterSrv.getTimeField();
+ var start_time = filterSrv.getStartTime();
+ var end_time = filterSrv.getEndTime();
+
+ // timeseries does NOT accept * as a value, need to convert it to NOW
+ if (end_time === '*') {
+ end_time = 'NOW';
+ }
+
+ if (!$scope.panel.metric) {
+ $scope.panel.error = "A metric must be specified";
+ return;
+ }
+ if (!$scope.panel.field) {
+ $scope.panel.error = "A field must be specified";
+ return;
+ }
+
+ var gap = $scope.sjs.convertFacetGap($scope.panel.interval);
+ var timeseries_params = ',field=' + time_field +
+ ',start=' + start_time +
+ ',end=' + end_time +
+ ',gap=' + gap;
+ var timeseries_metric = ',' + $scope.panel.metric + '(' + $scope.panel.field + ')';
+
+ var promises = [];
+ _.each($scope.panel.queries.ids, function (id) {
+ var temp_q = querySrv.getQuery(id) /* + fq // fq seems not supported by timeseries */
+ + timeseries_params + timeseries_metric;
+ temp_q = $scope.build_expression(temp_q);
+ $scope.panel.queries.query += temp_q + "\n";
+ if ($scope.panel.queries.custom !== null) {
+ request = request.setQuery(temp_q + $scope.panel.queries.custom);
+ } else {
+ request = request.setQuery(temp_q);
+ }
+ promises.push(request.streamExpression());
+ });
+
+ if (dashboard.current.services.query.ids.length >= 1) {
+ $q.all(promises).then(function (results) {
+ $scope.panelMeta.loading = false;
+ if (segment === 0) {
+ $scope.hits = 0;
+ $scope.data = [];
+ query_id = $scope.query_id = new Date().getTime();
+ }
+
+ var i = 0,
+ time_series,
+ hits;
+
+ _.each($scope.panel.queries.ids, function (id, index) {
+ // Check for error and abort if found
+ if (_.isArray(results[index]['result-set'].docs)
+ && !(_.isUndefined(results[index]['result-set'].docs[0].EXCEPTION))) {
+ $scope.panel.error = results[index]['result-set'].docs[0].EXCEPTION;
+ return;
+ }
+ // we need to initialize the data variable on the first run,
+ // and when we are working on the first segment of the data.
+ if (_.isUndefined($scope.data[i]) || segment === 0) {
+ time_series = new timeSeries.ZeroFilled({
+ interval: _interval,
+ start_date: _range && _range.from,
+ end_date: _range && _range.to,
+ fill_style: 'minimal'
+ });
+ hits = 0;
+ } else {
+ time_series = $scope.data[i].time_series;
+ hits = 0;
+ $scope.hits = 0;
+ }
+
+ // Solr timeseries counts response is in one big array.
+ var entry_time, entries, entry_value;
+ // Filter out EOF
+ entries = results[index]['result-set'].docs;
+ entries.pop();
+ for (var j = 0; j < entries.length; j++) {
+ entry_time = new Date(entries[j][time_field]).getTime(); // convert to millisec
+ var entry_count = entries[j][$scope.panel.metric + '(' + $scope.panel.field + ')'];
+ if (!entry_count)
+ entry_count = 0;
+ time_series.addValue(entry_time, entry_count);
+ hits += entry_count; // The series level hits counter
+ $scope.hits += entry_count; // Entire dataset level hits counter
+ }
+
+ $scope.data[i] = {
+ info: querySrv.list[id],
+ time_series: time_series,
+ hits: hits
+ };
+
+ i++;
+ });
+
+ // Tell the histogram directive to render.
+ $scope.$emit('render');
+ });
+ }
+ };
+
+ // function $scope.zoom
+ // factor :: Zoom factor, so 0.5 = cuts timespan in half, 2 doubles timespan
+ $scope.zoom = function (factor) {
+ var _range = filterSrv.timeRange('min');
+ var _timespan = (_range.to.valueOf() - _range.from.valueOf());
+ var _center = _range.to.valueOf() - _timespan / 2;
+
+ var _to = (_center + (_timespan * factor) / 2);
+ var _from = (_center - (_timespan * factor) / 2);
+
+ // If we're not already looking into the future, don't.
+ if (_to > Date.now() && _range.to < Date.now()) {
+ var _offset = _to - Date.now();
+ _from = _from - _offset;
+ _to = Date.now();
+ }
+
+ var time_field = filterSrv.getTimeField();
+ if (factor > 1) {
+ filterSrv.removeByType('time');
+ }
+
+ filterSrv.set({
+ type: 'time',
+ from: moment.utc(_from).toDate(),
+ to: moment.utc(_to).toDate(),
+ field: time_field
+ });
+
+ dashboard.refresh();
+
+ };
+
+ // I really don't like this function, too much dom manip. Break out into directive?
+ $scope.populate_modal = function (request) {
+ $scope.inspector = angular.toJson(JSON.parse(request.toString()), true);
+ };
+
+ $scope.set_refresh = function (state) {
+ $scope.refresh = state;
+ };
+
+ $scope.close_edit = function () {
+ // Start refresh timer if enabled
+ if ($scope.panel.refresh.enable) {
+ $scope.set_timer($scope.panel.refresh.interval);
+ }
+ if ($scope.refresh) {
+ $scope.get_data();
+ }
+ $scope.refresh = false;
+ $scope.$emit('render');
+ };
+
+ $scope.render = function () {
+ $scope.$emit('render');
+ };
+ });
+
+ module.directive('timeseriesChart', function (dashboard, filterSrv) {
+ return {
+ restrict: 'A',
+ template: '',
+ link: function (scope, elem) {
+
+ // Receive render events
+ scope.$on('render', function () {
+ render_panel();
+ });
+
+ // Re-render if the window is resized
+ angular.element(window).bind('resize', function () {
+ render_panel();
+ });
+
+ // Function for rendering panel
+ function render_panel() {
+ // IE doesn't work without this
+ elem.css({height: scope.panel.height || scope.row.height});
+
+ // Populate from the query service
+ try {
+ _.each(scope.data, function (series) {
+ series.label = series.info.alias;
+ series.color = series.info.color;
+ });
+ } catch (e) {
+ return;
+ }
+
+ // Set barwidth based on specified interval
+ var barwidth = kbn.interval_to_ms(scope.panel.interval);
+
+ var stack = scope.panel.stack ? true : null;
+
+ // Populate element
+ try {
+ var options = {
+ legend: {show: false},
+ series: {
+ stackpercent: scope.panel.stack ? scope.panel.percentage : false,
+ stack: scope.panel.percentage ? null : stack,
+ lines: {
+ show: scope.panel.lines,
+ // Silly, but fixes bug in stacked percentages
+ fill: scope.panel.fill === 0 ? 0.001 : scope.panel.fill / 10,
+ lineWidth: scope.panel.linewidth,
+ steps: false
+ },
+ bars: {
+ show: scope.panel.bars,
+ fill: 1,
+ barWidth: barwidth / 1.8,
+ zero: false,
+ lineWidth: 0
+ },
+ points: {
+ show: scope.panel.points,
+ fill: 1,
+ fillColor: false,
+ radius: 5
+ },
+ shadowSize: 1
+ },
+ axisLabels: {
+ show: true
+ },
+ yaxis: {
+ show: scope.panel['y-axis'],
+ min: null, // TODO - make this adjusted dynamicmally, and add it to configuration panel
+ max: scope.panel.percentage && scope.panel.stack ? 100 : null,
+ axisLabel: scope.panel.metric + '(' + scope.panel.field + ')',
+ },
+ xaxis: {
+ timezone: scope.panel.timezone,
+ show: scope.panel['x-axis'],
+ mode: "time",
+ min: _.isUndefined(scope.range.from) ? null : scope.range.from.getTime(),
+ max: _.isUndefined(scope.range.to) ? null : scope.range.to.getTime(),
+ timeformat: time_format(scope.panel.interval),
+ label: "Datetime",
+ axisLabel: filterSrv.getTimeField(),
+ },
+ grid: {
+ backgroundColor: null,
+ borderWidth: 0,
+ hoverable: true,
+ color: '#c8c8c8'
+ }
+ };
+
+ if (scope.panel.interactive) {
+ options.selection = {mode: "x", color: '#666'};
+ }
+
+ // when rendering stacked bars, we need to ensure each point that has data is zero-filled
+ // so that the stacking happens in the proper order
+ var required_times = [];
+ if (scope.data.length > 1) {
+ required_times = Array.prototype.concat.apply([], _.map(scope.data, function (query) {
+ return query.time_series.getOrderedTimes();
+ }));
+ required_times = _.uniq(required_times.sort(function (a, b) {
+ // decending numeric sort
+ return a - b;
+ }), true);
+ }
+
+ for (var i = 0; i < scope.data.length; i++) {
+ scope.data[i].data = scope.data[i].time_series.getFlotPairs(required_times);
+ }
+
+ // ISSUE: SOL-76
+ // If 'lines_smooth' is enabled, loop through $scope.data[] and remove zero filled entries.
+ // Without zero values, the line chart will appear smooth as SiLK ;-)
+ if (scope.panel.lines_smooth) {
+ for (var i = 0; i < scope.data.length; i++) { // jshint ignore: line
+ var new_data = [];
+ for (var j = 0; j < scope.data[i].data.length; j++) {
+ // if value of the timestamp !== 0, then add it to new_data
+ if (scope.data[i].data[j][1] !== 0) {
+ new_data.push(scope.data[i].data[j]);
+ }
+ }
+ scope.data[i].data = new_data;
+ }
+ }
+
+ scope.plot = $.plot(elem, scope.data, options);
+ } catch (e) {
+ // TODO: Need to fix bug => "Invalid dimensions for plot, width = 0, height = 200"
+ console.log(e);
+ }
+ }
+
+ function time_format(interval) {
+ var _int = kbn.interval_to_seconds(interval);
+ if (_int >= 2628000) {
+ return "%m/%y";
+ }
+ if (_int >= 86400) {
+ return "%m/%d/%y";
+ }
+ if (_int >= 60) {
+ return "%H:%M %m/%d";
+ }
+
+ return "%H:%M:%S";
+ }
+
+ var $tooltip = $('
');
+ elem.bind("plothover", function (event, pos, item) {
+ var group, value;
+ if (item) {
+ if (item.series.info.alias || scope.panel.tooltip.query_as_alias) {
+ group = '' +
+ '' + ' ' +
+ (item.series.info.alias || item.series.info.query) +
+ ' ';
+ } else {
+ group = kbn.query_color_dot(item.series.color, 15) + ' ';
+ }
+ if (scope.panel.stack && scope.panel.tooltip.value_type === 'individual') {
+ value = item.datapoint[1] - item.datapoint[2];
+ } else {
+ value = item.datapoint[1];
+ }
+
+ var lnLastValue = value;
+
+ var lbPositiveValue = (lnLastValue > 0);
+
+ var lsItemTT = group + dashboard.numberWithCommas(value) + " @ " + (scope.panel.timezone === 'utc' ? moment.utc(item.datapoint[0]).format('MM/DD HH:mm:ss') : moment(item.datapoint[0]).format('MM/DD HH:mm:ss'));
+
+ var hoverSeries = item.series;
+ var x = item.datapoint[0];
+ // y = item.datapoint[1];
+
+ var lsTT = lsItemTT;
+ var allSeries = scope.plot.getData();
+ var posSerie = -1;
+ for (var i = allSeries.length - 1; i >= 0; i--) {
+
+ //if stack stop at the first positive value
+ if (scope.panel.stack && lbPositiveValue) {
+ break;
+ }
+
+ var s = allSeries[i];
+ i = parseInt(i);
+
+
+ if (s === hoverSeries) {
+ posSerie = i;
+ }
+
+ //not consider serie "upper" the hover serie
+ if (i >= posSerie) {
+ continue;
+ }
+
+ //search in current serie a point with de same position.
+ for (var j = 0; j < s.data.length; j++) {
+ var p = s.data[j];
+ if (p[0] === x) {
+
+ if (scope.panel.stack && scope.panel.tooltip.value_type === 'individual' && !isNaN(p[2])) {
+ value = p[1] - p[2];
+ } else {
+ value = p[1];
+ }
+
+ lbPositiveValue = value > 0;
+
+ if (!scope.panel.stack && value !== lnLastValue) {
+ break;
+ }
+
+ posSerie = i;
+ lnLastValue = value;
+
+
+ if (s.info.alias || scope.panel.tooltip.query_as_alias) {
+ group = '' +
+ '' + ' ' +
+ (s.info.alias || s.info.query) +
+ ' ';
+ } else {
+ group = kbn.query_color_dot(s.color, 15) + ' ';
+ }
+
+ lsItemTT = group + dashboard.numberWithCommas(value) + " @ " + (scope.panel.timezone === 'utc' ? moment.utc(p[0]).format('MM/DD HH:mm:ss') : moment(p[0]).format('MM/DD HH:mm:ss'));
+ lsTT = lsTT + "" + lsItemTT;
+ break;
+ }
+ }
+ }
+
+
+ $tooltip
+ .html(lsTT)
+ .place_tt(pos.pageX, pos.pageY);
+ } else {
+ $tooltip.detach();
+ }
+ });
+
+ elem.bind("plotselected", function (event, ranges) {
+ filterSrv.set({
+ type: 'time',
+ // from : moment.utc(ranges.xaxis.from),
+ // to : moment.utc(ranges.xaxis.to),
+ from: moment.utc(ranges.xaxis.from).toDate(),
+ to: moment.utc(ranges.xaxis.to).toDate(),
+ field: filterSrv.getTimeField()
+ });
+ dashboard.refresh();
+ });
+ }
+ };
+ });
+
+ });
diff --git a/src/app/panels/timeseries/timeSeries.js b/src/app/panels/timeseries/timeSeries.js
new file mode 100755
index 000000000..c12587e85
--- /dev/null
+++ b/src/app/panels/timeseries/timeSeries.js
@@ -0,0 +1,203 @@
+define([
+ 'underscore',
+ './interval'
+],
+function (_, Interval) {
+ 'use strict';
+
+ var ts = {};
+
+ // map compatable parseInt
+ function base10Int(val) {
+ return parseInt(val, 10);
+ }
+
+ // trim the ms off of a time, but return it with empty ms.
+ function getDatesTime(date) {
+ return Math.floor(date.getTime() / 1000)*1000;
+ }
+
+ /**
+ * Certain graphs require 0 entries to be specified for them to render
+ * properly (like the line graph). So with this we will caluclate all of
+ * the expected time measurements, and fill the missing ones in with 0
+ * @param {object} opts An object specifying some/all of the options
+ *
+ * OPTIONS:
+ * @opt {string} interval The interval notion describing the expected spacing between
+ * each data point.
+ * @opt {date} start_date (optional) The start point for the time series, setting this and the
+ * end_date will ensure that the series streches to resemble the entire
+ * expected result
+ * @opt {date} end_date (optional) The end point for the time series, see start_date
+ * @opt {string} fill_style Either "minimal", or "all" describing the strategy used to zero-fill
+ * the series.
+ */
+ ts.ZeroFilled = function (opts) {
+ opts = _.defaults(opts, {
+ interval: '10m',
+ start_date: null,
+ end_date: null,
+ fill_style: 'minimal'
+ });
+
+ // the expected differenece between readings.
+ this.interval = new Interval(opts.interval);
+
+ // will keep all values here, keyed by their time
+ this._data = {};
+ this.start_time = opts.start_date && getDatesTime(opts.start_date);
+ this.end_time = opts.end_date && getDatesTime(opts.end_date);
+ this.opts = opts;
+ };
+
+ /**
+ * Add a row
+ * @param {int} time The time for the value, in
+ * @param {any} value The value at this time
+ */
+ ts.ZeroFilled.prototype.addValue = function (time, value) {
+ if (time instanceof Date) {
+ time = getDatesTime(time);
+ } else {
+ time = base10Int(time);
+ }
+ if (!isNaN(time)) {
+ this._data[time] = (_.isUndefined(value) ? 0 : value);
+ }
+ this._cached_times = null;
+ };
+
+ /**
+ * Add a row and sum the value
+ * @param {int} time The time for the value, in
+ * @param {any} value The value at this time
+ */
+ ts.ZeroFilled.prototype.sumValue = function (time, value) {
+ if (time instanceof Date) {
+ time = getDatesTime(time);
+ } else {
+ time = base10Int(time);
+ }
+ if (!isNaN(time)) {
+
+ var curValue = 0;
+ if (!isNaN(this._data[time])){
+ curValue = this._data[time];
+ }
+
+
+ this._data[time] = curValue + (_.isUndefined(value) ? 0 : value);
+ }
+ this._cached_times = null;
+ };
+
+ /**
+ * Get an array of the times that have been explicitly set in the series
+ * @param {array} include (optional) list of timestamps to include in the response
+ * @return {array} An array of integer times.
+ */
+ ts.ZeroFilled.prototype.getOrderedTimes = function (include) {
+ var times = _.map(_.keys(this._data), base10Int);
+ if (_.isArray(include)) {
+ times = times.concat(include);
+ }
+ return _.uniq(times.sort(function (a, b) {
+ // decending numeric sort
+ return a - b;
+ }), true);
+ };
+
+ /**
+ * return the rows in the format:
+ * [ [time, value], [time, value], ... ]
+ *
+ * Heavy lifting is done by _get(Min|All)FlotPairs()
+ * @param {array} required_times An array of timestamps that must be in the resulting pairs
+ * @return {array}
+ */
+ ts.ZeroFilled.prototype.getFlotPairs = function (required_times) {
+ var times = this.getOrderedTimes(required_times),
+ strategy,
+ pairs;
+
+ if(this.opts.fill_style === 'all') {
+ strategy = this._getAllFlotPairs;
+ } else {
+ strategy = this._getMinFlotPairs;
+ }
+
+ pairs = _.reduce(
+ times, // what
+ strategy, // how
+ [], // where
+ this // context
+ );
+
+ // if the first or last pair is inside either the start or end time,
+ // add those times to the series with null values so the graph will stretch to contain them.
+ if (this.start_time && (pairs.length === 0 || pairs[0][0] > this.start_time)) {
+ pairs.unshift([this.start_time, null]);
+ }
+ if (this.end_time && (pairs.length === 0 || pairs[pairs.length - 1][0] < this.end_time)) {
+ pairs.push([this.end_time, null]);
+ }
+
+ return pairs;
+ };
+
+ /**
+ * ** called as a reduce stragegy in getFlotPairs() **
+ * Fill zero's on either side of the current time, unless there is already a measurement there or
+ * we are looking at an edge.
+ * @return {array} An array of points to plot with flot
+ */
+ ts.ZeroFilled.prototype._getMinFlotPairs = function (result, time, i, times) {
+ var next, expected_next, prev, expected_prev;
+
+ // check for previous measurement
+ if (i > 0) {
+ prev = times[i - 1];
+ expected_prev = this.interval.before(time);
+ if (prev < expected_prev) {
+ result.push([expected_prev, 0]);
+ }
+ }
+
+ // add the current time
+ result.push([ time, this._data[time] || 0 ]);
+
+ // check for next measurement
+ if (times.length > i) {
+ next = times[i + 1];
+ expected_next = this.interval.after(time);
+ if (next > expected_next) {
+ result.push([expected_next, 0]);
+ }
+ }
+
+ return result;
+ };
+
+ /**
+ * ** called as a reduce stragegy in getFlotPairs() **
+ * Fill zero's to the right of each time, until the next measurement is reached or we are at the
+ * last measurement
+ * @return {array} An array of points to plot with flot
+ */
+ ts.ZeroFilled.prototype._getAllFlotPairs = function (result, time, i, times) {
+ var next, expected_next;
+
+ result.push([ times[i], this._data[times[i]] || 0 ]);
+ next = times[i + 1];
+ expected_next = this.interval.after(time);
+ for(; times.length > i && next > expected_next; expected_next = this.interval.after(expected_next)) {
+ result.push([expected_next, 0]);
+ }
+
+ return result;
+ };
+
+
+ return ts;
+});
diff --git a/src/app/partials/dasheditor.html b/src/app/partials/dasheditor.html
index 0b5e0adf3..3c5f864fb 100755
--- a/src/app/partials/dasheditor.html
+++ b/src/app/partials/dasheditor.html
@@ -39,7 +39,7 @@
ng-options="f for f in ['dark','light']">
A facet which returns the N most frequent terms within a collection
@@ -249,7 +249,7 @@
/**
The internal facet object.
- @member ejs.DateHistogramFacet
+ @member sjs.DateHistogramFacet
@property {Object} facet
*/
var facet = {};
@@ -263,7 +263,7 @@
/**
Sets the field to be used to construct the this facet.
- @member ejs.DateHistogramFacet
+ @member sjs.DateHistogramFacet
@param {String} fieldName The field name whose data will be used to construct the facet.
@returns {Object} returns this so that calls can be chained.
*/
@@ -279,7 +279,7 @@
/**
Allows you to specify a different key field to be used to group intervals.
- @member ejs.DateHistogramFacet
+ @member sjs.DateHistogramFacet
@param {String} fieldName The name of the field to be used.
@returns {Object} returns this so that calls can be chained.
*/
@@ -18502,6 +18502,19 @@
return this;
},
+ /** Stream expression **/
+
+ streamExpression: function(custom_handler, successcb, errorcb) {
+ if (DEBUG) { console.debug('streamExpression()'); }
+
+ // make sure the user has set a client
+ if (sjs.client == null) {
+ throw new Error("No Client Set");
+ }
+
+ return sjs.client.get(getRestPath(custom_handler? custom_handler: 'stream'), query.solrquery, successcb, errorcb);
+ },
+
/**
Executes the search.