import {timeDay, timeHour, timeMinute, timeMonth, timeSecond, timeWeek, timeYear} from 'd3-time'; import {format} from 'd3-format'; import {constants} from './constants'; import {config} from './config'; /** * Returns a function that given a string property name, can be used to pluck the property off an object. A function * can be passed as the second argument to also alter the data being returned. * * This can be a useful shorthand method to create accessor functions. * @example * var xPluck = pluck('x'); * var objA = {x: 1}; * xPluck(objA) // 1 * @example * var xPosition = pluck('x', function (x, i) { * // `this` is the original datum, * // `x` is the x property of the datum, * // `i` is the position in the array * return this.radius + x; * }); * selectAll('.circle').data(...).x(xPosition); * @function pluck * @param {String} n * @param {Function} [f] * @returns {Function} */ export const pluck = function (n, f) { if (!f) { return function (d) { return d[n]; }; } return function (d, i) { return f.call(d, d[n], i); }; }; /** * @namespace utils * @type {{}} */ export const utils = {}; /** * Print a single value filter. * @method printSingleValue * @memberof utils * @param {any} filter * @returns {String} */ utils.printSingleValue = function (filter) { let s = `${filter}`; if (filter instanceof Date) { s = config.dateFormat(filter); } else if (typeof (filter) === 'string') { s = filter; } else if (utils.isFloat(filter)) { s = utils.printSingleValue.fformat(filter); } else if (utils.isInteger(filter)) { s = Math.round(filter); } return s; }; utils.printSingleValue.fformat = format('.2f'); // convert 'day' to d3.timeDay and similar utils._toTimeFunc = function (t) { const mappings = { 'second': timeSecond, 'minute': timeMinute, 'hour': timeHour, 'day': timeDay, 'week': timeWeek, 'month': timeMonth, 'year': timeYear }; return mappings[t]; }; /** * Arbitrary add one value to another. * * If the value l is of type Date, adds r units to it. t becomes the unit. * For example utils.add(dt, 3, 'week') will add 3 (r = 3) weeks (t= 'week') to dt. * * If l is of type numeric, t is ignored. In this case if r is of type string, * it is assumed to be percentage (whether or not it includes %). For example * utils.add(30, 10) will give 40 and utils.add(30, '10') will give 33. * * They also generate strange results if l is a string. * @method add * @memberof utils * @param {Date|Number} l the value to modify * @param {String|Number} r the amount by which to modify the value * @param {Function|String} [t=d3.timeDay] if `l` is a `Date`, then this should be a * [d3 time interval](https://github.com/d3/d3-time/blob/master/README.md#_interval). * For backward compatibility with dc.js 2.0, it can also be the name of an interval, i.e. * 'millis', 'second', 'minute', 'hour', 'day', 'week', 'month', or 'year' * @returns {Date|Number} */ utils.add = function (l, r, t) { if (typeof r === 'string') { r = r.replace('%', ''); } if (l instanceof Date) { if (typeof r === 'string') { r = +r; } if (t === 'millis') { return new Date(l.getTime() + r); } t = t || timeDay; if (typeof t !== 'function') { t = utils._toTimeFunc(t); } return t.offset(l, r); } else if (typeof r === 'string') { const percentage = (+r / 100); return l > 0 ? l * (1 + percentage) : l * (1 - percentage); } else { return l + r; } }; /** * Arbitrary subtract one value from another. * * If the value l is of type Date, subtracts r units from it. t becomes the unit. * For example utils.subtract(dt, 3, 'week') will subtract 3 (r = 3) weeks (t= 'week') from dt. * * If l is of type numeric, t is ignored. In this case if r is of type string, * it is assumed to be percentage (whether or not it includes %). For example * utils.subtract(30, 10) will give 20 and utils.subtract(30, '10') will give 27. * * They also generate strange results if l is a string. * @method subtract * @memberof utils * @param {Date|Number} l the value to modify * @param {String|Number} r the amount by which to modify the value * @param {Function|String} [t=d3.timeDay] if `l` is a `Date`, then this should be a * [d3 time interval](https://github.com/d3/d3-time/blob/master/README.md#_interval). * For backward compatibility with dc.js 2.0, it can also be the name of an interval, i.e. * 'millis', 'second', 'minute', 'hour', 'day', 'week', 'month', or 'year' * @returns {Date|Number} */ utils.subtract = function (l, r, t) { if (typeof r === 'string') { r = r.replace('%', ''); } if (l instanceof Date) { if (typeof r === 'string') { r = +r; } if (t === 'millis') { return new Date(l.getTime() - r); } t = t || timeDay; if (typeof t !== 'function') { t = utils._toTimeFunc(t); } return t.offset(l, -r); } else if (typeof r === 'string') { const percentage = (+r / 100); return l < 0 ? l * (1 + percentage) : l * (1 - percentage); } else { return l - r; } }; /** * Is the value a number? * @method isNumber * @memberof utils * @param {any} n * @returns {Boolean} */ utils.isNumber = function (n) { return n === +n; }; /** * Is the value a float? * @method isFloat * @memberof utils * @param {any} n * @returns {Boolean} */ utils.isFloat = function (n) { return n === +n && n !== (n | 0); }; /** * Is the value an integer? * @method isInteger * @memberof utils * @param {any} n * @returns {Boolean} */ utils.isInteger = function (n) { return n === +n && n === (n | 0); }; /** * Is the value very close to zero? * @method isNegligible * @memberof utils * @param {any} n * @returns {Boolean} */ utils.isNegligible = function (n) { return !utils.isNumber(n) || (n < constants.NEGLIGIBLE_NUMBER && n > -constants.NEGLIGIBLE_NUMBER); }; /** * Ensure the value is no greater or less than the min/max values. If it is return the boundary value. * @method clamp * @memberof utils * @param {any} val * @param {any} min * @param {any} max * @returns {any} */ utils.clamp = function (val, min, max) { return val < min ? min : (val > max ? max : val); }; /** * Given `x`, return a function that always returns `x`. * * {@link https://github.com/d3/d3/blob/master/CHANGES.md#internals `d3.functor` was removed in d3 version 4}. * This function helps to implement the replacement, * `typeof x === "function" ? x : utils.constant(x)` * @method constant * @memberof utils * @param {any} x * @returns {Function} */ utils.constant = function (x) { return function () { return x; }; }; /** * Using a simple static counter, provide a unique integer id. * @method uniqueId * @memberof utils * @returns {Number} */ let _idCounter = 0; utils.uniqueId = function () { return ++_idCounter; }; /** * Convert a name to an ID. * @method nameToId * @memberof utils * @param {String} name * @returns {String} */ utils.nameToId = function (name) { return name.toLowerCase().replace(/[\s]/g, '_').replace(/[\.']/g, ''); }; /** * Append or select an item on a parent element. * @method appendOrSelect * @memberof utils * @param {d3.selection} parent * @param {String} selector * @param {String} tag * @returns {d3.selection} */ utils.appendOrSelect = function (parent, selector, tag) { tag = tag || selector; let element = parent.select(selector); if (element.empty()) { element = parent.append(tag); } return element; }; /** * Return the number if the value is a number; else 0. * @method safeNumber * @memberof utils * @param {Number|any} n * @returns {Number} */ utils.safeNumber = function (n) { return utils.isNumber(+n) ? +n : 0;}; /** * Return true if both arrays are equal, if both array are null these are considered equal * @method arraysEqual * @memberof utils * @param {Array|null} a1 * @param {Array|null} a2 * @returns {Boolean} */ utils.arraysEqual = function (a1, a2) { if (!a1 && !a2) { return true; } if (!a1 || !a2) { return false; } return a1.length === a2.length && // If elements are not integers/strings, we hope that it will match because of toString // Test cases cover dates as well. a1.every((elem, i) => elem.valueOf() === a2[i].valueOf()); }; // ******** Sunburst Chart ******** utils.allChildren = function (node) { let paths = []; paths.push(node.path); console.log('currentNode', node); if (node.children) { for (let i = 0; i < node.children.length; i++) { paths = paths.concat(utils.allChildren(node.children[i])); } } return paths; }; // builds a d3 Hierarchy from a collection // TODO: turn this monster method something better. utils.toHierarchy = function (list, accessor) { const root = {'key': 'root', 'children': []}; for (let i = 0; i < list.length; i++) { const data = list[i]; const parts = data.key; const value = accessor(data); let currentNode = root; for (let j = 0; j < parts.length; j++) { const currentPath = parts.slice(0, j + 1); const children = currentNode.children; const nodeName = parts[j]; let childNode; if (j + 1 < parts.length) { // Not yet at the end of the sequence; move down the tree. childNode = findChild(children, nodeName); // If we don't already have a child node for this branch, create it. if (childNode === void 0) { childNode = {'key': nodeName, 'children': [], 'path': currentPath}; children.push(childNode); } currentNode = childNode; } else { // Reached the end of the sequence; create a leaf node. childNode = {'key': nodeName, 'value': value, 'data': data, 'path': currentPath}; children.push(childNode); } } } return root; }; function findChild (children, nodeName) { for (let k = 0; k < children.length; k++) { if (children[k].key === nodeName) { return children[k]; } } } utils.getAncestors = function (node) { const path = []; let current = node; while (current.parent) { path.unshift(current.name); current = current.parent; } return path; }; utils.arraysIdentical = function (a, b) { let i = a.length; if (i !== b.length) { return false; } while (i--) { if (a[i] !== b[i]) { return false; } } return true; };