Created
July 24, 2014 01:19
-
-
Save mikaelhm/e921cced496eb6586092 to your computer and use it in GitHub Desktop.
Pivot operations from node.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* This file is a simplified version of https://github.com/nicolaskruchten/pivottable | |
* that does not depend on UI web components or jQuery/ | |
* | |
* This makes it possible to run from node.js | |
* | |
* / Mikael | |
*/ | |
var _ = require('lodash-node'); | |
/* | |
Short handles | |
*/ | |
var __slice = [].slice, | |
__indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }, | |
_this = this, | |
__hasProp = {}.hasOwnProperty, | |
__bind = function (fn, me) { | |
return function () { | |
return fn.apply(me, arguments); | |
}; | |
}; | |
function deriveAttributes(record, derivedAttributes, f) { | |
var k, v, _ref; | |
for (k in derivedAttributes) { | |
v = derivedAttributes[k]; | |
record[k] = (_ref = v(record)) !== null ? _ref : record[k]; | |
} | |
for (k in record) { | |
if (!__hasProp.call(record, k)) { | |
continue; | |
} | |
if (record[k] === null) { | |
record[k] = "null"; | |
} | |
} | |
return f(record); | |
} | |
function forEachRecord(input, derivedAttributes, f) { | |
var addRecord, compactRecord, i, j, k, record, tblCols, _i, _len, _ref, _results, _results1; | |
addRecord = function (record) { | |
return deriveAttributes(record, derivedAttributes, f); | |
}; | |
if (_.isFunction(input)) { | |
return input(addRecord); | |
} | |
if (_.isArray(input)) { | |
if (_.isArray(input[0])) { | |
_results = []; | |
for (i in input) { | |
if (!__hasProp.call(input, i)) { | |
continue; | |
} | |
compactRecord = input[i]; | |
if (!(i > 0)) { | |
continue; | |
} | |
record = {}; | |
_ref = input[0]; | |
for (j in _ref) { | |
if (!__hasProp.call(_ref, j)) continue; | |
k = _ref[j]; | |
record[k] = compactRecord[j]; | |
} | |
_results.push(addRecord(record)); | |
} | |
return _results; | |
} else { | |
_results1 = []; | |
for (_i = 0, _len = input.length; _i < _len; _i++) { | |
record = input[_i]; | |
_results1.push(addRecord(record)); | |
} | |
return _results1; | |
} | |
// } else if (input instanceof jQuery) { | |
// tblCols = []; | |
// $("thead > tr > th", input).each(function (i) { | |
// return tblCols.push($(this).text()); | |
// }); | |
// return $("tbody > tr", input).each(function (i) { | |
// record = {}; | |
// $("td", this).each(function (j) { | |
// return record[tblCols[j]] = $(this).text(); | |
// }); | |
// return addRecord(record); | |
// }); | |
} else { | |
throw new Error("unknown input format"); | |
} | |
}; | |
PivotData = (function () { | |
function PivotData(aggregator, colAttrs, rowAttrs) { | |
this.aggregator = aggregator; | |
this.colAttrs = colAttrs; | |
this.rowAttrs = rowAttrs; | |
this.getAggregator = __bind(this.getAggregator, this); | |
this.flattenKey = __bind(this.flattenKey, this); | |
this.getRowKeys = __bind(this.getRowKeys, this); | |
this.getColKeys = __bind(this.getColKeys, this); | |
this.sortKeys = __bind(this.sortKeys, this); | |
this.arrSort = __bind(this.arrSort, this); | |
this.natSort = __bind(this.natSort, this); | |
this.tree = {}; | |
this.rowKeys = []; | |
this.colKeys = []; | |
this.flatRowKeys = []; | |
this.flatColKeys = []; | |
this.rowTotals = {}; | |
this.colTotals = {}; | |
this.allTotal = this.aggregator(this, [], []); | |
this.sorted = false; | |
} | |
PivotData.prototype.natSort = function (as, bs) { | |
return naturalSort(as, bs); | |
}; | |
PivotData.prototype.arrSort = function (a, b) { | |
return this.natSort(a.join(), b.join()); | |
}; | |
PivotData.prototype.sortKeys = function () { | |
if (!this.sorted) { | |
this.rowKeys.sort(this.arrSort); | |
this.colKeys.sort(this.arrSort); | |
} | |
return this.sorted = true; | |
}; | |
PivotData.prototype.getColKeys = function () { | |
this.sortKeys(); | |
return this.colKeys; | |
}; | |
PivotData.prototype.getRowKeys = function () { | |
this.sortKeys(); | |
return this.rowKeys; | |
}; | |
PivotData.prototype.flattenKey = function (x) { | |
return x.join(String.fromCharCode(0)); | |
}; | |
PivotData.prototype.processRecord = function (record) { | |
var colKey, flatColKey, flatRowKey, rowKey, x; | |
colKey = (function () { | |
var _i, _len, _ref, _results; | |
_ref = this.colAttrs; | |
_results = []; | |
for (_i = 0, _len = _ref.length; _i < _len; _i++) { | |
x = _ref[_i]; | |
_results.push(record[x]); | |
} | |
return _results; | |
}).call(this); | |
rowKey = (function () { | |
var _i, _len, _ref, _results; | |
_ref = this.rowAttrs; | |
_results = []; | |
for (_i = 0, _len = _ref.length; _i < _len; _i++) { | |
x = _ref[_i]; | |
_results.push(record[x]); | |
} | |
return _results; | |
}).call(this); | |
flatRowKey = this.flattenKey(rowKey); | |
flatColKey = this.flattenKey(colKey); | |
this.allTotal.push(record); | |
if (rowKey.length !== 0) { | |
if (__indexOf.call(this.flatRowKeys, flatRowKey) < 0) { | |
this.rowKeys.push(rowKey); | |
this.flatRowKeys.push(flatRowKey); | |
} | |
if (!this.rowTotals[flatRowKey]) { | |
this.rowTotals[flatRowKey] = this.aggregator(this, rowKey, []); | |
} | |
this.rowTotals[flatRowKey].push(record); | |
} | |
if (colKey.length !== 0) { | |
if (__indexOf.call(this.flatColKeys, flatColKey) < 0) { | |
this.colKeys.push(colKey); | |
this.flatColKeys.push(flatColKey); | |
} | |
if (!this.colTotals[flatColKey]) { | |
this.colTotals[flatColKey] = this.aggregator(this, [], colKey); | |
} | |
this.colTotals[flatColKey].push(record); | |
} | |
if (colKey.length !== 0 && rowKey.length !== 0) { | |
if (!(flatRowKey in this.tree)) { | |
this.tree[flatRowKey] = {}; | |
} | |
if (!(flatColKey in this.tree[flatRowKey])) { | |
this.tree[flatRowKey][flatColKey] = this.aggregator(this, rowKey, colKey); | |
} | |
return this.tree[flatRowKey][flatColKey].push(record); | |
} | |
}; | |
PivotData.prototype.getAggregator = function (rowKey, colKey) { | |
var agg, flatColKey, flatRowKey; | |
flatRowKey = this.flattenKey(rowKey); | |
flatColKey = this.flattenKey(colKey); | |
if (rowKey.length === 0 && colKey.length === 0) { | |
agg = this.allTotal; | |
} else if (rowKey.length === 0) { | |
agg = this.colTotals[flatColKey]; | |
} else if (colKey.length === 0) { | |
agg = this.rowTotals[flatRowKey]; | |
} else { | |
agg = this.tree[flatRowKey][flatColKey]; | |
} | |
return agg != null ? agg : { | |
value: (function () { | |
return null; | |
}), | |
format: function () { | |
return ""; | |
} | |
}; | |
}; | |
return PivotData; | |
})(); | |
module.exports.getPivotData = function (input, cols, rows, aggregator, filter, derivedAttributes) { | |
var pivotData; | |
pivotData = new PivotData(aggregator, cols, rows); | |
forEachRecord(input, derivedAttributes, function (record) { | |
if (filter(record)) { | |
return pivotData.processRecord(record); | |
} | |
}); | |
return pivotData; | |
}; | |
// aggregators | |
function addSeparators(nStr, thousandsSep, decimalSep) { | |
var rgx, x, x1, x2; | |
nStr += ''; | |
x = nStr.split('.'); | |
x1 = x[0]; | |
x2 = x.length > 1 ? decimalSep + x[1] : ''; | |
rgx = /(\d+)(\d{3})/; | |
while (rgx.test(x1)) { | |
x1 = x1.replace(rgx, '$1' + thousandsSep + '$2'); | |
} | |
return x1 + x2; | |
} | |
function numberFormat(sigfig, scaler, thousandsSep, decimalSep) { | |
if (sigfig === null) { | |
sigfig = 3; | |
} | |
if (scaler === null) { | |
scaler = 1; | |
} | |
if (thousandsSep === null) { | |
thousandsSep = ","; | |
} | |
if (decimalSep === null) { | |
decimalSep = "."; | |
} | |
return function (x) { | |
if (x === 0 || isNaN(x) || !isFinite(x)) { | |
return ""; | |
} else { | |
return addSeparators((scaler * x).toFixed(sigfig), thousandsSep, decimalSep); | |
} | |
}; | |
} | |
var aggregatorTemplates = { | |
sum: function (sigfig, scaler) { | |
if (sigfig === null) { | |
sigfig = 3; | |
} | |
if (scaler === null) { | |
scaler = 1; | |
} | |
return function (_arg) { | |
var attr; | |
attr = _arg[0]; | |
return function () { | |
return { | |
sum: 0, | |
push: function (record) { | |
if (!isNaN(parseFloat(record[attr]))) { | |
return this.sum = parseFloat(record[attr]); | |
} | |
}, | |
value: function () { | |
return this.sum; | |
}, | |
format: numberFormat(sigfig, scaler), | |
label: "Sum of " + attr | |
}; | |
}; | |
}; | |
}, | |
average: function (sigfig, scaler) { | |
if (sigfig === null) { | |
sigfig = 3; | |
} | |
if (scaler === null) { | |
scaler = 1; | |
} | |
return function (_arg) { | |
var attr; | |
attr = _arg[0]; | |
return function () { | |
return { | |
sum: 0, | |
len: 0, | |
push: function (record) { | |
if (!isNaN(parseFloat(record[attr]))) { | |
this.sum += parseFloat(record[attr]); | |
return this.len++; | |
} | |
}, | |
value: function () { | |
return this.sum / this.len; | |
}, | |
format: numberFormat(sigfig, scaler), | |
label: "Average of " + attr | |
}; | |
}; | |
}; | |
}, | |
sumOverSum: function (sigfig, scaler) { | |
if (sigfig === null) { | |
sigfig = 3; | |
} | |
if (scaler === null) { | |
scaler = 1; | |
} | |
return function (_arg) { | |
var denom, num; | |
num = _arg[0]; | |
denom = _arg[1]; | |
return function () { | |
return { | |
sumNum: 0, | |
sumDenom: 0, | |
push: function (record) { | |
if (!isNaN(parseFloat(record[num]))) { | |
this.sumNum += parseFloat(record[num]); | |
} | |
if (!isNaN(parseFloat(record[denom]))) { | |
return this.sumDenom += parseFloat(record[denom]); | |
} | |
}, | |
value: function () { | |
return this.sumNum / this.sumDenom; | |
}, | |
format: numberFormat(sigfig, scaler), | |
label: "" + num + "/" + denom | |
}; | |
}; | |
}; | |
}, | |
sumOverSumBound80: function (sigfig, scaler, upper) { | |
if (sigfig === null) { | |
sigfig = 3; | |
} | |
if (scaler === null) { | |
scaler = 1; | |
} | |
if (upper === null) { | |
upper = true; | |
} | |
return function (_arg) { | |
var denom, num; | |
num = _arg[0]; | |
denom = _arg[1]; | |
return function () { | |
return { | |
sumNum: 0, | |
sumDenom: 0, | |
push: function (record) { | |
if (!isNaN(parseFloat(record[num]))) { | |
this.sumNum += parseFloat(record[num]); | |
} | |
if (!isNaN(parseFloat(record[denom]))) { | |
return this.sumDenom += parseFloat(record[denom]); | |
} | |
}, | |
value: function () { | |
var sign; | |
sign = upper ? 1 : -1; | |
return (0.821187207574908 / this.sumDenom + this.sumNum / this.sumDenom + 1.2815515655446004 * sign * Math.sqrt(0.410593603787454 / (this.sumDenom * this.sumDenom) + (this.sumNum * (1 - this.sumNum / this.sumDenom)) / (this.sumDenom * this.sumDenom))) / (1 + 1.642374415149816 / this.sumDenom); | |
}, | |
format: numberFormat(sigfig, scaler), | |
label: "" + (upper ? "Upper" : "Lower") + " Bound of " + num + "/" + denom | |
}; | |
}; | |
}; | |
}, | |
fractionOf: function (wrapped, type) { | |
if (type === null) { | |
type = "total"; | |
} | |
return function () { | |
var x; | |
x = 1 <= arguments.length ? __slice.call(arguments, 0) : []; | |
return function (data, rowKey, colKey) { | |
return { | |
selector: { | |
total: [ | |
[], | |
[] | |
], | |
row: [rowKey, []], | |
col: [ | |
[], colKey | |
] | |
}[type], | |
inner: wrapped.apply(null, x)(data, rowKey, colKey), | |
push: function (record) { | |
return this.inner.push(record); | |
}, | |
format: function (v) { | |
return numberFormat(2)(100 * v) + "%"; | |
}, | |
label: wrapped.apply(null, x)(data, rowKey, colKey).label + " % of " + type, | |
value: function () { | |
return this.inner.value() / data.getAggregator.apply(data, this.selector).inner.value(); | |
} | |
}; | |
}; | |
}; | |
}, | |
l10nWrapper: function (wrapped, formatter, labelFn) { | |
return function () { | |
var x; | |
x = 1 <= arguments.length ? __slice.call(arguments, 0) : []; | |
return function (data, rowKey, colKey) { | |
return { | |
inner: wrapped.apply(null, x)(data, rowKey, colKey), | |
push: function (record) { | |
return this.inner.push(record); | |
}, | |
format: formatter, | |
label: labelFn(data), | |
value: function () { | |
return this.inner.value(); | |
} | |
}; | |
}; | |
}; | |
} | |
}; | |
var aggregators = { | |
count: function () { | |
return function () { | |
return { | |
count: 0, | |
push: function () { | |
return this.count++; | |
}, | |
value: function () { | |
return this.count; | |
}, | |
format: numberFormat(0), | |
label: "Count" | |
}; | |
}; | |
}, | |
countUnique: function (_arg) { | |
var attr; | |
attr = _arg[0]; | |
return function () { | |
return { | |
uniq: [], | |
push: function (record) { | |
var _ref; | |
if (_ref = record[attr], __indexOf.call(this.uniq, _ref) < 0) { | |
return this.uniq.push(record[attr]); | |
} | |
}, | |
value: function () { | |
return this.uniq.length; | |
}, | |
format: numberFormat(0), | |
label: "Count Unique " + attr | |
}; | |
}; | |
}, | |
listUnique: function (_arg) { | |
var attr; | |
attr = _arg[0]; | |
return function () { | |
return { | |
uniq: [], | |
push: function (record) { | |
var _ref; | |
if (_ref = record[attr], __indexOf.call(this.uniq, _ref) < 0) { | |
return this.uniq.push(record[attr]); | |
} | |
}, | |
value: function () { | |
return this.uniq.join(", "); | |
}, | |
format: function (x) { | |
return x; | |
}, | |
label: "List Unique " + attr | |
}; | |
}; | |
}, | |
intSum: aggregatorTemplates.sum(0), | |
sum: aggregatorTemplates.sum(3), | |
average: aggregatorTemplates.average(3), | |
sumOverSum: aggregatorTemplates.sumOverSum(3), | |
ub80: aggregatorTemplates.sumOverSumBound80(3, 1, true), | |
lb80: aggregatorTemplates.sumOverSumBound80(3, 1, false) | |
}; | |
aggregators.sumAsFractionOfTotal = aggregatorTemplates.fractionOf(aggregators.sum); | |
aggregators.sumAsFractionOfRow = aggregatorTemplates.fractionOf(aggregators.sum, "row"); | |
aggregators.sumAsFractionOfCol = aggregatorTemplates.fractionOf(aggregators.sum, "col"); | |
aggregators.countAsFractionOfTotal = aggregatorTemplates.fractionOf(aggregators.count); | |
aggregators.countAsFractionOfRow = aggregatorTemplates.fractionOf(aggregators.count, "row"); | |
aggregators.countAsFractionOfCol = aggregatorTemplates.fractionOf(aggregators.count, "col"); | |
module.exports.aggregators = aggregators; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Could you please brief on getDataAsTableRows in your nodejs implementation.
Thanks