แก้ coding style

This commit is contained in:
Pitchaya Boonsarngsuk
2018-03-22 16:02:45 +00:00
parent b601af68b4
commit 2256af7448
9 changed files with 866 additions and 873 deletions

View File

@@ -1,158 +1,152 @@
import constant from "./constant"; import constant from "./constant";
import jiggle from "./jiggle"; import jiggle from "./jiggle";
import {x, y} from "./xy"; import {x, y} from "./xy";
import {quadtree} from "d3-quadtree"; import {quadtree} from "d3-quadtree";
/** /**
* The refinement of the existing Barnes-Hut implementation in D3 * The refinement of the existing Barnes-Hut implementation in D3
* to fit the use case of the project. Previously the algorithm stored * to fit the use case of the project. Previously the algorithm stored
* strength as internal node, now the random child is stored as internal * strength as internal node, now the random child is stored as internal
* node and the force calculations are done between the node and that internal * node and the force calculations are done between the node and that internal
* object if they are sufficiently far away. * object if they are sufficiently far away.
* The check to see if the nodes are far away was also changed to the one described in original Barnes-Hut paper. * The check to see if the nodes are far away was also changed to the one described in original Barnes-Hut paper.
* @return {force} calculated forces. * @return {force} calculated forces.
*/ */
export default function() { export default function() {
var nodes, var nodes,
node, node,
alpha, alpha,
distance = constant(300), distance = constant(300),
theta = 0.5; theta = 0.5;
/** /**
* Constructs a quadtree at every iteration and apply the forces by visiting * Constructs a quadtree at every iteration and apply the forces by visiting
* each node in a tree. * each node in a tree.
* @param {number} _ - controls the stopping of the * @param {number} _ - controls the stopping of the
* particle simulations. * particle simulations.
*/ */
function force(_) { function force(_) {
var i, n = nodes.length, tree = quadtree(nodes, x, y).visitAfter(accumulate); var i, n = nodes.length, tree = quadtree(nodes, x, y).visitAfter(accumulate);
for (alpha = _, i = 0; i < n; ++i) { for (alpha = _, i = 0; i < n; ++i) {
node = nodes[i], tree.visit(apply); node = nodes[i], tree.visit(apply);
} }
} }
/** /**
* Function used during the tree construction to fill out the nodes with * Function used during the tree construction to fill out the nodes with
* correct data. Internal nodes acquire the random child while the leaf * correct data. Internal nodes acquire the random child while the leaf
* nodes accumulate forces from coincident quadrants. * nodes accumulate forces from coincident quadrants.
* @param {quadrant} quad - node representing the quadrant in quadtree. * @param {quadrant} quad - node representing the quadrant in quadtree.
*/ */
function accumulate(quad) { function accumulate(quad) {
var q, d, children = []; var q, d, children = [];
// For internal nodes, accumulate forces from child quadrants. // For internal nodes, accumulate forces from child quadrants.
if (quad.length) { if (quad.length) {
for (var i = 0; i < 4; ++i) { for (var i = 0; i < 4; ++i) {
if ((q = quad[i]) && (d = q.data)) { if ((q = quad[i]) && (d = q.data)) {
children.push(d); children.push(d);
} }
} }
// Choose a random child. // Choose a random child.
quad.data = children[Math.floor(Math.random() * children.length)]; quad.data = children[Math.floor(Math.random() * children.length)];
quad.x = quad.data.x; quad.x = quad.data.x;
quad.y = quad.data.y; quad.y = quad.data.y;
} } else { // For leaf nodes, accumulate forces from coincident quadrants.
q = quad;
// For leaf nodes, accumulate forces from coincident quadrants. q.x = q.data.x;
else { q.y = q.data.y;
q = quad; }
q.x = q.data.x; }
q.y = q.data.y;
} /**
} * Function that applies the forces for each node. If the objects are
* far away, the approximation is made. Otherwise, forces are calculated
/** * directly between the nodes.
* Function that applies the forces for each node. If the objects are * @param {quadrant} quad - node representing the quadrant in quadtree.
* far away, the approximation is made. Otherwise, forces are calculated * @param {number} x1 - lower x bound of the node.
* directly between the nodes. * @param {number} _ - lower y bound of the node.
* @param {quadrant} quad - node representing the quadrant in quadtree. * @param {number} x2 - upper x bound of the node.
* @param {number} x1 - lower x bound of the node. * @return {boolean} - true if the approximation was applied.
* @param {number} _ - lower y bound of the node. */
* @param {number} x2 - upper x bound of the node. function apply(quad, x1, _, x2) {
* @return {boolean} - true if the approximation was applied.
*/ var x = quad.data.x + quad.data.vx - node.x - node.vx,
function apply(quad, x1, _, x2) { y = quad.data.y + quad.data.vy - node.y - node.vy,
w = x2 - x1,
var x = quad.data.x + quad.data.vx - node.x - node.vx, l = Math.sqrt(x * x + y * y);
y = quad.data.y + quad.data.vy - node.y - node.vy,
w = x2 - x1, // Apply the Barnes-Hut approximation if possible.
l = Math.sqrt(x * x + y * y); // Limit forces for very close nodes; randomize direction if coincident.
if (w / l < theta) {
// Apply the Barnes-Hut approximation if possible. if (x === 0) x = jiggle(), l += x * x;
// Limit forces for very close nodes; randomize direction if coincident. if (y === 0) y = jiggle(), l += y * y;
if (w / l < theta) { if (quad.data) {
if (x === 0) x = jiggle(), l += x * x; l = (l - +distance(node, quad.data)) / l * alpha;
if (y === 0) y = jiggle(), l += y * y; x *= l, y *= l;
if (quad.data) { quad.data.vx -= x;
l = (l - +distance(node, quad.data)) / l * alpha; quad.data.vy -= y;
x *= l, y *= l; node.vx += x;
quad.data.vx -= x; node.vy += y;
quad.data.vy -= y; }
node.vx += x; return true;
node.vy += y; } else if (quad.length) return; // Otherwise, process points directly.
}
return true; // Limit forces for very close nodes; randomize direction if coincident.
} if (quad.data !== node || quad.next) {
if (x === 0) x = jiggle(), l += x * x;
// Otherwise, process points directly. if (y === 0) y = jiggle(), l += y * y;
else if (quad.length) return; }
// Limit forces for very close nodes; randomize direction if coincident. do if (quad.data !== node) {
if (quad.data !== node || quad.next) { l = (l - +distance(node, quad.data)) / l * alpha;
if (x === 0) x = jiggle(), l += x * x; x *= l, y *= l;
if (y === 0) y = jiggle(), l += y * y; quad.data.vx -= x;
} quad.data.vy -= y;
node.vx += x;
do if (quad.data !== node) { node.vy += y;
l = (l - +distance(node, quad.data)) / l * alpha; } while (quad = quad.next);
x *= l, y *= l; }
quad.data.vx -= x;
quad.data.vy -= y; /**
node.vx += x; * Calculates the stress. Basically, it computes the difference between
node.vy += y; * high dimensional distance and real distance. The lower the stress is,
} while (quad = quad.next); * the better layout.
} * @return {number} - stress of the layout.
*/
/** function getStress() {
* Calculates the stress. Basically, it computes the difference between var totalDiffSq = 0, totalHighDistSq = 0;
* high dimensional distance and real distance. The lower the stress is, for (var i = 0, source, target, realDist, highDist; i < nodes.length; i++) {
* the better layout. for (var j = 0; j < nodes.length; j++) {
* @return {number} - stress of the layout. if (i !== j) {
*/ source = nodes[i], target = nodes[j];
function getStress() { realDist = Math.hypot(target.x-source.x, target.y-source.y);
var totalDiffSq = 0, totalHighDistSq = 0; highDist = +distance(nodes[i], nodes[j]);
for (var i = 0, source, target, realDist, highDist; i < nodes.length; i++) { totalDiffSq += Math.pow(realDist-highDist, 2);
for (var j = 0; j < nodes.length; j++) { totalHighDistSq += highDist * highDist;
if (i !== j) { }
source = nodes[i], target = nodes[j]; }
realDist = Math.hypot(target.x-source.x, target.y-source.y); }
highDist = +distance(nodes[i], nodes[j]); return Math.sqrt(totalDiffSq/totalHighDistSq);
totalDiffSq += Math.pow(realDist-highDist, 2); }
totalHighDistSq += highDist * highDist;
} // API for initializing the algorithm, setting parameters and querying
} // metrics.
} force.initialize = function(_) {
return Math.sqrt(totalDiffSq/totalHighDistSq); nodes = _;
} };
// API for initializing the algorithm, setting parameters and querying force.distance = function(_) {
// metrics. return arguments.length ? (distance = typeof _ === "function" ? _ : constant(+_), force) : distance;
force.initialize = function(_) { };
nodes = _;
}; force.theta = function(_) {
return arguments.length ? (theta = _, force) : theta;
force.distance = function(_) { };
return arguments.length ? (distance = typeof _ === "function" ? _ : constant(+_), force) : distance;
}; force.stress = function() {
return getStress();
force.theta = function(_) { };
return arguments.length ? (theta = _, force) : theta;
}; return force;
}
force.stress = function() {
return getStress();
};
return force;
}

