UNPKG

vis-utils

Version:

Utility functions for data visualization

438 lines (380 loc) 14.9 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('d3-array')) : typeof define === 'function' && define.amd ? define(['exports', 'd3-array'], factory) : (factory((global.visUtils = global.visUtils || {}),global.d3)); }(this, (function (exports,d3Array) { 'use strict'; /** * Compute the extent (min and max) of an array, limiting the min and the max * by the specified percentiles. Percentiles are values between 0 and 1. * * @param {Array} array The array to iterate over * @param {Function} [valueAccessor] How to read a value in the array (defaults to identity) * @param {Number} [minPercentile] If provided, limits the min to this percentile value (between 0 and 1). * If provided, the data is sorted by taking the difference of the valueAccessor results. * @param {Number} [maxPercentile] If provided, limits the max to this percentile value (between 0 and 1). * If provided, the data is sorted by taking the difference of the valueAccessor results. * @return {Array} the extent, limited by the min/max percentiles */ function extentLimited(array) { var valueAccessor = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : function (d) { return d; }; var minPercentile = arguments[2]; var maxPercentile = arguments[3]; if (!array || !array.length) { return undefined; } // neither limits defined, just use d3 extent. if (minPercentile == null && maxPercentile == null) { return d3Array.extent(array, valueAccessor); } array.sort(function (a, b) { return valueAccessor(a) - valueAccessor(b); }); var minValue = array[0]; var maxValue = array[array.length - 1]; var bisectValue = d3Array.bisector(valueAccessor).left; // limit to minPercentile if passed in if (minPercentile != null) { // get the value at the percentile var minQuantileValue = d3Array.quantile(array, minPercentile, valueAccessor); var quantileInsertIndex = Math.max(0, bisectValue(array, minQuantileValue)); // this may not exist in the array, so find the nearest point to it // and use that. minValue = valueAccessor(array[quantileInsertIndex]); } // limit to maxPercentile if passed in if (maxPercentile != null) { var maxQuantileValue = d3Array.quantile(array, maxPercentile, valueAccessor); var _quantileInsertIndex = Math.min(array.length - 1, bisectValue(array, maxQuantileValue)); maxValue = valueAccessor(array[_quantileInsertIndex]); // ensure we do not get a value bigger than the quantile value if (maxValue > maxQuantileValue && _quantileInsertIndex > 0) { maxValue = valueAccessor(array[_quantileInsertIndex - 1]); } } return [minValue, maxValue]; } /** * Compute the extent (min and max) across an array of arrays/objects * * For example: * ``` * extentMulti([[4, 3], [1, 2]], d => d); * > 1, 4 * ``` * ``` * extentMulti([{ results: [{ x: 4 }, { x: 3 }] }, { results: [{ x: 1 }, { x: 2 }] }], * d => d.x, array => array.results); * > 1, 4 * ``` * * @param {Array} outerArray An array of arrays or objects * @param {Function} [valueAccessor] How to read a value in the array (defaults to identity) * @param {Function} [arrayAccessor] How to read an inner array (defaults to identity) * @param {Number} [minPercentile] If provided, limits the min to this percentile value (between 0 and 1). * If provided, the data is sorted by taking the difference of the valueAccessor results. * @param {Number} [maxPercentile] If provided, limits the max to this percentile value (between 0 and 1). * If provided, the data is sorted by taking the difference of the valueAccessor results. * @return {Array} the extent */ function extentMulti(outerArray) { var valueAccessor = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : function (d) { return d; }; var arrayAccessor = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : function (d) { return d; }; var minPercentile = arguments[3]; var maxPercentile = arguments[4]; if (!outerArray || !outerArray.length) { return undefined; } // flatten the arrays into one big array var combined = outerArray.reduce(function (carry, inner) { return carry.concat(arrayAccessor(inner)); }, []); return extentLimited(combined, valueAccessor, minPercentile, maxPercentile); } var X = 0; var Y = 1; var TOP_LEFT = 0; var BOTTOM_RIGHT = 1; /** * Determines if a point is inside a rectangle. The rectangle is * defined by two points: * - the upper left corner (rx1, ry1) * - the bottom right corner (rx2, ry2) * Note that it is assumed that the top Y value is less than the bottom Y value. * * @param {Number[][]} rect The rectangle, a pair of two points * [[x, y], [x, y]] * @param {Number[]} point The point ([x, y]) * * @return {Boolean} true if the point is inside the rectangle, false otherwise */ function rectContains(rect, point) { return rect[TOP_LEFT][X] <= point[X] && point[X] <= rect[BOTTOM_RIGHT][X] && rect[TOP_LEFT][Y] <= point[Y] && point[Y] <= rect[BOTTOM_RIGHT][Y]; } /** * Filters the elements in the passed in array to those that are contained within * the specified rectangle. * * @param {Array} array The input array to filter * @param {Number[][]} rect The rectangle, a pair of two points [[x, y], [x, y]] * @param {Function} x Function that maps a point in the array to its x value * (defaults to d => d[0]) * @param {Function} y Function that maps a point in the array to its y value * (defaults to d => d[1]) * * @return {Array} The subset of the input array that is contained within the * rectangle */ function filterInRect(array, rect) { var x = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : function (d) { return d[0]; }; var y = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : function (d) { return d[1]; }; return array.filter(function (d) { return rectContains(rect, [x(d), y(d)]); }); } var X$1 = 0; var Y$1 = 1; var TOP_LEFT$1 = 0; var BOTTOM_RIGHT$1 = 1; /** * Determines if two rectangles intersect. Here a rectangle is defined * by its upper left and lower right corners. * * Note that it is assumed that the top Y value is less than the bottom Y value. * * @param {Number[][]} rect1 The first rectangle, a pair of two points * [[x, y], [x, y]] * @param {Number[][]} rect2 The second rectangle, a pair of two points * [[x, y], [x, y]] * * @return {Boolean} true if the rectangles intersect, false otherwise */ function rectIntersects(rect1, rect2) { return rect1[TOP_LEFT$1][X$1] <= rect2[BOTTOM_RIGHT$1][X$1] && rect2[TOP_LEFT$1][X$1] <= rect1[BOTTOM_RIGHT$1][X$1] && rect1[TOP_LEFT$1][Y$1] <= rect2[BOTTOM_RIGHT$1][Y$1] && rect2[TOP_LEFT$1][Y$1] <= rect1[BOTTOM_RIGHT$1][Y$1]; } /** * Filters the elements in the passed in quadtree to those that are contained within * the specified rectangle. * * @param {Object} quadtree The input data as a d3-quadtree to filter * @param {Number[][]} rect The rectangle, a pair of two points [[x, y], [x, y]] * @param {Function} x Function that maps a point in the array to its x value * (defaults to d => d[0]) * @param {Function} y Function that maps a point in the array to its y value * (defaults to d => d[1]) * * @return {Array} The subset of the input data that is contained within the * rectangle */ function filterInRectFromQuadtree(quadtree, rect) { var x = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : function (d) { return d[0]; }; var y = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : function (d) { return d[1]; }; var filtered = []; quadtree.visit(function (node, x1, y1, x2, y2) { // check that quadtree node intersects var overlaps = rectIntersects(rect, [[x1, y1], [x2, y2]]); // skip if it doesn't overlap the brush if (!overlaps) { return true; } // if this is a leaf node (node.length is falsy), verify it is within the brush // we have to do this since an overlapping quadtree box does not guarantee // that all the points within that box are covered by the brush. if (!node.length) { var d = node.data; if (rectContains(rect, [x(d), y(d)])) { filtered.push(d); } } // return false so that we traverse into branch (only useful for non-leaf nodes) return false; }); return filtered; } var index = function(haystack, needle, comparator, low, high) { var mid, cmp; if(low === undefined) low = 0; else { low = low|0; if(low < 0 || low >= haystack.length) throw new RangeError("invalid lower bound"); } if(high === undefined) high = haystack.length - 1; else { high = high|0; if(high < low || high >= haystack.length) throw new RangeError("invalid upper bound"); } while(low <= high) { /* Note that "(low + high) >>> 1" may overflow, and results in a typecast * to double (which gives the wrong results). */ mid = low + (high - low >> 1); cmp = +comparator(haystack[mid], needle, mid, haystack); /* Too low. */ if(cmp < 0.0) low = mid + 1; /* Too high. */ else if(cmp > 0.0) high = mid - 1; /* Key found. */ else return mid; } /* Key not found. */ return ~low; }; /** * Helper function to compute distance and find the closest item * Since it assumes the data is sorted, it does a binary search O(log n) * * @param {Array} array the input array to search * @param {Number} value the value to match against (typically pixels) * @param {Function} accessor applied to each item in the array to get equivalent * value to compare against * @return {Any} The item in the array that is closest to `value` */ function findClosestSorted(array, value) { var accessor = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : function (d) { return d; }; // binary search uses the value directly in comparisons, so make sure not to // run the accessor on it var index$$1 = index(array, value, function (a, b) { var aValue = a === value ? value : accessor(a); var bValue = b === value ? value : accessor(b); return aValue - bValue; }); // index is positive = we found it exactly if (index$$1 < 0) { // should match first element if (index$$1 === -1) { index$$1 = 0; } else { // map back to the input location since the binary search uses -(low + 1) as the result index$$1 = -index$$1 - 1; // should match last element if (index$$1 >= array.length) { index$$1 = array.length - 1; } } } // this result is always to the right, so see if the one to the left is closer // and use it if it is. var result = array[index$$1]; var before = array[index$$1 - 1]; if (before != null && Math.abs(accessor(result) - value) > Math.abs(accessor(before) - value)) { result = before; } return result; } /** * Helper function to compute distance and find the closest item * Since it assumes the data is unsorted, it does a linear scan O(n). * * @param {Array} array the input array to search * @param {Number} value the value to match against (typically pixels) * @param {Function} accessor applied to each item in the array to get equivalent * value to compare against * @return {Any} The item in the array that is closest to `value` */ function findClosestUnsorted(array, value) { var accessor = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : function (d) { return d; }; var closest = null; var closestDist = null; array.forEach(function (elem) { var dist = Math.abs(accessor(elem) - value); if (closestDist == null || dist < closestDist) { closestDist = dist; closest = elem; } }); return closest; } /** * Helper function to find the item that matches this value. * Since it assumes the data is sorted, it does a binary search O(log n) * * @param {Array} array the input array to search * @param {Number} value the value to match against (typically pixels) * @param {Function} accessor applied to each item in the array to get equivalent * value to compare against * @return {Any} The item in the array that has this value or null if not found */ function findEqualSorted(array, value) { var accessor = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : function (d) { return d; }; // binary search uses the value directly in comparisons, so make sure not to // run the accessor on it var index$$1 = index(array, value, function (a, b) { var aValue = a === value ? value : accessor(a); var bValue = b === value ? value : accessor(b); return aValue - bValue; }); return array[index$$1]; } /** * Helper function to find the item that matches this value. * Since it assumes the data is unsorted, it does a linear scan O(n). * * @param {Array} array the input array to search * @param {Number} value the value to match against (typically pixels) * @param {Function} accessor applied to each item in the array to get equivalent * value to compare against * @return {Any} The item in the array that has this value or null if not found */ function findEqualUnsorted(array, value) { var accessor = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : function (d) { return d; }; return array.find(function (d) { return accessor(d) === value; }); } var X$2 = 0; var Y$2 = 1; /** * Rotate a point ([x, y]) around an origin ([x, y]) by theta radians * * @param {Number[]} point [x, y] * @param {Number} thetaRadians How many radians to rotate the point around origin * @param {Number[]} [origin] [x, y] (defaults to [0, 0]) * * @return {Number[]} The rotated point [x, y] */ function rotate(point, thetaRadians) { var origin = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : [0, 0]; var rotatedEndX = origin[X$2] + (point[X$2] - origin[X$2]) * Math.cos(thetaRadians) - (point[Y$2] - origin[Y$2]) * Math.sin(thetaRadians); var rotatedEndY = origin[Y$2] + (point[X$2] - origin[X$2]) * Math.sin(thetaRadians) + (point[Y$2] - origin[Y$2]) * Math.cos(thetaRadians); return [rotatedEndX, rotatedEndY]; } exports.extentLimited = extentLimited; exports.extentMulti = extentMulti; exports.filterInRect = filterInRect; exports.filterInRectFromQuadtree = filterInRectFromQuadtree; exports.findClosestSorted = findClosestSorted; exports.findClosestUnsorted = findClosestUnsorted; exports.findEqualSorted = findEqualSorted; exports.findEqualUnsorted = findEqualUnsorted; exports.rectContains = rectContains; exports.rectIntersects = rectIntersects; exports.rotate = rotate; Object.defineProperty(exports, '__esModule', { value: true }); })));