Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 3 commits into from May 12, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -1,33 +1,59 @@
const filename = require('path').basename(__filename);
const fn = require(`../${filename}`);
const expect = require('chai').expect;

import moment from 'moment';
import _ from 'lodash';
const expect = require('chai').expect;
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]);
});
});
});


});
31 changes: 24 additions & 7 deletions src/core_plugins/timelion/server/series_functions/movingaverage.js
@@ -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: [
{
Expand All @@ -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',
Expand All @@ -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(', '));
Expand All @@ -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));
});

Expand Down