View File

@@ -1,203 +1,203 @@
import {dispatch} from "d3-dispatch"; import {dispatch} from "d3-dispatch";
import constant from "./constant"; import constant from "./constant";
import interpBruteForce from "./interpolation/interpBruteForce"; import interpBruteForce from "./interpolation/interpBruteForce";
import interpolationPivots from "./interpolation/interpolationPivots"; import interpolationPivots from "./interpolation/interpolationPivots";
import {takeSampleFrom} from "./interpolation/helpers"; import {takeSampleFrom} from "./interpolation/helpers";
/** /**
* An implementation of Chalmers, Morrison, and Ross' 2002 hybrid layout * An implementation of Chalmers, Morrison, and Ross' 2002 hybrid layout
* algorithm with an option to use the 2003 pivot-based near neighbour searching * algorithm with an option to use the 2003 pivot-based near neighbour searching
* method. * method.
* It performs the 1996 neighbour sampling spring simulation model, on a * It performs the 1996 neighbour sampling spring simulation model, on a
* "sample set", sqrt(n) samples of the data. * "sample set", sqrt(n) samples of the data.
* Other data points are then interpolated into the model. * Other data points are then interpolated into the model.
* Finally, another spring simulation may be performed on the entire dataset to * Finally, another spring simulation may be performed on the entire dataset to
* clean up the model. * clean up the model.
* @param {object} sim - D3 Simulation object * @param {object} sim - D3 Simulation object
* @param {object} forceS - Pre-configured D3 force object for the sample set. * @param {object} forceS - Pre-configured D3 force object for the sample set.
The ending handler will be re-configured. The ending handler will be re-configured.
Neighbour sampling force is expected, but other D3 Neighbour sampling force is expected, but other D3
forces may also work. forces may also work.
* @param {object} forceF - Pre-configured D3 force object for the simultion ran * @param {object} forceF - Pre-configured D3 force object for the simultion ran
on the entire dataset at the end. on the entire dataset at the end.
Neighbour sampling force is expected, but other D3 Neighbour sampling force is expected, but other D3
forces may also work. forces may also work.
The force should not have any ending condition. The force should not have any ending condition.
*/ */
export default function (sim, forceS, forceF) { export default function (sim, forceS, forceF) {
var var
SAMPLE_ITERATIONS = 300, SAMPLE_ITERATIONS = 300,
FULL_ITERATIONS = 20, FULL_ITERATIONS = 20,
interpDistanceFn, interpDistanceFn,
NUM_PIVOTS = 0, NUM_PIVOTS = 0,
INTERP_FINE_ITS = 20, INTERP_FINE_ITS = 20,
sample = [], sample = [],
remainder = [], remainder = [],
simulation = sim, simulation = sim,
forceSample = forceS, forceSample = forceS,
forceFull = forceF, forceFull = forceF,
event = d3.dispatch("sampleTick", "fullTick", "startInterp", "end"), event = d3.dispatch("sampleTick", "fullTick", "startInterp", "end"),
initAlready = false, initAlready = false,
nodes, nodes,
alreadyRanIterations, alreadyRanIterations,
hybrid; hybrid;
if(simulation != undefined) initSimulation(); if(simulation != undefined) initSimulation();
if(forceS != undefined || forceF != undefined) initForces(); if(forceS != undefined || forceF != undefined) initForces();
// Performed on first run // Performed on first run
function initialize() { function initialize() {
initAlready = true; initAlready = true;
alreadyRanIterations = 0; alreadyRanIterations = 0;
simulation simulation
.on("tick", sampleTick) .on("tick", sampleTick)
.on("end", sampleEnded) .on("end", sampleEnded)
.nodes(sample) .nodes(sample)
.force("Sample force", forceSample); .force("Sample force", forceSample);
console.log("Initialized Simulation for Hybrid"); console.log("Initialized Simulation for Hybrid");
} }
function initForces(){ function initForces(){
if (forceSample.onStableVelo) { if (forceSample.onStableVelo) {
forceSample.onStableVelo(sampleEnded); forceSample.onStableVelo(sampleEnded);
} }
if (forceFull.onStableVelo) { if (forceFull.onStableVelo) {
forceFull.onStableVelo(fullEnded); forceFull.onStableVelo(fullEnded);
} }
// Set default value for interpDistanceFn if not been specified yet // Set default value for interpDistanceFn if not been specified yet
if(interpDistanceFn === undefined) { if(interpDistanceFn === undefined) {
if(forceFull.distance == 'function') if(forceFull.distance == 'function')
interpDistanceFn = forceFull.distance(); interpDistanceFn = forceFull.distance();
else else
interpDistanceFn = constant(300); interpDistanceFn = constant(300);
} }
} }
function initSimulation(){ function initSimulation(){
nodes = simulation.nodes(); nodes = simulation.nodes();
simulation simulation
.stop() .stop()
.alphaDecay(0) .alphaDecay(0)
.alpha(1) .alpha(1)
let sets = takeSampleFrom(nodes, Math.sqrt(nodes.length)); let sets = takeSampleFrom(nodes, Math.sqrt(nodes.length));
sample = sets.sample; sample = sets.sample;
remainder = sets.remainder; remainder = sets.remainder;
} }
// Sample simulation ticked 1 frame, keep track of number of iterations here. // Sample simulation ticked 1 frame, keep track of number of iterations here.
function sampleTick() { function sampleTick() {
event.call("sampleTick"); event.call("sampleTick");
if(alreadyRanIterations++ >= SAMPLE_ITERATIONS){ if(alreadyRanIterations++ >= SAMPLE_ITERATIONS){
sampleEnded(); sampleEnded();
} }
} }
// Full simulation ticked 1 frame, keep track of number of iterations here. // Full simulation ticked 1 frame, keep track of number of iterations here.
function fullTick() { function fullTick() {
event.call("fullTick"); event.call("fullTick");
if(alreadyRanIterations++ >= FULL_ITERATIONS){ if(alreadyRanIterations++ >= FULL_ITERATIONS){
fullEnded(); fullEnded();
} }
} }
function fullEnded() { function fullEnded() {
simulation.stop(); simulation.stop();
initAlready = false; initAlready = false;
simulation.force("Full force", null); simulation.force("Full force", null);
event.call("end"); event.call("end");
} }
function sampleEnded() { function sampleEnded() {
simulation.stop(); simulation.stop();
simulation.force("Sample force", null); simulation.force("Sample force", null);
// Reset velocity of all nodes // Reset velocity of all nodes
for (let i=sample.length-1; i>=0; i--){ for (let i=sample.length-1; i>=0; i--){
sample[i].vx=0; sample[i].vx=0;
sample[i].vy=0; sample[i].vy=0;
} }
event.call("startInterp"); event.call("startInterp");
if (NUM_PIVOTS>=1) { if (NUM_PIVOTS>=1) {
interpolationPivots(sample, remainder, NUM_PIVOTS, interpDistanceFn, INTERP_FINE_ITS); interpolationPivots(sample, remainder, NUM_PIVOTS, interpDistanceFn, INTERP_FINE_ITS);
} else { } else {
interpBruteForce(sample, remainder, interpDistanceFn, INTERP_FINE_ITS); interpBruteForce(sample, remainder, interpDistanceFn, INTERP_FINE_ITS);
} }
event.call("fullTick"); event.call("fullTick");
alreadyRanIterations = 0; alreadyRanIterations = 0;
simulation simulation
.on("tick", null) .on("tick", null)
.on("end", null) // The ending condition should be iterations count .on("end", null) // The ending condition should be iterations count
.nodes(nodes); .nodes(nodes);
if (FULL_ITERATIONS<1 || forceF === undefined || forceF === null) { if (FULL_ITERATIONS<1 || forceF === undefined || forceF === null) {
event.call("end"); event.call("end");
return; return;
} }
simulation simulation
.on("tick", fullTick) .on("tick", fullTick)
.force("Full force", forceFull) .force("Full force", forceFull)
.restart(); .restart();
} }
return hybrid = { return hybrid = {
restart: function () { restart: function () {
if(!initAlready) initialize(); if(!initAlready) initialize();
simulation.restart(); simulation.restart();
return hybrid; return hybrid;
}, },
stop: function () { stop: function () {
simulation.stop(); simulation.stop();
return hybrid; return hybrid;
}, },
numPivots: function (_) { numPivots: function (_) {
return arguments.length ? (NUM_PIVOTS = +_, hybrid) : NUM_PIVOTS; return arguments.length ? (NUM_PIVOTS = +_, hybrid) : NUM_PIVOTS;
}, },
sampleIterations: function (_) { sampleIterations: function (_) {
return arguments.length ? (SAMPLE_ITERATIONS = +_, hybrid) : SAMPLE_ITERATIONS; return arguments.length ? (SAMPLE_ITERATIONS = +_, hybrid) : SAMPLE_ITERATIONS;
}, },
fullIterations: function (_) { fullIterations: function (_) {
return arguments.length ? (FULL_ITERATIONS = +_, hybrid) : FULL_ITERATIONS; return arguments.length ? (FULL_ITERATIONS = +_, hybrid) : FULL_ITERATIONS;
}, },
interpFindTuneIts: function (_) { interpFindTuneIts: function (_) {
return arguments.length ? (INTERP_FINE_ITS = +_, hybrid) : INTERP_FINE_ITS; return arguments.length ? (INTERP_FINE_ITS = +_, hybrid) : INTERP_FINE_ITS;
}, },
on: function (name, _) { on: function (name, _) {
return arguments.length > 1 ? (event.on(name, _), hybrid) : event.on(name); return arguments.length > 1 ? (event.on(name, _), hybrid) : event.on(name);
}, },
subSet: function (_) { subSet: function (_) {
return arguments.length ? (sample = _, hybrid) : sample; return arguments.length ? (sample = _, hybrid) : sample;
}, },
nonSubSet: function (_) { nonSubSet: function (_) {
return arguments.length ? (remainder = _, hybrid) : remainder; return arguments.length ? (remainder = _, hybrid) : remainder;
}, },
interpDistanceFn: function (_) { interpDistanceFn: function (_) {
return arguments.length ? (interpDistanceFn = typeof _ === "function" ? _ : constant(+_), hybrid) : interpDistanceFn; return arguments.length ? (interpDistanceFn = typeof _ === "function" ? _ : constant(+_), hybrid) : interpDistanceFn;
}, },
simulation: function (_) { simulation: function (_) {
return arguments.length ? (initAlready = false, simulation = _, hybrid) : simulation; return arguments.length ? (initAlready = false, simulation = _, hybrid) : simulation;
}, },
forceSample: function (_) { forceSample: function (_) {
return arguments.length ? (forceSample = _, initForces(), hybrid) : forceSample; return arguments.length ? (forceSample = _, initForces(), hybrid) : forceSample;
}, },
forceFull: function (_) { forceFull: function (_) {
return arguments.length ? (forceFull = _, initForces(), hybrid) : forceFull; return arguments.length ? (forceFull = _, initForces(), hybrid) : forceFull;
}, },
}; };
} }

