define([ 'angular', 'lodash', 'moment', './utils' ], function (angular, _, moment, utils) { 'use strict'; var module = angular.module('grafana.services'); module.service('DataProcessingService', function() { var self = this; /** * Downsample datapoints series */ this.downsampleSeries = function(datapoints, time_to, ms_interval, func) { var downsampledSeries = []; var timeWindow = { from: time_to * 1000 - ms_interval, to: time_to * 1000 }; var points_sum = 0; var points_num = 0; var value_avg = 0; var frame = []; for (var i = datapoints.length - 1; i >= 0; i -= 1) { if (timeWindow.from < datapoints[i][1] && datapoints[i][1] <= timeWindow.to) { points_sum += datapoints[i][0]; points_num++; frame.push(datapoints[i][0]); } else { value_avg = points_num ? points_sum / points_num : 0; if (func === "max") { downsampledSeries.push([_.max(frame), timeWindow.to]); } else if (func === "min") { downsampledSeries.push([_.min(frame), timeWindow.to]); } // avg by default else { downsampledSeries.push([value_avg, timeWindow.to]); } // Shift time window timeWindow.to = timeWindow.from; timeWindow.from -= ms_interval; points_sum = 0; points_num = 0; frame = []; // Process point again i++; } } return downsampledSeries.reverse(); }; /** * Group points by given time interval * datapoints: [[, ], ...] */ this.groupBy = function(interval, groupByCallback, datapoints) { var ms_interval = utils.parseInterval(interval); // Calculate frame timestamps var min_timestamp = datapoints[0][1]; var frames = _.groupBy(datapoints, function(point) { var group_time = Math.floor(point[1] / ms_interval) * ms_interval; // Prevent points outside of time range if (group_time < min_timestamp) { group_time = min_timestamp; } return group_time; }); // frame: { '': [[, ], ...] } // return [{ '': }, { '': }, ...] var grouped = _.mapValues(frames, function(frame) { var points = _.map(frame, function(point) { return point[0]; }); return groupByCallback(points); }); // Convert points to Grafana format return sortByTime(_.map(grouped, function(value, timestamp) { return [Number(value), Number(timestamp)]; })); }; this.sumSeries = function(timeseries) { // Calculate new points for interpolation var new_timestamps = _.uniq(_.map(_.flatten(timeseries, true), function(point) { return point[1]; })); new_timestamps = _.sortBy(new_timestamps); var interpolated_timeseries = _.map(timeseries, function(series) { var timestamps = _.map(series, function(point) { return point[1]; }); var new_points = _.map(_.difference(new_timestamps, timestamps), function(timestamp) { return [null, timestamp]; }); var new_series = series.concat(new_points); return sortByTime(new_series); }); _.each(interpolated_timeseries, interpolateSeries); var new_timeseries = []; var sum; for (var i = new_timestamps.length - 1; i >= 0; i--) { sum = 0; for (var j = interpolated_timeseries.length - 1; j >= 0; j--) { sum += interpolated_timeseries[j][i][0]; } new_timeseries.push([sum, new_timestamps[i]]); } return sortByTime(new_timeseries); }; function sortByTime(series) { return _.sortBy(series, function(point) { return point[1]; }); } /** * Interpolate series with gaps */ function interpolateSeries(series) { var left, right; // Interpolate series for (var i = series.length - 1; i >= 0; i--) { if (!series[i][0]) { left = findNearestLeft(series, series[i]); right = findNearestRight(series, series[i]); if (!left) { left = right; } if (!right) { right = left; } series[i][0] = linearInterpolation(series[i][1], left, right); } } return series; } function linearInterpolation(timestamp, left, right) { if (left[1] === right[1]) { return (left[0] + right[0]) / 2; } else { return (left[0] + (right[0] - left[0]) / (right[1] - left[1]) * (timestamp - left[1])); } } function findNearestRight(series, point) { var point_index = _.indexOf(series, point); var nearestRight; for (var i = point_index; i < series.length; i++) { if (series[i][0]) { return series[i]; } } return nearestRight; } function findNearestLeft(series, point) { var point_index = _.indexOf(series, point); var nearestLeft; for (var i = point_index; i > 0; i--) { if (series[i][0]) { return series[i]; } } return nearestLeft; } this.AVERAGE = function(values) { var sum = 0; _.each(values, function(value) { sum += value; }); return sum / values.length; }; this.MIN = function(values) { return _.min(values); }; this.MAX = function(values) { return _.max(values); }; this.MEDIAN = function(values) { var sorted = _.sortBy(values); return sorted[Math.floor(sorted.length / 2)]; }; this.setAlias = function(alias, timeseries) { timeseries.target = alias; return timeseries; }; this.aggregationFunctions = { avg: this.AVERAGE, min: this.MIN, max: this.MAX, median: this.MEDIAN }; this.groupByWrapper = function(interval, groupFunc, datapoints) { var groupByCallback = self.aggregationFunctions[groupFunc]; return self.groupBy(interval, groupByCallback, datapoints); }; this.aggregateWrapper = function(groupByCallback, interval, datapoints) { var flattenedPoints = _.flatten(datapoints, true); return self.groupBy(interval, groupByCallback, flattenedPoints); }; this.metricFunctions = { groupBy: this.groupByWrapper, average: _.partial(this.aggregateWrapper, this.AVERAGE), min: _.partial(this.aggregateWrapper, this.MIN), max: _.partial(this.aggregateWrapper, this.MAX), median: _.partial(this.aggregateWrapper, this.MEDIAN), sumSeries: this.sumSeries, setAlias: this.setAlias, }; }); });