From b61ce057a334d760e966427197b3b049da0e8aa6 Mon Sep 17 00:00:00 2001 From: Rashid Khan Date: Fri, 23 Aug 2013 08:53:27 -0700 Subject: [PATCH 1/3] Adding grunt step to CONTRIBUTING.md --- CONTRIBUTING.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e57fe84b11c..e0a3aa85af9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,10 +7,12 @@ The process for contributing to any of the Elasticsearch repositories is similar 1. Sign the contributor license agreement Please make sure you have signed the [Contributor License Agreement](http://www.elasticsearch.org/contributor-agreement/). We are not asking you to assign copyright to us, but to give us the right to distribute your code without restriction. We ask this of all contributors in order to assure our users of the origin and continuing existence of the code. You only need to sign the CLA once. -2. Rebase your changes +2. Run the grunt build process and ensure it completes without errors with your changes. + +3. Rebase your changes Update your local repository with the most recent code from the main Kibana repository, and rebase your branch on top of the latest master branch. We prefer your changes to be squashed into a single commit. -3. Submit a pull request +4. Submit a pull request Push your local changes to your forked copy of the repository and submit a pull request. In the pull request, describe what your changes do and mention the number of the issue where discussion has taken place, eg “Closes #123″. Then sit back and wait. There will probably be discussion about the pull request and, if any changes are needed, we would love to work with you to get your pull request merged into Kibana. From 85fd491b136bc8e60ae3b46aedf4190b21413a03 Mon Sep 17 00:00:00 2001 From: Rashid Khan Date: Mon, 26 Aug 2013 10:21:28 -0700 Subject: [PATCH 2/3] Fixed subsecond intervals in interval_to_seconds --- js/shared.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/shared.js b/js/shared.js index 217289ba09b..94c09d3f30d 100644 --- a/js/shared.js +++ b/js/shared.js @@ -192,7 +192,7 @@ // histogram & trends kbn.interval_to_seconds = function(string) { - var matches = string.match(/(\d+)([Mwdhmsy])/); + var matches = string.match(/(\d+(?:\.\d+)?)([Mwdhmsy])/); switch (matches[2]) { case 'y': return matches[1]*31536000; From 196d39e462014f250aeba69dc52a7232835d9fd0 Mon Sep 17 00:00:00 2001 From: Rashid Khan Date: Mon, 26 Aug 2013 15:32:53 -0700 Subject: [PATCH 3/3] squashing merge --- panels/histogram/module.js | 198 +++++++++++++++++++++++++++---------- 1 file changed, 145 insertions(+), 53 deletions(-) diff --git a/panels/histogram/module.js b/panels/histogram/module.js index 5593e6d9e90..fe7de099773 100644 --- a/panels/histogram/module.js +++ b/panels/histogram/module.js @@ -108,11 +108,17 @@ angular.module('kibana.histogram', []) $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 + * + * The results of this function are stored on the scope's data property. This property will be an + * array of objects with the properties info, time_series, and hits. These objects are used in the + * render_panel function to create the historgram. + * + * @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) { @@ -197,12 +203,12 @@ angular.module('kibana.histogram', []) // 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 - ); + 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; @@ -216,9 +222,8 @@ angular.module('kibana.histogram', []) $scope.hits += entry.count; // Entire dataset level hits counter }); $scope.data[i] = { - time_series: time_series, info: querySrv.list[id], - data: time_series.getFlotPairs(), + time_series: time_series, hits: hits }; @@ -310,7 +315,7 @@ angular.module('kibana.histogram', []) // Populate from the query service try { - _.each(scope.data,function(series) { + _.each(scope.data, function(series) { series.label = series.info.alias; series.color = series.info.color; }); @@ -383,6 +388,19 @@ angular.module('kibana.histogram', []) 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.panel.bars && stack) { + required_times = Array.prototype.concat.apply([], _.map(scope.data, function (series) { + return series.time_series.getOrderedTimes(); + })); + } + + for (var i = 0; i < scope.data.length; i++) { + scope.data[i].data = scope.data[i].time_series.getFlotPairs(required_times); + } + scope.plot = $.plot(elem, scope.data, options); } catch(e) { @@ -448,36 +466,53 @@ angular.module('kibana.histogram', []) }; }) .service('timeSeries', function () { + // map compatable parseInt + function base10Int(val) { + return parseInt(val, 10); + } + /** * 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) + * @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. */ - var undef; - function base10Int(val) { - return parseInt(val, 10); - } - this.ZeroFilled = function (interval, start, end) { + this.ZeroFilled = function (opts) { + this.opts = _.defaults(opts, { + interval: '10m', + start_date: null, + end_date: null, + fill_style: 'minimal' + }); + // the expected differenece between readings. - this.interval_ms = base10Int(kbn.interval_to_seconds(interval)) * 1000; + this.interval_ms = base10Int(kbn.interval_to_seconds(opts.interval)) * 1000; + // will keep all values here, keyed by their time this._data = {}; - if (start) { - this.addValue(start, null); + if (opts.start_date) { + this.addValue(opts.start_date, null); } - if (end) { - this.addValue(end, null); + if (opts.end_date) { + this.addValue(opts.end_date, null); } }; + /** * Add a row - * @param int time The time for the value, in - * @param any value The value at this time + * @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) { @@ -486,44 +521,101 @@ angular.module('kibana.histogram', []) time = base10Int(time); } if (!isNaN(time)) { - this._data[time] = (value === undef ? 0 : value); + this._data[time] = (_.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. + */ + this.ZeroFilled.prototype.getOrderedTimes = function (include) { + var times = _.map(_.keys(this._data), base10Int).sort(); + if (_.isArray(include)) { + times = times.concat(include); + } + return times; + }; + /** * return the rows in the format: * [ [time, value], [time, value], ... ] - * @return array + * + * 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} */ - this.ZeroFilled.prototype.getFlotPairs = function () { - // var startTime = performance.now(); - var times = _.map(_.keys(this._data), base10Int).sort(), - result = []; - _.each(times, function (time, i, times) { - var next, expected_next, prev, expected_prev; + this.ZeroFilled.prototype.getFlotPairs = function (required_times) { + var times = this.getOrderedTimes(required_times), + strategy, + pairs; - // check for previous measurement - if (i > 0) { - prev = times[i - 1]; - expected_prev = time - this.interval_ms; - if (prev < expected_prev) { - result.push([expected_prev, 0]); - } + if(this.opts.fill_style === 'all') { + strategy = this._getAllFlotPairs; + } else { + strategy = this._getMinFlotPairs; + } + + return _.reduce( + times, // what + strategy, // how + [], // where + this // context + ); + }; + + /** + * ** 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 + */ + this.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 = time - this.interval_ms; + if (prev < expected_prev) { + result.push([expected_prev, 0]); } + } - // add the current time - result.push([ time, this._data[time] ]); + // 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 = time + this.interval_ms; - if (next > expected_next) { - result.push([expected_next, 0]); - } + // check for next measurement + if (times.length > i) { + next = times[i + 1]; + expected_next = time + this.interval_ms; + if (next > expected_next) { + result.push([expected_next, 0]); } + } - }, this); - // console.log(Math.round((performance.now() - startTime)*100)/100, 'ms to get', result.length, 'pairs'); 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 + */ + this.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 = times[i] + this.interval_ms; + for(; times.length > i && next > expected_next; expected_next+= this.interval_ms) { + result.push([expected_next, 0]); + } + + return result; + }; + }); \ No newline at end of file