Skip to content

Commit af7c0c1

Browse files
committed
Merge pull request #935 from g76r/f_intergralbyinterval
adding an integralByInterval() function
2 parents 053060f + 0b94cdc commit af7c0c1

File tree

4 files changed

+61
-3
lines changed

4 files changed

+61
-3
lines changed

webapp/content/js/composer_widgets.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1089,6 +1089,7 @@ function createFunctionsMenu() {
10891089
{text: 'Square Root', handler: applyFuncToEach('squareRoot')},
10901090
{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})},
10911091
{text: 'Integral', handler: applyFuncToEach('integral')},
1092+
{text: 'Integral by Interval', handler: applyFuncToEachWithInput('integralByInterval', 'Integral this metric with a reset every ___ (examples: 1d, 1h, 10min)', {quote: true})},
10921093
{text: 'Percentile Values', handler: applyFuncToEachWithInput('percentileOfSeries', "Please enter the percentile to use")},
10931094
{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})},
10941095
{text: 'Log', handler: applyFuncToEachWithInput('log', 'Please enter a base')},

webapp/graphite/render/functions.py

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
from graphite.logger import log
2525
from graphite.render.attime import parseTimeOffset, parseATTime
2626
from graphite.events import models
27-
from graphite.util import epoch
27+
from graphite.util import epoch, timestamp, deltaseconds
2828

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

36-
3736
NAN = float('NaN')
3837
INF = float('inf')
3938
DAY = 86400
@@ -1121,6 +1120,47 @@ def integral(requestContext, seriesList):
11211120
results.append(newSeries)
11221121
return results
11231122

1123+
1124+
def integralByInterval(requestContext, seriesList, intervalUnit):
1125+
"""
1126+
This will do the same as integral() funcion, except resetting the total to 0
1127+
at the given time in the parameter "from"
1128+
Useful for finding totals per hour/day/week/..
1129+
1130+
Example:
1131+
1132+
.. code-block:: none
1133+
1134+
&target=integralByInterval(company.sales.perMinute, "1d")&from=midnight-10days
1135+
1136+
This would start at zero on the left side of the graph, adding the sales each
1137+
minute, and show the evolution of sales per day during the last 10 days.
1138+
"""
1139+
intervalDuration = int(abs(deltaseconds(parseTimeOffset(intervalUnit))))
1140+
startTime = int(timestamp(requestContext['startTime']))
1141+
results = []
1142+
for series in seriesList:
1143+
newValues = []
1144+
currentTime = series.start # current time within series iteration
1145+
current = 0.0 # current accumulated value
1146+
for val in series:
1147+
# reset integral value if crossing an interval boundary
1148+
if (currentTime - startTime)/intervalDuration != (currentTime - startTime - series.step)/intervalDuration:
1149+
current = 0.0
1150+
if val is None:
1151+
# keep previous value since val can be None when resetting current to 0.0
1152+
newValues.append(current)
1153+
else:
1154+
current += val
1155+
newValues.append(current)
1156+
currentTime += series.step
1157+
newName = "integralByInterval(%s,'%s')" % (series.name, intervalUnit)
1158+
newSeries = TimeSeries(newName, series.start, series.end, series.step, newValues)
1159+
newSeries.pathExpression = newName
1160+
results.append(newSeries)
1161+
return results
1162+
1163+
11241164
def nonNegativeDerivative(requestContext, seriesList, maxValue=None):
11251165
"""
11261166
Same as the derivative function above, but ignores datapoints that trend
@@ -3523,6 +3563,7 @@ def pieMinimum(requestContext, series):
35233563
'pow': pow,
35243564
'perSecond': perSecond,
35253565
'integral': integral,
3566+
'integralByInterval' : integralByInterval,
35263567
'nonNegativeDerivative': nonNegativeDerivative,
35273568
'log': logarithm,
35283569
'invert': invert,

webapp/graphite/util.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,10 @@ def timestamp(datetime):
152152
"Convert a datetime object into epoch time"
153153
return time.mktime( datetime.timetuple() )
154154

155+
def deltaseconds(timedelta):
156+
"Convert a timedelta object into seconds (same as timedelta.total_seconds() in Python 2.7+)"
157+
return (timedelta.microseconds + (timedelta.seconds + timedelta.days * 24 * 3600) * 10**6) / 10**6
158+
155159
# This whole song & dance is due to pickle being insecure
156160
# The SafeUnpickler classes were largely derived from
157161
# http://nadiana.com/python-pickle-insecure

webapp/tests/test_functions.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import copy
22
import math
33
import pytz
4-
from datetime import datetime
54
from fnmatch import fnmatch
65

76
from django.test import TestCase
87
from django.conf import settings
98
from mock import patch, call, MagicMock
9+
from datetime import datetime
1010

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

63+
def test_integral(self):
64+
seriesList = [TimeSeries('test', 0, 600, 60, [None, 1, 2, 3, 4, 5, None, 6, 7, 8])]
65+
expected = [TimeSeries('integral(test)', 0, 600, 60, [None, 1, 3, 6, 10, 15, None, 21, 28, 36])]
66+
result = functions.integral({}, seriesList)
67+
self.assertEqual(expected, result, 'integral result incorrect')
68+
69+
def test_integralByInterval(self):
70+
seriesList = [TimeSeries('test', 0, 600, 60, [None, 1, 2, 3, 4, 5, None, 6, 7, 8])]
71+
expected = [TimeSeries("integral(test,'2min')", 0, 600, 60, [0, 1, 2, 5, 4, 9, 0, 6, 7, 15])]
72+
result = functions.integralByInterval({'startTime' : datetime(1970,1,1)}, seriesList, '2min')
73+
self.assertEqual(expected, result, 'integralByInterval result incorrect %s %s' %(result, result[0]))
74+
6375
def test_n_percentile(self):
6476
seriesList = []
6577
config = [

0 commit comments

Comments
 (0)