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.
645 lines
15 KiB
645 lines
15 KiB
4 years ago
|
/*!
|
||
|
* map-schema <https://github.com/jonschlinkert/map-schema>
|
||
|
*
|
||
|
* Copyright (c) 2016, Jon Schlinkert.
|
||
|
* Licensed under the MIT License.
|
||
|
*/
|
||
|
|
||
|
'use strict';
|
||
|
|
||
|
var util = require('util');
|
||
|
var Emitter = require('component-emitter');
|
||
|
var debug = require('debug')('map-schema');
|
||
|
var Data = require('./lib/data');
|
||
|
var Field = require('./lib/field');
|
||
|
var utils = require('./lib/utils');
|
||
|
|
||
|
/**
|
||
|
* Expose `Schema`
|
||
|
*/
|
||
|
|
||
|
module.exports = Schema;
|
||
|
|
||
|
/**
|
||
|
* Create a new `Schema` with the given `options`.
|
||
|
*
|
||
|
* ```js
|
||
|
* var schema = new Schema()
|
||
|
* .field('name', 'string')
|
||
|
* .field('version', 'string')
|
||
|
* .field('license', 'string')
|
||
|
* .field('licenses', 'array', {
|
||
|
* normalize: function(val, key, config) {
|
||
|
* // convert license array to `license` string
|
||
|
* config.license = val[0].type;
|
||
|
* delete config[key];
|
||
|
* }
|
||
|
* })
|
||
|
* .normalize(require('./package'))
|
||
|
* ```
|
||
|
* @param {Object} `options`
|
||
|
* @api public
|
||
|
*/
|
||
|
|
||
|
function Schema(options) {
|
||
|
this.options = options || {};
|
||
|
this.data = new Data();
|
||
|
this.isSchema = true;
|
||
|
this.utils = utils;
|
||
|
this.initSchema();
|
||
|
this.addFields(this.options);
|
||
|
var only = utils.arrayify(this.options.pick || this.options.only);
|
||
|
utils.define(this.options, 'only', only);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Inherit Emitter
|
||
|
*/
|
||
|
|
||
|
Emitter(Schema.prototype);
|
||
|
|
||
|
/**
|
||
|
* Initalize Schema instance properties
|
||
|
*/
|
||
|
|
||
|
Schema.prototype.initSchema = function() {
|
||
|
this.normalizers = {};
|
||
|
this.validators = {};
|
||
|
this.defaults = {};
|
||
|
this.required = [];
|
||
|
this.warnings = [];
|
||
|
this.fields = {};
|
||
|
this.remove = [];
|
||
|
this.fns = [];
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Set `key` on the instance with the given `value`.
|
||
|
*
|
||
|
* @param {String} `key`
|
||
|
* @param {Object} `value`
|
||
|
* @api public
|
||
|
*/
|
||
|
|
||
|
Schema.prototype.set = function(key, value) {
|
||
|
utils.set(this, key, value);
|
||
|
return this;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Push a warning onto the `schema.warnings` array. Placeholder for
|
||
|
* better message handling and a reporter (planned).
|
||
|
*
|
||
|
* @param {String} `method` The name of the method where the warning is recorded.
|
||
|
* @param {String} `prop` The name of the field for which the warning is being created.
|
||
|
* @param {String} `message` The warning message.
|
||
|
* @param {String} `value` The value associated with the warning.
|
||
|
* @return {any}
|
||
|
* @api public
|
||
|
*/
|
||
|
|
||
|
Schema.prototype.warning = function(method, prop, msg, value) {
|
||
|
var warning = { method: method, prop: prop, message: msg, args: value };
|
||
|
if (typeof value !== 'undefined') {
|
||
|
warning.value = value;
|
||
|
}
|
||
|
this.warnings.push(warning);
|
||
|
this.emit('warning', method, prop, warning);
|
||
|
return this;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Add a field to the schema with the given `name`, `type` or types,
|
||
|
* and options.
|
||
|
*
|
||
|
* ```js
|
||
|
* var semver = require('semver');
|
||
|
*
|
||
|
* schema
|
||
|
* .field('keywords', 'array')
|
||
|
* .field('version', 'string', {
|
||
|
* validate: function(val, key, config, schema) {
|
||
|
* return semver.valid(val) !== null;
|
||
|
* }
|
||
|
* })
|
||
|
* ```
|
||
|
* @param {String} `name`
|
||
|
* @param {String|Array} `type`
|
||
|
* @param {Object} `options`
|
||
|
* @return {Object} Returns the instance for chaining.
|
||
|
* @api public
|
||
|
*/
|
||
|
|
||
|
Schema.prototype.field = function(name, type, options) {
|
||
|
var pick = utils.arrayify(this.options.pick || this.options.only);
|
||
|
if (pick && pick.length && !utils.hasElement(name, pick)) {
|
||
|
return this;
|
||
|
}
|
||
|
|
||
|
if (typeof options === 'function') {
|
||
|
options = { normalize: options };
|
||
|
}
|
||
|
|
||
|
debug('adding field "%s"', name);
|
||
|
var field = new Field(type, options || {});
|
||
|
field.name = name;
|
||
|
|
||
|
if (field.hasOwnProperty('default')) {
|
||
|
this.defaults[name] = field.default;
|
||
|
}
|
||
|
if (field.required) {
|
||
|
this.required.push(name);
|
||
|
}
|
||
|
|
||
|
this.fields[name] = field;
|
||
|
return this;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Add an object of fields to `schema.fields`. If `options.extend` is true,
|
||
|
* and a field with the given name already exists, the new field will extend
|
||
|
* the existing field.
|
||
|
*
|
||
|
* @param {Object} `options`
|
||
|
*/
|
||
|
|
||
|
Schema.prototype.addFields = function(options) {
|
||
|
options = options || {};
|
||
|
if (options.fields) {
|
||
|
for (var key in options.fields) {
|
||
|
var val = options.fields[key];
|
||
|
if (this.fields[key] && (val.extend || options.extend)) {
|
||
|
val = utils.merge({}, val, this.fields[key]);
|
||
|
}
|
||
|
this.field(key, val);
|
||
|
}
|
||
|
}
|
||
|
return this;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Get field `name` from the schema. Get a specific property from
|
||
|
* the field by passing the property name as a second argument.
|
||
|
*
|
||
|
* ```js
|
||
|
* schema.field('bugs', ['object', 'string']);
|
||
|
* var field = schema.get('bugs', 'types');
|
||
|
* //=> ['object', 'string']
|
||
|
* ```
|
||
|
* @param {Strign} `name`
|
||
|
* @param {String} `prop`
|
||
|
* @return {Object|any} Returns the field instance or the value of `prop` if specified.
|
||
|
* @api public
|
||
|
*/
|
||
|
|
||
|
Schema.prototype.get = function(name, prop) {
|
||
|
return utils.get(this.fields, prop ? [name, prop] : name);
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Omit a property from the returned object. This method can be used
|
||
|
* in normalize functions as a way of removing undesired properties.
|
||
|
*
|
||
|
* @param {String} `key` The property to remove
|
||
|
* @return {Object} Returns the instance for chaining.
|
||
|
* @api public
|
||
|
*/
|
||
|
|
||
|
Schema.prototype.omit = function(key) {
|
||
|
this.remove.push(key);
|
||
|
return this;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Remove `key` from the `config` object.
|
||
|
*
|
||
|
* @param {String} key
|
||
|
* @param {Object} config
|
||
|
* @return {undefined} The object is modified in place
|
||
|
*/
|
||
|
|
||
|
Schema.prototype.removeKey = function(key, config) {
|
||
|
if (this.remove.indexOf(key) !== -1) {
|
||
|
delete this.defaults[key];
|
||
|
delete config[key];
|
||
|
return true;
|
||
|
}
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Update a property on the returned object. This method will trigger validation
|
||
|
* and normalization of the updated property.
|
||
|
*
|
||
|
* @param {String} `key` The property to update.
|
||
|
* @param {*} `val` Value of the property to update.
|
||
|
* @return {Object} Returns the instance for chaining.
|
||
|
* @api public
|
||
|
*/
|
||
|
|
||
|
Schema.prototype.update = function(key, val, config) {
|
||
|
debug('updating field "%s"', key);
|
||
|
|
||
|
if (arguments.length === 2) {
|
||
|
config = val;
|
||
|
val = config[key];
|
||
|
}
|
||
|
|
||
|
if (typeof val !== 'undefined' && config) {
|
||
|
config[key] = val;
|
||
|
}
|
||
|
|
||
|
this.normalizeField(key, val, config);
|
||
|
return this;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Returns true if field `name` is an optional field.
|
||
|
*
|
||
|
* @param {String} `name`
|
||
|
* @return {Boolean}
|
||
|
* @api public
|
||
|
*/
|
||
|
|
||
|
Schema.prototype.isOptional = function(name) {
|
||
|
return this.get(name, 'optional') === true;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Returns true if field `name` was defined as a required field.
|
||
|
*
|
||
|
* @param {String} `name`
|
||
|
* @return {Boolean}
|
||
|
* @api public
|
||
|
*/
|
||
|
|
||
|
Schema.prototype.isRequired = function(name) {
|
||
|
return this.get(name, 'required') === true;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* When `options.defaults` is true, any default values defined
|
||
|
* on fields will be set on properties that aren't set or do not
|
||
|
* have a value already define.
|
||
|
*
|
||
|
* We need to loop over the config again, since defaults might
|
||
|
* be defined for properties that don't exist on the config.
|
||
|
*
|
||
|
* @param {Object} `config`
|
||
|
* @return {Object} returns the config object with defaults defined.
|
||
|
*/
|
||
|
|
||
|
Schema.prototype.setDefaults = function(config) {
|
||
|
config = utils.extend({}, config);
|
||
|
if (this.options.defaults === false) {
|
||
|
return config;
|
||
|
}
|
||
|
for (var key in this.defaults) {
|
||
|
if (!utils.hasValue(key, config)) {
|
||
|
config[key] = this.defaults[key];
|
||
|
}
|
||
|
}
|
||
|
return config;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Checks the config object for missing fields and. If found,
|
||
|
* a warning message is pushed onto the `schema.warnings` array,
|
||
|
* which can be used for reporting.
|
||
|
*
|
||
|
* @param {Object} `config`
|
||
|
* @return {Array}
|
||
|
* @api public
|
||
|
*/
|
||
|
|
||
|
Schema.prototype.missingFields = function(config) {
|
||
|
if (this.options.required === false) {
|
||
|
return false;
|
||
|
}
|
||
|
var len = this.required.length, i = -1;
|
||
|
var res = [];
|
||
|
while (++i < len) {
|
||
|
var prop = this.required[i];
|
||
|
if (!config.hasOwnProperty(prop)) {
|
||
|
this.warning('missing', prop);
|
||
|
res.push(prop);
|
||
|
}
|
||
|
}
|
||
|
return res;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* If a `keys` array is passed on the constructor options, or
|
||
|
* as a second argument to `sortObject`, this sorts the given
|
||
|
* object so that keys are in the same order as the supplied
|
||
|
* array of `keys`.
|
||
|
*
|
||
|
* ```js
|
||
|
* schema.sortObject({z: '', a: ''}, ['a', 'z']);
|
||
|
* //=> {a: '', z: ''}
|
||
|
* ```
|
||
|
* @param {Object} `config`
|
||
|
* @return {Object} Returns the config object with keys sorted to match the given array of keys.
|
||
|
* @api public
|
||
|
*/
|
||
|
|
||
|
Schema.prototype.sortObject = function(config, options) {
|
||
|
var opts = utils.merge({}, this.options, options);
|
||
|
var keys = opts.keys;
|
||
|
|
||
|
if (Array.isArray(keys) && keys.length) {
|
||
|
keys = utils.union(keys, Object.keys(config));
|
||
|
var len = keys.length, i = -1;
|
||
|
var res = {};
|
||
|
while (++i < len) {
|
||
|
var key = keys[i];
|
||
|
if (config.hasOwnProperty(key)) {
|
||
|
res[key] = config[key];
|
||
|
}
|
||
|
}
|
||
|
return res;
|
||
|
}
|
||
|
return config;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* When `options.sortArrays` _is not false_, sorts all arrays in the
|
||
|
* given `config` object using JavaScript's native `.localeCompare`
|
||
|
* method.
|
||
|
*
|
||
|
* @param {Object} `config`
|
||
|
* @return {Object} returns the config object with sorted arrays
|
||
|
* @api public
|
||
|
*/
|
||
|
|
||
|
Schema.prototype.sortArrays = function(config) {
|
||
|
if (this.options.sortArrays !== false) {
|
||
|
return utils.sortArrays(config);
|
||
|
}
|
||
|
return config;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Returns true if the given value is valid for field `key`.
|
||
|
*
|
||
|
* @param {String} `key`
|
||
|
* @param {any} `val`
|
||
|
* @param {Object} `config`
|
||
|
* @return {Boolean}
|
||
|
* @api public
|
||
|
*/
|
||
|
|
||
|
Schema.prototype.isValidField = function(key) {
|
||
|
var field = this.get(key);
|
||
|
if (typeof field === 'undefined') {
|
||
|
return this.options.knownOnly !== true;
|
||
|
}
|
||
|
return field.isValidType(this.config[key], key, this.config, this);
|
||
|
};
|
||
|
|
||
|
Schema.prototype.isValidType = function(key, val, config) {
|
||
|
config = config || {};
|
||
|
this.config = config;
|
||
|
|
||
|
var field = this.get(key);
|
||
|
if (typeof field === 'undefined') {
|
||
|
if (this.options.knownOnly) {
|
||
|
this.warning('invalidField', key);
|
||
|
return false;
|
||
|
}
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
if (field.isValidType(val, key, config, this)) {
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
if (config.hasOwnProperty(key) || typeof field.normalize === 'function') {
|
||
|
if (this.isRequired(key) || typeof val !== 'undefined') {
|
||
|
var types = this.get(key, 'types');
|
||
|
var warning = {expected: types, actual: val};
|
||
|
this.warning('invalidType', key, warning);
|
||
|
}
|
||
|
}
|
||
|
return false;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Iterate over the given `config` and validate any properties
|
||
|
* that have a `field` with a `validate` function on the schema.
|
||
|
*
|
||
|
* @param {Object} `config`
|
||
|
* @return {Object}
|
||
|
*/
|
||
|
|
||
|
Schema.prototype.validate = function(config) {
|
||
|
if (!utils.isObject(config)) {
|
||
|
throw new TypeError('expected config to be an object');
|
||
|
}
|
||
|
for (var key in config) {
|
||
|
this.validateField(config[key], key, config);
|
||
|
}
|
||
|
return this.warnings;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Validate a single property on the config.
|
||
|
*
|
||
|
* @param {any} `val`
|
||
|
* @param {String} `key`
|
||
|
* @param {Object} `config`
|
||
|
* @return {undefined}
|
||
|
*/
|
||
|
|
||
|
Schema.prototype.validateField = function(val, key, config) {
|
||
|
var field = this.fields[key];
|
||
|
|
||
|
if (!utils.isObject(field) && this.options.knownOnly === true) {
|
||
|
this.warning('invalidField', key);
|
||
|
}
|
||
|
|
||
|
this.isValidType(key, val, config);
|
||
|
|
||
|
if (typeof field.validate === 'function') {
|
||
|
var isValid = field.validate(val, key, config, this);
|
||
|
if (!isValid) {
|
||
|
this.warning('invalidValue', key, {actual: val});
|
||
|
}
|
||
|
return isValid;
|
||
|
}
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Normalize the given `config` object.
|
||
|
*
|
||
|
* @param {String} key
|
||
|
* @param {any} value
|
||
|
* @param {Object} config
|
||
|
* @return {Object}
|
||
|
* @api public
|
||
|
*/
|
||
|
|
||
|
Schema.prototype.normalize = function(config, options) {
|
||
|
debug('normalizing config');
|
||
|
|
||
|
if (utils.typeOf(config) !== 'object') {
|
||
|
throw new TypeError('expected config to be an object');
|
||
|
}
|
||
|
|
||
|
options = options || {};
|
||
|
this.addFields(options);
|
||
|
this.config = config;
|
||
|
|
||
|
// set defaults and call normalizers
|
||
|
config = this.setDefaults(config);
|
||
|
var opts = utils.merge({}, this.options, options);
|
||
|
|
||
|
var pick = utils.arrayify(opts.only || opts.pick);
|
||
|
if (pick.length) {
|
||
|
this.fields = utils.pick(this.fields, pick);
|
||
|
}
|
||
|
|
||
|
for (var key in this.fields) {
|
||
|
if (opts.existingOnly === true && !config.hasOwnProperty(key)) {
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
if (this.fields.hasOwnProperty(key)) {
|
||
|
this.normalizeField.call(this, key, config[key], config, opts);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// check for missing required fields
|
||
|
this.missingFields(config);
|
||
|
|
||
|
// get omitted keys
|
||
|
var omitted = utils.arrayify(opts.omit);
|
||
|
|
||
|
// remove empty objects if specified on options
|
||
|
if (opts.omitEmpty === true) {
|
||
|
config = utils.omitEmpty(config);
|
||
|
}
|
||
|
|
||
|
if (pick.length) {
|
||
|
config = utils.pick(config, pick);
|
||
|
}
|
||
|
|
||
|
var omit = utils.union([], this.remove, omitted);
|
||
|
config = utils.omit(config, omit);
|
||
|
|
||
|
// sort object and arrays
|
||
|
config = this.sortObject(config, opts);
|
||
|
config = this.sortArrays(config);
|
||
|
|
||
|
this.logWarnings(config);
|
||
|
utils.define(config, 'isNormalized', true);
|
||
|
this.emit('normalized', config);
|
||
|
return config;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Normalize a field on the schema.
|
||
|
*
|
||
|
* @param {String} key
|
||
|
* @param {any} value
|
||
|
* @param {Object} config
|
||
|
* @return {Object}
|
||
|
* @api public
|
||
|
*/
|
||
|
|
||
|
Schema.prototype.normalizeField = function(key, value, config, options) {
|
||
|
debug('normalizing field "%s", "%j"', key, util.inspect(value));
|
||
|
|
||
|
if (!this.fields.hasOwnProperty(key)) {
|
||
|
this.removeKey(key, config);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
var field = this.fields[key];
|
||
|
var val = config[key];
|
||
|
|
||
|
if (utils.isObject(field)) {
|
||
|
field.isNormalized = true;
|
||
|
var fn = field.normalize;
|
||
|
|
||
|
if (typeof fn === 'function' && !this.options.validate) {
|
||
|
if (field.isSchema && field.warnings.length) {
|
||
|
utils.union(this.warnings, field.warnings);
|
||
|
}
|
||
|
|
||
|
val = fn.call(this, val, key, config, this);
|
||
|
if (field.isValidType(val)) {
|
||
|
config[key] = val;
|
||
|
}
|
||
|
|
||
|
if (this.removeKey(key, config)) {
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (this.isValidType(key, val, config) === false) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (typeof field.validate === 'function' && !this.options.normalize) {
|
||
|
if (!this.validateField(val, key, config)) {
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
} else {
|
||
|
this.isValidType(key, val, config);
|
||
|
}
|
||
|
|
||
|
this.removeKey(key, config);
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Visit `method` over the given object or array.
|
||
|
*
|
||
|
* @param {String} `method`
|
||
|
* @param {Object|Array} `value`
|
||
|
* @return {Object} Returns the instance for chaining.
|
||
|
* @api public
|
||
|
*/
|
||
|
|
||
|
Schema.prototype.visit = function(method, value) {
|
||
|
utils.visit(this, method, value);
|
||
|
return this;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Log warnings and warnings that were recorded during normalization.
|
||
|
*
|
||
|
* This is a placeholder for a reporter (planned)
|
||
|
*
|
||
|
* @return {String}
|
||
|
*/
|
||
|
|
||
|
Schema.prototype.logWarnings = function(warnings) {
|
||
|
warnings = warnings || this.warnings;
|
||
|
var omit = utils.arrayify(this.options.omit);
|
||
|
var len = warnings.length;
|
||
|
var idx = -1;
|
||
|
|
||
|
if (this.options.verbose !== true || len === 0) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
var max = utils.longest(Object.keys(this.fields)).length;
|
||
|
var msg = '\n';
|
||
|
|
||
|
// warnings body
|
||
|
while (++idx < len) {
|
||
|
if (idx > 0) {
|
||
|
msg += '\n';
|
||
|
}
|
||
|
var warning = warnings[idx];
|
||
|
if (omit.indexOf(warning.prop) !== -1) {
|
||
|
continue;
|
||
|
}
|
||
|
msg += utils.formatWarning(warning, max);
|
||
|
}
|
||
|
|
||
|
// log out warnings
|
||
|
console.log(msg);
|
||
|
};
|