Add support for date math in Timelion's .movingaverage() (#11555)

* Add support for date math to movingaverage

* Fix excess phase shift and dropped point in .movingaverage()

* Change help language, use if() instead of self executing function
This commit is contained in:
Rashid Khan 2017-05-12 14:43:11 -07:00 committed by GitHub
parent 11581ae136
commit 01e54022ad
2 changed files with 59 additions and 16 deletions

View file

@ -1,33 +1,59 @@
const filename = require('path').basename(__filename);
const fn = require(`../${filename}`);
import _ from 'lodash';
const expect = require('chai').expect;
import moment from 'moment';
import _ from 'lodash';
import buckets from './fixtures/bucketList';
import getSeries from './helpers/get_series';
import getSeriesList from './helpers/get_series_list';
import invoke from './helpers/invoke_series_fn.js';
function getFivePointSeries() {
return getSeriesList([
getSeries('Five', [].concat(buckets).push(moment('1984-01-01T00:00:00.000Z')), [10, 20, 30, 40, 50]),
]);
}
describe(filename, () => {
let seriesList;
beforeEach(() => {
seriesList = require('./fixtures/seriesList.js')();
seriesList = getFivePointSeries();
});
it('centers the averaged series by default', () => {
return invoke(fn, [seriesList, 2]).then((r) => {
expect(_.map(r.output.list[1].data, 1)).to.eql([null, 75, 50, null]);
return invoke(fn, [seriesList, 3]).then((r) => {
expect(_.map(r.output.list[0].data, 1)).to.eql([null, 20, 30, 40, null]);
});
});
it('aligns the moving average to the left', () => {
return invoke(fn, [seriesList, 2, 'left']).then((r) => {
expect(_.map(r.output.list[1].data, 1)).to.eql([null, null, 75, 50]);
return invoke(fn, [seriesList, 3, 'left']).then((r) => {
expect(_.map(r.output.list[0].data, 1)).to.eql([null, null, 20, 30, 40]);
});
});
it('aligns the moving average to the right', () => {
return invoke(fn, [seriesList, 2, 'right']).then((r) => {
expect(_.map(r.output.list[1].data, 1)).to.eql([75, 50, null, null]);
return invoke(fn, [seriesList, 3, 'right']).then((r) => {
expect(_.map(r.output.list[0].data, 1)).to.eql([20, 30, 40, null, null]);
});
});
describe('date math', () => {
it('accepts 2 years', () => {
return invoke(fn, [seriesList, '2y', 'left']).then((r) => {
expect(_.map(r.output.list[0].data, 1)).to.eql([null, 15, 25, 35, 45]);
});
});
it('accepts 3 years', () => {
return invoke(fn, [seriesList, '3y', 'left']).then((r) => {
expect(_.map(r.output.list[0].data, 1)).to.eql([null, null, 20, 30, 40]);
});
});
});
});

View file

@ -1,6 +1,8 @@
import alter from '../lib/alter.js';
import _ from 'lodash';
import Chainable from '../lib/classes/chainable';
import toMS from '../lib/to_milliseconds.js';
module.exports = new Chainable('movingaverage', {
args: [
{
@ -9,8 +11,10 @@ module.exports = new Chainable('movingaverage', {
},
{
name: 'window',
types: ['number'],
help: 'Number of points to average over'
types: ['number', 'string'],
help: 'Number of points, or a date math expression (eg 1d, 1M) to average over. ' +
'If a date math expression is specified, the function will get as close as possible given the currently select interval' +
'If the date math expression is not evenly divisible by the interval the results may appear abnormal.'
},
{
name: 'position',
@ -20,9 +24,21 @@ module.exports = new Chainable('movingaverage', {
],
aliases: ['mvavg'],
help: 'Calculate the moving average over a given window. Nice for smoothing noisey series',
fn: function movingaverageFn(args) {
fn: function movingaverageFn(args, tlConfig) {
return alter(args, function (eachSeries, _window, _position) {
// _window always needs to be a number, if isn't we have to make it into one.
if (typeof _window !== 'number') {
// Ok, I guess its a datemath expression
const windowMilliseconds = toMS(_window);
// calculate how many buckets that _window represents
const intervalMilliseconds = toMS(tlConfig.time.interval);
// Round, floor, ceil? We're going with round because it splits the difference.
_window = Math.round(windowMilliseconds / intervalMilliseconds) || 1;
}
_position = _position || 'center';
const validPositions = ['left', 'right', 'center'];
if (!_.contains(validPositions, _position)) throw new Error('Valid positions are: ' + validPositions.join(', '));
@ -44,18 +60,19 @@ module.exports = new Chainable('movingaverage', {
const windowLeft = Math.floor(_window / 2);
const windowRight = _window - windowLeft;
eachSeries.data = _.map(pairs, function (point, i) {
if (i < windowLeft || i >= pairsLen - windowRight) return [point[0], null];
if (i < windowLeft || i > pairsLen - windowRight) return [point[0], null];
return toPoint(point, pairs.slice(i - windowLeft, i + windowRight));
});
} else if (_position === 'left') {
eachSeries.data = _.map(pairs, function (point, i) {
if (i < _window) return [point[0], null];
return toPoint(point, pairs.slice(i - _window, i));
const cursor = i + 1;
if (cursor < _window) return [point[0], null];
return toPoint(point, pairs.slice(cursor - _window , cursor));
});
} else if (_position === 'right') {
eachSeries.data = _.map(pairs, function (point, i) {
if (i >= pairsLen - _window) return [point[0], null];
if (i > pairsLen - _window) return [point[0], null];
return toPoint(point, pairs.slice(i , i + _window));
});