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 functions resample() and smooth() #92

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions webapp/content/js/composer_widgets.js
Original file line number Diff line number Diff line change
Expand Up @@ -917,7 +917,8 @@ function createFunctionsMenu() {
{text: 'Absolute Value', handler: applyFuncToEach('absolute')},
{text: 'timeShift', handler: applyFuncToEachWithInput('timeShift', 'Shift this metric ___ back in time (examples: 10min, 7d, 2w)', {quote: true})},
{text: 'Summarize', handler: applyFuncToEachWithInput('summarize', 'Please enter a summary interval (examples: 10min, 1h, 7d)', {quote: true})},
{text: 'Hit Count', handler: applyFuncToEachWithInput('hitcount', 'Please enter a summary interval (examples: 10min, 1h, 7d)', {quote: true})}
{text: 'Hit Count', handler: applyFuncToEachWithInput('hitcount', 'Please enter a summary interval (examples: 10min, 1h, 7d)', {quote: true})},
{text: 'Smooth', handler: applyFuncToEachWithInput('smooth', 'Please enter the number of pixels to average for each potin')}
]
}, {
text: 'Calculate',
Expand All @@ -930,7 +931,8 @@ function createFunctionsMenu() {
{text: 'Holt-Winters Aberration', handler: applyFuncToEach('holtWintersAberration')},
{text: 'As Percent', handler: applyFuncToEachWithInput('asPercent', 'Please enter the value that corresponds to 100% or leave blank to use the total', {allowBlank: true})},
{text: 'Difference (of 2 series)', handler: applyFuncToAll('diffSeries')},
{text: 'Ratio (of 2 series)', handler: applyFuncToAll('divideSeries')}
{text: 'Ratio (of 2 series)', handler: applyFuncToAll('divideSeries')},
{text: 'Resample', handler: applyFuncToEachWithInput('resample', 'Please enter the desired points-per-pixel for the new resolution')}
]
}, {
text: 'Filter',
Expand Down
108 changes: 108 additions & 0 deletions webapp/graphite/render/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -645,6 +645,112 @@ def consolidateBy(requestContext, seriesList, consolidationFunc):
series.name = 'consolidateBy(%s,"%s")' % (series.name, series.consolidationFunc)
return seriesList


def resample(requestContext, seriesList, pointsPerPx = 1):
"""
Resamples the given series according to the requested graph width and
$pointsPerPx aggregating by average. Total number of points after this
function == graph width * pointsPerPx.

This has two significant uses:

* Drastically speeds up render time when graphing high resolution data
or many metrics.
* Allows movingAverage() to have a consistent smoothness across timescales.
* Example: movingAverage(resample(metric,2),20) would end up with
a 10px moving average no matter what the scale of your graph.
* Allows a consistent number-of-samples to be returned from JSON requests
* the number of samples returned == graph width * points per pixel

Example:

.. code-block:: none

&target=resample(metric, 2)
&target=movingAverage(resample(metric, 2), 20)
"""
newSampleCount = requestContext['width']

for seriesIndex, series in enumerate(seriesList):
newValues = []
seriesLength = (series.end - series.start)
newStep = (float(seriesLength) / float(newSampleCount)) / float(pointsPerPx)

# Leave this series alone if we're asked to do upsampling
if newStep < series.step:
continue

sampleWidth = 0
sampleCount = 0
sampleSum = 0

for value in series:
if (value is not None):
sampleCount += 1
sampleSum += value
sampleWidth += series.step

# If the current sample covers the width of a new step, add it to the
# result
if (sampleWidth >= newStep):
if sampleCount > 0:
newValues.append(sampleSum / sampleCount)
else:
newValues.append(None)
sampleWidth -= newStep
sampleSum = 0
sampleCount = 0

# Process and add the left-over sample if it's not empty
if sampleCount > 0:
newValues.append(sampleSum / sampleCount)

newName = "resample(%s, %s)" % (series.name, pointsPerPx)
newSeries = TimeSeries(newName, series.start, series.end, newStep, newValues)
newSeries.pathExpression = newName
seriesList[seriesIndex] = newSeries

return seriesList


def smooth(requestContext, seriesList, windowPixelSize = 5):
"""
Resample and smooth a set of metrics. Provides line smoothing that is
independent of time scale (windowPixelSize ~ movingAverage over pixels)

An shorter and safer way of calling:
movingAverage(resample(seriesList, 2), smoothFactor * 2)

The windowPixelSize is effectively the number of pixels over which to perform
the movingAverage.

Note: This is safer in that if a series has fewer data points than pixels,
the metric won't be upsampled. Instead the movingAverage window size will be
adjusted to cover the same number of pixels.
"""
pointsPerPixel = 2
resampled = resample(requestContext, seriesList, pointsPerPixel)

sampleSize = int(windowPixelSize * pointsPerPixel)
expectedSamples = requestContext['width'] * pointsPerPixel

for index, series in enumerate(resampled):
# if we have fewer samples than expected, adjust the movingAverage sample
# size so it covers the same number of pixels
if (len(series) < expectedSamples * 0.95):
movingAverageSize = int((float(len(series)) / (expectedSamples)) * sampleSize)
else:
movingAverageSize = sampleSize

# If we are being asked to do a movingAverage over one point or less,
# don't bother
if (movingAverageSize <= 1):
continue

resampled[index] = movingAverage(requestContext, [series], movingAverageSize)[0]

return resampled

def derivative(requestContext, seriesList):
"""
This is the opposite of the integral function. This is useful for taking a
Expand Down Expand Up @@ -2488,6 +2594,8 @@ def pieMinimum(requestContext, series):
'summarize' : summarize,
'smartSummarize' : smartSummarize,
'hitcount' : hitcount,
'resample' : resample,
'smooth' : smooth,
'absolute' : absolute,

# Calculate functions
Expand Down
2 changes: 2 additions & 0 deletions webapp/graphite/render/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ def renderView(request):
'startTime' : requestOptions['startTime'],
'endTime' : requestOptions['endTime'],
'localOnly' : requestOptions['localOnly'],
'width' : graphOptions['width'],
'height' : graphOptions['height'],
'data' : []
}
data = requestContext['data']
Expand Down