supergroup
Version:
Nested groups on arrays of objects where groups are Strings that know what you want them to know about themselves and their relatives.
1,042 lines (987 loc) • 37.9 kB
JavaScript
/* eslint-disable */
/*
* # supergroup.js
* Author: [Sigfried Gold](http://sigfried.org)
* License: [MIT](http://sigfried.mit-license.org/)
* Version: 1.1.8
*
* usage examples at [http://sigfried.github.io/blog/supergroup](http://sigfried.github.io/blog/supergroup)
*/
; // jshint -W053
'use strict';
/* gotta fix all this, but:
*
* sg.parentVal = this List constitutes the children (val.getChildren())
* of sg.parentVal
*
* val.parentList = the supergroup List containing this val
*
* val.parent = val.parentList.parentVal
*/
var preventScalarInMultiValuedGroup=false; // brought from vocab-pop version, untested, un-thought-about, just trying to sync up
if (typeof require !== "undefined") {
var _ = require('lodash');
var createAggregator = require('lodash/_createAggregator');
}
var supergroup = (function() {
// @description local reference to supergroup namespace
var sg = {};
/* @exported function supergroup.group(recs, dim, opts)
* @param {Object[]} recs list of records to be grouped
* @param {string or Function} dim either the property name to
group by or a function returning a group by string or number
* @param {Object} [opts]
* @param {String} opts.childProp='children' If group ends up being
* hierarchical, this will be the property name of any children
* @param {String[]} [opts.excludeValues] to exlude specific group values
* @param {function} [opts.preListRecsHook] run recs through this
* function before continuing processing
* @param {function} [opts.dimName] defaults to the value of `dim`.
* If `dim` is a function, the dimName will be ugly.
* @param {function} [opts.truncateBranchOnEmptyVal]
* @param {boolean} [opts.multiValuedGroup=false]
* @param {boolean} [opts.preventScalarInMultiValuedGroup=false] // setting globally
* @return {Array of Values} enhanced with all the List methods
*
* Avaailable as _.supergroup, Underscore mixin
*/
sg.supergroup = function(recs, dim, opts) {
// if dim is an array, use multiDimList to create hierarchical grouping
// commented out stuff here from vocab-pop version, never did whatever I was trying
// wanted to keep opts clean, but it breaks the parent ref
//opts = _.cloneDeep(opts || {})
opts = opts || {}
//if (opts.allowCloning) {
recs = recs.map((rec,i)=> {
let clone = _.clone(rec)
clone._recIdx = i
return clone
})
//}
if (_(dim).isArray()) return sg.multiDimList(recs, dim, opts);
recs = opts.preListRecsHook ? opts.preListRecsHook(recs) : recs;
childProp = opts.childProp || childProp;
if (opts.multiValuedGroup) {
if (opts.wasMultiDim) {
throw new Error("If you want multValuedGroups on multi-level groupings, you need to use addLevel}");
} else {
if (!opts.preventScalarInMultiValuedGroup) {
var dimFunc = typeof dim === 'function'
? dim : d => d[dim];
var arrayAlwaysDim =
val => {
var retVal = dimFunc(val);
if (Array.isArray(retVal))
return retVal;
return [retVal];
};
}
var groups = _.multiValuedGroupBy(recs, arrayAlwaysDim);
}
} else {
if (opts.truncateBranchOnEmptyVal)
recs = filterOutEmpty(recs, dim);
var groups = _.groupBy(recs, dim); // use Underscore's groupBy: http://underscorejs.org/#groupBy
}
if (opts.excludeValues) { // why isn't truncateBranchOnEmptyVal treated as an excludeValue?
_.each(opts.excludeValues, function(d) {
delete groups[d];
});
}
var isNumeric = _(opts).has('isNumeric') ?
opts.isNumeric :
wholeListNumeric(groups); // does every group Value look like a number or a missing value?
var groups = _.map(_.toPairs(groups), function(pair, i) { // setup Values for each group in List
var rawVal = pair[0];
var val;
if(isNumeric) {
val = makeNumberValue(rawVal); // either everything's a Number
} else {
val = makeStringValue(rawVal); // or everything's a String
}
/* The original records in this group are stored as an Array in
* the records property (should probably be a getter method).
*/
val.records = pair[1];
/* val.records is enhanced with Underscore methods for
* convenience, but also with the supergroup method that's
* been mixed in to Underscore. So you can group this specific
* subset like: val.records.supergroup
* on FIX!!!!!!
*/
sg.addSupergroupMethods(val.records);
val.dim = (opts.dimName) ? opts.dimName : dim;
val.records.parentVal = val; // NOT TESTED, NOT USED, PROBABLY WRONG
if (opts.parent)
val.parent = opts.parent;
if (val.parent) {
if ('depth' in val.parent) {
val.depth = val.parent.depth + 1;
} else {
val.parent.depth = 0;
val.depth = 1;
}
} else {
val.depth = 0;
}
return val;
});
//groups = makeList(groups); // turns groups into a List object
groups = sg.addListMethods(groups); // turns groups into a List object
groups.records = recs; // NOT TESTED, NOT USED, PROBABLY WRONG
groups.dim = (opts.dimName) ? opts.dimName : dim;
groups.isNumeric = isNumeric;
_.each(groups, function(group, i) {
group.parentList = groups;
//group.idxInParentList = i; // maybe a good idea, but don't need it yet
});
// pointless without recursion
//if (opts.postListListHook) groups = opts.postListListHook(groups);
// next line deleted from vocab-pop version, not sure why
groups._optsAtThisLevel = opts;
return groups;
};
// nested groups, each dim is a level in hierarchy
sg.multiDimList = function(recs, dims, opts) {
opts.wasMultiDim = true; // pretty kludgy
var groups = sg.supergroup(recs, dims[0], opts);
_.chain(dims).tail().each(function(dim) {
groups.addLevel(dim, opts);
}).value();
return groups;
};
// @class List
// @description Native Array of groups with various added methods and properties.
// Methods described below.
function List() {}
// @class Value
// @description Supergroup Lists are composed of Values which are
// String or Number objects representing group values.
// Methods described below.
function Value() {}
// @class State
// @description with a couple exceptions, supergroups should not
// mutate after creation. States are a way to track selection/highlighting
// states without mutating.
function State(list) {
this.list = list;
this.selectedVals = [];
//this.selectedRecs = [];
}
sg.State = State;
State.prototype.selectByVal = function(val) {
if (val.rootList() !== this.list) // assume state only on root lists
throw new Error("state only on root lists (if state even does anything)");
this.selectedVals.push(val);
}
/*
State.prototype.selectByFilter = function(filt) {
this.selectedVals.push(val);
}
*/
State.prototype.selectedRecs = function() {
return _.chain(this.selectedVals).map('records').flatten().value();
}
List.prototype.state = function() {
return new State(this);
}
List.prototype.isSupergroupList = true;
// sometimes a root value is needed as the top of a hierarchy
List.prototype.asRootVal = function(name, dimName) {
var val = makeValue(name || 'Root');
val.dim = dimName || 'root';
val.depth = 0;
val.records = this.records;
val.setChildren(this);
_.each(val.getChildren(), function(d) { d.parent = val; });
_.each(val.descendants(), function(d) { d.depth = d.depth + 1; });
return val;
};
List.prototype.leafNodes = function(level) { // level isn't passed along. probably broken
return _.chain(this).invokeMap('leafNodes').flatten()
.addSupergroupMethods()
.value();
};
/* not working yet...
List.prototype.clone = function() {
var parentVal = this.parentVal,
return _.chain(this).invokeMap('clone')
.tap(sg.addListMethods)
.value();
};
*/
List.prototype.rawValues = function() {
return _.chain(this).map(function(d) { return d.valueOf(); }).value();
};
// lookup a value in a list, or, if query is an array
// it is interpreted as a path down the group hierarchy
List.prototype.lookup = function(query) {
if (_.isArray(query)) {
// if group has children, can search down the tree
var values = query.slice(0);
var list = this;
var ret;
while(values.length) {
ret = list.singleLookup(values.shift());
list = ret.getChildren();
}
return ret;
} else {
return this.singleLookup(query);
}
};
List.prototype.getLookupMap = function() {
var self = this;
if (! ('lookupMap' in self)) {
self.lookupMap = {};
self.forEach(function(d) {
if (d in self.lookupMap)
console.warn('multiple occurrence of ' + d +
' in list. Lookup will only get the last');
self.lookupMap[d] = d;
});
}
return self.lookupMap;
};
List.prototype.singleLookup = function(query) {
return this.getLookupMap()[query];
};
// lookup more than one thing at a time
List.prototype.lookupMany = function(query) {
var list = this;
return sg.addSupergroupMethods(_.chain(query).map(function(d) {
return list.singleLookup(d)
}).compact().value());
};
List.prototype.flattenTree = function() {
return _.chain(this)
.map(function(d) {
var desc = d.descendants();
return [d].concat(desc);
})
.flatten()
.filter(_.identity) // expunge nulls
.tap(sg.addListMethods)
.value();
};
List.prototype.nodesAtLevel = function(level, currentLevel = 0) {
if (level === currentLevel)
return this;
this.forEach(d => { if (!d.hasChildren()) throw new Error("asking for deeper level than exists") });
return sg.addListMethods(_.flatten(
this.map(d=>d.getChildren().nodesAtLevel(level, currentLevel + 1))));
};
List.prototype.addLevel = function(dim, opts) {
_.each(this, function(val) {
val.addLevel(dim, opts);
});
return this;
};
// VERY QUESTIONABLE STUFF FROM vocab-pop, NEED TO REVIEW IF NEEDED OR BROKEN
//if (opts.allowCloning) {
List.prototype.addLevelPure = function(dim, opts) {
// breaks prototype two levels up!!!!!!!!!!!!! no time to fix
let clone = this.clone()
//if (clone[0] && clone[0].children) debugger
_.each(clone, function(val) {
//val.addLevelPure(dim, opts);
val.addLevel(dim, opts);
});
return clone;
};
List.prototype.clone = function() {
let clone = Object.assign([], this)
clone.records = _.cloneDeep(this.records)
_.addSupergroupMethods(clone)
let list = this
clone.splice(0, clone.length, ...clone.map(
val => {
let newVal = makeValue(val)
_.extend(newVal, _.cloneDeep(val))
newVal.records = val.records.map(rec=>clone.records[rec._recIdx])
newVal.parentList = clone
//if (val.children) debugger
// WRONG RECORDS!!!!
if (val.hasChildren()) {
newVal[childProp] = val.getChildren().clone()
}
return newVal
}
))
return clone
}
//}
List.prototype.namePaths = function(opts) {
return _.map(this, function(d) {
return d.namePath(opts);
});
};
// apply a function to the records of each group
//
List.prototype.aggregates = function(func, field, ret) {
var results = _.map(this, function(val) {
return val.aggregate(func, field);
});
if (ret === 'dict')
return _.zipObject(this, results);
return results;
};
List.prototype.d3NestEntries = function() {
return _.map(this, function(val) {
if (childProp in val)
return {key: val.toString(), values: val.getChildren().d3NestEntries()};
return {key: val.toString(), values: val.records};
});
};
List.prototype.d3NestMap = function() {
return _.chain(this).map(
function(val) {
if (val.children)
return [val+'', val.children.d3NestMap()];
return [val+'', val.records.slice(0)];
}).fromPairs().value();
}
List.prototype._sort = Array.prototype.sort;
List.prototype.sort = function(func) {
return sg.addListMethods(this._sort(func));
};
List.prototype.sortBy = function(func) {
return sg.addListMethods(_.sortBy(this, func));
};
List.prototype.rootList = function(func) {
if ('parentVal' in this)
return this.parentVal.rootList();
return this;
};
// MORE STUFF ADDED FROM vocab-pop, NEEDS REVIEW
List.prototype.collapseOnlyChildren = function() {
this.forEach(val => {
if (val.hasChildren() && val.getChildren().length === 1) {
var child = val.getChildren()[0];
if (child.hasChildren()) {
val[childProp] = child[childProp];
// reduce depths?
}
val[child.dim] = child;
delete val[childProp];
}
if (val.hasChildren()) {
val.getChildren().collapseOnlyChildren()
}
})
}
/*
* something broke when I added func option...will fix later
List.prototype.summary = function(opts={}) {
let {depth=0, funcs={}} = opts
let out = []
//let indent = ' '.repeat(depth)
let indent = ''
let dim = `${this.dim}`
let vals = `${this.length} vals`
let recs = `${this.records.length} recs`
out.push(`${indent}${dim}, ${recs} (${depth}) ${vals}:`)
out.push(this.map(val=>val.summary({...opts,depth:depth+1})).join('\n'))
return out.join('\n')
}
Value.prototype.hasSiblings = function() {
return this.parentList.length > 1
}
Value.prototype.summary = function(opts={}) {
let {depth=0, funcs={}} = opts
let out = []
let indent = ' '.repeat(depth)
let recs = `${this.records.length} recs`
if (depth === 0) {
let dimPath = this.dimPath()
let namePath = this.namePath()
let valDepth = `lvl ${this.depth}`
let sibs = this.hasSiblings() ? `, ${this.parentList.length - 1} siblings` : ''
out.push(`${indent}${valDepth} ${namePath}(${dimPath}), ${recs}${sibs}`)
} else {
out.push(`${indent}${this}, ${recs}`)
}
if (funcs) {
_.each(funcs, (f,k) => {
out.push(`${indent} ${k}: ${f(this)}`)
})
out.push('')
}
let summary = out.join('\n')
if (this.hasChildren()) {
//summary += (' has: ' + this.getChildren().summary(opts))
out.push(`${indent}has:\n` + this.getChildren().summary(opts))
}
return summary
}
*/
List.prototype.summary = function(depth=0) {
let out = []
//let indent = ' '.repeat(depth)
let indent = ''
let dim = `${this.dim}`
let vals = `${this.length} vals`
let recs = `${this.records.length} recs`
out.push(`${indent}${dim}, ${recs} (${depth}) ${vals}:`)
out.push(this.map(val=>val.summary(depth+1)).join('\n'))
return out.join('\n')
}
Value.prototype.hasSiblings = function() {
return this.parentList && this.parentList.length > 1
}
Value.prototype.summary = function(depth=0) {
let out = []
let indent = ' '.repeat(depth)
let recs = `${this.records.length} recs`
if (depth === 0) {
let dimPath = this.dimPath()
let namePath = this.namePath()
let valDepth = `lvl ${this.depth}`
let sibs = this.hasSiblings() ? `, ${this.parentList.length - 1} siblings` : ''
out.push(`${indent}${valDepth} ${namePath}(${dimPath}), ${recs}${sibs}`)
} else {
out.push(`${indent}${this}, ${recs}`)
}
let summary = out.join('\n')
if (this.hasChildren()) {
summary += (' has: ' + this.getChildren().summary(depth))
}
return summary
}
function makeValue(v_arg) {
if (isNaN(v_arg)) {
return makeStringValue(v_arg);
} else {
return makeNumberValue(v_arg);
}
}
function StringValue() {}
//StringValue.prototype = new String;
function makeStringValue(s_arg) {
var S = new String(s_arg);
//S.__proto__ = StringValue.prototype; // won't work in IE10
for(var method in StringValue.prototype) {
Object.defineProperty(S, method, {
value: StringValue.prototype[method]
});
}
return S;
}
function NumberValue() {}
//NumberValue.prototype = new Number;
function makeNumberValue(n_arg) {
var N = new Number(n_arg);
//N.__proto__ = NumberValue.prototype;
for(var method in NumberValue.prototype) {
Object.defineProperty(N, method, {
value: NumberValue.prototype[method]
});
}
return N;
}
function wholeListNumeric(groups) {
var isNumeric = _.every(_.keys(groups), function(k) {
return k === null ||
k === undefined ||
(!isNaN(Number(k))) ||
["null", ".", "undefined"].indexOf(k.toLowerCase()) > -1;
});
if (isNumeric) {
_.each(_.keys(groups), function(k) {
if (isNaN(k)) {
delete groups[k]; // getting rid of NULL values in dim list!!
}
});
}
return isNumeric;
}
var childProp = 'children';
Value.prototype.extendGroupBy = // backward compatibility
Value.prototype.addLevel = function(dim, opts) {
opts = opts || {};
_.each(this.leafNodes() || [this], function(d) {
opts.parent = d;
if (!('in' in d)) { // d.in means it's part of a diffList
d.setChildren(sg.supergroup(d.records, dim, opts));
} else { // allows adding levels to diffLists. haven't used for a long time
if (d['in'] === "both") {
d.setChildren(sg.diffList(d.from, d.to, dim, opts));
} else {
d.setChildren(sg.supergroup(d.records, dim, opts));
_.each(d.getChildren(), function(c) {
c['in'] = d['in'];
c[d['in']] = d[d['in']];
});
}
}
d.getChildren().parentVal = d;
});
};
/* goal here is to make version of addLevel that doesn't
* modify existing list/vals at all. but it's hard to
* make a decent clone... gotta do this. */
// DOESN'T EXIST IN vocab-pop, LEAVING HERE BUT NEEDS REVIEW
/*
Value.prototype.concatLevel = function(dim, opts) {
opts = opts || {};
_.each(this.leafNodes() || [this], function(d) {
opts.parent = d;
if ('in' in d) {
throw new Error("not handling diffLists in concatLevel");
}
d.setChildren(sg.supergroup(d.records, dim, opts));
d.getChildren().parentVal = d;
});
};
*/
Value.prototype.leafNodes = function(level) {
// until commit 31278a35b91a8f4bd4ddc4376c840fb14d2723f9
// supported level param, to only go down so many levels
// not supporting that any more. wasn't using it
//if (!(childProp in this && this.getChildren().length)) return [this];
if (!this.hasChildren()) return [this];
return _.chain(this.descendants()).filter(
function(d){
return _.isEmpty(d.children);
}).addSupergroupMethods().value();
var ret = [this];
if (typeof level === "undefined") {
level = Infinity;
}
if (level !== 0 && this.getChildren() && this.getChildren().length && (!level || this.depth < level)) {
ret = _.flatten(_.map(this.getChildren(), function(c) {
return c.leafNodes(level);
}), true);
}
//return makeList(ret);
return sg.addListMethods(ret);
};
Value.prototype.getChildren = function(emptyListOk = false) {
if (emptyListOk)
// ADDED '|| []' FROM vocab-pop
return childProp in this && this[childProp] || [];
return childProp in this && this[childProp].length && this[childProp];
};
Value.prototype.setChildren = function(sg, clobber=false, returnThis=false) {
if (this.hasChildren() && !clobber) // clobbers empty children lists regardless of clobber setting
throw new Error("can't setChildren on value that already has children");
this[childProp] = sg; // assume sg is appropriate child list
if (returnThis)
return this;
return this[childProp];
};
Value.prototype.hasChildren = function(emptyListOk = false) {
return !!this.getChildren(emptyListOk);
};
Value.prototype.addRecordsAsChildrenToLeafNodes = function(truncateEmpty) {
// this method is to help with d3 layouts that expect the leaf level
// to be an array of raw records
function fixLeaf(node) {
node.children = node.records;
_.each(node.children, function(rec) {
rec.parent = node;
rec.depth = node.depth + 1;
for(var method in Value.prototype) {
Object.defineProperty(rec, method, {
value: Value.prototype[method]
});
}
});
}
if (typeof truncateEmpty === "undefined")
truncateEmpty = true;
if (truncateEmpty) {
var self = this;
self.descendants().forEach(function(node) {
if (self.parent && self.parent.children.length === 1) {
fixLeaf(node);
}
});
} else {
_.each(this.leafNodes(), function(node) {
fixLeaf(node);
});
}
return this;
};
/* didn't make this yet, just copied from above
Value.prototype.descendants = function(level) {
var ret = [this];
if (level !== 0 && this.getChildren() && (!level || this.depth < level))
ret = _.flatten(_.map(this.getChildren(), function(c) {
return c.leafNodes(level);
}), true);
return makeList(ret);
};
*/
function delimOpts(opts) {
if (typeof opts === "string") opts = {delim: opts};
opts = opts || {};
if (!_(opts).has('delim')) opts.delim = '/';
return opts;
}
Value.prototype.dimPath = function(opts) {
opts = delimOpts(opts);
opts.dimName = true;
return this.namePath(opts);
};
Value.prototype.namePath = function(opts) {
opts = delimOpts(opts);
var path = this.pedigree(opts);
if (opts.dimName) path = _.map(path, 'dim');
if (opts.asArray) return path;
return path.join(opts.delim);
/*
var delim = opts.delim || '/';
return (this.parent ?
this.parent.namePath(_.extend({},opts,{notLeaf:true})) : '') +
((opts.noRoot && this.depth===0) ? '' :
(this + (opts.notLeaf ? delim : ''))
)
*/
};
Value.prototype.path = // better than 'pedigree', right?
// FROM vocab-pop
Value.prototype.clone = function() {
// just throwing together quick...need to look at later
let newVal = makeValue(this)
_.extend(newVal, _.cloneDeep(this))
if (this.hasChildren()) {
newVal[childProp] = this.getChildren().clone()
}
return newVal
}
Value.prototype.pedigree = function(opts) {
opts = opts || {};
var path = [];
if (!opts.notThis) path.push(this);
var ptr = this;
while ((ptr = ptr.parent)) {
path.unshift(ptr);
}
if (opts.noRoot) path.shift();
if (opts.backwards || this.backwards) path.reverse(); //kludgy?
// FROM vocab-pop
//path = path.map(val=>val.clone())
//path = path.map(val=>makeValue(val))
_.addSupergroupMethods(path)
return path;
/* commented out in vocab-pop, doing same here
// CHANGING -- HOPE THIS DOESN'T BREAK STUFF (pedigree isn't
// documented yet)
if (!opts.asValues) return _.chain(path).invokeMap('valueOf').value();
return path;
*/
};
Value.prototype.descendants = function(opts) {
// these two lines fix a treelike bug, hope they don't do harm
if (!this.hasChildren())
this.setChildren(_.addSupergroupMethods([]));
return this.getChildren() ? this.getChildren().flattenTree() : undefined;
};
Value.prototype.lookup = function(query) {
if (_.isArray(query)) {
if (this.valueOf() == query[0]) { // allow string/num comparison to succeed?
query = query.slice(1);
if (query.length === 0)
return this;
}
} else if (_.isString(query)) {
if (this.valueOf() == query) {
return this;
}
} else {
throw new Error("invalid param: " + query);
}
if (!this.hasChildren())
throw new Error("can only call lookup on Values with kids");
return this.getChildren().lookup(query);
};
Value.prototype.pct = function() {
return this.records.length / this.parentList.records.length;
};
Value.prototype.previous = function() {
if (this.parentList) {
// could store pos on each value, but not doing that now
var pos = this.parentList.indexOf(this);
if (pos > 0) {
return this.parentList[pos - 1];
}
}
};
Value.prototype.aggregate = function(func, field) {
if (_.isFunction(field))
return func(_.map(this.records, field));
return func(_.map(this.records, field));
};
Value.prototype.rootList = function() {
return this.parentList.rootList();
};
/* not working yet
Value.clone() {
var holdChildren = this.getChildren(),
parent = this.parent,
parentList = this.parentList,
}
*/
/** Summarize records by a dimension
*
* @param {list} Records to be summarized
* @param {numericDim} Dimension to summarize by
*
* @memberof supergroup
*/
sg.aggregate = function(list, numericDim) {
if (numericDim) {
list = _.map(list, numericDim);
}
return _.reduce(list, function(memo,num){
memo.sum+=num;
memo.cnt++;
memo.avg=memo.sum/memo.cnt;
memo.max = Math.max(memo.max, num);
return memo;
},{sum:0,cnt:0,max:-Infinity});
};
/** Compare groups across two similar root nodes
*
* @param {from} ...
* @param {to} ...
* @param {dim} ...
* @param {opts} ...
*
* used by treelike and some earlier code
*
* @memberof supergroup
*/
sg.diffList = function(from, to, dim, opts) {
var fromList = sg.supergroup(from.records, dim, opts);
var toList = sg.supergroup(to.records, dim, opts);
//var list = makeList(sg.compare(fromList, toList, dim));
var list = sg.addListMethods(sg.compare(fromList, toList, dim));
list.dim = (opts && opts.dimName) ? opts.dimName : dim;
return list;
};
/** Compare two groups by a dimension
*
* @param {A} ...
* @param {B} ...
* @param {dim} ...
*
* @memberof supergroup
*/
sg.compare = function(A, B, dim) {
var a = _.chain(A).map(function(d) { return d+''; }).value();
var b = _.chain(B).map(function(d) { return d+''; }).value();
var comp = {};
_.each(A, function(d, i) {
comp[d+''] = {
name: d+'',
'in': 'from',
from: d,
fromIdx: i,
dim: dim
};
});
_.each(B, function(d, i) {
if ((d+'') in comp) {
var c = comp[d+''];
c['in'] = "both";
c.to = d;
c.toIdx = i;
} else {
comp[d+''] = {
name: d+'',
'in': 'to',
to: d,
toIdx: i,
dim: dim
};
}
});
var list = _.chain(comp).values().sort(function(a,b) {
return (a.fromIdx - b.fromIdx) || (a.toIdx - b.toIdx);
}).map(function(d) {
var val = makeValue(d.name);
_.extend(val, d);
val.records = [];
if ('from' in d)
val.records = val.records.concat(d.from.records);
if ('to' in d)
val.records = val.records.concat(d.to.records);
return val;
}).value();
_.chain(list).map(function(d) {
d.parentList = list;
d.records.parentVal = d;
}).value();
return list;
};
/** Concatenate two Values into a new one (??)
*
* @param {from} ...
* @param {to} ...
*
* @memberof supergroup
*/
sg.compareValue = function(from, to) { // any reason to keep this?
if (from.dim !== to.dim) {
throw new Error("not sure what you're trying to do");
}
var name = from + ' to ' + to;
var val = makeValue(name);
val.from = from;
val.to = to;
val.depth = 0;
val['in'] = "both";
val.records = [].concat(from.records,to.records);
val.records.parentVal = val;
val.dim = from.dim;
return val;
};
_.extend(StringValue.prototype, Value.prototype);
_.extend(NumberValue.prototype, Value.prototype);
/** Sometimes a List gets turned into a standard array,
* sg.g., through slicing or sorting or filtering.
* addListMethods turns it back into a List
*
* `List` would be a constructor if IE10 supported
* \_\_proto\_\_, so it pretends to be one instead.
*
* @param {Array} Array to be extended
*
* @memberof supergroup
*/
sg.addSupergroupMethods =
sg.addListMethods = function(arr) {
arr = arr || []; // KLUDGE for treelike
if (arr.isSupergroupList) return arr;
for(var method in List.prototype) {
Object.defineProperty(arr, method, {
value: List.prototype[method]
});
}
return arr;
};
// can't easily subclass Array, so this explicitly puts the List
// methods on an Array that's supposed to be a List
function makeList(arr_arg) {
var arr = [ ];
arr.push.apply(arr, arr_arg);
sg.addListMethods(arr);
/*
//arr.__proto__ = List.prototype;
for(var method in List.prototype) {
Object.defineProperty(arr, method, {
value: List.prototype[method]
});
}
*/
return arr;
}
sg.hierarchicalTableToTree = function(data, parentProp, childProp) {
// does not do the right thing if a value has two parents
// also, does not yet fix depth numbers
var parents = sg.supergroup(data,[parentProp, childProp]); // 2-level grouping with all parent/child pairs
var children = parents.leafNodes();
var topParents = _.filter(parents, function(parent) {
var adoptiveParent = children.lookup(parent); // is this parent also a child?
if (adoptiveParent) { // if so, make it the parent
adoptiveParent.children = sg.addSupergroupMethods([]);
_.each(parent.children, function(c) {
c.parent = adoptiveParent;
adoptiveParent.children.push(c)
});
} else { // if not, this is a top parent
return parent;
}
// if so, make use that child node, move this parent node's children over to it
});
return sg.addSupergroupMethods(topParents);
};
return sg;
function filterOutEmpty(recs, dim) {
var func = _.isFunction(dim) ? dim : d=>d[dim];
recs = recs.filter(r => !_.isEmpty(func(r)) || (_.isNumber(func(r)) && isFinite(func(r)))); // _.isEmpty(0) === true
return recs;
}
}());
// allows grouping by a field that contains an array of values rather than just a single value
if (createAggregator) {
var multiValuedGroupBy = createAggregator(
function(result, value, keys) {
if (!Array.isArray(keys)) {
//if (preventScalarInMultiValuedGroup)
throw new Error("not array")
}
_.each(keys, function(key) {
// FROM vocab-pop (line replaces commented section)
result[key] = _.uniq([...(result[key]||[]), value])
/*
if (hasOwnProperty.call(result, key)) {
result[key].push(value);
} else {
result[key] = [value];
}
*/
});
}, null, preventScalarInMultiValuedGroup = false);
} else {
var multiValuedGroupBy = function() { throw new Error("couldn't install multiValuedGroupBy") };
}
_.mixin({
supergroup: supergroup.supergroup,
addSupergroupMethods: supergroup.addSupergroupMethods,
multiValuedGroupBy: multiValuedGroupBy,
sgDiffList: supergroup.diffList,
sgCompare: supergroup.compare,
sgCompareValue: supergroup.compareValue,
sgAggregate: supergroup.aggregate,
hierarchicalTableToTree: supergroup.hierarchicalTableToTree,
stateClass: supergroup.State,
// FROM https://gist.github.com/AndreasBriese/1670507
// Return aritmethic mean of the elements
// if an iterator function is given, it is applied before
sum : function(obj, iterator, context) {
if (!iterator && _.isEmpty(obj)) return 0;
var result = 0;
if (!iterator && _.isArray(obj)){
for(var i=obj.length-1;i>-1;i-=1){
result += obj[i];
};
return result;
};
_.each(obj, function(value, index, list) {
var computed = iterator ? iterator.call(context, value, index, list) : value;
result += computed;
});
return result;
},
mean : function(obj, iterator, context) {
if (!iterator && _.isEmpty(obj)) return Infinity;
if (!iterator && _.isArray(obj)) return _.sum(obj)/obj.length;
if (_.isArray(obj) && !_.isEmpty(obj)) return _.sum(obj, iterator, context)/obj.length;
},
// Return median of the elements
// if the object element number is odd the median is the
// object in the "middle" of a sorted array
// in case of an even number, the arithmetic mean of the two elements
// in the middle (in case of characters or strings: obj[n/2-1] ) is returned.
// if an iterator function is provided, it is applied before
median : function(obj, iterator, context) {
if (_.isEmpty(obj)) return Infinity;
var tmpObj = [];
if (!iterator && _.isArray(obj)){
tmpObj = _.clone(obj);
tmpObj.sort(function(f,s){return f-s;});
}else{
_.isArray(obj) && _.each(obj, function(value, index, list) {
tmpObj.push(iterator ? iterator.call(context, value, index, list) : value);
tmpObj.sort();
});
};
return tmpObj.length%2 ? tmpObj[Math.floor(tmpObj.length/2)] : (_.isNumber(tmpObj[tmpObj.length/2-1]) && _.isNumber(tmpObj[tmpObj.length/2])) ? (tmpObj[tmpObj.length/2-1]+tmpObj[tmpObj.length/2]) /2 : tmpObj[tmpObj.length/2-1];
},
});
if (typeof module !== "undefined")
module.exports = _;