vis-utils
Version:
Utility functions for data visualization
438 lines (380 loc) • 14.9 kB
JavaScript
(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 });
})));