define([ './interval', 'lodash' ], 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; }; /** * 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|Default|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 if(this.opts.fill_style === 'null') { strategy = this._getNullFlotPairs; } 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. // Removing, flot 0.8.1's max/min params satisfy this /* 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; }; /** * ** called as a reduce stragegy in getFlotPairs() ** * Same as min, but fills with nulls * @return {array} An array of points to plot with flot */ ts.ZeroFilled.prototype._getNullFlotPairs = 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, null]); } } // add the current time result.push([ time, this._data[time] || null]); // 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, null]); } } return result; }; return ts; });