StackGenVis: Alignment of Data, Algorithms, and Models for Stacking Ensemble Learning Using Performance Metrics
https://doi.org/10.1109/TVCG.2020.3030352
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
561 lines
13 KiB
561 lines
13 KiB
/*!
|
|
* async-helpers <https://github.com/doowb/async-helpers>
|
|
*
|
|
* Copyright (c) 2015-2017, Brian Woodward.
|
|
* Released under the MIT License.
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
var typeOf = require('kind-of');
|
|
var co = require('co');
|
|
|
|
/**
|
|
* Caches
|
|
*/
|
|
|
|
var cache = {};
|
|
var stash = {};
|
|
|
|
/**
|
|
* Create a new instance of AsyncHelpers
|
|
*
|
|
* ```js
|
|
* var asyncHelpers = new AsyncHelpers();
|
|
* ```
|
|
*
|
|
* @param {Object} `options` options to pass to instance
|
|
* @return {Object} new AsyncHelpers instance
|
|
* @api public
|
|
*/
|
|
|
|
function AsyncHelpers(options) {
|
|
if (!(this instanceof AsyncHelpers)) {
|
|
return new AsyncHelpers(options);
|
|
}
|
|
this.options = Object.assign({}, options);
|
|
this.prefix = this.options.prefix || '{$ASYNCID$';
|
|
this.globalCounter = AsyncHelpers.globalCounter++;
|
|
this.helpers = {};
|
|
this.counter = 0;
|
|
this.prefixRegex = toRegex(this.prefix);
|
|
}
|
|
|
|
/**
|
|
* Keep track of instances created for generating globally
|
|
* unique ids
|
|
* @type {Number}
|
|
*/
|
|
|
|
AsyncHelpers.globalCounter = 0;
|
|
AsyncHelpers.cache = cache;
|
|
AsyncHelpers.stash = stash;
|
|
|
|
/**
|
|
* Add a helper to the cache.
|
|
*
|
|
* ```js
|
|
* asyncHelpers.set('upper', function(str, cb) {
|
|
* cb(null, str.toUpperCase());
|
|
* });
|
|
* ```
|
|
*
|
|
* @param {String} `name` Name of the helper
|
|
* @param {Function} `fn` Helper function
|
|
* @return {Object} Returns `this` for chaining
|
|
* @api public
|
|
*/
|
|
|
|
AsyncHelpers.prototype.set = function(name, fn) {
|
|
if (isObject(name)) {
|
|
var keys = Object.keys(name);
|
|
for (var i = 0; i < keys.length; i++) {
|
|
var key = keys[i];
|
|
this.set(key, name[key]);
|
|
}
|
|
return this;
|
|
}
|
|
|
|
if (typeof name !== 'string') {
|
|
throw new TypeError('AsyncHelpers#set: expected `name` to be a string');
|
|
}
|
|
if (typeof fn !== 'function' && !isObject(fn)) {
|
|
throw new TypeError('AsyncHelpers#set: expected `fn` to be a function or object');
|
|
}
|
|
|
|
this.helpers[name] = fn;
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Get all helpers or a helper with the given name.
|
|
*
|
|
* ```js
|
|
* var helpers = asyncHelpers.get();
|
|
* var wrappedHelpers = asyncHelpers.get({wrap: true});
|
|
* ```
|
|
*
|
|
* @param {String} `name` Optionally pass in a name of a helper to get.
|
|
* @param {Object} `options` Additional options to use.
|
|
* @option {Boolean} `wrap` Wrap the helper(s) with async processing capibilities
|
|
* @return {Function|Object} Single helper function when `name` is provided, otherwise object of all helpers
|
|
* @api public
|
|
*/
|
|
|
|
AsyncHelpers.prototype.get = function(helper, options) {
|
|
if (typeof helper === 'string') {
|
|
return this.wrapHelper.apply(this, arguments);
|
|
}
|
|
return this.wrapHelpers(this.helpers, helper);
|
|
};
|
|
|
|
/**
|
|
* Wrap a helper with async handling capibilities.
|
|
*
|
|
* ```js
|
|
* var wrappedHelper = asyncHelpers.wrap('upper');
|
|
* var wrappedHelpers = asyncHelpers.wrap();
|
|
* ```
|
|
*
|
|
* @param {String} `helper` Optionally pass the name of the helper to wrap
|
|
* @return {Function|Object} Single wrapped helper function when `name` is provided, otherwise object of all wrapped helpers.
|
|
* @api public
|
|
*/
|
|
|
|
AsyncHelpers.prototype.wrapHelper = function(helper, options) {
|
|
if (isObject(helper) && typeof options === 'undefined') {
|
|
options = helper;
|
|
helper = this.helpers;
|
|
}
|
|
|
|
options = options || {};
|
|
helper = helper || this.helpers;
|
|
|
|
var type = typeOf(helper);
|
|
switch (type) {
|
|
case 'string':
|
|
return this.wrapHelper(this.helpers[helper], options);
|
|
case 'object':
|
|
return this.wrapHelpers(helper, options);
|
|
case 'function':
|
|
if (isHelperGroup(helper)) {
|
|
return this.wrapHelpers(helper, options);
|
|
}
|
|
if (options.wrap && helper.wrapped !== true) {
|
|
return this.wrapper(helper.name || helper.displayName, helper, this);
|
|
}
|
|
return helper;
|
|
default: {
|
|
throw new TypeError('AsyncHelpers.wrapHelper: unsupported type: ' + type);
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Wrap an object of helpers to enable async handling
|
|
* @param {Object} `helpers`
|
|
* @param {Object} `options`
|
|
*/
|
|
|
|
AsyncHelpers.prototype.wrapHelpers = function(helpers, options) {
|
|
if (!isObject(helpers) && isHelperGroup(helpers)) {
|
|
throw new TypeError('expected helpers to be an object');
|
|
}
|
|
|
|
var res = {};
|
|
var keys = Object.keys(helpers);
|
|
for (var i = 0; i < keys.length; i++) {
|
|
var key = keys[i];
|
|
var helper = helpers[key];
|
|
if (isObject(helper)) {
|
|
res[key] = this.wrapHelpers(helper, options);
|
|
} else {
|
|
if (helper.wrapped !== true) {
|
|
if (typeOf(helper) === 'function' && !helper.name && !helper.displayName) {
|
|
helper.displayName = key;
|
|
}
|
|
res[key] = this.wrapHelper(helper, options);
|
|
} else {
|
|
res[key] = helper;
|
|
}
|
|
}
|
|
}
|
|
return res;
|
|
};
|
|
|
|
/**
|
|
* Returns a wrapper function for a single helper.
|
|
* @param {String} `name` The name of the helper
|
|
* @param {Function} `fn` The actual helper function
|
|
* @return {String} Returns an async ID to use for resolving the value. ex: `{$ASYNCID$!$8$}`
|
|
*/
|
|
|
|
AsyncHelpers.prototype.wrapper = function(name, fn) {
|
|
var prefix = appendPrefix(this.prefix, this.globalCounter);
|
|
var self = this;
|
|
|
|
// wrap the helper and generate a unique ID for resolving it
|
|
function wrapper() {
|
|
var num = self.counter++;
|
|
var id = createId(prefix, num);
|
|
|
|
var token = {
|
|
name: name,
|
|
async: !!fn.async,
|
|
prefix: prefix,
|
|
num: num,
|
|
id: id,
|
|
fn: fn,
|
|
args: [].slice.call(arguments)
|
|
};
|
|
|
|
define(token, 'context', this);
|
|
stash[id] = token;
|
|
return id;
|
|
}
|
|
|
|
define(wrapper, 'wrapped', true);
|
|
define(wrapper, 'helperName', name);
|
|
return wrapper;
|
|
};
|
|
|
|
/**
|
|
* Reset all the stashed helpers.
|
|
*
|
|
* ```js
|
|
* asyncHelpers.reset();
|
|
* ```
|
|
* @return {Object} Returns `this` to enable chaining
|
|
* @api public
|
|
*/
|
|
|
|
AsyncHelpers.prototype.reset = function() {
|
|
stash = {};
|
|
this.counter = 0;
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Get all matching ids from the given `str`
|
|
* @return {Array} Returns an array of matching ids
|
|
*/
|
|
|
|
AsyncHelpers.prototype.matches = function(str) {
|
|
if (typeof str !== 'string') {
|
|
throw new TypeError('AsyncHelpers#matches expects a string');
|
|
}
|
|
return str.match(this.prefixRegex);
|
|
};
|
|
|
|
/**
|
|
* Returns true if the given string has an async helper id
|
|
* @return {Boolean}
|
|
*/
|
|
|
|
AsyncHelpers.prototype.hasAsyncId = function(str) {
|
|
if (typeof str !== 'string') {
|
|
throw new TypeError('AsyncHelpers#hasAsyncId expects a string');
|
|
}
|
|
return str.indexOf(this.prefix) !== -1;
|
|
};
|
|
|
|
/**
|
|
* Resolve a stashed helper by the generated id.
|
|
* This is a generator function and should be used with [co][]
|
|
*
|
|
* ```js
|
|
* var upper = asyncHelpers.get('upper', {wrap: true});
|
|
* var id = upper('doowb');
|
|
*
|
|
* co(asyncHelpers.resolveId(id))
|
|
* .then(console.log)
|
|
* .catch(console.error);
|
|
*
|
|
* //=> DOOWB
|
|
* ```
|
|
*
|
|
* @param {String} `key` ID generated when from executing a wrapped helper.
|
|
* @api public
|
|
*/
|
|
|
|
AsyncHelpers.prototype.resolveId = function * (key) {
|
|
if (typeof key !== 'string') {
|
|
throw new Error('AsyncHelpers#resolveId: expects `key` to be a string.');
|
|
}
|
|
|
|
var helper = stash[key];
|
|
if (!helper) {
|
|
throw new Error('AsyncHelpers#resolveId: cannot resolve helper: "' + key + '"');
|
|
}
|
|
|
|
var args = yield this.resolveArgs(helper);
|
|
var self = this;
|
|
var str;
|
|
|
|
return yield function(cb) {
|
|
if (typeof helper.fn !== 'function') {
|
|
cb(null, helper.fn);
|
|
return;
|
|
}
|
|
|
|
var next = function(err, val) {
|
|
if (typeof val !== 'undefined') {
|
|
helper.fn = val;
|
|
cb(err, helper.fn);
|
|
return;
|
|
}
|
|
cb(err, '');
|
|
return;
|
|
};
|
|
|
|
if (helper.fn.async) {
|
|
var callback = function(err, result) {
|
|
if (err) {
|
|
next(formatError(err, helper, args));
|
|
return;
|
|
}
|
|
|
|
if (typeof result === 'string' && self.hasAsyncId(result)) {
|
|
self.resolveIds(result, next);
|
|
return;
|
|
}
|
|
|
|
next(null, result);
|
|
return;
|
|
};
|
|
|
|
args.push(callback);
|
|
}
|
|
|
|
try {
|
|
str = helper.fn.apply(helper.context, args);
|
|
if (typeof str === 'string' && self.hasAsyncId(str)) {
|
|
self.resolveIds(str, next);
|
|
return;
|
|
}
|
|
} catch (err) {
|
|
next(formatError(err, helper, args));
|
|
return;
|
|
}
|
|
|
|
if (!helper.fn.async) {
|
|
next(null, str);
|
|
return;
|
|
}
|
|
|
|
// do nothing
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Generator function for resolving helper arguments
|
|
* that contain async ids. This function should be used
|
|
* with [co][].
|
|
*
|
|
* This is used inside `resolveId`:
|
|
*
|
|
* ```js
|
|
* var args = yield co(asyncHelpers.resolveArgs(helper));
|
|
* ```
|
|
* @param {Object} `helper` helper object with an `argRefs` array.
|
|
*/
|
|
|
|
AsyncHelpers.prototype.resolveArgs = function * (helper) {
|
|
for (var i = 0; i < helper.args.length; i++) {
|
|
var arg = helper.args[i];
|
|
if (!arg) continue;
|
|
|
|
if (typeof arg === 'string' && this.hasAsyncId(arg)) {
|
|
helper.args[i] = yield this.resolveId(arg);
|
|
|
|
} else if (isObject(arg) && isObject(arg.hash)) {
|
|
arg.hash = yield this.resolveObject(arg.hash);
|
|
}
|
|
}
|
|
return helper.args;
|
|
};
|
|
|
|
/**
|
|
* Generator function for resolving values on an object
|
|
* that contain async ids. This function should be used
|
|
* with [co][].
|
|
*
|
|
* This is used inside `resolveArgs`:
|
|
*
|
|
* ```js
|
|
* var args = yield co(asyncHelpers.resolveObject(options.hash));
|
|
* ```
|
|
* @param {Object} `obj` object with with values that may be async ids.
|
|
* @returns {Object} Object with resolved values.
|
|
*/
|
|
|
|
AsyncHelpers.prototype.resolveObject = function * (obj) {
|
|
var keys = Object.keys(obj);
|
|
var self = this;
|
|
|
|
return yield keys.reduce(function(acc, key) {
|
|
return co(function * () {
|
|
var val = acc[key];
|
|
if (typeof val === 'string' && self.hasAsyncId(val)) {
|
|
acc[key] = yield self.resolveId(val);
|
|
}
|
|
return acc;
|
|
});
|
|
}, obj);
|
|
};
|
|
|
|
/**
|
|
* After rendering a string using wrapped async helpers,
|
|
* use `resolveIds` to invoke the original async helpers and replace
|
|
* the async ids with results from the async helpers.
|
|
*
|
|
* ```js
|
|
* asyncHelpers.resolveIds(renderedString, function(err, content) {
|
|
* if (err) return console.error(err);
|
|
* console.log(content);
|
|
* });
|
|
* ```
|
|
* @param {String} `str` String containing async ids
|
|
* @param {Function} `cb` Callback function accepting an `err` and `content` parameters.
|
|
* @api public
|
|
*/
|
|
|
|
AsyncHelpers.prototype.resolveIds = function(str, cb) {
|
|
if (typeof cb !== 'function') {
|
|
throw new TypeError('AsyncHelpers#resolveIds() expects a callback function.');
|
|
}
|
|
if (typeof str !== 'string') {
|
|
return cb(new TypeError('AsyncHelpers#resolveIds() expects a string.'));
|
|
}
|
|
|
|
var matches = this.matches(str);
|
|
var self = this;
|
|
|
|
co(function * () {
|
|
if (!matches) {
|
|
return str;
|
|
};
|
|
|
|
for (var i = 0; i < matches.length; i++) {
|
|
var key = matches[i];
|
|
var val = yield self.resolveId(key);
|
|
str = str.split(key).join(val);
|
|
}
|
|
return str;
|
|
})
|
|
.then((res) => cb(null, res))
|
|
.catch(cb);
|
|
};
|
|
|
|
/**
|
|
* Format an error message to provide better information about the
|
|
* helper and the arguments passed to the helper when the error occurred.
|
|
*
|
|
* @param {Object} `err` Error object
|
|
* @param {Object} `helper` helper object to provide more information
|
|
* @param {Array} `args` Array of arguments passed to the helper.
|
|
* @return {Object} Formatted Error object
|
|
*/
|
|
|
|
function formatError(err, helper, args) {
|
|
err.helper = helper;
|
|
define(err, 'args', args);
|
|
return err;
|
|
}
|
|
|
|
/**
|
|
* Create a prefix to use when generating an async id.
|
|
*
|
|
* @param {String} `prefix` prefix string to start with.
|
|
* @param {String} `counter` string to append.
|
|
* @return {String} new prefix
|
|
*/
|
|
|
|
function appendPrefix(prefix, counter) {
|
|
return prefix + counter + '$';
|
|
}
|
|
|
|
/**
|
|
* Create an async id from the provided prefix and counter.
|
|
*
|
|
* @param {String} `prefix` prefix string to start with
|
|
* @param {String} `counter` string to append.
|
|
* @return {String} async id
|
|
*/
|
|
|
|
function createId(prefix, counter) {
|
|
return appendPrefix(prefix, counter) + '}';
|
|
}
|
|
|
|
/**
|
|
* Create a regular expression based on the given `prefix`.
|
|
* @param {String} `prefix`
|
|
* @return {RegExp}
|
|
*/
|
|
|
|
function toRegex(prefix) {
|
|
var key = appendPrefix(prefix, '(\\d+)');
|
|
if (cache.hasOwnProperty(key)) {
|
|
return cache[key];
|
|
}
|
|
var regex = new RegExp(createRegexString(key), 'g');
|
|
cache[key] = regex;
|
|
return regex;
|
|
}
|
|
|
|
/**
|
|
* Create a string to pass into `RegExp` for checking for and finding async ids.
|
|
* @param {String} `prefix` prefix to use for the first part of the regex
|
|
* @return {String} string to pass into `RegExp`
|
|
*/
|
|
|
|
function createRegexString(prefix) {
|
|
var key = 'createRegexString:' + prefix;
|
|
if (cache.hasOwnProperty(key)) {
|
|
return cache[key];
|
|
}
|
|
var str = (prefix + '(\\d+)$}').replace(/\\?([${}])/g, '\\$1');
|
|
cache[key] = str;
|
|
return str;
|
|
}
|
|
|
|
/**
|
|
* Return true if the given value is a helper "group"
|
|
*/
|
|
|
|
function isHelperGroup(helpers) {
|
|
if (!helpers) return false;
|
|
if (helpers.isGroup) {
|
|
return true;
|
|
}
|
|
if (typeof helpers === 'function' || isObject(helpers)) {
|
|
var keys = Object.keys(helpers).filter(function(name) {
|
|
return ['async', 'sync', 'displayName'].indexOf(name) === -1;
|
|
});
|
|
return keys.length > 1;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Return true if the given value is an object
|
|
*/
|
|
|
|
function isObject(val) {
|
|
return typeOf(val) === 'object';
|
|
}
|
|
|
|
function define(obj, key, val) {
|
|
Object.defineProperty(obj, key, {
|
|
configurable: true,
|
|
enumerable: false,
|
|
value: val
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Expose `AsyncHelpers`
|
|
*/
|
|
|
|
module.exports = AsyncHelpers;
|
|
|
|
|