lumenize
Version:
Illuminating the forest AND the trees in your data.
552 lines (517 loc) • 21.3 kB
HTML
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>The source code</title>
<link href="../resources/prettify/prettify.css" type="text/css" rel="stylesheet" />
<script type="text/javascript" src="../resources/prettify/prettify.js"></script>
<style type="text/css">
.highlight { display: block; background-color: #ddd; }
</style>
<script type="text/javascript">
function highlight() {
document.getElementById(location.hash.replace(/#/, "")).className = "highlight";
}
</script>
</head>
<body onload="prettyPrint(); highlight();">
<pre class="prettyprint lang-js">/* <CoffeeScript>
{utils} = require('tztime')
</CoffeeScript> */
<span id='Lumenize-functions'>/**
</span> * @class Lumenize.functions
*
* Rules about dependencies:
*
* * If a function can be calculated incrementally from an oldResult and newValues, then you do not need to specify dependencies
* * If a funciton can be calculated from other incrementally calculable results, then you need only specify those dependencies
* * If a function needs the full list of values to be calculated (like percentile coverage), then you must specify 'values'
* * To support the direct passing in of OLAP cube cells, you can provide a prefix (field name) so the key in dependentValues
* can be generated
* * 'count' is special and does not use a prefix because it is not dependent up a particular field
* * You should calculate the dependencies before you calculate the thing that is depedent. The OLAP cube does some
* checking to confirm you've done this.
*/
/* <CoffeeScript>
functions = {}
functions._populateDependentValues = (values, dependencies, dependentValues = {}, prefix = '') ->
out = {}
for d in dependencies
if d == 'count'
if prefix == ''
key = 'count'
else
key = '_count'
else
key = prefix + d
unless dependentValues[key]?
dependentValues[key] = functions[d](values, undefined, undefined, dependentValues, prefix)
out[d] = dependentValues[key]
return out
</CoffeeScript> */
<span id='Lumenize-functions-static-method-sum'>/**
</span> * @method sum
* @member Lumenize.functions
* @static
* @param {Number[]} [values] Must either provide values or oldResult and newValues
* @param {Number} [oldResult] for incremental calculation
* @param {Number[]} [newValues] for incremental calculation
* @return {Number} The sum of the values
*/
/* <CoffeeScript>
functions.sum = (values, oldResult, newValues) ->
if oldResult?
temp = oldResult
tempValues = newValues
else
temp = 0
tempValues = values
for v in tempValues
temp += v
return temp
</CoffeeScript> */
<span id='Lumenize-functions-static-method-product'>/**
</span> * @method product
* @member Lumenize.functions
* @static
* @param {Number[]} [values] Must either provide values or oldResult and newValues
* @param {Number} [oldResult] for incremental calculation
* @param {Number[]} [newValues] for incremental calculation
* @return {Number} The product of the values
*/
/* <CoffeeScript>
functions.product = (values, oldResult, newValues) ->
if oldResult?
temp = oldResult
tempValues = newValues
else
temp = 1
tempValues = values
for v in tempValues
temp = temp * v
return temp
</CoffeeScript> */
<span id='Lumenize-functions-static-method-sumSquares'>/**
</span> * @method sumSquares
* @member Lumenize.functions
* @static
* @param {Number[]} [values] Must either provide values or oldResult and newValues
* @param {Number} [oldResult] for incremental calculation
* @param {Number[]} [newValues] for incremental calculation
* @return {Number} The sum of the squares of the values
*/
/* <CoffeeScript>
functions.sumSquares = (values, oldResult, newValues) ->
if oldResult?
temp = oldResult
tempValues = newValues
else
temp = 0
tempValues = values
for v in tempValues
temp += v * v
return temp
</CoffeeScript> */
<span id='Lumenize-functions-static-method-sumCubes'>/**
</span> * @method sumCubes
* @member Lumenize.functions
* @static
* @param {Number[]} [values] Must either provide values or oldResult and newValues
* @param {Number} [oldResult] for incremental calculation
* @param {Number[]} [newValues] for incremental calculation
* @return {Number} The sum of the cubes of the values
*/
/* <CoffeeScript>
functions.sumCubes = (values, oldResult, newValues) ->
if oldResult?
temp = oldResult
tempValues = newValues
else
temp = 0
tempValues = values
for v in tempValues
temp += v * v * v
return temp
</CoffeeScript> */
<span id='Lumenize-functions-static-method-lastValue'>/**
</span> * @method lastValue
* @member Lumenize.functions
* @static
* @param {Number[]} [values] Must either provide values or newValues
* @param {Number} [oldResult] Not used. It is included to make the interface consistent.
* @param {Number[]} [newValues] for incremental calculation
* @return {Number} The last value
*/
/* <CoffeeScript>
functions.lastValue = (values, oldResult, newValues) ->
if newValues?
return newValues[newValues.length - 1]
return values[values.length - 1]
</CoffeeScript> */
<span id='Lumenize-functions-static-method-firstValue'>/**
</span> * @method firstValue
* @member Lumenize.functions
* @static
* @param {Number[]} [values] Must either provide values or oldResult
* @param {Number} [oldResult] for incremental calculation
* @param {Number[]} [newValues] Not used. It is included to make the interface consistent.
* @return {Number} The first value
*/
/* <CoffeeScript>
functions.firstValue = (values, oldResult, newValues) ->
if oldResult?
return oldResult
return values[0]
</CoffeeScript> */
<span id='Lumenize-functions-static-method-count'>/**
</span> * @method count
* @member Lumenize.functions
* @static
* @param {Number[]} [values] Must either provide values or oldResult and newValues
* @param {Number} [oldResult] for incremental calculation
* @param {Number[]} [newValues] for incremental calculation
* @return {Number} The length of the values Array
*/
/* <CoffeeScript>
functions.count = (values, oldResult, newValues) ->
if oldResult?
return oldResult + newValues.length
return values.length
</CoffeeScript> */
<span id='Lumenize-functions-static-method-min'>/**
</span> * @method min
* @member Lumenize.functions
* @static
* @param {Number[]} [values] Must either provide values or oldResult and newValues
* @param {Number} [oldResult] for incremental calculation
* @param {Number[]} [newValues] for incremental calculation
* @return {Number} The minimum value or null if no values
*/
/* <CoffeeScript>
functions.min = (values, oldResult, newValues) ->
if oldResult?
return functions.min(newValues.concat([oldResult]))
if values.length == 0
return null
temp = values[0]
for v in values
if v < temp
temp = v
return temp
</CoffeeScript> */
<span id='Lumenize-functions-static-method-max'>/**
</span> * @method max
* @member Lumenize.functions
* @static
* @param {Number[]} [values] Must either provide values or oldResult and newValues
* @param {Number} [oldResult] for incremental calculation
* @param {Number[]} [newValues] for incremental calculation
* @return {Number} The maximum value or null if no values
*/
/* <CoffeeScript>
functions.max = (values, oldResult, newValues) ->
if oldResult?
return functions.max(newValues.concat([oldResult]))
if values.length == 0
return null
temp = values[0]
for v in values
if v > temp
temp = v
return temp
</CoffeeScript> */
<span id='Lumenize-functions-static-method-values'>/**
</span> * @method values
* @member Lumenize.functions
* @static
* @param {Object[]} [values] Must either provide values or oldResult and newValues
* @param {Number} [oldResult] for incremental calculation
* @param {Number[]} [newValues] for incremental calculation
* @return {Array} All values (allows duplicates). Can be used for drill down.
*/
/* <CoffeeScript>
functions.values = (values, oldResult, newValues) ->
if oldResult?
return oldResult.concat(newValues)
return values
# temp = []
# for v in values
# temp.push(v)
# return temp
</CoffeeScript> */
<span id='Lumenize-functions-static-method-uniqueValues'>/**
</span> * @method uniqueValues
* @member Lumenize.functions
* @static
* @param {Object[]} [values] Must either provide values or oldResult and newValues
* @param {Number} [oldResult] for incremental calculation
* @param {Number[]} [newValues] for incremental calculation
* @return {Array} Unique values. This is good for generating an OLAP dimension or drill down.
*/
/* <CoffeeScript>
functions.uniqueValues = (values, oldResult, newValues) ->
temp = {}
if oldResult?
for r in oldResult
temp[r] = null
tempValues = newValues
else
tempValues = values
temp2 = []
for v in tempValues
temp[v] = null
for key, value of temp
temp2.push(key)
return temp2
</CoffeeScript> */
<span id='Lumenize-functions-static-method-average'>/**
</span> * @method average
* @member Lumenize.functions
* @static
* @param {Number[]} [values] Must either provide values or oldResult and newValues
* @param {Number} [oldResult] not used by this function but included so all functions have a consistent signature
* @param {Number[]} [newValues] not used by this function but included so all functions have a consistent signature
* @param {Object} [dependentValues] If the function can be calculated from the results of other functions, this allows
* you to provide those pre-calculated values.
* @return {Number} The arithmetic mean
*/
/* <CoffeeScript>
functions.average = (values, oldResult, newValues, dependentValues, prefix) ->
{count, sum} = functions._populateDependentValues(values, functions.average.dependencies, dependentValues, prefix)
if count is 0
return null
else
return sum / count
functions.average.dependencies = ['count', 'sum']
</CoffeeScript> */
<span id='Lumenize-functions-static-method-errorSquared'>/**
</span> * @method errorSquared
* @member Lumenize.functions
* @static
* @param {Number[]} [values] Must either provide values or oldResult and newValues
* @param {Number} [oldResult] not used by this function but included so all functions have a consistent signature
* @param {Number[]} [newValues] not used by this function but included so all functions have a consistent signature
* @param {Object} [dependentValues] If the function can be calculated from the results of other functions, this allows
* you to provide those pre-calculated values.
* @return {Number} The error squared
*/
/* <CoffeeScript>
functions.errorSquared = (values, oldResult, newValues, dependentValues, prefix) ->
{count, sum} = functions._populateDependentValues(values, functions.errorSquared.dependencies, dependentValues, prefix)
mean = sum / count
errorSquared = 0
for v in values
difference = v - mean
errorSquared += difference * difference
return errorSquared
functions.errorSquared.dependencies = ['count', 'sum']
</CoffeeScript> */
<span id='Lumenize-functions-static-method-variance'>/**
</span> * @method variance
* @member Lumenize.functions
* @static
* @param {Number[]} [values] Must either provide values or oldResult and newValues
* @param {Number} [oldResult] not used by this function but included so all functions have a consistent signature
* @param {Number[]} [newValues] not used by this function but included so all functions have a consistent signature
* @param {Object} [dependentValues] If the function can be calculated from the results of other functions, this allows
* you to provide those pre-calculated values.
* @return {Number} The variance
*/
/* <CoffeeScript>
functions.variance = (values, oldResult, newValues, dependentValues, prefix) ->
{count, sum, sumSquares} = functions._populateDependentValues(values, functions.variance.dependencies, dependentValues, prefix)
if count is 0
return null
else if count is 1
return 0
else
return (count * sumSquares - sum * sum) / (count * (count - 1))
functions.variance.dependencies = ['count', 'sum', 'sumSquares']
</CoffeeScript> */
<span id='Lumenize-functions-static-method-standardDeviation'>/**
</span> * @method standardDeviation
* @member Lumenize.functions
* @static
* @param {Number[]} [values] Must either provide values or oldResult and newValues
* @param {Number} [oldResult] not used by this function but included so all functions have a consistent signature
* @param {Number[]} [newValues] not used by this function but included so all functions have a consistent signature
* @param {Object} [dependentValues] If the function can be calculated from the results of other functions, this allows
* you to provide those pre-calculated values.
* @return {Number} The standard deviation
*/
/* <CoffeeScript>
functions.standardDeviation = (values, oldResult, newValues, dependentValues, prefix) ->
return Math.sqrt(functions.variance(values, oldResult, newValues, dependentValues, prefix))
functions.standardDeviation.dependencies = functions.variance.dependencies
</CoffeeScript> */
<span id='Lumenize-functions-static-method-percentileCreator'>/**
</span> * @method percentileCreator
* @member Lumenize.functions
* @static
* @param {Number} p The percentile for the resulting function (50 = median, 75, 99, etc.)
* @return {Function} A function to calculate the percentile
*
* When the user passes in `p<n>` as an aggregation function, this `percentileCreator` is called to return the appropriate
* percentile function. The returned function will find the `<n>`th percentile where `<n>` is some number in the form of
* `##[.##]`. (e.g. `p40`, `p99`, `p99.9`).
*
* There is no official definition of percentile. The most popular choices differ in the interpolation algorithm that they
* use. The function returned by this `percentileCreator` uses the Excel interpolation algorithm which differs from the NIST
* primary method. However, NIST lists something very similar to the Excel approach as an acceptible alternative. The only
* difference seems to be for the edge case for when you have only two data points in your data set. Agreement with Excel,
* NIST's acceptance of it as an alternative (almost), and the fact that it makes the most sense to me is why this approach
* was chosen.
*
* http://en.wikipedia.org/wiki/Percentile#Alternative_methods
*
* Note: `median` is an alias for p50. The approach chosen for calculating p50 gives you the
* exact same result as the definition for median even for edge cases like sets with only one or two data points.
*
*/
/* <CoffeeScript>
functions.percentileCreator = (p) ->
f = (values, oldResult, newValues, dependentValues, prefix) ->
unless values?
{values} = functions._populateDependentValues(values, ['values'], dependentValues, prefix)
if values.length is 0
return null
sortfunc = (a, b) ->
return a - b
vLength = values.length
values.sort(sortfunc)
n = (p * (vLength - 1) / 100) + 1
k = Math.floor(n)
d = n - k
if n == 1
return values[1 - 1]
if n == vLength
return values[vLength - 1]
return values[k - 1] + d * (values[k] - values[k - 1])
f.dependencies = ['values']
return f
</CoffeeScript> */
<span id='Lumenize-functions-static-method-median'>/**
</span> * @method median
* @member Lumenize.functions
* @static
* @param {Number[]} [values] Must either provide values or oldResult and newValues
* @param {Number} [oldResult] not used by this function but included so all functions have a consistent signature
* @param {Number[]} [newValues] not used by this function but included so all functions have a consistent signature
* @param {Object} [dependentValues] If the function can be calculated from the results of other functions, this allows
* you to provide those pre-calculated values.
* @return {Number} The median
*/
/* <CoffeeScript>
functions.median = functions.percentileCreator(50)
functions.expandFandAs = (a) ->
</CoffeeScript> */
<span id='Lumenize-functions-static-method-expandFandAs'> /**
</span> * @method expandFandAs
* @member Lumenize.functions
* @static
* @param {Object} a Will look like this `{as: 'mySum', f: 'sum', field: 'Points'}`
* @return {Object} returns the expanded specification
*
* Takes specifications for functions and expands them to include the actual function and 'as'. If you do not provide
* an 'as' property, it will build it from the field name and function with an underscore between. Also, if the
* 'f' provided is a string, it is copied over to the 'metric' property before the 'f' property is replaced with the
* actual function. `{field: 'a', f: 'sum'}` would expand to `{as: 'a_sum', field: 'a', metric: 'sum', f: [Function]}`.
*/
/* <CoffeeScript>
utils.assert(a.f?, "'f' missing from specification: \n#{JSON.stringify(a, undefined, 4)}")
if utils.type(a.f) == 'function'
utils.assert(a.as?, 'Must provide "as" field with your aggregation when providing a user defined function')
a.metric = a.f.toString()
else if functions[a.f]?
a.metric = a.f
a.f = functions[a.f]
else if a.f.substr(0, 1) == 'p'
a.metric = a.f
p = /\p(\d+(.\d+)?)/.exec(a.f)[1]
a.f = functions.percentileCreator(Number(p))
else
throw new Error("#{a.f} is not a recognized built-in function")
unless a.as?
if a.metric == 'count'
a.field = ''
a.metric = 'count'
a.as = "#{a.field}_#{a.metric}"
utils.assert(a.field? or a.f == 'count', "'field' missing from specification: \n#{JSON.stringify(a, undefined, 4)}")
return a
functions.expandMetrics = (metrics = [], addCountIfMissing = false, addValuesForCustomFunctions = false) ->
</CoffeeScript> */
<span id='Lumenize-functions-static-method-expandMetrics'> /**
</span> * @method expandMetrics
* @member Lumenize.functions
* @static
* @private
*
* This is called internally by several Lumenize Calculators. You should probably not call it.
*/
/* <CoffeeScript>
confirmMetricAbove = (m, fieldName, aboveThisIndex) ->
if m is 'count'
lookingFor = '_' + m
else
lookingFor = fieldName + '_' + m
i = 0
while i < aboveThisIndex
currentRow = metrics[i]
if currentRow.as == lookingFor
return true
i++
# OK, it's not above, let's now see if it's below. Then throw error.
i = aboveThisIndex + 1
metricsLength = metrics.length
while i < metricsLength
currentRow = metrics[i]
if currentRow.as == lookingFor
throw new Error("Depdencies must appear before the metric they are dependant upon. #{m} appears after.")
i++
return false
assureDependenciesAbove = (dependencies, fieldName, aboveThisIndex) ->
for d in dependencies
unless confirmMetricAbove(d, fieldName, aboveThisIndex)
if d == 'count'
newRow = {f: 'count'}
else
newRow = {f: d, field: fieldName}
functions.expandFandAs(newRow)
metrics.unshift(newRow)
return false
return true
# add values for custom functions
if addValuesForCustomFunctions
for m, index in metrics
if utils.type(m.f) is 'function'
unless m.f.dependencies?
m.f.dependencies = []
unless m.f.dependencies[0] is 'values'
m.f.dependencies.push('values')
unless confirmMetricAbove('values', m.field, index)
valuesRow = {f: 'values', field: m.field}
functions.expandFandAs(valuesRow)
metrics.unshift(valuesRow)
hasCount = false
for m in metrics
functions.expandFandAs(m)
if m.metric is 'count'
hasCount = true
if addCountIfMissing and not hasCount
countRow = {f: 'count'}
functions.expandFandAs(countRow)
metrics.unshift(countRow)
index = 0
while index < metrics.length # intentionally not caching length because the loop can add rows
metricsRow = metrics[index]
if utils.type(metricsRow.f) is 'function'
dependencies = ['values']
if metricsRow.f.dependencies?
unless assureDependenciesAbove(metricsRow.f.dependencies, metricsRow.field, index)
index = -1
index++
return metrics
exports.functions = functions
</CoffeeScript> */</pre>
</body>
</html>