Skip to content

Commit

Permalink
Merge pull request #935 from g76r/f_intergralbyinterval
Browse files Browse the repository at this point in the history
adding an integralByInterval() function
  • Loading branch information
obfuscurity committed May 18, 2016
2 parents 053060f + 0b94cdc commit af7c0c1
Show file tree
Hide file tree
Showing 4 changed files with 61 additions and 3 deletions.
1 change: 1 addition & 0 deletions webapp/content/js/composer_widgets.js
Original file line number Diff line number Diff line change
Expand Up @@ -1089,6 +1089,7 @@ function createFunctionsMenu() {
{text: 'Square Root', handler: applyFuncToEach('squareRoot')},
{text: 'Time-adjusted Derivative', handler: applyFuncToEachWithInput('perSecond', "Please enter a maximum value if this metric is a wrapping counter (or just leave this blank)", {allowBlank: true})},
{text: 'Integral', handler: applyFuncToEach('integral')},
{text: 'Integral by Interval', handler: applyFuncToEachWithInput('integralByInterval', 'Integral this metric with a reset every ___ (examples: 1d, 1h, 10min)', {quote: true})},
{text: 'Percentile Values', handler: applyFuncToEachWithInput('percentileOfSeries', "Please enter the percentile to use")},
{text: 'Non-negative Derivative', handler: applyFuncToEachWithInput('nonNegativeDerivative', "Please enter a maximum value if this metric is a wrapping counter (or just leave this blank)", {allowBlank: true})},
{text: 'Log', handler: applyFuncToEachWithInput('log', 'Please enter a base')},
Expand Down
45 changes: 43 additions & 2 deletions webapp/graphite/render/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
from graphite.logger import log
from graphite.render.attime import parseTimeOffset, parseATTime
from graphite.events import models
from graphite.util import epoch
from graphite.util import epoch, timestamp, deltaseconds

# XXX format_units() should go somewhere else
if environ.get('READTHEDOCS'):
Expand All @@ -33,7 +33,6 @@
from graphite.render.glyph import format_units
from graphite.render.datalib import TimeSeries


NAN = float('NaN')
INF = float('inf')
DAY = 86400
Expand Down Expand Up @@ -1121,6 +1120,47 @@ def integral(requestContext, seriesList):
results.append(newSeries)
return results


def integralByInterval(requestContext, seriesList, intervalUnit):
"""
This will do the same as integral() funcion, except resetting the total to 0
at the given time in the parameter "from"
Useful for finding totals per hour/day/week/..
Example:
.. code-block:: none
&target=integralByInterval(company.sales.perMinute, "1d")&from=midnight-10days
This would start at zero on the left side of the graph, adding the sales each
minute, and show the evolution of sales per day during the last 10 days.
"""
intervalDuration = int(abs(deltaseconds(parseTimeOffset(intervalUnit))))
startTime = int(timestamp(requestContext['startTime']))
results = []
for series in seriesList:
newValues = []
currentTime = series.start # current time within series iteration
current = 0.0 # current accumulated value
for val in series:
# reset integral value if crossing an interval boundary
if (currentTime - startTime)/intervalDuration != (currentTime - startTime - series.step)/intervalDuration:
current = 0.0
if val is None:
# keep previous value since val can be None when resetting current to 0.0
newValues.append(current)
else:
current += val
newValues.append(current)
currentTime += series.step
newName = "integralByInterval(%s,'%s')" % (series.name, intervalUnit)
newSeries = TimeSeries(newName, series.start, series.end, series.step, newValues)
newSeries.pathExpression = newName
results.append(newSeries)
return results


def nonNegativeDerivative(requestContext, seriesList, maxValue=None):
"""
Same as the derivative function above, but ignores datapoints that trend
Expand Down Expand Up @@ -3523,6 +3563,7 @@ def pieMinimum(requestContext, series):
'pow': pow,
'perSecond': perSecond,
'integral': integral,
'integralByInterval' : integralByInterval,
'nonNegativeDerivative': nonNegativeDerivative,
'log': logarithm,
'invert': invert,
Expand Down
4 changes: 4 additions & 0 deletions webapp/graphite/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,10 @@ def timestamp(datetime):
"Convert a datetime object into epoch time"
return time.mktime( datetime.timetuple() )

def deltaseconds(timedelta):
"Convert a timedelta object into seconds (same as timedelta.total_seconds() in Python 2.7+)"
return (timedelta.microseconds + (timedelta.seconds + timedelta.days * 24 * 3600) * 10**6) / 10**6

# This whole song & dance is due to pickle being insecure
# The SafeUnpickler classes were largely derived from
# http://nadiana.com/python-pickle-insecure
Expand Down
14 changes: 13 additions & 1 deletion webapp/tests/test_functions.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import copy
import math
import pytz
from datetime import datetime
from fnmatch import fnmatch

from django.test import TestCase
from django.conf import settings
from mock import patch, call, MagicMock
from datetime import datetime

from graphite.render.datalib import TimeSeries
from graphite.render import functions
Expand Down Expand Up @@ -60,6 +60,18 @@ def testGetPercentile(self):
result = functions._getPercentile(series, 30)
self.assertEqual(expected, result, 'For series index <%s> the 30th percentile ordinal is not %d, but %d ' % (index, expected, result))

def test_integral(self):
seriesList = [TimeSeries('test', 0, 600, 60, [None, 1, 2, 3, 4, 5, None, 6, 7, 8])]
expected = [TimeSeries('integral(test)', 0, 600, 60, [None, 1, 3, 6, 10, 15, None, 21, 28, 36])]
result = functions.integral({}, seriesList)
self.assertEqual(expected, result, 'integral result incorrect')

def test_integralByInterval(self):
seriesList = [TimeSeries('test', 0, 600, 60, [None, 1, 2, 3, 4, 5, None, 6, 7, 8])]
expected = [TimeSeries("integral(test,'2min')", 0, 600, 60, [0, 1, 2, 5, 4, 9, 0, 6, 7, 15])]
result = functions.integralByInterval({'startTime' : datetime(1970,1,1)}, seriesList, '2min')
self.assertEqual(expected, result, 'integralByInterval result incorrect %s %s' %(result, result[0]))

def test_n_percentile(self):
seriesList = []
config = [
Expand Down

0 comments on commit af7c0c1

Please sign in to comment.