/* global window, requestAnimationFrame, cancelAnimationFrame */
import { FieldType, DimensionSubtype } from 'datamodel';
import {
axisLeft,
axisRight,
axisTop,
axisBottom
} from 'd3-axis';
import {
symbolCircle,
symbolCross,
symbolDiamond,
symbolSquare,
symbolStar,
symbolWye,
symbolTriangle,
symbol,
stack as d3Stack,
stackOffsetDiverging,
stackOrderNone,
stackOrderAscending,
stackOrderDescending,
stackOffsetNone,
stackOffsetExpand,
stackOffsetWiggle,
pie,
arc,
line,
curveLinear,
curveStepAfter,
curveStepBefore,
curveStep,
curveCatmullRom,
area
} from 'd3-shape';
import { scaleBand } from 'd3-scale';
import { nest } from 'd3-collection';
import {
interpolate,
interpolateRgb,
piecewise,
interpolateNumber,
interpolateHslLong
} from 'd3-interpolate';
import {
easeCubic,
easeBounce,
easePoly,
easeBack,
easeCircle,
easeLinear,
easeElastic
} from 'd3-ease';
import {
color,
rgb,
hsl
} from 'd3-color';
import { voronoi } from 'd3-voronoi';
import Model from 'hyperdis';
import * as STACK_CONFIG from './enums/stack-config';
const HTMLElement = window.HTMLElement;
const isSimpleObject = (obj) => {
let token;
if (typeof obj === 'object') {
if (obj === null) { return false; }
token = Object.prototype.toString.call(obj);
if (token === '[object Object]') {
return (obj.constructor.toString().match(/^function (.*)\(\)/m) || [])[1] === 'Object';
}
}
return false;
};
/**
* Returns unique id
* @return {string} Unique id string
*/
const
getUniqueId = () => `id-${new Date().getTime()}${Math.round(Math.random() * 10000)}`;
/**
* Deep copies an object and returns a new object.
* @param {Object} o Object to clone
* @return {Object} New Object.
*/
const clone = (o) => {
const output = {};
let v;
for (const key in o) {
if ({}.hasOwnProperty.call(o, key)) {
v = o[key];
output[key] = isSimpleObject(v) ? clone(v) : v;
}
}
return output;
};
/**
* Checks the existence of keys in an object
* @param {Array} keys Set of keys which are to be checked
* @param {Object} obj whose keys are checked from the set of keys provided
* @return {Object} Error if the keys are absent, or the object itself
*/
const checkExistence = (keys, obj) => {
const nonExistentKeys = [];
keys.forEach((key) => {
if (key in obj) {
return;
}
nonExistentKeys.push(key);
});
return nonExistentKeys;
};
const sanitizeIP = {
typeObj: (keys, obj) => {
if (typeof obj !== 'object') {
return Error('Argument type object expected');
}
const nonExistentKeys = checkExistence(keys, obj);
if (nonExistentKeys.length) {
return Error(`Missing keys from parameter ${nonExistentKeys.join(', ')}`);
}
return obj;
},
/* istanbul ignore next */ htmlElem: (elem) => {
if (!(elem instanceof HTMLElement)) {
return Error('HTMLElement required');
}
return elem;
}
};
/**
* Gets the maximum value from an array of objects for a given property name
* @param {Array.<Object>} data Array of objects
* @param {string} field Field name
* @return {number} Maximum value
*/
const getMax = (data, field) => Math.max(...data.filter(d => !isNaN(d[field])).map(d => d[field]));
/**
* Gets the minimum value from an array of objects for a given property name
* @param {Array.<Object>} data Array of objects
* @param {string} field Field name
* @return {number} Minimum value
*/
const getMin = (data, field) => Math.min(...data.filter(d => !isNaN(d[field])).map(d => d[field]));
/**
* Gets the domain from the data based on the field name and type of field
* @param {Array.<Object> | Array.<Array>} data Data Array
* @param {Array.<string>} fields Array of fields from where the domain will be calculated
* @param {string} fieldType Type of field - nominal, quantitiative, temporal.
* @return {Array} Usually contains a min and max value if field is quantitative or
* an array of values if field type is nominal or ordinal
*/
const getDomainFromData = (data, fields, fieldType) => {
let domain;
let domArr;
data = data[0] instanceof Array ? data : [data];
switch (fieldType) {
case DimensionSubtype.CATEGORICAL:
domain = [].concat(...data.map(arr => arr.map(d => d[fields[0]]).filter(d => d !== undefined)));
break;
default:
domArr = data.map((arr) => {
const firstMin = getMin(arr, fields[0]);
const secondMin = getMin(arr, fields[1]);
const firstMax = getMax(arr, fields[0]);
const secondMax = getMax(arr, fields[1]);
return [Math.min(firstMin, secondMin), Math.max(firstMax, secondMax)];
});
domain = [Math.min(...domArr.map(d => d[0])), Math.max(...domArr.map(d => d[1]))];
break;
}
return domain;
};
/**
* Union Domain values
* @param {Array.<Array>} domains Array of domain values
* @param {string} fieldType type of field - dimension,measure or datetime.
* @return {Array} Unioned domain of all domain values.
*/
const unionDomain = (domains, fieldType) => {
let domain;
domains = domains.filter(dom => dom.length);
if (fieldType === DimensionSubtype.CATEGORICAL) {
domain = domain = [].concat(...domains);
} else {
domain = [Math.min(...domains.map(d => d[0])), Math.max(...domains.map(d => d[1]))];
}
return domain;
};
const symbolFns = {
circle: symbolCircle,
cross: symbolCross,
diamond: symbolDiamond,
square: symbolSquare,
star: symbolStar,
wye: symbolWye,
triangle: symbolTriangle
};
const easeFns = {
cubic: easeCubic,
bounce: easeBounce,
linear: easeLinear,
elastic: easeElastic,
back: easeBack,
poly: easePoly,
circle: easeCircle
};
/**
* Returns the maximum or minimum points of a compare value from an array of objects.
* @param {Array} points Array of objects
* @param {string} compareValue Key in the object on which the comparing will be done.
* @param {string} minOrMax minimum or maximum.
* @return {Object} Minimum or maximum point.
*/
const getExtremePoint = (points, compareValue, minOrMax) => {
let extremePoint;
let point;
const len = points.length;
let minOrMaxVal = minOrMax === 'max' ? -Infinity : Infinity;
let val;
for (let i = 0; i < len; i++) {
point = points[i];
val = point[compareValue];
if (minOrMax === 'min' ? val < minOrMaxVal : val > minOrMaxVal) {
minOrMaxVal = val;
extremePoint = point;
}
}
return extremePoint;
};
/**
* Returns the minimum point of a compare value from an array of objects.
* @param {Array} points Array of objects
* @param {string} compareValue Key in the object on which the comparing will be done.
* @return {Object} Minimum point.
*/
const getMinPoint = (points, compareValue) => getExtremePoint(points, compareValue, 'min');
/**
* Returns the maximum point of a compare value from an array of objects.
* @param {Array} points Array of objects
* @param {string} compareValue Key in the object on which the comparing will be done.
* @return {Object} Maximum point.
*/
const getMaxPoint = (points, compareValue) => getExtremePoint(points, compareValue, 'max');
/**
* Gets the index of the closest value of the given value from the array.
* @param {Array} arr Array of values
* @param {number} value Value from which the nearest value will be calculated.
* @param {string} side side property.
* @return {number} index of the closest value
*/
/* istanbul ignore next */const getClosestIndexOf = (arr, value, side) => {
let low = 0;
const arrLen = arr.length;
let high = arrLen - 1;
let mid;
let d1;
let d2;
while (low < high) {
mid = Math.floor((low + high) / 2);
d1 = Math.abs(arr[mid] - value);
d2 = Math.abs(arr[mid + 1] - value);
if (d2 <= d1) {
low = mid + 1;
} else {
high = mid;
}
}
if (!side) {
return high;
}
const highVal = arr[high];
if (highVal === value) {
return high;
} else if (highVal > value) {
if (high === 0) { return high; }
return side === 'left' ? high - 1 : high;
}
if (high === arr.length - 1) { return high; }
return side === 'left' ? high : high + 1;
};
/**
* Returns the browser window object
* @return {Window} Window object
*/
const getWindow = () => window;
/**
* Returns the browser window object
* @return {Window} Window object
*/
const reqAnimFrame = (() => requestAnimationFrame)();
const cancelAnimFrame = (() => cancelAnimationFrame)();
/**
* Capitalizes the first letter of the word
* @param {string} text word
* @return {string} Capitalized word
*/
const capitalizeFirst = (text) => {
text = text.toLowerCase();
return text.replace(/\w\S*/g, txt => txt.charAt(0).toUpperCase() + txt.substr(1));
};
/**
*
*
* @param {*} arr
*/
const unique = arr => ([...new Set(arr)]);
/**
* Gets the minimum difference between two consecutive numbers in an array.
* @param {Array} arr Array of numbers
* @param {number} index index of the value
* @return {number} minimum difference between values
*/
/* istanbul ignore next */ const getMinDiff = (arr, index) => {
let diff;
let uniqueVals;
if (index !== undefined) {
uniqueVals = unique(arr.map(d => d[index]));
} else {
uniqueVals = unique(arr);
}
if (uniqueVals.length > 1) {
diff = Math.abs(uniqueVals[1] - uniqueVals[0]);
for (let i = 2, len = uniqueVals.length; i < len; i++) {
diff = Math.min(diff, Math.abs(uniqueVals[i] - uniqueVals[i - 1]));
}
} else {
diff = uniqueVals[0];
}
return diff;
};
/**
* Returns the class name appended with a given id.
* @param {string} cls class name
* @param {string} id unique identifier
* @param {string} prefix string needed to add before the classname
* @return {string} qualified class name
*/
/* istanbul ignore next */const getQualifiedClassName = (cls, id, prefix) => {
cls = cls.replace(/^\.*/, '');
return [`${prefix}-${cls}`, `${prefix}-${cls}-${id}`];
};
/**
* This method is used to set the default value for variables
* without sullying the code with conditional statements.
*
* @export
* @param {any} param The parameter to test.
* @param {any} value The default value to assign.
* @return {any} The value.
*/
/* istanbul ignore next */ const defaultValue = (param, value) => {
if (typeof param === 'undefined' || (typeof param === 'object' && !param)) {
return value;
}
return param;
};
/**
* DESCRIPTION TODO
*
* @export
* @param {Object} graph graph whose dependency order has to be generated
* @return {Object}
*/
const getDependencyOrder = (graph) => {
const dependencyOrder = [];
const visited = {};
const keys = Object.keys(graph);
/**
* DESCRIPTION TODO
*
* @export
* @param {Object} name
* @return {Object}
*/
const visit = (name) => {
if (dependencyOrder.length === keys.length) {
return true;
}
visited[name] = true;
const edges = graph[name];
for (let e = 0; e < edges.length; e++) {
const dep = edges[e];
if (!visited[dep]) {
visit(dep);
}
}
dependencyOrder.push(name);
return false;
};
for (let i = 0; i < keys.length; i++) {
if (visit(keys[i], i)) break;
}
return dependencyOrder;
};
/**
* Iterates over the properties of an object and applies the function
*
* @param {any} obj object to be iterated upon
* @param {any} fn function to be applied on it
*/
const objectIterator = (obj, fn) => {
for (const key in obj) {
if (Object.hasOwnProperty.call(obj, key)) {
fn(key, obj);
}
}
};
/**
* This class creates a d3 voronoi for retrieving the nearest neighbour of any point from a set of two
* dimensional points
* @class Voronoi
*/
/* istanbul ignore next */ class Voronoi {
/**
* Initialize the voronoi with the data given.
* @param {Array.<Object>} data Array of points.
*/
constructor (data) {
this._voronoi = voronoi().x(d => d.x).y(d => d.y);
this.data(data);
}
/**
* Sets the data to voronoi
* @param {Array.<Object>} data Array of objects.
* @return {Voronoi} Instance of voronoi.
*/
data (data) {
if (data) {
this._voronoiFn = this._voronoi(data);
}
return this;
}
/**
* Finds the closest point to the x and y position given.
* @param {number} x x value
* @param {number} y y value
* @param {number} radius search radius.
* @return {Object} Details of the nearest point.
*/
find (x, y, radius) {
return this._voronoiFn.find(x, y, radius);
}
}
/**
* Methods to handle changes to table configuration and reactivity are handled by this
* class.
*/
/**
* Common store class
*
* @class Store
*/
class Store {
/**
* Creates an instance of Store.
* @param {Object} config The object to create the state store with.
* @memberof Store
*/
constructor (config) {
// create reactive model
this.model = Model.create(config);
this._listeners = [];
}
/**
* This method returns a plain JSON object
* with all the fields in the state store.
*
* @return {Object} Serialized representation of state store.
* @memberof Store
*/
serialize () {
return this.model.serialize();
}
/**
* This method is used to update the value of a property in the state store.
*
* @param {string} propName The name of the property.
* @param {number} value The new value of the property.
* @memberof Store
*/
commit (propName, value) {
// check if appropriate enum has been used
this.model.prop(propName, value);
}
/**
* This method is used to register a callbacl that will execute
* when one or more properties change.
*
* @param {string | Array} propNames name of property or array of props.
* @param {Function} callBack The callback to execute.
* @memberof Store
*/
/* istanbul ignore next */registerChangeListener (propNames, callBack, instantCall) {
let props = propNames;
if (!Array.isArray(propNames)) {
props = [propNames];
}
const fn = this.model.next(props, callBack, instantCall);
this._listeners.push(fn);
return this;
}
/**
* This method is used to register a callbacl that will execute
* when one or more properties change.
*
* @param {string | Array} propNames name of property or array of props.
* @param {Function} callBack The callback to execute.
* @memberof Store
*/
/* istanbul ignore next */ registerImmediateListener (propNames, callBack, instantCall) {
let props = propNames;
if (!Array.isArray(propNames)) {
props = [propNames];
}
const fn = this.model.on(props, callBack, instantCall);
this._listeners.push(fn);
return this;
}
/**
* This method is used to get the name of the property
* from the state store.
*
* @param {string} propName The name of the field in state store.
* @return {any} The value of the field.
* @memberof Store
*/
get (propName) {
return this.model.prop(propName);
}
/**
* This method is used to register a computed property that is computed every time
* the store value changes.
*
* @param {string} propName The name of the property to create.
* @param {Function} callBack The function to execute when depemdent props change.
* @memberof Store
*/
computed (propName, callBack) {
return this.model.calculatedProp(propName, callBack);
}
unsubscribeAll () {
this._listeners.forEach(fn => fn());
}
}
/**
* Sanitize an input number / string mixed number. Currently dot in the no is not supported.
*
* @param {number | string} val pure number or string mixed number
* @return {number | null} Number if it can be extracted. Otherwise null
*/
const intSanitizer = (val) => {
const arr = val.toString().match(/(\d+)(px)*/g);
if (!arr) {
// If only characters are passed
return null;
}
return parseInt(arr[0], 10);
};
/**
* Setter getter creator from config
* Format
* PROPERTRY_NAME: {
* value: // default value of the property,
* meta: {
* typeCheck: // The setter value will be checked using this. If the value is function then the setter value
* // is passed as args. (Optional)
* typeExpected: // The output of typecheck action will be tested against this. Truthy value will set the
* // value to the setter
* sanitizaiton: // Need for sanitization before type is checked
* }
* }
*
* @param {Object} holder an empty object on which the getters and setters will be mounted
* @param {Object} options options config based on which the getters and setters are determined.
* @param {Hyperdis} model optional model to attach the property. If not sent new moel is created.
* @return {Array}
*/
const transactor = (holder, options, model) => {
let conf;
const store = model && model instanceof Model ? model : Model.create({});
for (const prop in options) {
if ({}.hasOwnProperty.call(options, prop)) {
conf = options[prop];
if (!store.prop(prop)) {
store.append({ [prop]: conf.value });
}
holder[prop] = ((context, key, meta) => (...params) => {
let val;
let compareTo;
const paramsLen = params.length;
const prevVal = store.prop(prop);
if (paramsLen) {
// If parameters are passed then it's a setter
const spreadParams = meta && meta.spreadParams;
val = params;
const values = [];
if (meta) {
for (let i = 0; i < paramsLen; i++) {
val = params[i];
const sanitization = meta.sanitization && (spreadParams ? meta.sanitization[i] :
meta.sanitization);
const typeCheck = meta.typeCheck && (spreadParams ? meta.typeCheck[i] : meta.typeCheck);
if (sanitization && typeof sanitization === 'function') {
// Sanitize if required
val = sanitization(val, prevVal, holder);
}
if (typeCheck) {
// Checking if a setter is valid
if (typeof typeCheck === 'function') {
let typeExpected = meta.typeExpected;
if (typeExpected && spreadParams) {
typeExpected = typeExpected[i];
}
if (typeExpected) {
compareTo = typeExpected;
} else {
compareTo = true;
}
if (typeCheck(val) === compareTo) {
values.push(val);
}
} else if (typeof typeCheck === 'string') {
if (typeCheck === 'constructor') {
const typeExpected = spreadParams ? meta.typeExpected[i] : meta.typeExpected;
if (val && (val.constructor.name === typeExpected)) {
values.push(val);
}
}
} else {
// context.prop(key, val);
values.push(val);
}
} else {
values.push(val);
}
}
const preset = meta.preset;
const oldValues = context.prop(key);
preset && preset(values[0], holder);
if (spreadParams) {
oldValues.forEach((value, i) => {
if (values[i] === undefined) {
values[i] = value;
}
});
}
values.length && context.prop(key, spreadParams ? values : values[0]);
} else {
context.prop(key, spreadParams ? val : val[0]);
}
return holder;
}
// No parameters are passed hence its a getter
return context.prop(key);
})(store, prop, conf.meta);
}
}
return [holder, store];
};
/**
*
*
* @param {*} context
* @param {*} props
*/
const generateGetterSetters = (context, props) => {
Object.entries(props).forEach((propInfo) => {
const prop = propInfo[0];
const typeChecker = propInfo[1].typeChecker;
const sanitization = propInfo[1].sanitization;
const prototype = context.constructor.prototype;
if (!(Object.hasOwnProperty.call(prototype, prop))) {
context[prop] = (...params) => {
if (params.length) {
let value = params[0];
if (sanitization) {
value = sanitization(context, params[0]);
}
if (typeChecker && !typeChecker(value)) {
return context[`_${prop}`];
}
context[`_${prop}`] = value;
return context;
} return context[`_${prop}`];
};
}
});
};
/**
*
*
* @param {*} arr
* @param {*} prop
*/
const getArraySum = (arr, prop) => arr.reduce((total, elem) => {
total += prop ? elem[prop] : elem;
return total;
}, 0);
/**
*
*
* @param {*} arr1
* @param {*} arr2
*
*/
const arraysEqual = (arr1, arr2) => {
if (arr1.length !== arr2.length) { return false; }
for (let i = arr1.length; i >= 0; i--) {
if (arr1[i] !== arr2[i]) { return false; }
}
return true;
};
/* eslint valid-typeof:0 */
/**
* Returns a validation function which can be used to validate variables against a type and value
*
* @param {any} type type of value that the object should have
* @return {Object} validation function
*/
const isEqual = type => (oldVal, newVal) => {
if (type === 'Array') {
if (!oldVal) {
return false;
}
return arraysEqual(oldVal, newVal);
} else if (type === 'Object') {
return Object.is(oldVal, newVal);
} return oldVal === newVal;
};
/**
* Description @todo
*
* @param {any} transactionModel @todo
* @param {any} transactionEndpoint @todo
* @param {any} transactionItems @todo
* @return {any} @todo
*/
const enableChainedTransaction = (transactionModel, transactionEndpoint, transactionItems) =>
transactionItems.forEach(item => transactionModel.on(item, ([, newVal]) => transactionEndpoint[item](newVal)));
/**
* Chceks if the element is istanceof HTMLElement
*
* @param {Object} elem any JS Object
*/
const isHTMLElem = elem => elem instanceof HTMLElement;
const ERROR_MSG = {
INTERFACE_IMPL: 'Method not implemented'
};
/**
* Merges the sink object in the source by recursively iterating through the object properties
* @param {Object} source Source Object
* @param {Object} sink Sink Object
* @return {Object} Merged object
*/
const mergeRecursive = (source, sink) => {
for (const prop in sink) {
if (isSimpleObject(source[prop]) && isSimpleObject(sink[prop])) {
mergeRecursive(source[prop], sink[prop]);
} else if (sink[prop] instanceof Object && sink[prop].constructor === Object) {
source[prop] = {};
mergeRecursive(source[prop], sink[prop]);
} else {
source[prop] = sink[prop];
}
}
return source;
};
const interpolateArray = (data, fitCount) => {
const linearInterpolate = function (before, after, atPoint) {
return before + (after - before) * atPoint;
};
const newData = [];
const springFactor = ((data.length - 1) / (fitCount - 1));
newData[0] = data[0]; // for new allocation
for (let i = 1; i < fitCount - 1; i++) {
const tmp = i * springFactor;
const before = (Math.floor(tmp)).toFixed();
const after = (Math.ceil(tmp)).toFixed();
const atPoint = tmp - before;
newData[i] = linearInterpolate(data[before], data[after], atPoint);
}
newData[fitCount - 1] = data[data.length - 1]; // for new allocation
return newData;
};
/**
*
*
* @param {*} fn
*/
const nextFrame = (fn) => {
setTimeout(() => {
fn();
}, 0);
};
/**
*
*
* @param {*} angle
*/
const angleToRadian = angle => angle * Math.PI / 180;
/**
*
*
* @param {*} newName
* @param {*} oldName
*/
const replaceCSSPrefix = () => {
// @todo
};
/**
* Gets the interpolator function from d3 color
*
*/
const interpolator = () => interpolate;
/**
* Gets the number interpolator from d3 color
*
*/
const numberInterpolator = () => interpolateNumber;
/**
* Gets the rgb interpolator from d3 color
*
*/
const colorInterpolator = () => interpolateRgb;
/**
* Gets the hsl interpolator from d3 color
*
*/
const hslInterpolator = () => interpolateHslLong;
const transformColors = () => ({
color,
rgb,
hsl
});
/**
* Gets the piecewise interpolator from d3 color
*
*/
const piecewiseInterpolator = () => piecewise;
function hue2rgb (p, q, t) {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1 / 6) return p + (q - p) * 6 * t;
if (t < 1 / 2) return q;
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
return p;
}
/**
* Converts an HSL color value to RGB. Conversion formula
* adapted from http://en.wikipedia.org/wiki/HSL_color_space.
* Assumes h, s, and l are contained in the set [0, 1] and
* returns r, g, and b in the set [0, 255].
*
* @param Number h The hue
* @param Number s The saturation
* @param Number l The lightness
* @return Array The RGB representation
*/
const hslToRgb = (h, s, l, a = 1) => {
let r;
let g;
let b;
if (s === 0) {
r = g = b = l; // achromatic
} else {
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
r = hue2rgb(p, q, h + 1 / 3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1 / 3);
}
return [r * 255, g * 255, b * 255, a];
};
/**
* Converts an RGB color value to HSV. Conversion formula
* adapted from http://en.wikipedia.org/wiki/HSV_color_space.
* Assumes r, g, and b are contained in the set [0, 255] and
* returns h, s, and v in the set [0, 1].
*
* @param Number r The red color value
* @param Number g The green color value
* @param Number b The blue color value
* @return Array The HSV representation
*/
const rgbToHsv = (r, g, b, a = 1) => {
r = +r; g = +g; b = +b; a = +a;
r /= 255; g /= 255; b /= 255;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
let h;
let s;
const l = (max + min) / 2;
if (max === min) {
h = s = 0; // achromatic
} else {
const d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
case g: h = (b - r) / d + 2; break;
case b: h = (r - g) / d + 4; break;
default: break;
}
h /= 6;
}
return [h, s, l, a];
};
/**
* Converts an HSV color value to RGB. Conversion formula
* adapted from http://en.wikipedia.org/wiki/HSV_color_space.
* Assumes h, s, and v are contained in the set [0, 1] and
* returns r, g, and b in the set [0, 255].
*
* @param Number h The hue
* @param Number s The saturation
* @param Number v The value
* @return Array The RGB representation
*/
const hsvToRgb = (h, s, v, a = 1) => {
let r;
let g;
let b;
const i = Math.floor(h * 6);
const f = h * 6 - i;
const p = v * (1 - s);
const q = v * (1 - f * s);
const t = v * (1 - (1 - f) * s);
switch (i % 6) {
case 0: r = v; g = t; b = p; break;
case 1: r = q; g = v; b = p; break;
case 2: r = p; g = v; b = t; break;
case 3: r = p; g = q; b = v; break;
case 4: r = t; g = p; b = v; break;
case 5: r = v; g = p; b = q; break;
default: break;
}
return [r * 255, g * 255, b * 255, a];
};
const hexToHsv = (hex) => {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
const r = parseInt(result[1], 16);
const g = parseInt(result[2], 16);
const b = parseInt(result[3], 16);
const a = result[4] ? parseInt(result[4], 16) : 1;
return rgbToHsv(r, g, b, a);
};
const detectColor = (col) => {
const matchRgb = /rgb\((\d{1,3}), (\d{1,3}), (\d{1,3})\)/;
const matchHsl = /hsl\((\d+),\s*([\d.]+)%,\s*([\d.]+)%\)/g;
// Source : https://gist.github.com/sethlopezme/d072b945969a3cc2cc11
// eslint-disable-next-line
const matchRgba = /rgba?\(((25[0-5]|2[0-4]\d|1\d{1,2}|\d\d?)\s*,\s*?){2}(25[0-5]|2[0-4]\d|1\d{1,2}|\d\d?)\s*,?\s*([01]\.?\d*?)?\)/;
// eslint-disable-next-line
const matchHsla = /^hsla\((0|360|35\d|3[0-4]\d|[12]\d\d|0?\d?\d),(0|100|\d{1,2})%,(0|100|\d{1,2})%,(0?\.\d|1(\.0)?)\)$/;
const matchHex = /^#([0-9a-f]{3}){1,2}$/i;
if (matchRgb.test(col) || matchRgba.test(col)) {
return 'rgb';
} else if (matchHsl.test(col) || matchHsla.test(col)) {
return 'hsl';
} else if (matchHex.test(col)) {
return 'hex';
} return col;
};
/**
*
*
* @param {*} model
* @param {*} propModel
*
*/
const filterPropagationModel = (model, propModel, measures) => {
const { data, schema } = propModel.getData();
let filteredModel;
if (schema.length) {
const fieldMap = model.getFieldsConfig();
filteredModel = model.select((fields) => {
const include = data.some(row => schema.every((propField, idx) => {
if (!measures && (!(propField.name in fieldMap) ||
fieldMap[propField.name].def.type === FieldType.MEASURE)) {
return true;
}
return row[idx] === fields[propField.name].valueOf();
}));
return include;
}, {
saveChild: false
});
} else {
filteredModel = propModel;
}
return filteredModel;
};
const assembleModelFromIdentifiers = (model, identifiers) => {
let schema = [];
let data;
const fieldMap = model.getFieldsConfig();
if (identifiers.length) {
const fields = identifiers[0];
const len = fields.length;
for (let i = 0; i < len; i++) {
const field = fields[i];
const fieldObj = fieldMap[field] && Object.assign({}, fieldMap[field].def);
if (fieldObj) {
schema.push(Object.assign(fieldObj));
}
}
data = [];
const header = identifiers[0];
for (let i = 1; i < identifiers.length; i += 1) {
const vals = identifiers[i];
const temp = {};
vals.forEach((fieldVal, cIdx) => {
temp[header[cIdx]] = fieldVal;
});
data.push(temp);
}
} else {
data = [];
schema = [];
}
return new model.constructor(data, schema);
};
/**
*
*
* @param {*} dataModel
* @param {*} criteria
*
*/
const getDataModelFromRange = (dataModel, criteria, mode) => {
if (criteria === null) {
return null;
}
const selFields = Object.keys(criteria);
const selFn = fields => selFields.every((field) => {
const val = fields[field].value;
const range = criteria[field][0] instanceof Array ? criteria[field][0] : criteria[field];
if (typeof range[0] === 'string') {
return range.find(d => d === val) !== undefined;
}
return range ? val >= range[0] && val <= range[1] : true;
});
return dataModel.select(selFn, {
saveChild: false,
mode
});
};
/**
*
*
* @param {*} dataModel
* @param {*} identifiers
*
*/
const getDataModelFromIdentifiers = (dataModel, identifiers, mode) => {
let filteredDataModel;
if (identifiers instanceof Array) {
const fieldsConfig = dataModel.getFieldsConfig();
const dataArr = identifiers.slice(1, identifiers.length);
if (identifiers instanceof Function) {
filteredDataModel = identifiers(dataModel, {}, false);
} else if (identifiers instanceof Array && identifiers[0].length) {
const filteredSchema = identifiers[0].filter(d => d in fieldsConfig);
filteredDataModel = dataModel.select((fields) => {
let include = true;
filteredSchema.forEach((propField, idx) => {
const value = fields[propField].valueOf();
const index = dataArr.findIndex(d => d[idx] === value);
include = include && index !== -1;
});
return include;
}, {
saveChild: false,
mode
});
}
} else {
filteredDataModel = getDataModelFromRange(dataModel, identifiers, mode);
}
return filteredDataModel;
};
/**
*
*
* @param {*} context
* @param {*} listenerMap
*/
const registerListeners = (context, listenerMap) => {
const propListenerMap = listenerMap(context);
for (const key in propListenerMap) {
if ({}.hasOwnProperty.call(propListenerMap, key)) {
const mapObj = propListenerMap[key];
const propType = mapObj.type;
const props = mapObj.props;
const listenerFn = mapObj.listener;
context.store()[propType](props, listenerFn);
}
}
};
const isValidValue = value => !isNaN(value) && value !== -Infinity && value !== Infinity;
/**
*
*
* @param {*} obj
* @param {*} fields
*
*/
const getObjProp = (obj, ...fields) => {
if (obj === undefined || obj === null) {
return obj;
}
let retObj = obj;
for (let i = 0, len = fields.length; i < len; i++) {
retObj = retObj[fields[i]];
if (retObj === undefined || retObj === null) {
break;
}
}
return retObj;
};
/**
*
*
* @param {*} str
*
*/
const escapeHTML = (str) => {
const htmlEscapes = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
'/': '/'
};
const htmlEscaper = /[&<>"'/]/g;
return (`${str}`).replace(htmlEscaper, match => htmlEscapes[match]);
};
/**
*
*
* @param {*} arr
*/
const transposeArray = arr => arr[0].map((col, i) => arr.map(row => row[i]));
const toArray = arr => (arr instanceof Array ? arr : [arr]);
const extendsClass = (cls, extendsFrom, found) => {
if (!cls) {
return false;
}
const prototype = cls.prototype;
if (prototype instanceof extendsFrom) {
found = true;
} else {
found = extendsClass(prototype, extendsFrom, found);
}
return found;
};
/**
*
* @param {*} dm1
* @param {*} dm2
*/
const concatModels = (dm1, dm2) => {
const dataObj1 = dm1.getData();
const dataObj2 = dm2.getData();
const data1 = dataObj1.data;
const data2 = dataObj2.data;
const schema1 = dataObj1.schema;
const schema2 = dataObj2.schema;
const tuples1 = {};
const tuples2 = {};
const commonTuples = {};
for (let i = 0; i < data1.length; i++) {
for (let ii = 0; ii < data2.length; ii++) {
const row1 = data1[i];
const row2 = data2[ii];
const dim1Values = row1.filter((d, idx) => schema1[idx].type === FieldType.DIMENSION);
const dim2Values = row2.filter((d, idx) => schema2[idx].type === FieldType.DIMENSION);
const allDimSame = dim1Values.every(value => dim2Values.indexOf(value) !== -1);
if (allDimSame) {
const key = dim1Values.join();
!commonTuples[key] && (commonTuples[key] = {});
row1.forEach((value, idx) => {
commonTuples[key][schema1[idx].name] = value;
});
row2.forEach((value, idx) => {
commonTuples[key][schema2[idx].name] = value;
});
} else {
const dm1Key = dim1Values.join();
const dm2Key = dim2Values.join();
if (!commonTuples[dm1Key] && !commonTuples[dm2Key]) {
!tuples1[dm1Key] && (tuples1[dm1Key] = {});
!tuples2[dm2Key] && (tuples2[dm2Key] = {});
row1.forEach((value, idx) => {
tuples1[dm1Key][schema1[idx].name] = value;
});
row2.forEach((value, idx) => {
tuples2[dm2Key][schema2[idx].name] = value;
});
}
}
}
}
const commonSchema = [...schema1, ...schema2.filter(s2 => schema1.findIndex(s1 => s1.name === s2.name) === -1)];
const data = [...Object.values(tuples1), ...Object.values(tuples2), ...Object.values(commonTuples)];
return [data, commonSchema];
};
const getSymbol = type => symbol().type(symbolFns[type]);
const stackOrders = {
[STACK_CONFIG.ORDER_NONE]: stackOrderNone,
[STACK_CONFIG.ORDER_ASCENDING]: stackOrderAscending,
[STACK_CONFIG.ORDER_DESCENDING]: stackOrderDescending
};
const stackOffsets = {
[STACK_CONFIG.OFFSET_DIVERGING]: stackOffsetDiverging,
[STACK_CONFIG.OFFSET_NONE]: stackOffsetNone,
[STACK_CONFIG.OFFSET_EXPAND]: stackOffsetExpand,
[STACK_CONFIG.OFFSET_WIGGLE]: stackOffsetWiggle
};
// eslint-disable-next-line require-jsdoc
const stack = params => d3Stack().keys(params.keys).offset(stackOffsets[params.offset])
.order(stackOrders[params.order])(params.data);
/**
* Groups the data into a hierarchical tree structure based on one or more fields.
* @param { Object } params Configuration properties for nesting data
* @param { Array.<Array> } params.data Data which needs to be grouped
* @param { Array.<number> } params.keys Field indices by which the data will be grouped
* @return { Array.<Object> } Grouped data array
*/
const nestCollection = (params) => {
const nestFn = nest();
params.keys.forEach(key => nestFn.key(d => d[key]));
return nestFn.entries(params.data);
};
const pathInterpolators = {
curveLinear,
curveStepAfter,
curveStepBefore,
curveStep,
curveCatmullRom,
stepAfter: curveStepAfter,
catmullRom: curveCatmullRom,
step: curveStep,
stepBefore: curveStepBefore,
linear: curveLinear
};
const Symbols = {
axisLeft,
axisRight,
axisTop,
axisBottom,
line,
area,
pie,
arc,
nest
};
const Scales = {
band: scaleBand
};
const getSmallestDiff = (points) => {
points = points.sort((a, b) => a - b);
let minDiff = points[1] - points[0];
for (let i = 2; i < points.length; i++) {
minDiff = Math.min(minDiff, points[i] - points[i - 1]);
}
return minDiff;
};
const require = (lookupWhat, lookupDetails) => ({
resolvable: (store) => {
const lookupTarget = store[lookupWhat];
const depArr = lookupDetails.slice(0, lookupDetails.length - 1);
const fn = lookupDetails[lookupDetails.length - 1]; // fn
const deps = depArr.map(str => lookupTarget[str]);
return {
fn: fn(...deps),
depArr
};
}
});
const nextAnimFrame = window.requestAnimationFrame || window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame ||
function (callback) {
setTimeout(callback, 16);
};
export {
require,
Scales,
Symbols,
pathInterpolators,
stack,
nestCollection,
getSymbol,
transformColors,
detectColor,
hexToHsv,
hslToRgb,
rgbToHsv,
hsvToRgb,
concatModels,
toArray,
angleToRadian,
escapeHTML,
generateGetterSetters,
getArraySum,
interpolator,
piecewiseInterpolator,
getDataModelFromIdentifiers,
getDataModelFromRange,
colorInterpolator,
numberInterpolator,
ERROR_MSG,
reqAnimFrame,
nextAnimFrame,
filterPropagationModel,
transposeArray,
cancelAnimFrame,
getMax,
getMin,
getDomainFromData,
getUniqueId,
mergeRecursive,
unionDomain,
symbolFns,
easeFns,
clone,
isEqual,
interpolateArray,
getMinPoint,
defaultValue,
getMaxPoint,
getClosestIndexOf,
Voronoi,
checkExistence,
sanitizeIP,
getMinDiff,
capitalizeFirst,
getWindow,
getQualifiedClassName,
Store,
getDependencyOrder,
objectIterator,
intSanitizer,
transactor,
enableChainedTransaction,
isHTMLElem,
isSimpleObject,
nextFrame,
registerListeners,
replaceCSSPrefix,
getObjProp,
extendsClass,
assembleModelFromIdentifiers,
isValidValue,
hslInterpolator,
getSmallestDiff
};