From 8a9661bf26cce4a1c7c6b1f244c2b79196b3385d Mon Sep 17 00:00:00 2001 From: Spencer Alger Date: Thu, 22 Aug 2013 13:51:15 -0700 Subject: [PATCH 1/3] Added a time series service, which provides a ZeroFilled class, setup the histogram to use it so that zero measurements will be displayed --- js/services.js | 114 ++++++++++++++++++++++----- panels/histogram/module.js | 157 +++++++++++++++++++++---------------- 2 files changed, 185 insertions(+), 86 deletions(-) diff --git a/js/services.js b/js/services.js index bd7f98b2eea1a..9ae25950af42f 100644 --- a/js/services.js +++ b/js/services.js @@ -30,7 +30,7 @@ angular.module('kibana.services', []) this.clearAll = function() { self.list = []; - }; + }; }) .service('fields', function(dashboard, $rootScope, $http, alertSrv) { @@ -116,7 +116,7 @@ angular.module('kibana.services', []) ret[propName] = obj; } } - return ret; + return ret; }; }) @@ -242,11 +242,11 @@ angular.module('kibana.services', []) ids : [], }); - // For convenience - var ejs = ejsResource(config.elasticsearch); + // For convenience + var ejs = ejsResource(config.elasticsearch); var _q = dashboard.current.services.query; - this.colors = [ + this.colors = [ "#7EB26D","#EAB839","#6ED0E0","#EF843C","#E24D42","#1F78C1","#BA43A9","#705DA0", //1 "#508642","#CCA300","#447EBC","#C15C17","#890F02","#0A437C","#6D1F62","#584477", //2 "#B7DBAB","#F4D598","#70DBED","#F9BA8F","#F29191","#82B5D8","#E5A8E2","#AEA2E0", //3 @@ -264,7 +264,7 @@ angular.module('kibana.services', []) _q = dashboard.current.services.query; self.list = dashboard.current.services.query.list; self.ids = dashboard.current.services.query.ids; - + if (self.ids.length === 0) { self.set({}); } @@ -330,7 +330,7 @@ angular.module('kibana.services', []) }; this.idsByMode = function(config) { - switch(config.mode) + switch(config.mode) { case 'all': return self.ids; @@ -370,7 +370,7 @@ angular.module('kibana.services', []) }); // For convenience - var ejs = ejsResource(config.elasticsearch); + var ejs = ejsResource(config.elasticsearch); var _f = dashboard.current.services.filter; // Save a reference to this @@ -390,7 +390,7 @@ angular.module('kibana.services', []) }; - // This is used both for adding filters and modifying them. + // This is used both for adding filters and modifying them. // If an id is passed, the filter at that id is updated this.set = function(filter,id) { _.defaults(filter,{mandate:'must'}); @@ -425,7 +425,7 @@ angular.module('kibana.services', []) var either_bool = ejs.BoolFilter().must(ejs.MatchAllFilter()); _.each(ids,function(id) { if(self.list[id].active) { - switch(self.list[id].mandate) + switch(self.list[id].mandate) { case 'mustNot': bool = bool.mustNot(self.getEjsObj(id)); @@ -563,7 +563,7 @@ angular.module('kibana.services', []) }; // An elasticJS client to use - var ejs = ejsResource(config.elasticsearch); + var ejs = ejsResource(config.elasticsearch); var gist_pattern = /(^\d{5,}$)|(^[a-z0-9]{10,}$)|(gist.github.com(\/*.*)\/[a-z0-9]{5,}\/*$)/; // Store a reference to this @@ -602,8 +602,8 @@ angular.module('kibana.services', []) // No dashboard in the URL } else { - // Check if browser supports localstorage, and if there's a dashboard - if (window.Modernizr.localstorage && + // Check if browser supports localstorage, and if there's a dashboard + if (window.Modernizr.localstorage && !(_.isUndefined(window.localStorage['dashboard'])) && window.localStorage['dashboard'] !== '' ) { @@ -612,11 +612,11 @@ angular.module('kibana.services', []) // No? Ok, grab default.json, its all we have now } else { self.file_load('default.json'); - } + } } }; - // Since the dashboard is responsible for index computation, we can compute and assign the indices + // Since the dashboard is responsible for index computation, we can compute and assign the indices // here before telling the panels to refresh this.refresh = function() { if(self.current.index.interval !== 'none') { @@ -626,7 +626,7 @@ angular.module('kibana.services', []) self.current.index.pattern,self.current.index.interval ).then(function (p) { if(p.length > 0) { - self.indices = p; + self.indices = p; } else { //TODO: Option to not failover if(self.current.failover) { @@ -711,7 +711,7 @@ angular.module('kibana.services', []) return true; } else { return false; - } + } }; this.purge_default = function() { @@ -770,7 +770,7 @@ angular.module('kibana.services', []) // Clone object so we can modify it without influencing the existing obejct var save = _.clone(self.current); var id; - + // Change title on object clone if (type === 'dashboard') { id = save.title = _.isUndefined(title) ? self.current.title : title; @@ -783,7 +783,7 @@ angular.module('kibana.services', []) title: save.title, dashboard: angular.toJson(save) }); - + request = type === 'temp' && ttl ? request.ttl(ttl) : request; // TOFIX: Implement error handling here @@ -868,5 +868,79 @@ angular.module('kibana.services', []) return false; }); }; - +}) +.service('timeSeries', function () { + /** + * 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 date start The start time for the result set + * @param date end The end time for the result set + * @param integer interval The length between measurements, in es interval + * notation (1m, 30s, 1h, 15d) + */ + var undef; + function dateToSecondsWithBlankMs(date) { + // return the date as millis since epoch, with 0 millis + return Math.floor(date.getTime() / 1000)*1000; + } + function base10Int(val) { + return parseInt(val, 10); + } + this.ZeroFilled = function (interval, start, end) { + // the expected differenece between readings. + this.interval_ms = parseInt(kbn.interval_to_seconds(interval), 10) * 1000; + // will keep all values here, keyed by their time + this._data = {}; + + if (start) { + this.addValue(start, null); + } + if (end) { + this.addValue(end, null); + } + } + /** + * Add a row + * @param int time The time for the value, in + * @param any value The value at this time + */ + this.ZeroFilled.prototype.addValue = function (time, value) { + if (time instanceof Date) { + time = dateToSecondsWithBlankMs(time); + } else { + time = parseInt(time, 10); + } + if (!isNaN(time)) { + this._data[time] = (value === undef ? 0 : value); + } + }; + /** + * return the rows in the format: + * [ [time, value], [time, value], ... ] + * @return array + */ + this.ZeroFilled.prototype.getFlotPairs = function () { + // var startTime = performance.now(); + var times = _.map(_.keys(this._data), base10Int).sort() + , result = [] + , i + , next + , expected_next; + for(i = 0; i < times.length; i++) { + result.push([ times[i], this._data[times[i]] ]); + next = times[i + 1]; + expected_next = times[i] + this.interval_ms; + for(; times.length > i && next > expected_next; expected_next+= this.interval_ms) { + /** + * since we don't know how the server will round subsequent segments + * we have to recheck for blanks each time. + */ + // this._data[expected_next] = 0; + result.push([expected_next, 0]); + } + } + // console.log(Math.round((performance.now() - startTime)*100)/100, 'ms to get', result.length, 'pairs'); + return result; + }; }); \ No newline at end of file diff --git a/panels/histogram/module.js b/panels/histogram/module.js index 78630e3d373ac..83a88566cb1b8 100644 --- a/panels/histogram/module.js +++ b/panels/histogram/module.js @@ -12,10 +12,10 @@ * 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. + 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 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? @@ -34,7 +34,7 @@ 'use strict'; angular.module('kibana.histogram', []) -.controller('histogram', function($scope, querySrv, dashboard, filterSrv) { +.controller('histogram', function($scope, querySrv, dashboard, filterSrv, timeSeries) { $scope.panelMeta = { editorTabs : [ @@ -56,7 +56,7 @@ angular.module('kibana.histogram', []) }, value_field : null, auto_int : true, - resolution : 100, + resolution : 100, interval : '5m', fill : 0, linewidth : 3, @@ -85,7 +85,39 @@ angular.module('kibana.histogram', []) }; - $scope.get_data = function(segment,query_id) { + /** + * 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 + } + /** + * Fetch the data for a chunk of a queries results. Multiple segments occur when several indicies + * need to be consulted (like timestamped logstash indicies) + * @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 @@ -94,16 +126,16 @@ angular.module('kibana.histogram', []) } - var _range = $scope.range = filterSrv.timeRange('min'); - + 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; - var _segment = _.isUndefined(segment) ? 0 : segment; - var request = $scope.ejs.Request().indices(dashboard.indices[_segment]); + var request = $scope.ejs.Request().indices(dashboard.indices[segment]); $scope.panel.queries.ids = querySrv.idsByMode($scope.panel.queries); // Build the query @@ -114,7 +146,7 @@ angular.module('kibana.histogram', []) ); var facet = $scope.ejs.DateHistogramFacet(id); - + if($scope.panel.mode === 'count') { facet = facet.field($scope.panel.time_field); } else { @@ -124,7 +156,7 @@ angular.module('kibana.histogram', []) } facet = facet.keyField($scope.panel.time_field).valueField($scope.panel.value_field); } - facet = facet.interval($scope.panel.interval).facetFilter($scope.ejs.QueryFilter(query)); + facet = facet.interval(_interval).facetFilter($scope.ejs.QueryFilter(query)); request = request.facet(facet).size(0); }); @@ -137,12 +169,12 @@ angular.module('kibana.histogram', []) // Populate scope when we have results results.then(function(results) { $scope.panelMeta.loading = false; - if(_segment === 0) { + if(segment === 0) { $scope.hits = 0; $scope.data = []; query_id = $scope.query_id = new Date().getTime(); } - + // Check for error and abort if found if(!(_.isUndefined(results.error))) { $scope.panel.error = $scope.parse_error(results.error); @@ -153,49 +185,42 @@ angular.module('kibana.histogram', []) var facetIds = _.map(_.keys(results.facets),function(k){return parseInt(k, 10);}); // Make sure we're still on the same query/queries - if($scope.query_id === query_id && - _.intersection(facetIds,$scope.panel.queries.ids).length === $scope.panel.queries.ids.length - ) { + if($scope.query_id === query_id && _.difference(facetIds, $scope.panel.queries.ids).length === 0) { - var i = 0; - var data, hits; + var i = 0 + , time_series + , hits; _.each($scope.panel.queries.ids, function(id) { - var v = results.facets[id]; - - // Null values at each end of the time range ensure we see entire range - if(_.isUndefined($scope.data[i]) || _segment === 0) { - data = []; - if(filterSrv.idsByType('time').length > 0) { - data = [[_range.from.getTime(), null],[_range.to.getTime(), null]]; - //data = []; - } + var query_results = results.facets[id]; + // 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, + // range may be false + _range && _range.from, + _range && _range.to + ); hits = 0; } else { - data = $scope.data[i].data; + time_series = $scope.data[i].time_series; hits = $scope.data[i].hits; } - // Assemble segments - var segment_data = []; - _.each(v.entries, function(v, k) { - segment_data.push([v.time,v[$scope.panel.mode]]); - hits += v.count; // The series level hits counter - $scope.hits += v.count; // Entire dataset level hits counter + // push each entry into the time series, while incrementing counters + _.each(query_results.entries, function(entry) { + time_series.addValue(entry.time, entry[$scope.panel.mode]); + hits += entry.count; // The series level hits counter + $scope.hits += entry.count; // Entire dataset level hits counter }); - data.splice.apply(data,[1,0].concat(segment_data)); // Join histogram data - - // Create the flot series object - var series = { - data: { - info: querySrv.list[id], - data: data, - hits: hits - }, + $scope.data[i] = { + time_series: time_series, + info: querySrv.list[id], + data: time_series.getFlotPairs(), + hits: hits }; - $scope.data[i] = series.data; - i++; }); @@ -203,10 +228,9 @@ angular.module('kibana.histogram', []) $scope.$emit('render'); // If we still have segments left, get them - if(_segment < dashboard.indices.length-1) { - $scope.get_data(_segment+1,query_id); + if(segment < dashboard.indices.length-1) { + $scope.get_data(segment+1,query_id); } - } }); }; @@ -238,7 +262,7 @@ angular.module('kibana.histogram', []) to:moment.utc(_to), field:$scope.panel.time_field }); - + dashboard.refresh(); }; @@ -248,8 +272,8 @@ angular.module('kibana.histogram', []) $scope.inspector = angular.toJson(JSON.parse(request.toString()),true); }; - $scope.set_refresh = function (state) { - $scope.refresh = state; + $scope.set_refresh = function (state) { + $scope.refresh = state; }; $scope.close_edit = function() { @@ -271,7 +295,7 @@ angular.module('kibana.histogram', []) scope.$on('render',function(){ render_panel(); }); - + // Re-render if the window is resized angular.element(window).bind('resize', function(){ render_panel(); @@ -282,7 +306,7 @@ angular.module('kibana.histogram', []) // 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) { @@ -299,21 +323,21 @@ angular.module('kibana.histogram', []) .script("common/lib/panels/jquery.flot.stack.js") .script("common/lib/panels/jquery.flot.selection.js") .script("common/lib/panels/timezone.js"); - + // Populate element. Note that jvectormap appends, does not replace. scripts.wait(function(){ var stack = scope.panel.stack ? true : null; // Populate element - try { + 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, - fill: scope.panel.fill/10, + lines: { + show: scope.panel.lines, + fill: scope.panel.fill/10, lineWidth: scope.panel.linewidth, steps: false }, @@ -321,10 +345,10 @@ angular.module('kibana.histogram', []) points: { show: scope.panel.points, fill: 1, fillColor: false, radius: 5}, shadowSize: 1 }, - yaxis: { - show: scope.panel['y-axis'], - min: 0, - max: scope.panel.percentage && scope.panel.stack ? 100 : null, + yaxis: { + show: scope.panel['y-axis'], + min: 0, + max: scope.panel.percentage && scope.panel.stack ? 100 : null, }, xaxis: { timezone: scope.panel.timezone, @@ -366,14 +390,15 @@ angular.module('kibana.histogram', []) if(_int >= 60) { return "%H:%M
%m/%d"; } - + return "%H:%M:%S"; } function tt(x, y, contents) { // If the tool tip already exists, don't recreate it, just update it - var tooltip = $('#pie-tooltip').length ? - $('#pie-tooltip') : $('
'); + var tooltip = $('#pie-tooltip').length + ? $('#pie-tooltip') + : $('
'); tooltip.html(contents).css({ position: 'absolute', @@ -393,7 +418,7 @@ angular.module('kibana.histogram', []) tt(pos.pageX, pos.pageY, "
"+ - item.datapoint[1].toFixed(0) + " @ " + + item.datapoint[1].toFixed(0) + " @ " + moment(item.datapoint[0]).format('MM/DD HH:mm:ss')); } else { $("#pie-tooltip").remove(); From 13d3da048d9ea913a76051a06c174d3fd764dcb4 Mon Sep 17 00:00:00 2001 From: Spencer Alger Date: Thu, 22 Aug 2013 13:53:31 -0700 Subject: [PATCH 2/3] created timeSeries service with a ZeroFilled class to manage the rows for the histogram --- js/services.js | 114 ++++++++++++++++++++++----- panels/histogram/module.js | 157 +++++++++++++++++++++---------------- 2 files changed, 185 insertions(+), 86 deletions(-) diff --git a/js/services.js b/js/services.js index bd7f98b2eea1a..9ae25950af42f 100644 --- a/js/services.js +++ b/js/services.js @@ -30,7 +30,7 @@ angular.module('kibana.services', []) this.clearAll = function() { self.list = []; - }; + }; }) .service('fields', function(dashboard, $rootScope, $http, alertSrv) { @@ -116,7 +116,7 @@ angular.module('kibana.services', []) ret[propName] = obj; } } - return ret; + return ret; }; }) @@ -242,11 +242,11 @@ angular.module('kibana.services', []) ids : [], }); - // For convenience - var ejs = ejsResource(config.elasticsearch); + // For convenience + var ejs = ejsResource(config.elasticsearch); var _q = dashboard.current.services.query; - this.colors = [ + this.colors = [ "#7EB26D","#EAB839","#6ED0E0","#EF843C","#E24D42","#1F78C1","#BA43A9","#705DA0", //1 "#508642","#CCA300","#447EBC","#C15C17","#890F02","#0A437C","#6D1F62","#584477", //2 "#B7DBAB","#F4D598","#70DBED","#F9BA8F","#F29191","#82B5D8","#E5A8E2","#AEA2E0", //3 @@ -264,7 +264,7 @@ angular.module('kibana.services', []) _q = dashboard.current.services.query; self.list = dashboard.current.services.query.list; self.ids = dashboard.current.services.query.ids; - + if (self.ids.length === 0) { self.set({}); } @@ -330,7 +330,7 @@ angular.module('kibana.services', []) }; this.idsByMode = function(config) { - switch(config.mode) + switch(config.mode) { case 'all': return self.ids; @@ -370,7 +370,7 @@ angular.module('kibana.services', []) }); // For convenience - var ejs = ejsResource(config.elasticsearch); + var ejs = ejsResource(config.elasticsearch); var _f = dashboard.current.services.filter; // Save a reference to this @@ -390,7 +390,7 @@ angular.module('kibana.services', []) }; - // This is used both for adding filters and modifying them. + // This is used both for adding filters and modifying them. // If an id is passed, the filter at that id is updated this.set = function(filter,id) { _.defaults(filter,{mandate:'must'}); @@ -425,7 +425,7 @@ angular.module('kibana.services', []) var either_bool = ejs.BoolFilter().must(ejs.MatchAllFilter()); _.each(ids,function(id) { if(self.list[id].active) { - switch(self.list[id].mandate) + switch(self.list[id].mandate) { case 'mustNot': bool = bool.mustNot(self.getEjsObj(id)); @@ -563,7 +563,7 @@ angular.module('kibana.services', []) }; // An elasticJS client to use - var ejs = ejsResource(config.elasticsearch); + var ejs = ejsResource(config.elasticsearch); var gist_pattern = /(^\d{5,}$)|(^[a-z0-9]{10,}$)|(gist.github.com(\/*.*)\/[a-z0-9]{5,}\/*$)/; // Store a reference to this @@ -602,8 +602,8 @@ angular.module('kibana.services', []) // No dashboard in the URL } else { - // Check if browser supports localstorage, and if there's a dashboard - if (window.Modernizr.localstorage && + // Check if browser supports localstorage, and if there's a dashboard + if (window.Modernizr.localstorage && !(_.isUndefined(window.localStorage['dashboard'])) && window.localStorage['dashboard'] !== '' ) { @@ -612,11 +612,11 @@ angular.module('kibana.services', []) // No? Ok, grab default.json, its all we have now } else { self.file_load('default.json'); - } + } } }; - // Since the dashboard is responsible for index computation, we can compute and assign the indices + // Since the dashboard is responsible for index computation, we can compute and assign the indices // here before telling the panels to refresh this.refresh = function() { if(self.current.index.interval !== 'none') { @@ -626,7 +626,7 @@ angular.module('kibana.services', []) self.current.index.pattern,self.current.index.interval ).then(function (p) { if(p.length > 0) { - self.indices = p; + self.indices = p; } else { //TODO: Option to not failover if(self.current.failover) { @@ -711,7 +711,7 @@ angular.module('kibana.services', []) return true; } else { return false; - } + } }; this.purge_default = function() { @@ -770,7 +770,7 @@ angular.module('kibana.services', []) // Clone object so we can modify it without influencing the existing obejct var save = _.clone(self.current); var id; - + // Change title on object clone if (type === 'dashboard') { id = save.title = _.isUndefined(title) ? self.current.title : title; @@ -783,7 +783,7 @@ angular.module('kibana.services', []) title: save.title, dashboard: angular.toJson(save) }); - + request = type === 'temp' && ttl ? request.ttl(ttl) : request; // TOFIX: Implement error handling here @@ -868,5 +868,79 @@ angular.module('kibana.services', []) return false; }); }; - +}) +.service('timeSeries', function () { + /** + * 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 date start The start time for the result set + * @param date end The end time for the result set + * @param integer interval The length between measurements, in es interval + * notation (1m, 30s, 1h, 15d) + */ + var undef; + function dateToSecondsWithBlankMs(date) { + // return the date as millis since epoch, with 0 millis + return Math.floor(date.getTime() / 1000)*1000; + } + function base10Int(val) { + return parseInt(val, 10); + } + this.ZeroFilled = function (interval, start, end) { + // the expected differenece between readings. + this.interval_ms = parseInt(kbn.interval_to_seconds(interval), 10) * 1000; + // will keep all values here, keyed by their time + this._data = {}; + + if (start) { + this.addValue(start, null); + } + if (end) { + this.addValue(end, null); + } + } + /** + * Add a row + * @param int time The time for the value, in + * @param any value The value at this time + */ + this.ZeroFilled.prototype.addValue = function (time, value) { + if (time instanceof Date) { + time = dateToSecondsWithBlankMs(time); + } else { + time = parseInt(time, 10); + } + if (!isNaN(time)) { + this._data[time] = (value === undef ? 0 : value); + } + }; + /** + * return the rows in the format: + * [ [time, value], [time, value], ... ] + * @return array + */ + this.ZeroFilled.prototype.getFlotPairs = function () { + // var startTime = performance.now(); + var times = _.map(_.keys(this._data), base10Int).sort() + , result = [] + , i + , next + , expected_next; + for(i = 0; i < times.length; i++) { + result.push([ times[i], this._data[times[i]] ]); + next = times[i + 1]; + expected_next = times[i] + this.interval_ms; + for(; times.length > i && next > expected_next; expected_next+= this.interval_ms) { + /** + * since we don't know how the server will round subsequent segments + * we have to recheck for blanks each time. + */ + // this._data[expected_next] = 0; + result.push([expected_next, 0]); + } + } + // console.log(Math.round((performance.now() - startTime)*100)/100, 'ms to get', result.length, 'pairs'); + return result; + }; }); \ No newline at end of file diff --git a/panels/histogram/module.js b/panels/histogram/module.js index 78630e3d373ac..83a88566cb1b8 100644 --- a/panels/histogram/module.js +++ b/panels/histogram/module.js @@ -12,10 +12,10 @@ * 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. + 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 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? @@ -34,7 +34,7 @@ 'use strict'; angular.module('kibana.histogram', []) -.controller('histogram', function($scope, querySrv, dashboard, filterSrv) { +.controller('histogram', function($scope, querySrv, dashboard, filterSrv, timeSeries) { $scope.panelMeta = { editorTabs : [ @@ -56,7 +56,7 @@ angular.module('kibana.histogram', []) }, value_field : null, auto_int : true, - resolution : 100, + resolution : 100, interval : '5m', fill : 0, linewidth : 3, @@ -85,7 +85,39 @@ angular.module('kibana.histogram', []) }; - $scope.get_data = function(segment,query_id) { + /** + * 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 + } + /** + * Fetch the data for a chunk of a queries results. Multiple segments occur when several indicies + * need to be consulted (like timestamped logstash indicies) + * @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 @@ -94,16 +126,16 @@ angular.module('kibana.histogram', []) } - var _range = $scope.range = filterSrv.timeRange('min'); - + 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; - var _segment = _.isUndefined(segment) ? 0 : segment; - var request = $scope.ejs.Request().indices(dashboard.indices[_segment]); + var request = $scope.ejs.Request().indices(dashboard.indices[segment]); $scope.panel.queries.ids = querySrv.idsByMode($scope.panel.queries); // Build the query @@ -114,7 +146,7 @@ angular.module('kibana.histogram', []) ); var facet = $scope.ejs.DateHistogramFacet(id); - + if($scope.panel.mode === 'count') { facet = facet.field($scope.panel.time_field); } else { @@ -124,7 +156,7 @@ angular.module('kibana.histogram', []) } facet = facet.keyField($scope.panel.time_field).valueField($scope.panel.value_field); } - facet = facet.interval($scope.panel.interval).facetFilter($scope.ejs.QueryFilter(query)); + facet = facet.interval(_interval).facetFilter($scope.ejs.QueryFilter(query)); request = request.facet(facet).size(0); }); @@ -137,12 +169,12 @@ angular.module('kibana.histogram', []) // Populate scope when we have results results.then(function(results) { $scope.panelMeta.loading = false; - if(_segment === 0) { + if(segment === 0) { $scope.hits = 0; $scope.data = []; query_id = $scope.query_id = new Date().getTime(); } - + // Check for error and abort if found if(!(_.isUndefined(results.error))) { $scope.panel.error = $scope.parse_error(results.error); @@ -153,49 +185,42 @@ angular.module('kibana.histogram', []) var facetIds = _.map(_.keys(results.facets),function(k){return parseInt(k, 10);}); // Make sure we're still on the same query/queries - if($scope.query_id === query_id && - _.intersection(facetIds,$scope.panel.queries.ids).length === $scope.panel.queries.ids.length - ) { + if($scope.query_id === query_id && _.difference(facetIds, $scope.panel.queries.ids).length === 0) { - var i = 0; - var data, hits; + var i = 0 + , time_series + , hits; _.each($scope.panel.queries.ids, function(id) { - var v = results.facets[id]; - - // Null values at each end of the time range ensure we see entire range - if(_.isUndefined($scope.data[i]) || _segment === 0) { - data = []; - if(filterSrv.idsByType('time').length > 0) { - data = [[_range.from.getTime(), null],[_range.to.getTime(), null]]; - //data = []; - } + var query_results = results.facets[id]; + // 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, + // range may be false + _range && _range.from, + _range && _range.to + ); hits = 0; } else { - data = $scope.data[i].data; + time_series = $scope.data[i].time_series; hits = $scope.data[i].hits; } - // Assemble segments - var segment_data = []; - _.each(v.entries, function(v, k) { - segment_data.push([v.time,v[$scope.panel.mode]]); - hits += v.count; // The series level hits counter - $scope.hits += v.count; // Entire dataset level hits counter + // push each entry into the time series, while incrementing counters + _.each(query_results.entries, function(entry) { + time_series.addValue(entry.time, entry[$scope.panel.mode]); + hits += entry.count; // The series level hits counter + $scope.hits += entry.count; // Entire dataset level hits counter }); - data.splice.apply(data,[1,0].concat(segment_data)); // Join histogram data - - // Create the flot series object - var series = { - data: { - info: querySrv.list[id], - data: data, - hits: hits - }, + $scope.data[i] = { + time_series: time_series, + info: querySrv.list[id], + data: time_series.getFlotPairs(), + hits: hits }; - $scope.data[i] = series.data; - i++; }); @@ -203,10 +228,9 @@ angular.module('kibana.histogram', []) $scope.$emit('render'); // If we still have segments left, get them - if(_segment < dashboard.indices.length-1) { - $scope.get_data(_segment+1,query_id); + if(segment < dashboard.indices.length-1) { + $scope.get_data(segment+1,query_id); } - } }); }; @@ -238,7 +262,7 @@ angular.module('kibana.histogram', []) to:moment.utc(_to), field:$scope.panel.time_field }); - + dashboard.refresh(); }; @@ -248,8 +272,8 @@ angular.module('kibana.histogram', []) $scope.inspector = angular.toJson(JSON.parse(request.toString()),true); }; - $scope.set_refresh = function (state) { - $scope.refresh = state; + $scope.set_refresh = function (state) { + $scope.refresh = state; }; $scope.close_edit = function() { @@ -271,7 +295,7 @@ angular.module('kibana.histogram', []) scope.$on('render',function(){ render_panel(); }); - + // Re-render if the window is resized angular.element(window).bind('resize', function(){ render_panel(); @@ -282,7 +306,7 @@ angular.module('kibana.histogram', []) // 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) { @@ -299,21 +323,21 @@ angular.module('kibana.histogram', []) .script("common/lib/panels/jquery.flot.stack.js") .script("common/lib/panels/jquery.flot.selection.js") .script("common/lib/panels/timezone.js"); - + // Populate element. Note that jvectormap appends, does not replace. scripts.wait(function(){ var stack = scope.panel.stack ? true : null; // Populate element - try { + 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, - fill: scope.panel.fill/10, + lines: { + show: scope.panel.lines, + fill: scope.panel.fill/10, lineWidth: scope.panel.linewidth, steps: false }, @@ -321,10 +345,10 @@ angular.module('kibana.histogram', []) points: { show: scope.panel.points, fill: 1, fillColor: false, radius: 5}, shadowSize: 1 }, - yaxis: { - show: scope.panel['y-axis'], - min: 0, - max: scope.panel.percentage && scope.panel.stack ? 100 : null, + yaxis: { + show: scope.panel['y-axis'], + min: 0, + max: scope.panel.percentage && scope.panel.stack ? 100 : null, }, xaxis: { timezone: scope.panel.timezone, @@ -366,14 +390,15 @@ angular.module('kibana.histogram', []) if(_int >= 60) { return "%H:%M
%m/%d"; } - + return "%H:%M:%S"; } function tt(x, y, contents) { // If the tool tip already exists, don't recreate it, just update it - var tooltip = $('#pie-tooltip').length ? - $('#pie-tooltip') : $('
'); + var tooltip = $('#pie-tooltip').length + ? $('#pie-tooltip') + : $('
'); tooltip.html(contents).css({ position: 'absolute', @@ -393,7 +418,7 @@ angular.module('kibana.histogram', []) tt(pos.pageX, pos.pageY, "
"+ - item.datapoint[1].toFixed(0) + " @ " + + item.datapoint[1].toFixed(0) + " @ " + moment(item.datapoint[0]).format('MM/DD HH:mm:ss')); } else { $("#pie-tooltip").remove(); From c8638e0581a992574ca9796cfd5021cdfe9a0e77 Mon Sep 17 00:00:00 2001 From: Spencer Alger Date: Thu, 22 Aug 2013 14:58:10 -0700 Subject: [PATCH 3/3] moved the timeSeries service into the histogram module. --- js/services.js | 75 -------------------------------------- panels/histogram/module.js | 71 ++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 75 deletions(-) diff --git a/js/services.js b/js/services.js index 9ae25950af42f..10c4de3105416 100644 --- a/js/services.js +++ b/js/services.js @@ -868,79 +868,4 @@ angular.module('kibana.services', []) return false; }); }; -}) -.service('timeSeries', function () { - /** - * 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 date start The start time for the result set - * @param date end The end time for the result set - * @param integer interval The length between measurements, in es interval - * notation (1m, 30s, 1h, 15d) - */ - var undef; - function dateToSecondsWithBlankMs(date) { - // return the date as millis since epoch, with 0 millis - return Math.floor(date.getTime() / 1000)*1000; - } - function base10Int(val) { - return parseInt(val, 10); - } - this.ZeroFilled = function (interval, start, end) { - // the expected differenece between readings. - this.interval_ms = parseInt(kbn.interval_to_seconds(interval), 10) * 1000; - // will keep all values here, keyed by their time - this._data = {}; - - if (start) { - this.addValue(start, null); - } - if (end) { - this.addValue(end, null); - } - } - /** - * Add a row - * @param int time The time for the value, in - * @param any value The value at this time - */ - this.ZeroFilled.prototype.addValue = function (time, value) { - if (time instanceof Date) { - time = dateToSecondsWithBlankMs(time); - } else { - time = parseInt(time, 10); - } - if (!isNaN(time)) { - this._data[time] = (value === undef ? 0 : value); - } - }; - /** - * return the rows in the format: - * [ [time, value], [time, value], ... ] - * @return array - */ - this.ZeroFilled.prototype.getFlotPairs = function () { - // var startTime = performance.now(); - var times = _.map(_.keys(this._data), base10Int).sort() - , result = [] - , i - , next - , expected_next; - for(i = 0; i < times.length; i++) { - result.push([ times[i], this._data[times[i]] ]); - next = times[i + 1]; - expected_next = times[i] + this.interval_ms; - for(; times.length > i && next > expected_next; expected_next+= this.interval_ms) { - /** - * since we don't know how the server will round subsequent segments - * we have to recheck for blanks each time. - */ - // this._data[expected_next] = 0; - result.push([expected_next, 0]); - } - } - // console.log(Math.round((performance.now() - startTime)*100)/100, 'ms to get', result.length, 'pairs'); - return result; - }; }); \ No newline at end of file diff --git a/panels/histogram/module.js b/panels/histogram/module.js index 83a88566cb1b8..90de03723366b 100644 --- a/panels/histogram/module.js +++ b/panels/histogram/module.js @@ -436,4 +436,75 @@ angular.module('kibana.histogram', []) }); } }; +}) +.service('timeSeries', function () { + /** + * 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 date start The start time for the result set + * @param date end The end time for the result set + * @param integer interval The length between measurements, in es interval + * notation (1m, 30s, 1h, 15d) + */ + var undef; + function base10Int(val) { + return parseInt(val, 10); + } + this.ZeroFilled = function (interval, start, end) { + // the expected differenece between readings. + this.interval_ms = base10Int(kbn.interval_to_seconds(interval)) * 1000; + // will keep all values here, keyed by their time + this._data = {}; + + if (start) { + this.addValue(start, null); + } + if (end) { + this.addValue(end, null); + } + } + /** + * Add a row + * @param int time The time for the value, in + * @param any value The value at this time + */ + this.ZeroFilled.prototype.addValue = function (time, value) { + if (time instanceof Date) { + time = Math.floor(time.getTime() / 1000)*1000; + } else { + time = base10Int(time); + } + if (!isNaN(time)) { + this._data[time] = (value === undef ? 0 : value); + } + }; + /** + * return the rows in the format: + * [ [time, value], [time, value], ... ] + * @return array + */ + this.ZeroFilled.prototype.getFlotPairs = function () { + // var startTime = performance.now(); + var times = _.map(_.keys(this._data), base10Int).sort() + , result = [] + , i + , next + , expected_next; + for(i = 0; i < times.length; i++) { + result.push([ times[i], this._data[times[i]] ]); + next = times[i + 1]; + expected_next = times[i] + this.interval_ms; + for(; times.length > i && next > expected_next; expected_next+= this.interval_ms) { + /** + * since we don't know how the server will round subsequent segments + * we have to recheck for blanks each time. + */ + // this._data[expected_next] = 0; + result.push([expected_next, 0]); + } + } + // console.log(Math.round((performance.now() - startTime)*100)/100, 'ms to get', result.length, 'pairs'); + return result; + }; }); \ No newline at end of file