View File

@@ -10,8 +10,8 @@
*/ */
export function takeSampleFrom(sourceList, amount) { export function takeSampleFrom(sourceList, amount) {
let randElements = [], let randElements = [],
max = sourceList.length, max = sourceList.length,
swap = false; swap = false;
if (amount >= max) { if (amount >= max) {
return {sample: sourceList, remainder: {}}; return {sample: sourceList, remainder: {}};
@@ -40,8 +40,7 @@ export function takeSampleFrom(sourceList, amount) {
sample: remainder, sample: remainder,
remainder: randElements remainder: randElements
}; };
} } else {
else {
return { return {
sample: randElements, sample: randElements,
remainder: remainder remainder: remainder

View File

@@ -23,7 +23,7 @@ export default function(sampleSet, remainderSet, distanceFn, endingIts) {
sampleSubset = takeSampleFrom(sampleSet, Math.sqrt(sampleSet.length)).sample, sampleSubset = takeSampleFrom(sampleSet, Math.sqrt(sampleSet.length)).sample,
sampleSubsetDistanceCache = []; sampleSubsetDistanceCache = [];
// For each datapoint "node" to be interpolated // For each datapoint "node" to be interpolated
for (let i = remainderSet.length-1; i>=0; i--) { for (let i = remainderSet.length-1; i>=0; i--) {
let let
node = remainderSet[i], node = remainderSet[i],

View File

@@ -36,7 +36,7 @@ export function placeNearToNearestNeighbour(node, nearNeighbour, radius, sampleS
lowBound = 0.0, lowBound = 0.0,
highBound = 0.0; highBound = 0.0;
// Determine the closest quadrant // Determine the closest quadrant
if (dist0 == dist180) { if (dist0 == dist180) {
if (dist90 > dist270) if (dist90 > dist270)
lowBound = highBound = 270; lowBound = highBound = 270;
@@ -55,14 +55,12 @@ export function placeNearToNearestNeighbour(node, nearNeighbour, radius, sampleS
lowBound = 90; lowBound = 90;
highBound = 180; highBound = 180;
} }
} else if (dist90 > dist270) {
lowBound = 270;
highBound = 360;
} else { } else {
if (dist90 > dist270) { lowBound = 0;
lowBound = 270; highBound = 90;
highBound = 360;
} else {
lowBound = 0;
highBound = 90;
}
} }
// Determine the angle // Determine the angle
@@ -84,8 +82,8 @@ export function placeNearToNearestNeighbour(node, nearNeighbour, radius, sampleS
function sumForcesToSample(node, samples, sampleCache) { function sumForcesToSample(node, samples, sampleCache) {
let nodeVx = 0, let nodeVx = 0,
nodeVy = 0, nodeVy = 0,
x, y, l, i, sample; x, y, l, i, sample;
for (i = samples.length-1; i >=0 ; i--) { for (i = samples.length-1; i >=0 ; i--) {
sample = samples[i]; sample = samples[i];

View File

@@ -1,108 +1,108 @@
import constant from "./constant"; import constant from "./constant";
import jiggle from "./jiggle"; import jiggle from "./jiggle";
/** /**
* Modified link force algorithm * Modified link force algorithm
* - simplify calculations for parameters locked for spring model * - simplify calculations for parameters locked for spring model
* - replace the use of links {} with loop. greatly reduce memory usage * - replace the use of links {} with loop. greatly reduce memory usage
* - removed other unused functions * - removed other unused functions
* Alpha should be constant 1 for accurate simulation * Alpha should be constant 1 for accurate simulation
*/ */
export default function() { export default function() {
var dataSizeFactor, var dataSizeFactor,
distance = constant(30), distance = constant(30),
distances = [], distances = [],
nodes, nodes,
stableVelocity = 0, stableVelocity = 0,
stableVeloHandler = null, stableVeloHandler = null,
latestVelocityDiff = 0, latestVelocityDiff = 0,
iterations = 1; iterations = 1;
function force(alpha) { function force(alpha) {
let n = nodes.length; let n = nodes.length;
// Cache old velocity for comparison later // Cache old velocity for comparison later
if (stableVeloHandler!==null && stableVelocity>=0) { if (stableVeloHandler!==null && stableVelocity>=0) {
for (let i = n-1, node; i>=0; i--) { for (let i = n-1, node; i>=0; i--) {
node = nodes[i]; node = nodes[i];
node.oldvx = node.vx; node.oldvx = node.vx;
node.oldvy = node.vy; node.oldvy = node.vy;
} }
} }
// Each iteration in a tick // Each iteration in a tick
for (var k = 0, source, target, i, j, x, y, l; k < iterations; ++k) { for (var k = 0, source, target, i, j, x, y, l; k < iterations; ++k) {
// For each link // For each link
for (i = 1; i < n; i++) for (j = 0; j < i; j++) { for (i = 1; i < n; i++) for (j = 0; j < i; j++) {
// jiggle so l won't be zero and divide by zero error after this // jiggle so l won't be zero and divide by zero error after this
source = nodes[i]; source = nodes[i];
target = nodes[j]; target = nodes[j];
x = target.x + target.vx - source.x - source.vx || jiggle(); x = target.x + target.vx - source.x - source.vx || jiggle();
y = target.y + target.vy - source.y - source.vy || jiggle(); y = target.y + target.vy - source.y - source.vy || jiggle();
l = Math.sqrt(x * x + y * y); l = Math.sqrt(x * x + y * y);
l = (l - distances[i*(i-1)/2+j]) / l * dataSizeFactor * alpha; l = (l - distances[i*(i-1)/2+j]) / l * dataSizeFactor * alpha;
x *= l, y *= l; x *= l, y *= l;
target.vx -= x; target.vx -= x;
target.vy -= y; target.vy -= y;
source.vx += x; source.vx += x;
source.vy += y; source.vy += y;
} }
} }
// Calculate velocity changes, aka force applied. // Calculate velocity changes, aka force applied.
if (stableVeloHandler!==null && stableVelocity>=0) { if (stableVeloHandler!==null && stableVelocity>=0) {
let velocityDiff = 0; let velocityDiff = 0;
for (let i = n-1, node; i>=0; i--) { for (let i = n-1, node; i>=0; i--) {
node = nodes[i]; node = nodes[i];
velocityDiff += Math.abs(Math.hypot(node.vx-node.oldvx, node.vy-node.oldvy)); velocityDiff += Math.abs(Math.hypot(node.vx-node.oldvx, node.vy-node.oldvy));
} }
velocityDiff /= n; velocityDiff /= n;
latestVelocityDiff = velocityDiff; latestVelocityDiff = velocityDiff;
if(velocityDiff<stableVelocity){ if(velocityDiff<stableVelocity){
stableVeloHandler(); stableVeloHandler();
} }
} }
} }
function initialize() { function initialize() {
if (!nodes) return; if (!nodes) return;
// 0.5 to divide the force to two part for source and target node // 0.5 to divide the force to two part for source and target node
dataSizeFactor = 0.5/(nodes.length-1); dataSizeFactor = 0.5/(nodes.length-1);
initializeDistance(); initializeDistance();
} }
function initializeDistance() { function initializeDistance() {
if (!nodes) return; if (!nodes) return;
for (let i = 1, n = nodes.length; i < n; i++) { for (let i = 1, n = nodes.length; i < n; i++) {
for (let j = 0; j < i; j++) { for (let j = 0; j < i; j++) {
distances.push(distance(nodes[i], nodes[j])); distances.push(distance(nodes[i], nodes[j]));
} }
} }
} }
force.initialize = function(_) { force.initialize = function(_) {
nodes = _; nodes = _;
initialize(); initialize();
}; };
force.iterations = function(_) { force.iterations = function(_) {
return arguments.length ? (iterations = +_, force) : iterations; return arguments.length ? (iterations = +_, force) : iterations;
}; };
force.distance = function(_) { force.distance = function(_) {
return arguments.length ? (distance = typeof _ === "function" ? _ : constant(+_), initializeDistance(), force) : distance; return arguments.length ? (distance = typeof _ === "function" ? _ : constant(+_), initializeDistance(), force) : distance;
}; };
force.latestAccel = function () { force.latestAccel = function () {
return latestVelocityDiff; return latestVelocityDiff;
}; };
force.onStableVelo = function (_) { force.onStableVelo = function (_) {
return arguments.length ? (stableVeloHandler = _, force) : stableVeloHandler; return arguments.length ? (stableVeloHandler = _, force) : stableVeloHandler;
}; };
force.stableVelocity = function (_) { force.stableVelocity = function (_) {
return arguments.length ? (stableVelocity = _, force) : stableVelocity; return arguments.length ? (stableVelocity = _, force) : stableVelocity;
}; };
return force; return force;
} }

View File

@@ -21,7 +21,7 @@ export default function () {
dataSizeFactor, dataSizeFactor,
latestVelocityDiff = 0; latestVelocityDiff = 0;
/** /**
* Apply spring forces at each simulation iteration. * Apply spring forces at each simulation iteration.
* @param {number} alpha - multiplier for amount of force applied * @param {number} alpha - multiplier for amount of force applied
*/ */

View File

@@ -1,375 +1,377 @@
/* eslint-disable block-scoped-var */ /* eslint-disable block-scoped-var */
import constant from "./constant"; import constant from "./constant";
/** /**
* Set the node id accessor to the specified i. * Set the node id accessor to the specified i.
* @param {node} d - node. * @param {node} d - node.
* @param {accessor} i - id accessor. * @param {accessor} i - id accessor.
* @return {accessor} - node id accessor. * @return {accessor} - node id accessor.
*/ */
function index(d, i) { function index(d, i) {
return i; return i;
} }
/** /**
* t-SNE implementation in D3 by using the code existing in tsnejs * t-SNE implementation in D3 by using the code existing in tsnejs
* (https://github.com/karpathy/tsnejs) to compute the solution. * (https://github.com/karpathy/tsnejs) to compute the solution.
*/ */
export default function() { export default function() {
var id = index, var id = index,
distance = constant(300), distance = constant(300),
nodes, nodes,
perplexity = 30, perplexity = 30,
learningRate = 10, learningRate = 10,
iteration = 0, iteration = 0,
dim = 2, dim = 2,
N, // length of the nodes. N, // length of the nodes.
P, // probability matrix. P, // probability matrix.
Y, // solution. Y, // solution.
gains, gains,
ystep; ystep;
/** /**
* Make a step in t-SNE algorithm and set the velocities for the nodes * Make a step in t-SNE algorithm and set the velocities for the nodes
* to accumulate the values from solution. * to accumulate the values from solution.
*/ */
function force() { function force() {
// Make a step at each iteration. // Make a step at each iteration.
step(); step();
var solution = getSolution(); var solution = getSolution();
// Set the velocity for each node using the solution. // Set the velocity for each node using the solution.
for (var i = 0; i < nodes.length; i++) { for (var i = 0; i < nodes.length; i++) {
nodes[i].vx += solution[i][0]; nodes[i].vx += solution[i][0];
nodes[i].vy += solution[i][1]; nodes[i].vy += solution[i][1];
} }
} }
/** /**
* Calculates the random number from Gaussian distribution. * Calculates the random number from Gaussian distribution.
* @return {number} random number. * @return {number} random number.
*/ */
function gaussRandom() { function gaussRandom() {
let u = 2 * Math.random() - 1; let u = 2 * Math.random() - 1;
let v = 2 * Math.random() - 1; let v = 2 * Math.random() - 1;
let r = u * u + v * v; let r = u * u + v * v;
if (r == 0 || r > 1) {return gaussRandom();} if (r == 0 || r > 1) {
return u * Math.sqrt(-2 * Math.log(r) / r); return gaussRandom();
} }
return u * Math.sqrt(-2 * Math.log(r) / r);
/** }
* Return the normalized number.
* @return {number} normalized random number from Gaussian distribution. /**
*/ * Return the normalized number.
function randomN() { * @return {number} normalized random number from Gaussian distribution.
return gaussRandom() * 1e-4; */
} function randomN() {
return gaussRandom() * 1e-4;
function sign(x) { }
return x > 0 ? 1 : x < 0 ? -1 : 0;
} function sign(x) {
return x > 0 ? 1 : x < 0 ? -1 : 0;
/** }
* Create an array of length n filled with zeros.
* @param {number} n - length of array. /**
* @return {Float64Array} - array of zeros with length n. * Create an array of length n filled with zeros.
*/ * @param {number} n - length of array.
function zeros(n) { * @return {Float64Array} - array of zeros with length n.
if (typeof n === 'undefined' || isNaN(n)) { */
return []; function zeros(n) {
} if (typeof n === 'undefined' || isNaN(n)) {
return new Float64Array(n); // typed arrays are faster return [];
} }
return new Float64Array(n); // typed arrays are faster
// Returns a 2d array of random numbers }
/**
* Creates a 2d array filled with random numbers. // Returns a 2d array of random numbers
* @param {number} n - rows. /**
* @param {number} d - columns. * Creates a 2d array filled with random numbers.
* @return {array} - 2d array * @param {number} n - rows.
*/ * @param {number} d - columns.
function random2d(n, d) { * @return {array} - 2d array
var x = []; */
for (var i = 0; i < n; i++) { function random2d(n, d) {
var y = []; var x = [];
for (var j = 0; j < d; j++) { for (var i = 0; i < n; i++) {
y.push(randomN()); var y = [];
} for (var j = 0; j < d; j++) {
x.push(y); y.push(randomN());
} }
return x; x.push(y);
} }
return x;
/** }
* Compute the probability matrix using the provided data.
* @param {array} data - nodes. /**
* @param {number} perplexity - used to calculate entropy of distribution. * Compute the probability matrix using the provided data.
* @param {number} tol - limit for entropy difference. * @param {array} data - nodes.
* @return {2d array} - 2d matrix containing probabilities. * @param {number} perplexity - used to calculate entropy of distribution.
*/ * @param {number} tol - limit for entropy difference.
function d2p(data, perplexity, tol) { * @return {2d array} - 2d matrix containing probabilities.
N = Math.floor(data.length); */
var Htarget = Math.log(perplexity); // target entropy of distribution. function d2p(data, perplexity, tol) {
var P1 = zeros(N * N); // temporary probability matrix. N = Math.floor(data.length);
var Htarget = Math.log(perplexity); // target entropy of distribution.
var prow = zeros(N); // a temporary storage compartment. var P1 = zeros(N * N); // temporary probability matrix.
for (var i = 0; i < N; i++) {
var betamin = -Infinity; var prow = zeros(N); // a temporary storage compartment.
var betamax = Infinity; for (var i = 0; i < N; i++) {
var beta = 1; // initial value of precision. var betamin = -Infinity;
var done = false; var betamax = Infinity;
var maxtries = 50; var beta = 1; // initial value of precision.
var done = false;
// Perform binary search to find a suitable precision beta var maxtries = 50;
// so that the entropy of the distribution is appropriate.
var num = 0; // Perform binary search to find a suitable precision beta
while (!done) { // so that the entropy of the distribution is appropriate.
// Compute entropy and kernel row with beta precision. var num = 0;
var psum = 0.0; while (!done) {
for (var j = 0; j < N; j++) { // Compute entropy and kernel row with beta precision.
var pj = Math.exp(-distance(data[i], data[j]) * beta); var psum = 0.0;
// Ignore the diagonals for (var j = 0; j < N; j++) {
if (i === j) { var pj = Math.exp(-distance(data[i], data[j]) * beta);
pj = 0; // Ignore the diagonals
} if (i === j) {
prow[j] = pj; pj = 0;
psum += pj; }
} prow[j] = pj;
// Normalize p and compute entropy. psum += pj;
var Hhere = 0.0; }
for (j = 0; j < N; j++) { // Normalize p and compute entropy.
if (psum == 0) { var Hhere = 0.0;
pj = 0; for (j = 0; j < N; j++) {
} else { if (psum == 0) {
pj = prow[j] / psum; pj = 0;
} } else {
prow[j] = pj; pj = prow[j] / psum;
if (pj > 1e-7) { }
Hhere -= pj * Math.log(pj); prow[j] = pj;
} if (pj > 1e-7) {
} Hhere -= pj * Math.log(pj);
}
// Adjust beta based on result. }
if (Hhere > Htarget) {
// Entropy was too high (distribution too diffuse) // Adjust beta based on result.
// so we need to increase the precision for more peaky distribution. if (Hhere > Htarget) {
betamin = beta; // move up the bounds. // Entropy was too high (distribution too diffuse)
if (betamax === Infinity) { // so we need to increase the precision for more peaky distribution.
beta = beta * 2; betamin = beta; // move up the bounds.
} else { if (betamax === Infinity) {
beta = (beta + betamax) / 2; beta = beta * 2;
} } else {
beta = (beta + betamax) / 2;
} else { }
// Converse case. Make distrubtion less peaky.
betamax = beta; } else {
if (betamin === -Infinity) { // Converse case. Make distrubtion less peaky.
beta = beta / 2; betamax = beta;
} else { if (betamin === -Infinity) {
beta = (beta + betamin) / 2; beta = beta / 2;
} } else {
} beta = (beta + betamin) / 2;
}
// Stopping conditions: too many tries or got a good precision. }
num++;
if (Math.abs(Hhere - Htarget) < tol || num >= maxtries) { // Stopping conditions: too many tries or got a good precision.
done = true; num++;
} if (Math.abs(Hhere - Htarget) < tol || num >= maxtries) {
} done = true;
}
// Copy over the final prow to P1 at row i }
for (j = 0; j < N; j++) {
P1[i * N + j] = prow[j]; // Copy over the final prow to P1 at row i
} for (j = 0; j < N; j++) {
P1[i * N + j] = prow[j];
} }
// Symmetrize P and normalize it to sum to 1 over all ij }
var Pout = zeros(N * N);
var N2 = N * 2; // Symmetrize P and normalize it to sum to 1 over all ij
for (i = 0; i < N; i++) { var Pout = zeros(N * N);
for (j = 0; j < N; j++) { var N2 = N * 2;
Pout[i * N + j] = Math.max((P1[i * N + j] + P1[j * N + i]) / N2, 1e-100); for (i = 0; i < N; i++) {
} for (j = 0; j < N; j++) {
} Pout[i * N + j] = Math.max((P1[i * N + j] + P1[j * N + i]) / N2, 1e-100);
return Pout; }
} }
return Pout;
/** }
* Initialize a starting (random) solution.
*/ /**
function initSolution() { * Initialize a starting (random) solution.
Y = random2d(N, dim); */
// Step gains to accelerate progress in unchanging directions. function initSolution() {
gains = random2d(N, dim, 1.0); Y = random2d(N, dim);
// Momentum accumulator. // Step gains to accelerate progress in unchanging directions.
ystep = random2d(N, dim, 0.0); gains = random2d(N, dim, 1.0);
iteration = 0; // Momentum accumulator.
} ystep = random2d(N, dim, 0.0);
iteration = 0;
/** }
* @return {2d array} the solution.
*/ /**
function getSolution() { * @return {2d array} the solution.
return Y; */
} function getSolution() {
return Y;
/** }
* Do a single step (iteration) for the layout.
* @return {number} the current cost. /**
*/ * Do a single step (iteration) for the layout.
function step() { * @return {number} the current cost.
iteration += 1; */
function step() {
var cg = costGrad(Y); // Evaluate gradient. iteration += 1;
var cost = cg.cost;
var grad = cg.grad; var cg = costGrad(Y); // Evaluate gradient.
var cost = cg.cost;
// Perform gradient step. var grad = cg.grad;
var ymean = zeros(dim);
for (var i = 0; i < N; i++) { // Perform gradient step.
for (var d = 0; d < dim; d++) { var ymean = zeros(dim);
var gid = grad[i][d]; for (var i = 0; i < N; i++) {
var sid = ystep[i][d]; for (var d = 0; d < dim; d++) {
var gainid = gains[i][d]; var gid = grad[i][d];
var sid = ystep[i][d];
// Compute gain update. var gainid = gains[i][d];
var newgain = sign(gid) === sign(sid) ? gainid * 0.8 : gainid + 0.2;
if (newgain < 0.01) { // Compute gain update.
newgain = 0.01; var newgain = sign(gid) === sign(sid) ? gainid * 0.8 : gainid + 0.2;
} if (newgain < 0.01) {
gains[i][d] = newgain; newgain = 0.01;
}
// Compute momentum step direction. gains[i][d] = newgain;
var momval = iteration < 250 ? 0.5 : 0.8;
var newsid = momval * sid - learningRate * newgain * grad[i][d]; // Compute momentum step direction.
ystep[i][d] = newsid; var momval = iteration < 250 ? 0.5 : 0.8;
var newsid = momval * sid - learningRate * newgain * grad[i][d];
// Do the step. ystep[i][d] = newsid;
Y[i][d] += newsid;
// Do the step.
// Accumulate mean so that we can center later. Y[i][d] += newsid;
ymean[d] += Y[i][d];
} // Accumulate mean so that we can center later.
} ymean[d] += Y[i][d];
}
// Reproject Y to have the zero mean. }
for (i = 0; i < N; i++) {
for (d = 0; d < dim; d++) { // Reproject Y to have the zero mean.
Y[i][d] -= ymean[d] / N; for (i = 0; i < N; i++) {
} for (d = 0; d < dim; d++) {
} Y[i][d] -= ymean[d] / N;
return cost; }
} }
return cost;
/** }
* Calculate the cost and the gradient.
* @param {2d array} Y - the current solution to evaluate. /**
* @return {object} that contains a cost and a gradient. * Calculate the cost and the gradient.
*/ * @param {2d array} Y - the current solution to evaluate.
function costGrad(Y) { * @return {object} that contains a cost and a gradient.
*/
var pmul = iteration < 100 ? 4 : 1; function costGrad(Y) {
// Compute current Q distribution, unnormalized first. var pmul = iteration < 100 ? 4 : 1;
var Qu = zeros(N * N);
var qsum = 0.0; // Compute current Q distribution, unnormalized first.
for (var i = 0; i < N; i++) { var Qu = zeros(N * N);
for (var j = i + 1; j < N; j++) { var qsum = 0.0;
var dsum = 0.0; for (var i = 0; i < N; i++) {
for (var d = 0; d < dim; d++) { for (var j = i + 1; j < N; j++) {
var dhere = Y[i][d] - Y[j][d]; var dsum = 0.0;
dsum += dhere * dhere; for (var d = 0; d < dim; d++) {
} var dhere = Y[i][d] - Y[j][d];
var qu = 1.0 / (1.0 + dsum); // Student t-distribution. dsum += dhere * dhere;
Qu[i * N + j] = qu; }
Qu[j * N + i] = qu; var qu = 1.0 / (1.0 + dsum); // Student t-distribution.
qsum += 2 * qu; Qu[i * N + j] = qu;
} Qu[j * N + i] = qu;
} qsum += 2 * qu;
// Normalize Q distribution to sum to 1. }
var NN = N * N; }
var Q = zeros(NN); // Normalize Q distribution to sum to 1.
for (var q = 0; q < NN; q++) { var NN = N * N;
Q[q] = Math.max(Qu[q] / qsum, 1e-100); var Q = zeros(NN);
} for (var q = 0; q < NN; q++) {
Q[q] = Math.max(Qu[q] / qsum, 1e-100);
var cost = 0.0; }
var grad = [];
for (i = 0; i < N; i++) { var cost = 0.0;
var gsum = new Array(dim); // Initialize gradiet for point i. var grad = [];
for (d = 0; d < dim; d++) { for (i = 0; i < N; i++) {
gsum[d] = 0.0; var gsum = new Array(dim); // Initialize gradiet for point i.
} for (d = 0; d < dim; d++) {
for (j = 0; j < N; j++) { gsum[d] = 0.0;
// Accumulate the cost. }
cost += -P[i * N + j] * Math.log(Q[i * N + j]); for (j = 0; j < N; j++) {
var premult = 4 * (pmul * P[i * N + j] - Q[i * N + j]) * Qu[i * N + j]; // Accumulate the cost.
for (d = 0; d < dim; d++) { cost += -P[i * N + j] * Math.log(Q[i * N + j]);
gsum[d] += premult * (Y[i][d] - Y[j][d]); var premult = 4 * (pmul * P[i * N + j] - Q[i * N + j]) * Qu[i * N + j];
} for (d = 0; d < dim; d++) {
} gsum[d] += premult * (Y[i][d] - Y[j][d]);
grad.push(gsum); }
} }
grad.push(gsum);
return { }
cost: cost,
grad: grad return {
}; cost: cost,
} grad: grad
};
/** }
* Calculates the stress. Basically, it computes the difference between
* high dimensional distance and real distance. The lower the stress is, /**
* the better layout. * Calculates the stress. Basically, it computes the difference between
* @return {number} - stress of the layout. * high dimensional distance and real distance. The lower the stress is,
*/ * the better layout.
function getStress() { * @return {number} - stress of the layout.
var totalDiffSq = 0, */
totalHighDistSq = 0; function getStress() {
for (var i = 0, source, target, realDist, highDist; i < nodes.length; i++) { var totalDiffSq = 0,
for (var j = 0; j < nodes.length; j++) { totalHighDistSq = 0;
if (i !== j) { for (var i = 0, source, target, realDist, highDist; i < nodes.length; i++) {
source = nodes[i], target = nodes[j]; for (var j = 0; j < nodes.length; j++) {
realDist = Math.hypot(target.x - source.x, target.y - source.y); if (i !== j) {
highDist = +distance(nodes[i], nodes[j]); source = nodes[i], target = nodes[j];
totalDiffSq += Math.pow(realDist - highDist, 2); realDist = Math.hypot(target.x - source.x, target.y - source.y);
totalHighDistSq += highDist * highDist; highDist = +distance(nodes[i], nodes[j]);
} totalDiffSq += Math.pow(realDist - highDist, 2);
} totalHighDistSq += highDist * highDist;
} }
return Math.sqrt(totalDiffSq / totalHighDistSq); }
} }
return Math.sqrt(totalDiffSq / totalHighDistSq);
// API for initializing the algorithm, setting parameters and querying }
// metrics.
force.initialize = function(_) { // API for initializing the algorithm, setting parameters and querying
nodes = _; // metrics.
N = nodes.length; force.initialize = function(_) {
// Initialize the probability matrix. nodes = _;
P = d2p(nodes, perplexity, 1e-4); N = nodes.length;
initSolution(); // Initialize the probability matrix.
}; P = d2p(nodes, perplexity, 1e-4);
initSolution();
force.id = function(_) { };
return arguments.length ? (id = _, force) : id;
}; force.id = function(_) {
return arguments.length ? (id = _, force) : id;
force.distance = function(_) { };
return arguments.length ? (distance = typeof _ === "function" ? _ : constant(+_), force) : distance;
}; force.distance = function(_) {
return arguments.length ? (distance = typeof _ === "function" ? _ : constant(+_), force) : distance;
force.stress = function() { };
return getStress();
}; force.stress = function() {
return getStress();
force.learningRate = function(_) { };
return arguments.length ? (learningRate = +_, force) : learningRate;
}; force.learningRate = function(_) {
return arguments.length ? (learningRate = +_, force) : learningRate;
force.perplexity = function(_) { };
return arguments.length ? (perplexity = +_, force) : perplexity;
}; force.perplexity = function(_) {
return arguments.length ? (perplexity = +_, force) : perplexity;
return force; };
}
return force;
}

View File

@@ -1,13 +1,13 @@
/** /**
* @return x value of a node * @return x value of a node
*/ */
export function x(d) { export function x(d) {
return d.x; return d.x;
} }
/** /**
* @return y value of a node * @return y value of a node
*/ */
export function y(d) { export function y(d) {
return d.y; return d.y;
} }