import constant from './constant'; import jiggle from './jiggle'; /** * An implementation of Chalmers' 1996 Neighbour and Sampling algorithm. * It uses random sampling to find the most suited neighbours from the * data set. */ function sortDistances (a, b) { return b[1] - a[1]; } export default function () { var neighbours = [], distance = constant(300), nodes, neighbourSize = 10, sampleSize = 10, stableVelocity = 0, stableVeloHandler = null, dataSizeFactor, latestVelocityDiff = 0; /** * Apply spring forces at each simulation iteration. * @param {number} alpha - multiplier for amount of force applied */ function force (alpha) { let n = nodes.length; // Cache old velocity for comparison later if (stableVeloHandler !== null && stableVelocity >= 0) { for (let i = n - 1, node; i >= 0; i--) { node = nodes[i]; node.oldvx = node.vx; node.oldvy = node.vy; } } for (let i = n - 1, node, samples; i >= 0; i--) { node = nodes[i]; samples = createRandomSamples(i); for (let [neighbourID, highDDist] of neighbours[i]) { setVelocity(node, nodes[neighbourID], highDDist, alpha); } for (let [sampleID, highDDist] of samples) { setVelocity(node, nodes[sampleID], highDDist, alpha); } neighbours[i] = findNewNeighbours(neighbours[i], samples); } // Calculate velocity changes, aka force applied. if (stableVeloHandler !== null && stableVelocity >= 0) { let velocityDiff = 0; for (let i = n - 1, node; i >= 0; i--) { node = nodes[i]; velocityDiff += Math.abs(Math.hypot(node.vx - node.oldvx, node.vy - node.oldvy)); } velocityDiff /= n; latestVelocityDiff = velocityDiff; if (velocityDiff < stableVelocity) { stableVeloHandler(); } } } /** * Apply force to both source and target nodes. * @param {number} source - source node object * @param {number} target - target node object * @param {number} dist - high dimensional distance between the two nodes * @param {number} alpha - multiplier for the amount of force applied */ function setVelocity (source, target, dist, alpha) { let x, y, l; // jiggle so l won't be zero and divide by zero error after this x = target.x + target.vx - source.x - source.vx || jiggle(); y = target.y + target.vy - source.y - source.vy || jiggle(); l = Math.sqrt(x * x + y * y); l = (l - dist) / l * dataSizeFactor * alpha; x *= l; y *= l; // Set the calculated velocites for both nodes. target.vx -= x; target.vy -= y; source.vx += x; source.vy += y; } // Called on nodes change and added to a simulation function initialize () { if (!nodes) return; // Initialize for each node some random neighbours. for (let i = nodes.length - 1; i >= 0; i--) { let neighbs = pickRandomNodesFor(i, [i], neighbourSize); // Sort the neighbour set by the distances. neighbours[i] = new Map(neighbs.sort(sortDistances)); } initDataSizeFactor(); } function initDataSizeFactor () { dataSizeFactor = 0.5 / (neighbourSize + sampleSize); } /** * Generates an array of array[index, high-d distance to the node of index] * where all indices are different and the size is as specified unless * impossible (may be due to too large size requested) * @param {number} index - index of a node to calculate distance against * @param {array} exclude - indices of the nodes to ignore. * @param {number} size - max number of elements in the map to return. * @return {array} */ function pickRandomNodesFor (index, exclude, size) { let randElements = []; let max = nodes.length; for (let i = 0; i < size; i++) { // Stop when no new elements can be found. if (randElements.length + exclude.length >= nodes.length) { break; } let rand = Math.floor(Math.random() * max); // Re-random until suitable value is found. while (randElements.includes(rand) || exclude.includes(rand)) { rand = Math.floor(Math.random() * max); } randElements.push(rand); } for (let i = randElements.length - 1, rand; i >= 0; i--) { rand = randElements[i]; randElements[i] = [rand, distance(nodes[index], nodes[rand])]; } return randElements; } /** * Generates a map {index: high-dimensional distance to the node of index} * to be used as samples set for the node of the specified index. * @param {number} index - index of the node to generate sample for * @return {map} */ function createRandomSamples (index) { // Ignore the current neighbours of the node and itself. let exclude = [index]; exclude = exclude.concat(Array.from(neighbours[index].keys())); return new Map(pickRandomNodesFor(index, exclude, sampleSize)); } /** * Compares the elements from sample set to the neighbour set and replaces the * elements in the neighbour set if any better neighbours are found. * @param {map} neighbours - map of neighbours * @param {map} samples - map of samples * @return {map} - new map of neighbours */ function findNewNeighbours (neighbours, samples) { let combined = [...neighbours.entries()].concat([...samples.entries()]); combined = combined.sort(sortDistances); return new Map(combined.slice(0, neighbourSize)); } // API for initializing the algorithm and setting parameters force.initialize = function (_) { nodes = _; initialize(); }; force.neighbourSize = function (_) { return arguments.length ? (neighbourSize = +_, initialize(), force) : neighbourSize; }; force.neighbours = function () { return neighbours; }; force.sampleSize = function (_) { return arguments.length ? (sampleSize = +_, initDataSizeFactor(), force) : sampleSize; }; force.distance = function (_) { return arguments.length ? (distance = typeof _ === 'function' ? _ : constant(+_), force) : distance; }; force.latestAccel = function () { return latestVelocityDiff; }; force.onStableVelo = function (_) { return arguments.length ? (stableVeloHandler = _, force) : stableVeloHandler; }; force.stableVelocity = function (_) { return arguments.length ? (stableVelocity = _, force) : stableVelocity; }; return force; }