Init from given files

This commit is contained in:
Pitchaya Boonsarngsuk
2017-11-07 21:33:16 +00:00
commit 61f2be55fe
45 changed files with 34460 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
.DS_Store
build/
node_modules
npm-debug.log

2
.npmignore Normal file
View File

@@ -0,0 +1,2 @@
build/*.zip
test/

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2017 Remigijus Bartasius, Matthew Chalmers
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

67
README.md Normal file
View File

@@ -0,0 +1,67 @@
# d3-neighbour-sampling
This module implements Chalmers' 1996 Neighbour and Sampling algorithm for drawing the force-directed layouts. It is a linear time algorithm that uses stochastic sampling to find the best neighbours for high-dimensional data and creates the layout in 2 dimensions.
Neighbour and Sampling algorithm is useful for producing visualizations that show relationships between the data. For instance:
![fsmvis data set](https://raw.githubusercontent.com/sReeper/d3-neighbour-sampling/master/img/chalmers-fsmvis.PNG)![Poker Hands data set](https://raw.githubusercontent.com/sReeper/d3-neighbour-sampling/master/img/chalmers-poker.PNG)
### Authors
Remigijus Bartasius and Matthew Chalmers
### Reference
- Chalmers, Matthew. ["A linear iteration time layout algorithm for visualising high-dimensional data."](http://dl.acm.org/citation.cfm?id=245035) Proceedings of the 7th conference on Visualization'96. IEEE Computer Society Press, 1996.
## Installing
Download the [latest release](https://github.com/sReeper/d3-neighbour-sampling/releases/latest).
## API Reference
#### NeighbourSampling
The Neighbour and Sampling algorithm tries to group the nodes based on the distance between them. If the nodes have a low distance, then the force attracts them to each other. If the nodes have a high distance, then the repulsive force pushes them further apart from each other.
In order for it to work properly, a distance function should be specified.
<a name="forceNeighbourSampling" href="#forceNeighbourSampling">#</a> d3.<b>forceNeighbourSampling</b>() [<>](https://github.com/sReeper/d3-neighbour-sampling/blob/master/src/neighbourSampling.js "Source")
Initializes the Neighbour and Sampling algorithm with default parameters.
<a name="neighbourSampling_id" href="#neighbourSampling_id">#</a> <i>neighbourSampling</i>.<b>id</b>([<i>id</i>]) [<>](https://github.com/sReeper/d3-neighbour-sampling/blob/master/src/neighbourSampling.js#L218 "Source")
If *id* is specified, sets the node id accessor to the specified function and returns this force. If *id* is not specified, returns the current node id accessor, which defaults to the numeric *node*.index:
```js
function id(d) {
return d.index;
}
```
The id accessor is invoked for each node whenever the force is initialized, as when the nodes change, being passed the node and its zero-based index.
<a name="neighbourSampling_distance" href="#neighbourSampling_distance">#</a> <i>neighbourSampling</i>.<b>distance</b>([<i>distance</i>]) [<>](https://github.com/sReeper/d3-neighbour-sampling/blob/master/src/neighbourSampling.js#L230 "Source")
If *distance* is specified, sets the distance accessor to the specified number or function, re-evaluates the distance accessor for each link, and returns this force. If *distance* is not specified, returns the current distance accessor, which defaults to:
```js
function distance() {
return 300;
}
```
<a name="neighbourSampling_neighbourSize" href="#neighbourSampling_neighbourSize">#</a> <i>neighbourSampling</i>.<b>neighbourSize</b>([<i>neighbourSize</i>]) [<>](https://github.com/sReeper/d3-neighbour-sampling/blob/master/src/neighbourSampling.js#L222 "Source")
If *neighbourSize* is specified, sets the neighbour set size to the specified number and returns this force. If *neighbourSize* is not specified, returns the current value, which defaults to 6.
<a name="neighbourSampling_sampleSize" href="#neighbourSampling_sampleSize">#</a> <i>neighbourSampling</i>.<b>sampleSize</b>([<i>sampleSize</i>]) [<>](https://github.com/sReeper/d3-neighbour-sampling/blob/master/src/neighbourSampling.js#L226 "Source")
If *sampleSize* is specified, sets the sample set size to the specified number and returns this force. If *sampleSize* is not specified, returns the current value, which defaults to 3.
<a name="neighbourSampling_stress" href="#neighbourSampling_stress">#</a> <i>neighbourSampling</i>.<b>stress</b>() [<>](https://github.com/sReeper/d3-neighbour-sampling/blob/master/src/neighbourSampling.js#L234 "Source")
Returns the stress of the layout.
<a name="neighbourSampling_velocity" href="#neighbourSampling_velocity">#</a> <i>neighbourSampling</i>.<b>velocity</b>() [<>](https://github.com/sReeper/d3-neighbour-sampling/blob/master/src/neighbourSampling.js#L238 "Source")
Returns the average velocity of the iteration.

View File

@@ -0,0 +1,291 @@
<!DOCTYPE html>
<html>
<meta charset="utf-8">
<style>
h1 {
text-align: center;
margin: 10px 0 10px 0;
}
.links line {
stroke: #999;
stroke-opacity: 0.6;
}
/*.nodes circle {
stroke: #fff;
stroke-width: 1.5px;
}*/
circle:hover {
fill: #8B0000;
opacity: 0.8;
}
.notSelected {
fill: #999;
opacity: 0.8;
}
.highlighted {
fill: black;
}
#svg {
border-style: solid;
margin: auto;
}
.left {
float: left;
width: 32%;
}
.controls {
flex-basis: 200px;
padding: 0 5px;
}
.controls input[type="range"] {
margin: 0 5% 0.5em 5%;
width: 90%;
}
.multiplier {
margin: 10px 0 10px 0;
}
.parameters {
margin: 10px 0 10px 30px;
}
.checkbox {
margin: 10px 0 10px 0;
}
.start {
text-align: center;
}
div.tooltip {
position: absolute;
text-align: center;
padding: 5px;
font: 12px sans-serif;
background: lightsteelblue;
border: 0px;
border-radius: 8px;
}
</style>
<body>
<h1>Evaluation</h1>
<svg id="svg" width="100%" height="600">
<div class="left controls">
<div class="input">
<input type="file" id="csv-file" name="files">
</div>
<div class="multiplier">
<label title="The size of the nodes">
Node size
<output id="nodeSizeSliderOutput">10</output>
<input type="range" min="5" max="200" value="10" step="5" oninput="d3.select('#nodeSizeSliderOutput').text(value); NODE_SIZE=value; d3.selectAll('circle').attr('r', value)">
</label>
<br/>
<label title="The number that distance is multiplied by in order to improve the visibility of the graph">
distanceMultiplier
<output id="distanceMultiplierSliderOutput">50</output>
<input type="range" min="5" max="1000" value="50" step="5" oninput="d3.select('#distanceMultiplierSliderOutput').text(value); MULTIPLIER=value;">
</label>
<br/>
<label title="Number of iterations before the simulation is stopped">
Iterations
<output id="iterationsSliderOutput">300</output>
<input type="range" min="5" max="5000" value="300" step="5" oninput="d3.select('#iterationsSliderOutput').text(value); ITERATIONS=value;">
</label>
<br/>
<label title="Attribute used for coloring nodes">
Color attribute
<select id="color_attr" onchange="COLOR_ATTRIBUTE=value; colorToAttribute();">
</select>
</label>
</div>
<div class="checkbox">
<label title="js/">
Rendering
<input type="checkbox" checked onclick="rendering=!rendering;">
</label>
</div>
<div class="start">
<button id="startSimulation" form="algorithmForm">Start</button>
<button id="pauseButton" onclick="pauseUnPause()">Pause</button>
<button onclick="stopSimulation()">Stop</button>
</div>
</div>
<div class="left">
<p>Select algorithm:</p>
<div id="algorithms">
<input id="HLButton" type="radio" name="algorithm" onclick="d3.select('#startSimulation').on('click', startHybridSimulation)">Hybrid
layout
<br>
<div id="HLParameters" class="parameters" style="display:none">
<div id="pivots">
<input id="BFButton" type="radio" name="pivots" checked="checked" onclick="PIVOTS=false;">Brute-force<br>
<input id="PButton" type="radio" name="pivots" onclick="PIVOTS=true;">Pivots<br>
<div id="numPivots" class="parameters" style="display:none">
<label title="The number of pivots">
Number of Pivots
<output id="numPivotsSliderOutput">3</output><br/>
<input type="range" min="1" max="50" value="3" step="1" oninput="d3.select('#numPivotsSliderOutput').text(value); NUM_PIVOTS=value;">
</label>
</div>
</div>
<br/>
<label title="Number of iterations done at the end">
Full iterations
<output id="fullIterationsSliderOutput">20</output><br/>
<input type="range" min="1" max="100" value="20" step="1" oninput="d3.select('#fullIterationsSliderOutput').text(value); FULL_ITERATIONS=value;">
</label>
<br/>
<label title="NeighbourSize">
Neighbour Set
<output id="hlneighbourSizeSliderOutput">6</output><br/>
<input type="range" min="1" max="100" value="6" step="1" oninput="d3.select('#hlneighbourSizeSliderOutput').text(value); NEIGHBOUR_SIZE=value;">
</label>
<br/>
<label title="SampleSize">
Sample Set
<output id="hlsampleSizeSliderOutput">3</output><br/>
<input type="range" min="1" max="100" value="3" step="1" oninput="d3.select('#hlsampleSizeSliderOutput').text(value); SAMPLE_SIZE=value;">
</label>
</div>
<input id="NSButton" type="radio" name="algorithm" onclick="d3.select('#startSimulation').on('click', startNeighbourSamplingSimulation)">Neighbour
and Sampling<br>
<div id="NSParameters" class="parameters" style="display:none">
<label title="NeighbourSize">
Neighbour Set
<output id="neighbourSizeSliderOutput">6</output><br/>
<input type="range" min="1" max="100" value="6" step="1" oninput="d3.select('#neighbourSizeSliderOutput').text(value); NEIGHBOUR_SIZE=value;">
</label>
<br/>
<label title="SampleSize">
Sample Set
<output id="sampleSizeSliderOutput">3</output><br/>
<input type="range" min="1" max="100" value="3" step="1" oninput="d3.select('#sampleSizeSliderOutput').text(value); SAMPLE_SIZE=value;">
</label>
<br/>
<label title="DistanceRange">
Distance Range
<output id="distanceRangeSliderOutput">10</output><br/>
<input type="range" min="1" max="100" value="10" step="1" oninput="d3.select('#distanceRangeSliderOutput').text(value); SELECTED_DISTANCE=value;">
</label>
</div>
<input class="noParameters" type="radio" name="algorithm" onclick="d3.select('#startSimulation').on('click', startLinkSimulation)">Link
force in D3<br>
<input class="noParameters" type="radio" name="algorithm" onclick="d3.select('#startSimulation').on('click', startBarnesHutSimulation)">Barnes-Hut<br>
<input id="tSNEButton" type="radio" name="algorithm" onclick="d3.select('#startSimulation').on('click', starttSNE)">t-SNE<br>
<div id="tSNEParameters" class="parameters" style="display:none">
<label title="Perplexity">
Perplexity
<output id="perplexitySliderOutput">30</output><br/>
<input type="range" min="1" max="500" value="30" step="1" oninput="d3.select('#perplexitySliderOutput').text(value); PERPLEXITY=value;">
</label>
<br/>
<label title="LearningRate">
Learning Rate
<output id="learningRateSliderOutput">10</output><br/>
<input type="range" min="1" max="500" value="10" step="1" oninput="d3.select('#learningRateSliderOutput').text(value); LEARNING_RATE=value;">
</label>
</div>
</div>
</div>
<div class="left">
<p>Select distance function:</p>
<div id="distance">
<input type="radio" name="distance" onclick="distanceFunction=calculateDistance"> General<br>
<input type="radio" name="distance" onclick="distanceFunction=calculateEuclideanDistance"> Euclidean<br>
<input type="radio" name="distance" onclick="distanceFunction=calculateManhattanDistance"> Manhattan<br>
<input type="radio" name="distance" onclick="distanceFunction=calculateJaccardDissimilarity"> Jaccard<br>
<input type="radio" name="distance" onclick="distanceFunction=calculateDiceDissimilarity"> Dice<br>
<input type="radio" name="distance" onclick="distanceFunction=calculateCosineSimilarity"> Cosine<br>
<input type="radio" name="distance" onclick="distanceFunction=calculateDistancePoker"> Poker Hands<br>
</div>
</div>
</svg>
</body>
<!-- Load the files and libraries used. -->
<script src="js/lib/d3.v4.min.js"></script>
<script src="js/lib/papaparse.js"></script>
<script src="js/lib/jquery-3.1.1.js"></script>
<script src="js/lib/intercom.js"></script>
<script src="../build/d3-neighbour-sampling.js"></script>
<script src="js/src/neighbourSampling-papaparsing.js"></script>
<script src="js/distances/distancePokerHands.js"></script>
<script src="js/distances/distance.js"></script>
<script src="js/distances/euclideanDistance.js"></script>
<script src="js/distances/euclideanDistanceInTSNE.js"></script>
<script src="js/distances/manhattanDistance.js"></script>
<script src="js/distances/jaccardDissimilarity.js"></script>
<script src="js/distances/diceDissimilarity.js"></script>
<script src="js/distances/cosineSimilarity.js"></script>
<script src="js/distances/normalization.js"></script>
<script src="js/distances/numeric.js"></script>
<script>
$(document).ready(function () {
$("#csv-file").change(function (d) {
parseFile(d);
$("#color_attr option").remove();
$("#pauseButton").text("Pause");
paused = false;
});
$("#startSimulation").click(function () {
$("#pauseButton").text("Pause");
paused = false;
});
$("#tSNEButton").click(function () {
$(".parameters").hide();
$("#tSNEParameters").show();
});
$("#NSButton").click(function () {
$(".parameters").hide();
$("#NSParameters").show();
});
$("#HLButton").click(function () {
$(".parameters").hide();
$("#HLParameters").show();
if ($("#PButton").is(":checked")) {
$("#numPivots").show();
}
});
$("#BFButton").click(function () {
$("#numPivots").hide();
});
$("#PButton").click(function () {
$("#numPivots").show();
});
$(".noParameters").click(function () {
$(".parameters").hide();
});
});
</script>
</html>

View File

@@ -0,0 +1,110 @@
<!DOCTYPE html>
<meta charset="utf-8">
<style>
/* set the CSS */
/* .line {
fill: none;
stroke: steelblue;
stroke-width: 2px;
} */
.chart rect {
fill: steelblue;
stroke: none;
}
text {
font-size: 12px;
}
</style>
<body>
<!-- <svg width="960" height="600"></svg> -->
<!-- load the d3.js library -->
<svg class="chart"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="js/lib/intercom.js"></script>
<script>
var data,
xDomain,
yDomain;
var intercom = Intercom.getInstance();
intercom.on("passedData", function (d) {
data = [];
var tmp = d.distribution;
for (var i = 0; i < d.l; i++) {
data.push({"index": i, "size": tmp[i]});
}
xDomain = d.l;
yDomain = d.maxSize;
showData(data);
});
// set the dimensions and margins of the graph
var margin = { top: 20, right: 20, bottom: 90, left: 70 },
width = 1500 - margin.left - margin.right,
height = 600 - margin.top - margin.bottom;
// append the svg obgect to the body of the page
// appends a 'group' element to 'svg'
// moves the 'group' element to the top left margin
var chart = d3.select(".chart")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
function showData(data) {
d3.selectAll(".bar").remove();
d3.selectAll(".xAxis").remove();
d3.selectAll(".yAxis").remove();
// set the ranges
var x = d3.scaleLinear().range([0, width]).domain([0, xDomain]);
var y = d3.scaleLinear().range([height, 0]).domain([0, yDomain]);
var barWidth = width / data.length;
// Add the X Axis
chart.append("g")
.attr("class", "xAxis")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
// Add the Y Axis
chart.append("g")
.attr("class", "yAxis")
.call(d3.axisLeft(y));
chart.selectAll(".bar")
.data(data)
.enter().append("rect")
.attr("class", "bar")
.attr("x", function (d) { return x(d.index); })
.attr("y", function (d) { return y(d.size); })
.attr("height", function (d) { return height - y(d.size); })
.attr("width", barWidth);
}
// Add x axis label
chart.append("text")
.attr("transform",
"translate(" + (width / 2) + " ," +
(height + margin.top + 30) + ")")
.style("text-anchor", "middle")
.style("font-size", "22px")
.text("Points");
// Add y axis label
chart.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 0 - margin.left)
.attr("x", 0 - (height / 2))
.attr("dy", "1em")
.style("text-anchor", "middle")
.style("font-size", "22px")
.text("Number of neighbours");
</script>
</body>

View File

@@ -0,0 +1,39 @@
/**
* Calculate the distances by using the numbers, strings and dates.
* @param {node} source
* @param {node} target
* @param {array} properties - the properties of the nodes.
* @return {number} the distance between source and target nodes.
*/
function calculateCosineSimilarity(source, target, properties, normArgs) {
var numerator = 0.0;
// console.log(properties);
// Iterate through every column of data
for (var i = 0; i < properties.length; i++) {
property = properties[i];
if (property.toLowerCase() !== "class" && property.toLowerCase() !== "app" && property.toLowerCase() !== "user" && property.toLowerCase() !== "weekday") {
var s = source[property],
t = target[property];
numerator += s * t;
}
}
let denominator = squareRooted(source, properties, normArgs) * squareRooted(target, properties, normArgs);
// console.log(Math.abs(numerator / denominator));
return Math.abs(numerator / denominator);
}
function squareRooted(node, properties, normArgs) {
var sum = 0.0;
for (var i = 0, s; i < properties.length; i++) {
var s = node[properties[i]];
sum += s * s;
}
return Math.sqrt(sum);
}

View File

@@ -0,0 +1,28 @@
/**
* Calculate the distances by using the numbers, strings and dates.
* @param {node} source
* @param {node} target
* @param {array} properties - the properties of the nodes.
* @return {number} the distance between source and target nodes.
*/
function calculateDiceDissimilarity(source, target, properties, normArgs) {
var notShared = 0.0;
// console.log(properties);
// Iterate through every column of data
for (var i = 0; i < properties.length; i++) {
property = properties[i];
if (property.toLowerCase() !== "class" && property.toLowerCase() !== "app" && property.toLowerCase() !== "user" && property.toLowerCase() !== "weekday") {
var s = source[property],
t = target[property];
if (s !== t) {
notShared++;
}
}
}
// console.log(Math.sqrt(sumDiff)/cols);
// console.log(cols);
return notShared / (notShared + 2 * (properties.length - notShared));
}

View File

@@ -0,0 +1,70 @@
/**
* Calculate the distances by using the numbers, strings and dates.
* @param {node} source
* @param {node} target
* @param {array} properties - the properties of the nodes.
* @param {object} normArgs - the normalization arguments.
* @return {number} the distance between source and target nodes.
*/
function calculateDistance(source, target, properties, normArgs) {
var val1 = 0.0, val2 = 0.0,
sumDiff = 0.0,
ordDiff = 1.0,
ORD_FACTOR = 0.75,
cols = 0,
average = normArgs.avg,
sigma = normArgs.sig,
st_dev = normArgs.st_d;
// Iterate through every column of data
for (var i = 0; i < properties.length; i++) {
property = properties[i];
if (source.hasOwnProperty(property) && target.hasOwnProperty(property)
&& property.toLowerCase() !== "index" ) {
var s = source[property],
t = target[property];
// Comparing Floats and Integers
if ((isNumeric(s) && isNumeric(t))) {
val1 = parseFloat(s);
val2 = parseFloat(t);
if (sigma[i] != 0) {
val1 = (val1 - average[i]) / (st_dev[i] * sigma[i]);
val2 = (val2 - average[i]) / (st_dev[i] * sigma[i]);
}
sumDiff += (val1-val2) * (val1-val2);
cols++;
// Comparing strings
} else if (/[a-zA-Z]/.test(s) && /[a-zA-Z]/.test(t) && s === t) {
ordDiff *= ORD_FACTOR;
cols++;
} else {
// Comparing Dates
var parsedDateS = Date.parse(s);
var parsedDateT = Date.parse(t);
if (isNaN(s) && !isNaN(parsedDateS)
&& isNaN(t) && !isNaN(parsedDateT)) {
val1 = parsedDateS.valueOf(),
val2 = parsedDateT.valueOf();
if (sigma[i] !== 0) {
val1 = (val1 - average[i]) / (st_dev[i] * sigma[i]);
val2 = (val2 - average[i]) / (st_dev[i] * sigma[i]);
}
sumDiff += (val1-val2) * (val1-val2);
cols++;
}
}
}
}
sumDiff = Math.sqrt(sumDiff);
sumDiff *= ordDiff;
if (cols > 0) {
sumDiff *= properties.length/cols;
}
console.log(sumDiff);
return sumDiff;
}

View File

@@ -0,0 +1,43 @@
/**
* The distance function that is specifically made for Poker Hands data set.
* The suit of the cards does not play an important role when finding
* the differences in poker hands so it was not used in calculations.
* @param {node} source
* @param {node} target
* @return {number} the distance between source and target nodes.
*/
function calculateDistancePoker(source, target) {
var sumDiff = 0.0,
ordDiff = 1.0,
ORD_FACTOR = 1.5,
cards = ["C1", "C2", "C3", "C4", "C5"],
cols = 0;
// Iterate through cards
for (var i = 0; i < cards.length; i++) {
card = cards[i];
if (source.hasOwnProperty(card) && target.hasOwnProperty(card)) {
var s = parseInt(source[card]),
t = parseInt(target[card]);
// Calculate the squared difference.
sumDiff += (s-t) * (s-t);
}
}
// Class of poker hands describes the similarities the best
// so give it more priority than checking the differences between cards.
if (source.hasOwnProperty("CLASS") && target.hasOwnProperty("CLASS")) {
var s = parseInt(source["CLASS"]),
t = parseInt(target["CLASS"]);
// If classes differ, then scale them by a factor.
if (s !== t) {
ordDiff *= (ORD_FACTOR * (Math.abs(s-t)))
}
}
sumDiff = Math.sqrt(sumDiff);
sumDiff *= ordDiff;
return sumDiff;
}

View File

@@ -0,0 +1,32 @@
/**
* Calculate the distances by using the numbers, strings and dates.
* @param {node} source
* @param {node} target
* @param {array} properties - the properties of the nodes.
* @return {number} the distance between source and target nodes.
*/
function calculateEuclideanDistance(source, target, properties, normArgs) {
var sumDiff = 0.0;
// console.log(normArgs);
// Iterate through every column of data
for (var i = 0; i < properties.length; i++) {
property = properties[i];
if (property.toLowerCase() !== "class" && property.toLowerCase() !== "app" && property.toLowerCase() !== "user" && property.toLowerCase() !== "weekday") {
var s = source[property],
t = target[property];
if (normArgs.sig[i] !== 0) {
s = (s - normArgs.avg[i]) / (2.0 * normArgs.sig[i]);
t = (t - normArgs.avg[i]) / (2.0 * normArgs.sig[i]);
}
sumDiff += (s - t) * (s - t);
}
}
// console.log(Math.sqrt(sumDiff)/cols);
// console.log(cols);
// sumDiff = Math.sqrt(sumDiff);
// console.log(sumDiff);
return Math.sqrt(sumDiff);
}

View File

@@ -0,0 +1,33 @@
/**
* Calculate the distances by using the numbers, strings and dates.
* @param {node} source
* @param {node} target
* @param {array} properties - the properties of the nodes.
* @return {number} the distance between source and target nodes.
*/
function calculateEuclideanDistanceTSNE(source, target, properties, normArgs) {
var dotProduct = 0.0,
sumX = 0.0,
sumY = 0.0;
// console.log(normArgs);
// Iterate through every column of data
for (var i = 0; i < properties.length; i++) {
property = properties[i];
if (source.hasOwnProperty(property) && target.hasOwnProperty(property) &&
property.toLowerCase() !== "class") {
var s = source[property],
t = target[property];
dotProduct += s * t;
sumX += s * s;
sumY += t * t;
}
}
// console.log("Dot", dotProduct);
// console.log((-2 * dotProduct) + sumX + sumY);
return -2 * dotProduct + sumX + sumY;
}

View File

@@ -0,0 +1,28 @@
/**
* Calculate the distances by using the numbers, strings and dates.
* @param {node} source
* @param {node} target
* @param {array} properties - the properties of the nodes.
* @return {number} the distance between source and target nodes.
*/
function calculateJaccardDissimilarity(source, target, properties, normArgs) {
var notShared = 0.0;
// console.log(properties);
// Iterate through every column of data
for (var i = 0; i < properties.length; i++) {
property = properties[i];
if (property.toLowerCase() !== "class" && property.toLowerCase() !== "app" && property.toLowerCase() !== "user" && property.toLowerCase() !== "weekday") {
var s = source[property],
t = target[property];
if (s !== t) {
notShared++;
}
}
}
// console.log(Math.sqrt(sumDiff)/cols);
// console.log(cols);
return notShared / properties.length;
}

View File

@@ -0,0 +1,34 @@
/**
* Calculate the distances by using the numbers, strings and dates.
* @param {node} source
* @param {node} target
* @param {array} properties - the properties of the nodes.
* @return {number} the distance between source and target nodes.
*/
function calculateManhattanDistance(source, target, properties, normArgs) {
var sum = 0.0,
cols = 0;
// console.log(properties);
// Iterate through every column of data
for (var i = 0; i < properties.length; i++) {
property = properties[i];
if (property.toLowerCase() !== "class" && property.toLowerCase() !== "app" && property.toLowerCase() !== "user" && property.toLowerCase() !== "weekday") {
var s = source[property],
t = target[property];
if (s !== t) {
cols++;
}
if (normArgs.sig[i] !== 0) {
s = (s - normArgs.avg[i]) / (2.0 * normArgs.sig[i]);
t = (t - normArgs.avg[i]) / (2.0 * normArgs.sig[i]);
}
sum += Math.abs(s - t);
}
}
// console.log(Math.sqrt(sumDiff)/cols);
return sum * (cols / properties.length);
}

View File

@@ -0,0 +1,93 @@
/**
* Calculate the values that are used for normalizing the data.
* @param {array} nodes
* @return {object} that contains the normalization parameters.
*/
function calculateNormalization(nodes) {
var STANDARD_DEV = 2.0,
properties = Object.keys(nodes[0]),
sums = calculateSums(nodes, properties),
average = [],
sigma = [];
// For each property, calculate mean and sigma.
for (var i = 0; i < properties.length; i++) {
var avg = sums.sumOfVal[i] / nodes.length;
average[i] = avg;
sigma[i] = Math.sqrt((sums.sumOfSq[i] - (nodes.length * Math.pow(avg, 2))) / nodes.length);
}
return {
avg: average,
sig: sigma,
st_d: standardDevation(nodes, properties, average)
};
}
function standardDevation(nodes, properties, avg) {
var stDev = new Array(properties.length).fill(0)
for (var i = 0; i < properties.length; i++) {
var sum = 0;
nodes.forEach(function (node) {
var val = node[properties[i]];
var parsedDate = Date.parse(val);
var propAvg = avg[i];
if (isNaN(val) && !isNaN(parsedDate)) {
val = parsedDate.valueOf();
} else if (isNumeric(val)) {
val = parseFloat(val);
// Ignore the strings.
} else {
val = 0;
}
sum += Math.pow(val - propAvg, 2);
});
stDev[i] = Math.sqrt(sum/nodes.length);
}
return stDev;
}
// Calculate the sum of values and the squared sum
/**
* Calculate the sums of each property.
* @param {array} nodes
* @param {array} properties - list of properties
* @return {object} that contains arrays with sum of values
* and the squared sums.
*/
function calculateSums(nodes, properties) {
var sumOfValues = new Array(properties.length).fill(0),
sumOfSquares = new Array(properties.length).fill(0);
// Calculate the sums for each node.
nodes.forEach(function (node) {
for (var i = 0; i < properties.length; i++) {
var val = node[properties[i]];
var parsedDate = Date.parse(val);
if (isNaN(val) && !isNaN(parsedDate)) {
sumOfValues[i] += parsedDate.valueOf();
sumOfSquares[i] += Math.pow(parsedDate.valueOf(), 2);
} else if (isNumeric(val)) {
sumOfValues[i] += parseFloat(val);
sumOfSquares[i] += Math.pow(parseFloat(val), 2);
// Ignore the strings.
} else {
sumOfValues[i] += 0;
sumOfSquares[i] += 0;
}
}
});
return {
sumOfVal: sumOfValues,
sumOfSq: sumOfSquares
};
}

View File

@@ -0,0 +1,8 @@
/**
* Check if the object (string, number, etc.) contains a number.
* @param {object} n - object to check.
* @return {Boolean} true, if it is a number, false otherwise.
*/
function isNumeric(n) {
return !isNaN(parseFloat(n)) && isFinite(n);
}

16866
examples/js/lib/d3.v4.js vendored Normal file

File diff suppressed because it is too large Load Diff

8
examples/js/lib/d3.v4.min.js vendored Normal file

File diff suppressed because one or more lines are too long

413
examples/js/lib/intercom.js Normal file
View File

@@ -0,0 +1,413 @@
/*! intercom.js | https://github.com/diy/intercom.js | Apache License (v2) */
var Intercom = (function() {
// --- lib/events.js ---
var EventEmitter = function() {};
EventEmitter.createInterface = function(space) {
var methods = {};
methods.on = function(name, fn) {
if (typeof this[space] === 'undefined') {
this[space] = {};
}
if (!this[space].hasOwnProperty(name)) {
this[space][name] = [];
}
this[space][name].push(fn);
};
methods.off = function(name, fn) {
if (typeof this[space] === 'undefined') return;
if (this[space].hasOwnProperty(name)) {
util.removeItem(fn, this[space][name]);
}
};
methods.trigger = function(name) {
if (typeof this[space] !== 'undefined' && this[space].hasOwnProperty(name)) {
var args = Array.prototype.slice.call(arguments, 1);
for (var i = 0; i < this[space][name].length; i++) {
this[space][name][i].apply(this[space][name][i], args);
}
}
};
return methods;
};
var pvt = EventEmitter.createInterface('_handlers');
EventEmitter.prototype._on = pvt.on;
EventEmitter.prototype._off = pvt.off;
EventEmitter.prototype._trigger = pvt.trigger;
var pub = EventEmitter.createInterface('handlers');
EventEmitter.prototype.on = function() {
pub.on.apply(this, arguments);
Array.prototype.unshift.call(arguments, 'on');
this._trigger.apply(this, arguments);
};
EventEmitter.prototype.off = pub.off;
EventEmitter.prototype.trigger = pub.trigger;
// --- lib/localstorage.js ---
var localStorage = window.localStorage;
if (typeof localStorage === 'undefined') {
localStorage = {
getItem : function() {},
setItem : function() {},
removeItem : function() {}
};
}
// --- lib/util.js ---
var util = {};
util.guid = (function() {
var S4 = function() {
return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
};
return function() {
return S4() + S4() + '-' + S4() + '-' + S4() + '-' + S4() + '-' + S4() + S4() + S4();
};
})();
util.throttle = function(delay, fn) {
var last = 0;
return function() {
var now = (new Date()).getTime();
if (now - last > delay) {
last = now;
fn.apply(this, arguments);
}
};
};
util.extend = function(a, b) {
if (typeof a === 'undefined' || !a) { a = {}; }
if (typeof b === 'object') {
for (var key in b) {
if (b.hasOwnProperty(key)) {
a[key] = b[key];
}
}
}
return a;
};
util.removeItem = function(item, array) {
for (var i = array.length - 1; i >= 0; i--) {
if (array[i] === item) {
array.splice(i, 1);
}
}
return array;
};
// --- lib/intercom.js ---
/**
* A cross-window broadcast service built on top
* of the HTML5 localStorage API. The interface
* mimic socket.io in design.
*
* @author Brian Reavis <brian@thirdroute.com>
* @constructor
*/
var Intercom = function() {
var self = this;
var now = (new Date()).getTime();
this.origin = util.guid();
this.lastMessage = now;
this.bindings = [];
this.receivedIDs = {};
this.previousValues = {};
var storageHandler = function() { self._onStorageEvent.apply(self, arguments); };
if (window.attachEvent) { document.attachEvent('onstorage', storageHandler); }
else { window.addEventListener('storage', storageHandler, false); };
};
Intercom.prototype._transaction = function(fn) {
var TIMEOUT = 1000;
var WAIT = 20;
var self = this;
var executed = false;
var listening = false;
var waitTimer = null;
var lock = function() {
if (executed) return;
var now = (new Date()).getTime();
var activeLock = parseInt(localStorage.getItem(INDEX_LOCK) || 0);
if (activeLock && now - activeLock < TIMEOUT) {
if (!listening) {
self._on('storage', lock);
listening = true;
}
waitTimer = window.setTimeout(lock, WAIT);
return;
}
executed = true;
localStorage.setItem(INDEX_LOCK, now);
fn();
unlock();
};
var unlock = function() {
if (listening) { self._off('storage', lock); }
if (waitTimer) { window.clearTimeout(waitTimer); }
localStorage.removeItem(INDEX_LOCK);
};
lock();
};
Intercom.prototype._cleanup_emit = util.throttle(100, function() {
var self = this;
this._transaction(function() {
var now = (new Date()).getTime();
var threshold = now - THRESHOLD_TTL_EMIT;
var changed = 0;
var messages = JSON.parse(localStorage.getItem(INDEX_EMIT) || '[]');
for (var i = messages.length - 1; i >= 0; i--) {
if (messages[i].timestamp < threshold) {
messages.splice(i, 1);
changed++;
}
}
if (changed > 0) {
localStorage.setItem(INDEX_EMIT, JSON.stringify(messages));
}
});
});
Intercom.prototype._cleanup_once = util.throttle(100, function() {
var self = this;
this._transaction(function() {
var timestamp, ttl, key;
var table = JSON.parse(localStorage.getItem(INDEX_ONCE) || '{}');
var now = (new Date()).getTime();
var changed = 0;
for (key in table) {
if (self._once_expired(key, table)) {
delete table[key];
changed++;
}
}
if (changed > 0) {
localStorage.setItem(INDEX_ONCE, JSON.stringify(table));
}
});
});
Intercom.prototype._once_expired = function(key, table) {
if (!table) return true;
if (!table.hasOwnProperty(key)) return true;
if (typeof table[key] !== 'object') return true;
var ttl = table[key].ttl || THRESHOLD_TTL_ONCE;
var now = (new Date()).getTime();
var timestamp = table[key].timestamp;
return timestamp < now - ttl;
};
Intercom.prototype._localStorageChanged = function(event, field) {
if (event && event.key) {
return event.key === field;
}
var currentValue = localStorage.getItem(field);
if (currentValue === this.previousValues[field]) {
return false;
}
this.previousValues[field] = currentValue;
return true;
};
Intercom.prototype._onStorageEvent = function(event) {
event = event || window.event;
var self = this;
if (this._localStorageChanged(event, INDEX_EMIT)) {
this._transaction(function() {
var now = (new Date()).getTime();
var data = localStorage.getItem(INDEX_EMIT);
var messages = JSON.parse(data || '[]');
for (var i = 0; i < messages.length; i++) {
if (messages[i].origin === self.origin) continue;
if (messages[i].timestamp < self.lastMessage) continue;
if (messages[i].id) {
if (self.receivedIDs.hasOwnProperty(messages[i].id)) continue;
self.receivedIDs[messages[i].id] = true;
}
self.trigger(messages[i].name, messages[i].payload);
}
self.lastMessage = now;
});
}
this._trigger('storage', event);
};
Intercom.prototype._emit = function(name, message, id) {
id = (typeof id === 'string' || typeof id === 'number') ? String(id) : null;
if (id && id.length) {
if (this.receivedIDs.hasOwnProperty(id)) return;
this.receivedIDs[id] = true;
}
var packet = {
id : id,
name : name,
origin : this.origin,
timestamp : (new Date()).getTime(),
payload : message
};
var self = this;
this._transaction(function() {
var data = localStorage.getItem(INDEX_EMIT) || '[]';
var delimiter = (data === '[]') ? '' : ',';
data = [data.substring(0, data.length - 1), delimiter, JSON.stringify(packet), ']'].join('');
localStorage.setItem(INDEX_EMIT, data);
self.trigger(name, message);
window.setTimeout(function() { self._cleanup_emit(); }, 50);
});
};
Intercom.prototype.bind = function(object, options) {
for (var i = 0; i < Intercom.bindings.length; i++) {
var binding = Intercom.bindings[i].factory(object, options || null, this);
if (binding) { this.bindings.push(binding); }
}
};
Intercom.prototype.emit = function(name, message) {
this._emit.apply(this, arguments);
this._trigger('emit', name, message);
};
Intercom.prototype.once = function(key, fn, ttl) {
if (!Intercom.supported) return;
var self = this;
this._transaction(function() {
var data = JSON.parse(localStorage.getItem(INDEX_ONCE) || '{}');
if (!self._once_expired(key, data)) return;
data[key] = {};
data[key].timestamp = (new Date()).getTime();
if (typeof ttl === 'number') {
data[key].ttl = ttl * 1000;
}
localStorage.setItem(INDEX_ONCE, JSON.stringify(data));
fn();
window.setTimeout(function() { self._cleanup_once(); }, 50);
});
};
util.extend(Intercom.prototype, EventEmitter.prototype);
Intercom.bindings = [];
Intercom.supported = (typeof localStorage !== 'undefined');
var INDEX_EMIT = 'intercom';
var INDEX_ONCE = 'intercom_once';
var INDEX_LOCK = 'intercom_lock';
var THRESHOLD_TTL_EMIT = 50000;
var THRESHOLD_TTL_ONCE = 1000 * 3600;
Intercom.destroy = function() {
localStorage.removeItem(INDEX_LOCK);
localStorage.removeItem(INDEX_EMIT);
localStorage.removeItem(INDEX_ONCE);
};
Intercom.getInstance = (function() {
var intercom = null;
return function() {
if (!intercom) {
intercom = new Intercom();
}
return intercom;
};
})();
// --- lib/bindings/socket.js ---
/**
* Socket.io binding for intercom.js.
*
* - When a message is received on the socket, it's emitted on intercom.
* - When a message is emitted via intercom, it's sent over the socket.
*
* @author Brian Reavis <brian@thirdroute.com>
*/
var SocketBinding = function(socket, options, intercom) {
options = util.extend({
id : null,
send : true,
receive : true
}, options);
if (options.receive) {
var watchedEvents = [];
var onEventAdded = function(name, fn) {
if (watchedEvents.indexOf(name) === -1) {
watchedEvents.push(name);
socket.on(name, function(data) {
var id = (typeof options.id === 'function') ? options.id(name, data) : null;
var emit = (typeof options.receive === 'function') ? options.receive(name, data) : true;
if (emit || typeof emit !== 'boolean') {
intercom._emit(name, data, id);
}
});
}
};
for (var name in intercom.handlers) {
for (var i = 0; i < intercom.handlers[name].length; i++) {
onEventAdded(name, intercom.handlers[name][i]);
}
}
intercom._on('on', onEventAdded);
}
if (options.send) {
intercom._on('emit', function(name, message) {
var emit = (typeof options.send === 'function') ? options.send(name, message) : true;
if (emit || typeof emit !== 'boolean') {
socket.emit(name, message);
}
});
}
};
SocketBinding.factory = function(object, options, intercom) {
if (typeof object.socket === 'undefined') { return false };
return new SocketBinding(object, options, intercom);
};
Intercom.bindings.push(SocketBinding);
return Intercom;
})();

12
examples/js/lib/intercom.min.js vendored Normal file
View File

@@ -0,0 +1,12 @@
/*! intercom.js | https://github.com/diy/intercom.js | Apache License (v2) */
var Intercom=function(){var g=function(){};g.createInterface=function(b){return{on:function(a,c){"undefined"===typeof this[b]&&(this[b]={});this[b].hasOwnProperty(a)||(this[b][a]=[]);this[b][a].push(c)},off:function(a,c){"undefined"!==typeof this[b]&&this[b].hasOwnProperty(a)&&i.removeItem(c,this[b][a])},trigger:function(a){if("undefined"!==typeof this[b]&&this[b].hasOwnProperty(a))for(var c=Array.prototype.slice.call(arguments,1),e=0;e<this[b][a].length;e++)this[b][a][e].apply(this[b][a][e],c)}}};
var m=g.createInterface("_handlers");g.prototype._on=m.on;g.prototype._off=m.off;g.prototype._trigger=m.trigger;var n=g.createInterface("handlers");g.prototype.on=function(){n.on.apply(this,arguments);Array.prototype.unshift.call(arguments,"on");this._trigger.apply(this,arguments)};g.prototype.off=n.off;g.prototype.trigger=n.trigger;var f=window.localStorage;"undefined"===typeof f&&(f={getItem:function(){},setItem:function(){},removeItem:function(){}});var i={},h=function(){return(65536*(1+Math.random())|
0).toString(16).substring(1)};i.guid=function(){return h()+h()+"-"+h()+"-"+h()+"-"+h()+"-"+h()+h()+h()};i.throttle=function(b,a){var c=0;return function(){var e=(new Date).getTime();e-c>b&&(c=e,a.apply(this,arguments))}};i.extend=function(b,a){if("undefined"===typeof b||!b)b={};if("object"===typeof a)for(var c in a)a.hasOwnProperty(c)&&(b[c]=a[c]);return b};i.removeItem=function(b,a){for(var c=a.length-1;0<=c;c--)a[c]===b&&a.splice(c,1);return a};var d=function(){var b=this,a=(new Date).getTime();
this.origin=i.guid();this.lastMessage=a;this.bindings=[];this.receivedIDs={};this.previousValues={};a=function(){b._onStorageEvent.apply(b,arguments)};window.attachEvent?document.attachEvent("onstorage",a):window.addEventListener("storage",a,!1)};d.prototype._transaction=function(b){var a=this,c=!1,e=!1,p=null,d=function(){if(!c){var g=(new Date).getTime(),s=parseInt(f.getItem(l)||0);s&&1E3>g-s?(e||(a._on("storage",d),e=!0),p=window.setTimeout(d,20)):(c=!0,f.setItem(l,g),b(),e&&a._off("storage",d),
p&&window.clearTimeout(p),f.removeItem(l))}};d()};d.prototype._cleanup_emit=i.throttle(100,function(){this._transaction(function(){for(var b=(new Date).getTime()-t,a=0,c=JSON.parse(f.getItem(j)||"[]"),e=c.length-1;0<=e;e--)c[e].timestamp<b&&(c.splice(e,1),a++);0<a&&f.setItem(j,JSON.stringify(c))})});d.prototype._cleanup_once=i.throttle(100,function(){var b=this;this._transaction(function(){var a,c=JSON.parse(f.getItem(k)||"{}");(new Date).getTime();var e=0;for(a in c)b._once_expired(a,c)&&(delete c[a],
e++);0<e&&f.setItem(k,JSON.stringify(c))})});d.prototype._once_expired=function(b,a){if(!a||!a.hasOwnProperty(b)||"object"!==typeof a[b])return!0;var c=a[b].ttl||u,e=(new Date).getTime();return a[b].timestamp<e-c};d.prototype._localStorageChanged=function(b,a){if(b&&b.key)return b.key===a;var c=f.getItem(a);if(c===this.previousValues[a])return!1;this.previousValues[a]=c;return!0};d.prototype._onStorageEvent=function(b){var b=b||window.event,a=this;this._localStorageChanged(b,j)&&this._transaction(function(){for(var b=
(new Date).getTime(),e=f.getItem(j),e=JSON.parse(e||"[]"),d=0;d<e.length;d++)if(e[d].origin!==a.origin&&!(e[d].timestamp<a.lastMessage)){if(e[d].id){if(a.receivedIDs.hasOwnProperty(e[d].id))continue;a.receivedIDs[e[d].id]=!0}a.trigger(e[d].name,e[d].payload)}a.lastMessage=b});this._trigger("storage",b)};d.prototype._emit=function(b,a,c){if((c="string"===typeof c||"number"===typeof c?String(c):null)&&c.length){if(this.receivedIDs.hasOwnProperty(c))return;this.receivedIDs[c]=!0}var e={id:c,name:b,origin:this.origin,
timestamp:(new Date).getTime(),payload:a},d=this;this._transaction(function(){var c=f.getItem(j)||"[]",c=[c.substring(0,c.length-1),"[]"===c?"":",",JSON.stringify(e),"]"].join("");f.setItem(j,c);d.trigger(b,a);window.setTimeout(function(){d._cleanup_emit()},50)})};d.prototype.bind=function(b,a){for(var c=0;c<d.bindings.length;c++){var e=d.bindings[c].factory(b,a||null,this);e&&this.bindings.push(e)}};d.prototype.emit=function(b,a){this._emit.apply(this,arguments);this._trigger("emit",b,a)};d.prototype.once=
function(b,a,c){if(d.supported){var e=this;this._transaction(function(){var d=JSON.parse(f.getItem(k)||"{}");e._once_expired(b,d)&&(d[b]={},d[b].timestamp=(new Date).getTime(),"number"===typeof c&&(d[b].ttl=1E3*c),f.setItem(k,JSON.stringify(d)),a(),window.setTimeout(function(){e._cleanup_once()},50))})}};i.extend(d.prototype,g.prototype);d.bindings=[];d.supported="undefined"!==typeof f;var j="intercom",k="intercom_once",l="intercom_lock",t=5E4,u=36E5;d.destroy=function(){f.removeItem(l);f.removeItem(j);
f.removeItem(k)};var q=null;d.getInstance=function(){q||(q=new d);return q};var r=function(b,a,c){a=i.extend({id:null,send:!0,receive:!0},a);if(a.receive){var d=[],f=function(f){-1===d.indexOf(f)&&(d.push(f),b.on(f,function(b){var d="function"===typeof a.id?a.id(f,b):null,e="function"===typeof a.receive?a.receive(f,b):!0;(e||"boolean"!==typeof e)&&c._emit(f,b,d)}))},g;for(g in c.handlers)for(var h=0;h<c.handlers[g].length;h++)f(g,c.handlers[g][h]);c._on("on",f)}a.send&&c._on("emit",function(c,d){var e=
"function"===typeof a.send?a.send(c,d):!0;(e||"boolean"!==typeof e)&&b.emit(c,d)})};r.factory=function(b,a,c){return"undefined"===typeof b.socket?!1:new r(b,a,c)};d.bindings.push(r);return d}();

10220
examples/js/lib/jquery-3.1.1.js vendored Normal file

File diff suppressed because it is too large Load Diff

1403
examples/js/lib/papaparse.js Normal file

File diff suppressed because it is too large Load Diff

390
examples/js/lib/parallel.js Normal file
View File

@@ -0,0 +1,390 @@
(function () {
var isCommonJS = typeof module !== 'undefined' && module.exports;
var isNode = !(typeof window !== 'undefined' && this === window);
var setImmediate = setImmediate || function (cb) {
setTimeout(cb, 0);
};
var Worker = isNode ? require(__dirname + '/Worker.js') : self.Worker;
var URL = typeof self !== 'undefined' ? (self.URL ? self.URL : self.webkitURL) : null;
var _supports = (isNode || self.Worker) ? true : false; // node always supports parallel
function extend(from, to) {
if (!to) to = {};
for (var i in from) {
if (to[i] === undefined) to[i] = from[i];
}
return to;
}
function Operation() {
this._callbacks = [];
this._errCallbacks = [];
this._resolved = 0;
this._result = null;
}
Operation.prototype.resolve = function (err, res) {
if (!err) {
this._resolved = 1;
this._result = res;
for (var i = 0; i < this._callbacks.length; ++i) {
this._callbacks[i](res);
}
} else {
this._resolved = 2;
this._result = err;
for (var iE = 0; iE < this._errCallbacks.length; ++iE) {
this._errCallbacks[iE](err);
}
}
this._callbacks = [];
this._errCallbacks = [];
};
Operation.prototype.then = function (cb, errCb) {
if (this._resolved === 1) { // result
if (cb) {
cb(this._result);
}
return;
} else if (this._resolved === 2) { // error
if (errCb) {
errCb(this._result);
}
return;
}
if (cb) {
this._callbacks[this._callbacks.length] = cb;
}
if (errCb) {
this._errCallbacks[this._errCallbacks.length] = errCb;
}
return this;
};
var defaults = {
evalPath: isNode ? __dirname + '/eval.js' : null,
maxWorkers: isNode ? require('os').cpus().length : (navigator.hardwareConcurrency || 4),
synchronous: true,
env: {},
envNamespace: 'env'
};
function Parallel(data, options) {
this.data = data;
this.options = extend(defaults, options);
this.operation = new Operation();
this.operation.resolve(null, this.data);
this.requiredScripts = [];
this.requiredFunctions = [];
}
// static method
Parallel.isSupported = function () { return _supports; }
Parallel.prototype.getWorkerSource = function (cb, env) {
var that = this;
var preStr = '';
var i = 0;
if (!isNode && this.requiredScripts.length !== 0) {
preStr += 'importScripts("' + this.requiredScripts.join('","') + '");\r\n';
}
for (i = 0; i < this.requiredFunctions.length; ++i) {
if (this.requiredFunctions[i].name) {
preStr += 'var ' + this.requiredFunctions[i].name + ' = ' + this.requiredFunctions[i].fn.toString() + ';';
} else {
preStr += this.requiredFunctions[i].fn.toString();
}
}
env = JSON.stringify(env || {});
var ns = this.options.envNamespace;
if (isNode) {
return preStr + 'process.on("message", function(e) {global.' + ns + ' = ' + env + ';process.send(JSON.stringify((' + cb.toString() + ')(JSON.parse(e).data)))})';
} else {
return preStr + 'self.onmessage = function(e) {var global = {}; global.' + ns + ' = ' + env + ';self.postMessage((' + cb.toString() + ')(e.data))}';
}
};
Parallel.prototype.require = function () {
var args = Array.prototype.slice.call(arguments, 0),
func;
for (var i = 0; i < args.length; i++) {
func = args[i];
if (typeof func === 'string') {
this.requiredScripts.push(func);
} else if (typeof func === 'function') {
this.requiredFunctions.push({ fn: func });
} else if (typeof func === 'object') {
this.requiredFunctions.push(func);
}
}
return this;
};
Parallel.prototype._spawnWorker = function (cb, env) {
var wrk;
var src = this.getWorkerSource(cb, env);
if (isNode) {
wrk = new Worker(this.options.evalPath);
wrk.postMessage(src);
} else {
if (Worker === undefined) {
return undefined;
}
try {
if (this.requiredScripts.length !== 0) {
if (this.options.evalPath !== null) {
wrk = new Worker(this.options.evalPath);
wrk.postMessage(src);
} else {
throw new Error('Can\'t use required scripts without eval.js!');
}
} else if (!URL) {
throw new Error('Can\'t create a blob URL in this browser!');
} else {
var blob = new Blob([src], { type: 'text/javascript' });
var url = URL.createObjectURL(blob);
wrk = new Worker(url);
}
} catch (e) {
if (this.options.evalPath !== null) { // blob/url unsupported, cross-origin error
wrk = new Worker(this.options.evalPath);
wrk.postMessage(src);
} else {
throw e;
}
}
}
return wrk;
};
Parallel.prototype.spawn = function (cb, env) {
var that = this;
var newOp = new Operation();
env = extend(this.options.env, env || {});
this.operation.then(function () {
var wrk = that._spawnWorker(cb, env);
if (wrk !== undefined) {
wrk.onmessage = function (msg) {
wrk.terminate();
that.data = msg.data;
newOp.resolve(null, that.data);
};
wrk.onerror = function (e) {
wrk.terminate();
newOp.resolve(e, null);
};
wrk.postMessage(that.data);
} else if (that.options.synchronous) {
setImmediate(function () {
try {
that.data = cb(that.data);
newOp.resolve(null, that.data);
} catch (e) {
newOp.resolve(e, null);
}
});
} else {
throw new Error('Workers do not exist and synchronous operation not allowed!');
}
});
this.operation = newOp;
return this;
};
Parallel.prototype._spawnMapWorker = function (i, cb, done, env, wrk) {
var that = this;
if (!wrk) wrk = that._spawnWorker(cb, env);
if (wrk !== undefined) {
wrk.onmessage = function (msg) {
that.data[i] = msg.data;
done(null, wrk);
};
wrk.onerror = function (e) {
wrk.terminate();
done(e);
};
wrk.postMessage(that.data[i]);
} else if (that.options.synchronous) {
setImmediate(function () {
that.data[i] = cb(that.data[i]);
done();
});
} else {
throw new Error('Workers do not exist and synchronous operation not allowed!');
}
};
Parallel.prototype.map = function (cb, env) {
env = extend(this.options.env, env || {});
if (!this.data.length) {
return this.spawn(cb, env);
}
var that = this;
var startedOps = 0;
var doneOps = 0;
function done(err, wrk) {
if (err) {
newOp.resolve(err, null);
} else if (++doneOps === that.data.length) {
newOp.resolve(null, that.data);
if (wrk) wrk.terminate();
} else if (startedOps < that.data.length) {
that._spawnMapWorker(startedOps++, cb, done, env, wrk);
} else {
if (wrk) wrk.terminate();
}
}
var newOp = new Operation();
this.operation.then(function () {
for (; startedOps - doneOps < that.options.maxWorkers && startedOps < that.data.length; ++startedOps) {
that._spawnMapWorker(startedOps, cb, done, env);
}
}, function (err) {
newOp.resolve(err, null);
});
this.operation = newOp;
return this;
};
Parallel.prototype._spawnReduceWorker = function (data, cb, done, env, wrk) {
var that = this;
if (!wrk) wrk = that._spawnWorker(cb, env);
if (wrk !== undefined) {
wrk.onmessage = function (msg) {
that.data[that.data.length] = msg.data;
done(null, wrk);
};
wrk.onerror = function (e) {
wrk.terminate();
done(e, null);
}
wrk.postMessage(data);
} else if (that.options.synchronous) {
setImmediate(function () {
that.data[that.data.length] = cb(data);
done();
});
} else {
throw new Error('Workers do not exist and synchronous operation not allowed!');
}
};
Parallel.prototype.reduce = function (cb, env) {
env = extend(this.options.env, env || {});
if (!this.data.length) {
throw new Error('Can\'t reduce non-array data');
}
var runningWorkers = 0;
var that = this;
function done(err, wrk) {
--runningWorkers;
if (err) {
newOp.resolve(err, null);
} else if (that.data.length === 1 && runningWorkers === 0) {
that.data = that.data[0];
newOp.resolve(null, that.data);
if (wrk) wrk.terminate();
} else if (that.data.length > 1) {
++runningWorkers;
that._spawnReduceWorker([that.data[0], that.data[1]], cb, done, env, wrk);
that.data.splice(0, 2);
} else {
if (wrk) wrk.terminate();
}
}
var newOp = new Operation();
this.operation.then(function () {
if (that.data.length === 1) {
newOp.resolve(null, that.data[0]);
} else {
for (var i = 0; i < that.options.maxWorkers && i < Math.floor(that.data.length / 2) ; ++i) {
++runningWorkers;
that._spawnReduceWorker([that.data[i * 2], that.data[i * 2 + 1]], cb, done, env);
}
that.data.splice(0, i * 2);
}
});
this.operation = newOp;
return this;
};
Parallel.prototype.then = function (cb, errCb) {
var that = this;
var newOp = new Operation();
errCb = typeof errCb === 'function' ? errCb : function(){};
this.operation.then(function () {
var retData;
try {
if (cb) {
retData = cb(that.data);
if (retData !== undefined) {
that.data = retData;
}
}
newOp.resolve(null, that.data);
} catch (e) {
if (errCb) {
retData = errCb(e);
if (retData !== undefined) {
that.data = retData;
}
newOp.resolve(null, that.data);
} else {
newOp.resolve(null, e);
}
}
}, function (err) {
if (errCb) {
var retData = errCb(err);
if (retData !== undefined) {
that.data = retData;
}
newOp.resolve(null, that.data);
} else {
newOp.resolve(null, err);
}
});
this.operation = newOp;
return this;
};
if (isCommonJS) {
module.exports = Parallel;
} else {
self.Parallel = Parallel;
}
})();

View File

@@ -0,0 +1,195 @@
function doInterpolation(sampleSet, remainderSet, interpSubset, properties) {
var distance = calculateDistancePoker;
// var distance = calculateEuclideanDistance;
console.log("Brute-force");
for (var i = 0; i < remainderSet.length; i++) {
var node = remainderSet[i],
minNode = sampleSet[0],
minDist = 0,
sampleCache = [];
minDist = distance(node, minNode, properties);
for (var j = 1, sample; j < sampleSet.length; j++) {
sample = sampleSet[j];
if ((sample !== node) && (distance(node, sample, properties) < minDist)) {
minDist = distance(node, sample, properties);
minNode = sample;
}
}
// console.log()
for (var k = 0; k < interpSubset.length; k++) {
sampleCache[k] = distance(node, interpSubset[k], properties);
}
var radius = distance(node, minNode, properties);
placeNearToNearestNeighbour(node, minNode, interpSubset, sampleCache, radius);
}
}
function placeNearToNearestNeighbour(node, minNode, sample, sampleCache, radius) {
var
dist0 = 0.0,
dist90 = 0.0,
dist180 = 0.0,
dist270 = 0.0,
lowBound = 0.0,
highBound = 0.0;
dist0 = sumDistToSample(node, centerPoint(0, radius, minNode.x, minNode.y), sample, sampleCache);
dist90 = sumDistToSample(node, centerPoint(90, radius, minNode.x, minNode.y), sample, sampleCache);
dist180 = sumDistToSample(node, centerPoint(180, radius, minNode.x, minNode.y), sample, sampleCache);
dist270 = sumDistToSample(node, centerPoint(270, radius, minNode.x, minNode.y), sample, sampleCache);
// console.log(dist0, dist90, dist180, dist270);
// Determine the closest quadrant
if (dist0 == dist180) {
if (dist90 > dist270)
lowBound = highBound = 270;
else
lowBound = highBound = 90;
} else if (dist90 == dist270) {
if (dist0 > dist180)
lowBound = highBound = 180;
else
lowBound = highBound = 0;
} else if (dist0 > dist180) {
if (dist90 > dist270) {
lowBound = 180;
highBound = 270;
} else {
lowBound = 90;
highBound = 180;
}
} else {
if (dist90 > dist270) {
lowBound = 270;
highBound = 360;
} else {
lowBound = 0;
highBound = 90;
}
}
var angle = binarySearch(lowBound, highBound, minNode.x, minNode.y, radius, node, sample, sampleCache);
var newPoint = centerPoint(angle, radius, minNode.x, minNode.y);
// console.log(newPoint);
node.x = newPoint.x;
node.y = newPoint.y;
// for (var i = 0; i < 20; i++) {
// var forces = sumForcesToSample(node, sample, sampleCache);
// // console.log(forces);
// node.x += forces.x;
// node.y += forces.y;
// }
}
function centerPoint(angle, radius, posX, posY) {
var x = posX + Math.cos(toRadians(angle) * radius);
var y = posY + Math.sin(toRadians(angle) * radius);
return {
x: x,
y: y
};
}
function toRadians(degrees) {
return degrees * (Math.PI / 180);
}
function sumDistToSample(node, point, sample, sampleCache) {
var total = 0.0;
// console.log(total, sample);
for (var i = 0; i < sample.length; i++) {
var s = sample[i];
var realDist = Math.hypot(s.x - point.x, s.y - point.y);
var desDist = sampleCache[i];
total += Math.abs(realDist - desDist);
}
return total;
}
function sumForcesToSample(node, sample, sampleCache) {
var x = 0,
y = 0,
// len = 0,
dist = 0,
force,
SPRING_FORCE = 0.7;
for (var i = 0, unitX, unitY; i < sample.length; i++) {
var s = sample[i];
if (s !== node) {
unitX = s.x - node.x;
unitY = s.y - node.y;
// Normalize coordinates
len = Math.sqrt(unitX * unitX + unitY * unitY);
unitX /= len;
unitY /= len;
console.log(unitX, unitY);
var realDist = Math.sqrt(unitX * unitX + unitY * unitY);
var desDist = sampleCache[i];
dist += realDist - desDist;
force = (SPRING_FORCE * dist);
x += unitX * force;
y += unitY * force;
}
x *= (1.0 / sample.length);
y *= (1.0 / sample.length);
return {
x: x,
y: y
};
}
}
function binarySearch(lb, hb, x, y, r, node, sample, sampleCache) {
while (lb <= hb) {
var mid = Math.round((lb + hb) / 2);
if ((mid === lb) || (mid === hb)) {
if (sumDistToSample(node, centerPoint(lb, r, x, y), sample, sampleCache) >=
sumDistToSample(node, centerPoint(hb, r, x, y), sample, sampleCache)) {
return hb;
} else {
return lb;
}
} else {
var distMidLeft = sumDistToSample(node, centerPoint(mid + 1, r, x, y), sample, sampleCache);
var distMidRight = sumDistToSample(node, centerPoint(mid - 1, r, x, y), sample, sampleCache);
var distMid = sumDistToSample(node, centerPoint(mid, r, x, y), sample, sampleCache);
if (distMid > distMidLeft) {
lb = mid + 1;
} else if (distMid > distMidRight) {
hb = mid - 1;
} else {
return mid;
}
}
}
return -1;
}

View File

@@ -0,0 +1,269 @@
function doInterpolationPivots(sampleSet, remainderSet, interpSubset, properties) {
var distance = calculateDistancePoker;
// var distance = calculateEuclideanDistance;
// Pivot based parent finding
var numBuckets = Math.floor(Math.sqrt(sampleSet.length)),
// numPivots = Math.floor(Math.sqrt(sampleSet.length)),
numPivots = 3,
parents = [],
maxDists = [],
bucketWidths = [],
pivotsBuckets = [];
console.log("Parents, pivots=", numPivots);
var pivots = createRandomSample(sampleSet.concat(remainderSet), sampleSet.length, numPivots);
for (var i = 0; i < numPivots; i++) {
pivotsBuckets[i] = [];
for (var j = 0; j < numBuckets; j++) {
pivotsBuckets[i][j] = [];
}
}
// Pre-processing
var fullDists = []
for (var i = 0; i < sampleSet.length; i++) {
fullDists[i] = [];
}
for (var j = 0, maxDist = -1; j < numPivots; j++) {
var c1 = pivots[j];
for (var i = 0; i < sampleSet.length; i++) {
var c2 = sampleSet[i];
if (c1 !== c2) {
var dist = distance(c1, c2, properties);
// console.log(dist, c1, c2);
if (dist > maxDist) {
maxDist = dist;
}
fullDists[i][j] = dist;
} else {
fullDists[i][j] = 0.0001;
}
}
maxDists.push(maxDist);
bucketWidths.push(maxDist / numBuckets);
}
// console.log(fullDists);
for (var j = 0; j < numPivots; j++) {
var bucketWidth = bucketWidths[j];
for (var i = 0; i < sampleSet.length; i++) {
var tmp = pivotsBuckets[j][Math.floor((fullDists[i][j] - 0.0001) / bucketWidth)];
// pivotsBuckets[j][Math.floor((fullDists[i][j] - 0.0001) / bucketWidth)].push(sampleSet[i]);
// console.log(tmp, i, j, bucketWidth, Math.floor((fullDists[i][j] - 0.0001) / bucketWidth));
tmp.push(sampleSet[i]);
}
}
for (var i = 0; i < remainderSet.length; i++) {
var node = remainderSet[i],
minNode = sampleSet[0],
minDist = 0,
sampleCache = [];
// Pivot based parent search
var node = remainderSet[i];
var clDist = Number.MAX_VALUE;
for (var p = 0; p < numPivots; p++) {
var comp = pivots[p];
var bucketWidth = bucketWidths[p];
if (node !== comp) {
var dist = distance(node, comp, properties);
bNum = Math.floor((dist - 0.0001) / bucketWidth);
if (bNum >= numBuckets) {
bNum = numBuckets - 1;
} else if (bNum < 0) {
bNum = 0;
}
var bucketContents = pivotsBuckets[p][bNum];
for (var w = 0; w < bucketContents.length; w++) {
var c1 = bucketContents[w];
if (c1 != node) {
dist = distance(c1, node, properties);
if (dist <= clDist) {
clDist = dist;
minNode = bucketContents[w];
}
}
}
}
}
for (var k = 0; k < interpSubset.length; k++) {
sampleCache[k] = distance(node, interpSubset[k], properties);
}
var radius = distance(node, minNode, properties);
placeNearToNearestNeighbour(node, minNode, interpSubset, sampleCache, radius);
}
}
function placeNearToNearestNeighbour(node, minNode, sample, sampleCache, radius) {
var
dist0 = 0.0,
dist90 = 0.0,
dist180 = 0.0,
dist270 = 0.0,
lowBound = 0.0,
highBound = 0.0;
dist0 = sumDistToSample(node, centerPoint(0, radius, minNode.x, minNode.y), sample, sampleCache);
dist90 = sumDistToSample(node, centerPoint(90, radius, minNode.x, minNode.y), sample, sampleCache);
dist180 = sumDistToSample(node, centerPoint(180, radius, minNode.x, minNode.y), sample, sampleCache);
dist270 = sumDistToSample(node, centerPoint(270, radius, minNode.x, minNode.y), sample, sampleCache);
// console.log(dist0, dist90, dist180, dist270);
// Determine the closest quadrant
if (dist0 == dist180) {
if (dist90 > dist270)
lowBound = highBound = 270;
else
lowBound = highBound = 90;
} else if (dist90 == dist270) {
if (dist0 > dist180)
lowBound = highBound = 180;
else
lowBound = highBound = 0;
} else if (dist0 > dist180) {
if (dist90 > dist270) {
lowBound = 180;
highBound = 270;
} else {
lowBound = 90;
highBound = 180;
}
} else {
if (dist90 > dist270) {
lowBound = 270;
highBound = 360;
} else {
lowBound = 0;
highBound = 90;
}
}
var angle = binarySearch(lowBound, highBound, minNode.x, minNode.y, radius, node, sample, sampleCache);
var newPoint = centerPoint(angle, radius, minNode.x, minNode.y);
// console.log(newPoint);
node.x = newPoint.x;
node.y = newPoint.y;
// for (var i = 0; i < 20; i++) {
// var forces = sumForcesToSample(node, sample, sampleCache);
// // console.log(forces);
// node.x += forces.x;
// node.y += forces.y;
// }
}
function centerPoint(angle, radius, posX, posY) {
var x = posX + Math.cos(toRadians(angle) * radius);
var y = posY + Math.sin(toRadians(angle) * radius);
return {
x: x,
y: y
};
}
function toRadians(degrees) {
return degrees * (Math.PI / 180);
}
function sumDistToSample(node, point, sample, sampleCache) {
var total = 0.0;
// console.log(total, sample);
for (var i = 0; i < sample.length; i++) {
var s = sample[i];
var realDist = Math.hypot(s.x - point.x, s.y - point.y);
var desDist = sampleCache[i];
total += Math.abs(realDist - desDist);
}
return total;
}
function sumForcesToSample(node, sample, sampleCache) {
var x = 0,
y = 0,
// len = 0,
dist = 0,
force,
SPRING_FORCE = 0.7;
for (var i = 0, unitX, unitY; i < sample.length; i++) {
var s = sample[i];
if (s !== node) {
unitX = s.x - node.x;
unitY = s.y - node.y;
// Normalize coordinates
len = Math.sqrt(unitX * unitX + unitY * unitY);
unitX /= len;
unitY /= len;
console.log(unitX, unitY);
var realDist = Math.sqrt(unitX * unitX + unitY * unitY);
var desDist = sampleCache[i];
dist += realDist - desDist;
force = (SPRING_FORCE * dist);
x += unitX * force;
y += unitY * force;
}
x *= (1.0 / sample.length);
y *= (1.0 / sample.length);
return {
x: x,
y: y
};
}
}
function binarySearch(lb, hb, x, y, r, node, sample, sampleCache) {
while (lb <= hb) {
var mid = Math.round((lb + hb) / 2);
if ((mid === lb) || (mid === hb)) {
if (sumDistToSample(node, centerPoint(lb, r, x, y), sample, sampleCache) >=
sumDistToSample(node, centerPoint(hb, r, x, y), sample, sampleCache)) {
return hb;
} else {
return lb;
}
} else {
var distMidLeft = sumDistToSample(node, centerPoint(mid + 1, r, x, y), sample, sampleCache);
var distMidRight = sumDistToSample(node, centerPoint(mid - 1, r, x, y), sample, sampleCache);
var distMid = sumDistToSample(node, centerPoint(mid, r, x, y), sample, sampleCache);
if (distMid > distMidLeft) {
lb = mid + 1;
} else if (distMid > distMidRight) {
hb = mid - 1;
} else {
return mid;
}
}
}
return -1;
}

View File

@@ -0,0 +1,579 @@
// Get the width and heigh of the SVG element.
var width = +document.getElementById('svg').clientWidth,
height = +document.getElementById('svg').clientHeight;
var svg = d3.select("svg")
.call(d3.zoom().scaleExtent([0.0001, 1000000]).on("zoom", function () {
svg.attr("transform", d3.event.transform);
}))
.append("g");
var div = d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
var brush = d3.brush()
.extent([[-9999999, -9999999], [9999999, 9999999]])
.on("end", brushEnded);
svg.append("g")
.attr("class", "brush")
.call(brush);
var intercom = Intercom.getInstance();
intercom.on("select", unSelectNodes);
var imgs = svg.selectAll("image");
var links = [],
nodes,
node,
props,
norm,
p1 = 0,
p2 = 0,
size,
distanceFunction,
simulation,
velocities = [],
rendering = true, // Rendering during the execution.
forceName = "forces",
springForce = false,
tooltipWidth = 0,
fileName = "",
selectedData,
clickedIndex = -1;
// Default parameters
var MULTIPLIER = 50,
PERPLEXITY = 30,
LEARNING_RATE = 10,
NEIGHBOUR_SIZE = 6,
SAMPLE_SIZE = 3,
PIVOTS = false,
NUM_PIVOTS = 3,
ITERATIONS = 300,
FULL_ITERATIONS = 20,
NODE_SIZE = 10,
COLOR_ATTRIBUTE = "";
// Create a color scheme for a range of numbers.
var color = d3.scaleOrdinal(d3.schemeCategory10);
/**
* Parse the data from the provided csv file using Papa Parse library
* @param {file} evt - csv file.
*/
function parseFile(evt) {
// Clear the previous nodes
d3.selectAll(".nodes").remove();
springForce = false;
fileName = evt.target.files[0].name;
Papa.parse(evt.target.files[0], {
header: true,
dynamicTyping: true,
skipEmptyLines: true,
complete: function (results) {
processData(results.data, results.error);
}
});
}
/**
* Process the data and pass it into D3 force simulation.
* @param {array} data
* @param {object} error
*/
function processData(data, error) {
if (error) throw error.message;
nodes = data;
// Number of iterations before stopping.
size = nodes.length;
console.log("Number of iterations: ", ITERATIONS);
// Start by placing the nodes at the center (its starting positions).
simulation = d3.forceSimulation();
console.log("n =", nodes.length);
// Calculate normalization arguments and get the list of
// properties of the nodes.
norm = calculateNormalization(nodes);
props = Object.keys(nodes[0]);
COLOR_ATTRIBUTE = props[0];
var opts = document.getElementById('color_attr').options;
props.forEach(function (d) {
opts.add(new Option(d, d, (d === COLOR_ATTRIBUTE) ? true : false));
});
// Add the nodes to DOM.
node = svg.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(nodes)
.enter().append("circle")
.attr("r", NODE_SIZE)
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")")
// Color code the data points by a property (for Poker Hands,
// it is a CLASS property).
.attr("fill", function (d) {
return color(d[COLOR_ATTRIBUTE]);
})
.on("mouseover", function (d) {
highlightOnHover(d[COLOR_ATTRIBUTE]);
})
.on("mouseout", function (d) {
div.transition()
.duration(500)
.style("opacity", 0);
node.attr("opacity", 1);
})
.on("click", function (d) {
if (clickedIndex !== d.index) {
highlightNeighbours(d.index, springForce ? Array.from(simulation.force(forceName).nodeNeighbours(d.index).keys()) : []);
clickedIndex = d.index;
} else {
svg.selectAll("image").remove();
node.attr("r", NODE_SIZE).attr("stroke-width", 0);
clickedIndex = -1;
}
});
// Pass the nodes to the D3 force simulation.
simulation
.nodes(nodes)
.on("tick", ticked)
.on("end", ended);
function ticked() {
// If rendering is selected, then draw at every iteration.
if (rendering === true) {
node
.attr("cx", function (d) {
return d.x;
})
.attr("cy", function (d) {
return d.y;
});
}
}
function ended() {
if (rendering !== true) {
node
.attr("cx", function (d) {
return d.x;
})
.attr("cy", function (d) {
return d.y;
});
}
if (p1 !== 0) {
// Performance time measurement
p2 = performance.now();
console.log("Execution time: " + (p2 - p1));
// Do not calculate stress for data sets bigger than 100 000.
// if (nodes.length <= 100000) {
// console.log("Stress: ", simulation.force(forceName).stress());
// }
p1 = 0;
p2 = 0;
}
}
};
function brushEnded() {
var s = d3.event.selection,
results = [];
if (s) {
var x0 = s[0][0] - width / 2,
y0 = s[0][1] - height / 2,
x1 = s[1][0] - width / 2,
y1 = s[1][1] - height / 2;
if (nodes) {
var sel = node.filter(function (d) {
if (d.x > x0 && d.x < x1 && d.y > y0 && d.y < y1) {
return true;
}
return false;
}).data();
results = sel.map(function (a) { return a.index; });
}
intercom.emit("select", { name: fileName, indices: results });
d3.select(".brush").call(brush.move, null);
}
}
/**
* Initialize the Chalmers' 1996 algorithm and start simulation.
*/
function startNeighbourSamplingSimulation() {
springForce = true;
simulation.stop();
p1 = performance.now();
simulation
.alphaDecay(1 - Math.pow(0.001, 1 / ITERATIONS))
.force(forceName, d3.forceNeighbourSamplingDistance()
// Set the parameters for the algorithm (optional).
.neighbourSize(NEIGHBOUR_SIZE)
.sampleSize(SAMPLE_SIZE)
.multiplier(MULTIPLIER)
// The distance function that will be used to calculate distances
// between nodes.
.distance(function (s, t) {
return distanceFunction(s, t, props, norm);
}));
// Restart the simulation.
console.log(simulation.force(forceName).neighbourSize(), simulation.force(forceName).sampleSize());
simulation.alpha(1).restart();
}
/**
* Initialize the hybrid layout algorithm and start simulation.
*/
function startHybridSimulation() {
springForce = false;
d3.selectAll(".nodes").remove();
simulation.stop();
p1 = performance.now();
hybridSimulation = d3.hybridSimulation(nodes);
hybridSimulation
.multiplier(MULTIPLIER)
.sampleIterations(ITERATIONS)
.pivots(PIVOTS)
.numPivots(NUM_PIVOTS)
.fullIterations(FULL_ITERATIONS)
.neighbourSize(NEIGHBOUR_SIZE)
.sampleSize(SAMPLE_SIZE);
var sample = hybridSimulation.sample();
var remainder = hybridSimulation.remainder();
// Add the nodes to DOM.
node = svg.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(sample)
.enter().append("circle")
.attr("r", NODE_SIZE)
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")")
// Color code the data points by a property (for Poker Hands,
// it is a CLASS property).
.attr("fill", function (d) {
return color(d[COLOR_ATTRIBUTE])
})
.on("mouseover", function (d) {
div.transition()
.duration(200)
.style("opacity", .9);
div.html(formatTooltip(d))
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - (15 * props.length)) + "px")
.style("width", (6 * tooltipWidth) + "px")
.style("height", (14 * props.length) + "px");
highlightOnHover(d[COLOR_ATTRIBUTE]);
})
.on("mouseout", function (d) {
div.transition()
.duration(500)
.style("opacity", 0);
node.attr("opacity", 1);
});
if (selectedData) {
unSelectNodes(selectedData);
}
hybridSimulation
.distance(distanceFunction);
hybridSimulation
.on("sampleTick", tickedHybrid)
.on("fullTick", tickedHybrid)
.on("startFull", started)
.on("end", endedHybrid);
function tickedHybrid() {
if (rendering === true) {
node
.attr("cx", function (d) {
return d.x;
})
.attr("cy", function (d) {
return d.y;
});
}
}
function started() {
d3.selectAll(".nodes").remove();
// Add the nodes to DOM.
node = svg.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(nodes)
.enter().append("circle")
.attr("r", NODE_SIZE)
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")")
// Color code the data points by a property (for Poker Hands,
// it is a CLASS property).
.attr("fill", function (d) {
return color(d[COLOR_ATTRIBUTE])
})
.on("mouseover", function (d) {
div.transition()
.duration(200)
.style("opacity", .9);
div.html(formatTooltip(d))
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - (15 * props.length)) + "px")
.style("width", (6 * tooltipWidth) + "px")
.style("height", (14 * props.length) + "px");
highlightOnHover(d[COLOR_ATTRIBUTE]);
})
.on("mouseout", function (d) {
div.transition()
.duration(500)
.style("opacity", 0);
node.attr("opacity", 1);
});
if (selectedData) {
unSelectNodes(selectedData);
}
}
function endedHybrid() {
if (rendering !== true) {
node
.attr("cx", function (d) {
return d.x;
})
.attr("cy", function (d) {
return d.y;
});
}
// Performance time measurement
p2 = performance.now();
console.log("Execution time: " + (p2 - p1));
// Do not calculate stress for data sets bigger than 100 000.
// if (nodes.length <= 100000) {
// console.log("Stress: ", hybridSimulation.stress());
// }
p1 = 0;
p2 = 0;
}
}
/**
* Initialize the t-SNE algorithm and start simulation.
*/
function starttSNE() {
springForce = false;
simulation.stop();
p1 = performance.now();
simulation
.alphaDecay(1 - Math.pow(0.001, 1 / ITERATIONS))
.force(forceName, d3.tSNE()
// Set the parameter for the algorithm (optional).
.perplexity(PERPLEXITY)
.learningRate(LEARNING_RATE)
// The distance function that will be used to calculate distances
// between nodes.
.distance(function (s, t) {
return distanceFunction(s, t, props, norm) * MULTIPLIER;
}));
// Restart the simulation.
console.log(simulation.force(forceName).perplexity(), simulation.force(forceName).learningRate());
simulation.alpha(1).restart();
}
/**
* Initialize the Barnes-Hut algorithm and start simulation.
*/
function startBarnesHutSimulation() {
springForce = false;
simulation.stop();
p1 = performance.now();
simulation
.alphaDecay(1 - Math.pow(0.001, 1 / ITERATIONS))
.force(forceName, d3.forceBarnesHut()
// The distance function that will be used to calculate distances
// between nodes.
.distance(function (s, t) {
return distanceFunction(s, t, props, norm) * MULTIPLIER;
}));
// Restart the simulation.
simulation.alpha(1).restart();
}
/**
* Initialize the link force algorithm and start simulation.
*/
function startLinkSimulation() {
springForce = false;
simulation.stop();
p1 = performance.now();
// Initialize link array.
nodes = simulation.nodes();
for (i = 0; i < nodes.length; i++) {
for (j = 0; j < nodes.length; j++) {
if (i !== j) {
links.push({
source: nodes[i],
target: nodes[j],
});
}
}
}
// Add the links to the simulation.
simulation.force(forceName, d3.forceLink().links(links));
simulation
.alphaDecay(1 - Math.pow(0.001, 1 / ITERATIONS))
.force(forceName)
// The distance function that will be used to calculate distances
// between nodes.
.distance(function (n) {
return distanceFunction(n.source, n.target, props, norm) * MULTIPLIER;
})
// Set the parameter for the algorithm (optional).
.strength(1);
// Restart the simulation.
simulation.alpha(1).restart();
}
/**
* Halt the execution.
*/
function stopSimulation() {
simulation.stop();
if (typeof hybridSimulation !== 'undefined') {
hybridSimulation.stop();
}
}
/**
* Calculate the average values of the array.
* @param {array} array
* @return {number} the mean of the array.
*/
function getAverage(array) {
var total = 0;
for (var i = 0; i < array.length; i++) {
total += array[i];
}
return total / array.length;
}
/**
* Deselect the nodes to match the selection from other window.
* @param {*} data
*/
function unSelectNodes(data) {
selectedData = data;
if (fileName === data.name && nodes) {
node
.classed("notSelected", function (d) {
if (data.indices.indexOf(d.index) < 0) {
return true;
}
return false;
});
}
}
/**
* Highlight the neighbours for neighbour and sampling algorithm and show the corresponding MNIST images for each node.
* @param {*} indices
*/
function highlightNeighbours(index, indices) {
var selectedNodes = [nodes[index]];
indices.forEach(function (i) {
selectedNodes.push(nodes[i]);
});
var ratio = NODE_SIZE / 10;
svg.selectAll("image").remove();
imgs.data(selectedNodes).enter()
.append("svg:image")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")")
.attr("xlink:href", function (d) {
return "data/mnist/images/" + d.index + ".png";
})
.attr("x", function (d) {
return d.x + (ratio * 20);
})
.attr("y", function (d) {
return d.y - (ratio * 20);
})
.attr("width", ratio * 28)
.attr("height", ratio * 28);
node
.attr("r", function (d) {
if (indices.indexOf(d.index) >= 0) {
return NODE_SIZE * 2;
}
return NODE_SIZE;
})
.attr("stroke-width", function (d) {
if (indices.indexOf(d.index) >= 0) {
return NODE_SIZE * 0.2 + "px";
}
return "0px";
})
.attr("stroke", "white");
}
/**
* Highlight all the nodes with the same class on hover
* @param {*} highlighValue
*/
function highlightOnHover(highlighValue) {
node.attr("opacity", function (d) {
return (highlighValue === d[COLOR_ATTRIBUTE]) ? 1 : 0.3;
});
}
/**
* Color the nodes according to given attribute.
*/
function colorToAttribute() {
node.attr("fill", function (d) {
return color(d[COLOR_ATTRIBUTE])
});
}

View File

@@ -0,0 +1,656 @@
// Get the width and heigh of the SVG element.
var width = +document.getElementById('svg').clientWidth,
height = +document.getElementById('svg').clientHeight;
var svg = d3.select("svg")
.call(d3.zoom().scaleExtent([0.0001, 1000000]).on("zoom", function () {
svg.attr("transform", d3.event.transform);
}))
.append("g");
var div = d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
var brush = d3.brush()
.extent([[-9999999, -9999999], [9999999, 9999999]])
.on("end", brushEnded);
svg.append("g")
.attr("class", "brush")
.call(brush);
var intercom = Intercom.getInstance();
intercom.on("select", unSelectNodes);
var links = [],
nodes,
node,
props,
norm,
p1 = 0,
p2 = 0,
size,
distanceFunction,
simulation,
velocities = [],
rendering = true, // Rendering during the execution.
forceName = "forces",
springForce = false,
tooltipWidth = 0,
fileName = "",
selectedData,
clickedIndex = -1,
paused = false;
// Default parameters
var MULTIPLIER = 50,
PERPLEXITY = 30,
LEARNING_RATE = 10,
NEIGHBOUR_SIZE = 6,
SAMPLE_SIZE = 3,
PIVOTS = false,
NUM_PIVOTS = 3,
ITERATIONS = 300,
FULL_ITERATIONS = 20,
NODE_SIZE = 10,
COLOR_ATTRIBUTE = "",
SELECTED_DISTANCE = 10;
// Create a color scheme for a range of numbers.
var color = d3.scaleOrdinal(d3.schemeCategory10);
/**
* Parse the data from the provided csv file using Papa Parse library
* @param {file} evt - csv file.
*/
function parseFile(evt) {
// Clear the previous nodes
d3.selectAll(".nodes").remove();
springForce = false;
fileName = evt.target.files[0].name;
Papa.parse(evt.target.files[0], {
header: true,
dynamicTyping: true,
skipEmptyLines: true,
complete: function (results) {
processData(results.data, results.error);
}
});
}
/**
* Process the data and pass it into D3 force simulation.
* @param {array} data
* @param {object} error
*/
function processData(data, error) {
if (error) throw error.message;
nodes = data;
// Number of iterations before stopping.
size = nodes.length;
console.log("Number of iterations: ", ITERATIONS);
// Start by placing the nodes at the center (its starting positions).
simulation = d3.forceSimulation();
//Put the nodes in random starting positions
nodes.forEach(function (d) {
d.x = (Math.random()-0.5) * 100000;
d.y = (Math.random()-0.5) * 100000;
});
console.log("n =", nodes.length);
// Calculate normalization arguments and get the list of
// properties of the nodes.
norm = calculateNormalization(nodes);
props = Object.keys(nodes[0]);
COLOR_ATTRIBUTE = props[0];
var opts = document.getElementById('color_attr').options;
props.forEach(function (d) {
opts.add(new Option(d, d, (d === COLOR_ATTRIBUTE) ? true : false));
});
// Add the nodes to DOM.
node = svg.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(nodes)
.enter().append("circle")
.attr("r", NODE_SIZE)
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")")
// Color code the data points by a property (for Poker Hands,
// it is a CLASS property).
.attr("fill", function (d) {
return color(d[COLOR_ATTRIBUTE]);
})
.on("mouseover", function (d) {
div.transition()
.duration(200)
.style("opacity", .9);
div.html(formatTooltip(d))
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - (15 * props.length)) + "px")
.style("width", (6 * tooltipWidth) + "px")
.style("height", (14 * props.length) + "px");
highlightOnHover(d[COLOR_ATTRIBUTE]);
})
.on("mouseout", function (d) {
div.transition()
.duration(500)
.style("opacity", 0);
node.attr("opacity", 1);
})
.on("click", function (d) {
if (clickedIndex !== d.index) {
if (springForce) {
highlightNeighbours(Array.from(simulation.force(forceName).nodeNeighbours(d.index).keys()));
clickedIndex = d.index;
}
} else {
node.attr("r", NODE_SIZE).attr("stroke-width", 0);
clickedIndex = -1;
}
});
// Pass the nodes to the D3 force simulation.
simulation
.nodes(nodes)
.on("tick", ticked)
.on("end", ended);
function ticked() {
// If rendering is selected, then draw at every iteration.
if (rendering === true) {
node
.attr("cx", function (d) {
return d.x;
})
.attr("cy", function (d) {
return d.y;
});
}
// Emit the distribution data to allow the drawing of the bar graph
if (springForce) {
intercom.emit("passedData", simulation.force(forceName).distributionData());
}
}
function ended() {
if (rendering !== true) {
node
.attr("cx", function (d) {
return d.x;
})
.attr("cy", function (d) {
return d.y;
});
}
if (p1 !== 0) {
// Performance time measurement
p2 = performance.now();
console.log("Execution time: " + (p2 - p1));
// Do not calculate stress for data sets bigger than 100 000.
// if (nodes.length <= 100000) {
// console.log("Stress: ", simulation.force(forceName).stress());
// }
// console.log(simulation.force(forceName).nodeNeighbours());
p1 = 0;
p2 = 0;
}
}
};
function brushEnded() {
var s = d3.event.selection,
results = [];
if (s) {
var x0 = s[0][0] - width / 2,
y0 = s[0][1] - height / 2,
x1 = s[1][0] - width / 2,
y1 = s[1][1] - height / 2;
if (nodes) {
var sel = node.filter(function (d) {
if (d.x > x0 && d.x < x1 && d.y > y0 && d.y < y1) {
return true;
}
return false;
}).data();
results = sel.map(function (a) { return a.index; });
}
intercom.emit("select", { name: fileName, indices: results });
d3.select(".brush").call(brush.move, null);
}
}
/**
* Format the tooltip for the data
* @param {*} node
*/
function formatTooltip(node) {
var textString = "",
temp = "";
tooltipWidth = 0;
props.forEach(function (element) {
temp = element + ": " + node[element] + "<br/>";
textString += temp;
if (temp.length > tooltipWidth) {
tooltipWidth = temp.length;
}
});
return textString;
}
/**
* Initialize the Chalmers' 1996 algorithm and start simulation.
*/
function startNeighbourSamplingSimulation() {
springForce = true;
simulation.stop();
p1 = performance.now();
simulation
.alphaDecay(1 - Math.pow(0.001, 1 / ITERATIONS))
.force(forceName, d3.forceNeighbourSamplingDistance()
// Set the parameters for the algorithm (optional).
.neighbourSize(NEIGHBOUR_SIZE)
.sampleSize(SAMPLE_SIZE)
// .freeness(0.5)
.multiplier(MULTIPLIER)
.distanceRange(SELECTED_DISTANCE)
// The distance function that will be used to calculate distances
// between nodes.
.distance(function (s, t) {
return distanceFunction(s, t, props, norm);
}));
// Restart the simulation.
console.log(simulation.force(forceName).neighbourSize(), simulation.force(forceName).sampleSize());
simulation.alpha(1).restart();
}
/**
* Initialize the hybrid layout algorithm and start simulation.
*/
function startHybridSimulation() {
springForce = false;
d3.selectAll(".nodes").remove();
simulation.stop();
p1 = performance.now();
hybridSimulation = d3.hybridSimulation(nodes);
hybridSimulation
.multiplier(MULTIPLIER)
.sampleIterations(ITERATIONS)
.pivots(PIVOTS)
.numPivots(NUM_PIVOTS)
.fullIterations(FULL_ITERATIONS)
.neighbourSize(NEIGHBOUR_SIZE)
.sampleSize(SAMPLE_SIZE);
var sample = hybridSimulation.sample();
var remainder = hybridSimulation.remainder();
// Add the nodes to DOM.
node = svg.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(sample)
.enter().append("circle")
.attr("r", NODE_SIZE)
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")")
// Color code the data points by a property (for Poker Hands,
// it is a CLASS property).
.attr("fill", function (d) {
return color(d[COLOR_ATTRIBUTE])
})
.on("mouseover", function (d) {
div.transition()
.duration(200)
.style("opacity", .9);
div.html(formatTooltip(d))
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - (15 * props.length)) + "px")
.style("width", (6 * tooltipWidth) + "px")
.style("height", (14 * props.length) + "px");
highlightOnHover(d[COLOR_ATTRIBUTE]);
})
.on("mouseout", function (d) {
div.transition()
.duration(500)
.style("opacity", 0);
node.attr("opacity", 1);
});
if (selectedData) {
unSelectNodes(selectedData);
}
hybridSimulation
.distance(distanceFunction);
hybridSimulation
.on("sampleTick", tickedHybrid)
.on("fullTick", tickedHybrid)
.on("startFull", started)
.on("end", endedHybrid);
function tickedHybrid() {
if (rendering === true) {
node
.attr("cx", function (d) {
return d.x;
})
.attr("cy", function (d) {
return d.y;
});
}
}
function started() {
d3.selectAll(".nodes").remove();
// Add the nodes to DOM.
node = svg.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(nodes)
.enter().append("circle")
.attr("r", NODE_SIZE)
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")")
// Color code the data points by a property (for Poker Hands,
// it is a CLASS property).
.attr("fill", function (d) {
return color(d[COLOR_ATTRIBUTE])
})
.on("mouseover", function (d) {
div.transition()
.duration(200)
.style("opacity", .9);
div.html(formatTooltip(d))
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - (15 * props.length)) + "px")
.style("width", (6 * tooltipWidth) + "px")
.style("height", (14 * props.length) + "px");
highlightOnHover(d[COLOR_ATTRIBUTE]);
})
.on("mouseout", function (d) {
div.transition()
.duration(500)
.style("opacity", 0);
node.attr("opacity", 1);
});
if (selectedData) {
unSelectNodes(selectedData);
}
}
function endedHybrid() {
if (rendering !== true) {
node
.attr("cx", function (d) {
return d.x;
})
.attr("cy", function (d) {
return d.y;
});
}
// Performance time measurement
p2 = performance.now();
console.log("Execution time: " + (p2 - p1));
// Do not calculate stress for data sets bigger than 100 000.
// if (nodes.length <= 100000) {
// console.log("Stress: ", hybridSimulation.stress());
// }
p1 = 0;
p2 = 0;
}
}
/**
* Initialize the t-SNE algorithm and start simulation.
*/
function starttSNE() {
springForce = false;
simulation.stop();
p1 = performance.now();
simulation
.alphaDecay(1 - Math.pow(0.001, 1 / ITERATIONS))
.force(forceName, d3.tSNE()
// Set the parameter for the algorithm (optional).
.perplexity(PERPLEXITY)
.learningRate(LEARNING_RATE)
// The distance function that will be used to calculate distances
// between nodes.
.distance(function (s, t) {
return distanceFunction(s, t, props, norm) * MULTIPLIER;
}));
// Restart the simulation.
console.log(simulation.force(forceName).perplexity(), simulation.force(forceName).learningRate());
simulation.alpha(1).restart();
}
/**
* Initialize the Barnes-Hut algorithm and start simulation.
*/
function startBarnesHutSimulation() {
springForce = false;
simulation.stop();
p1 = performance.now();
simulation
.alphaDecay(1 - Math.pow(0.001, 1 / ITERATIONS))
.force(forceName, d3.forceBarnesHut()
// The distance function that will be used to calculate distances
// between nodes.
.distance(function (s, t) {
return distanceFunction(s, t, props, norm) * MULTIPLIER;
}));
// Restart the simulation.
simulation.alpha(1).restart();
}
/**
* Initialize the link force algorithm and start simulation.
*/
function startLinkSimulation() {
springForce = false;
simulation.stop();
p1 = performance.now();
// Initialize link array.
nodes = simulation.nodes();
for (i = 0; i < nodes.length; i++) {
for (j = 0; j < nodes.length; j++) {
if (i !== j) {
links.push({
source: nodes[i],
target: nodes[j],
});
}
}
}
// Add the links to the simulation.
simulation.force(forceName, d3.forceLink().links(links));
simulation
.alphaDecay(1 - Math.pow(0.001, 1 / ITERATIONS))
.force(forceName)
// The distance function that will be used to calculate distances
// between nodes.
.distance(function (n) {
return distanceFunction(n.source, n.target, props, norm) * MULTIPLIER;
})
// Set the parameter for the algorithm (optional).
.strength(1);
// Restart the simulation.
simulation.alpha(1).restart();
}
/**
* Halt the execution.
*/
function stopSimulation() {
simulation.stop();
if (typeof hybridSimulation !== 'undefined') {
hybridSimulation.stop();
}
}
/**
* Calculate the average values of the array.
* @param {array} array
* @return {number} the mean of the array.
*/
function getAverage(array) {
var total = 0;
for (var i = 0; i < array.length; i++) {
total += array[i];
}
return total / array.length;
}
/**
* Deselect the nodes to match the selection from other window.
* @param {*} data
*/
function unSelectNodes(data) {
selectedData = data;
if (fileName === data.name && nodes) {
node
.classed("notSelected", function (d) {
if (data.indices.indexOf(d.index) < 0) {
return true;
}
return false;
});
}
}
/**
* Highlight the neighbours for neighbour and sampling algorithm
* @param {*} indices
*/
function highlightNeighbours(indices) {
node
.attr("r", function (d) {
if (indices.indexOf(d.index) >= 0) {
return NODE_SIZE * 2;
}
return NODE_SIZE;
})
.attr("stroke-width", function (d) {
if (indices.indexOf(d.index) >= 0) {
return NODE_SIZE * 0.2 + "px";
}
return "0px";
})
.attr("stroke", "white");
}
/**
* Highlight all the nodes with the same class on hover
* @param {*} highlighValue
*/
function highlightOnHover(highlighValue) {
node.attr("opacity", function (d) {
return (highlighValue === d[COLOR_ATTRIBUTE]) ? 1 : 0.3;
});
}
/**
* Color the nodes according to given attribute.
*/
function colorToAttribute() {
node.attr("fill", function (d) {
return color(d[COLOR_ATTRIBUTE])
});
}
/**
* Update the distance range.
*/
function updateDistanceRange() {
if (springForce) {
simulation.force(forceName).distanceRange(SELECTED_DISTANCE);
}
}
/**
* Implemented pause/resume functionality
*/
function pauseUnPause() {
if (simulation) {
if (paused) {
simulation.force(forceName).distanceRange(SELECTED_DISTANCE);
simulation.restart();
d3.select("#pauseButton").text("Pause");
paused = false;
} else {
simulation.stop();
d3.select("#pauseButton").text("Resume");
paused = true;
}
}
}
/**
* Average distances for each node.
* @param {*} dataNodes
* @param {*} properties
* @param {*} normalization
*/
function calculateAverageDistance(dataNodes, properties, normalization) {
var sum = 0,
n = nodes.length;
for (var i = 0; i < n; i++) {
var sumNode = 0;
for (var j = 0; j < n; j++) {
if (i !== j) {
sumNode += distanceFunction(nodes[i], nodes[j], properties, normalization);
// console.log(sumNode);
}
}
sum += sumNode / (n - 1);
}
return sum / n;
}

View File

@@ -0,0 +1,109 @@
function findPivots(dataSet, sampleSet, remainderSet) {
var distance = calculateDistancePoker;
// Initialize
var numBuckets = Math.floor(Math.sqrt(sampleSet.length)),
numPivots = Math.floor(Math.sqrt(sampleSet.length)),
parents = [],
maxDists = [],
bucketWidths = [],
pivotsBuckets = [];
var pivots = createRandomSample(dataSet, sampleSet.length, numPivots);
for (var i = 0; i < numPivots; i++) {
pivotsBuckets[i] = [];
for (var j = 0; j < numBuckets; j++) {
pivotsBuckets[i][j] = [];
}
}
// Pre-processing
var fullDists = []
for (var i = 0; i < sampleSet.length; i++) {
fullDists[i] = [];
}
for (var j = 0, maxDist = -1; j < numPivots; j++) {
var c1 = pivots[j];
for (var i = 0; i < sampleSet.length; i++) {
var c2 = sampleSet[i];
if (c1 !== c2) {
var dist = distance(c1, c2);
// console.log(dist, c1, c2);
if (dist > maxDist) {
maxDist = dist;
}
fullDists[i][j] = dist;
} else {
fullDists[i][j] = 0.0001;
}
}
maxDists.push(maxDist);
bucketWidths.push(maxDist / numBuckets);
}
// console.log(fullDists);
for (var j = 0; j < numPivots; j++) {
var bucketWidth = bucketWidths[j];
for (var i = 0; i < sampleSet.length; i++) {
var tmp = pivotsBuckets[j][Math.floor((fullDists[i][j] - 0.0001) / bucketWidth)];
// pivotsBuckets[j][Math.floor((fullDists[i][j] - 0.0001) / bucketWidth)].push(sampleSet[i]);
// console.log(tmp, i, j, bucketWidth, Math.floor((fullDists[i][j] - 0.0001) / bucketWidth));
tmp.push(sampleSet[i]);
}
}
// Find parents
for (var r = 0, bNum, minIndex; r < remainderSet.length; r++) {
var node = remainderSet[r];
var clDist = Number.MAX_VALUE;
for (var p = 0; p < numPivots; p++) {
var comp = pivots[p];
var bucketWidth = bucketWidths[p];
if (node !== comp) {
var dist = distance(node, comp);
bNum = Math.floor((dist - 0.0001) / bucketWidth);
if (bNum >= numBuckets) {
bNum = numBuckets - 1;
} else if (bNum < 0) {
bNum = 0;
}
var bucketContents = pivotsBuckets[p][bNum];
for (var w = 0; w < bucketContents.length; w++) {
var c1 = bucketContents[w];
if (c1 != node) {
dist = distance(c1, node);
if (dist <= clDist) {
clDist = dist;
minIndex = bucketContents[w];
}
}
}
}
}
parents.push(minIndex);
}
return parents;
}
function createRandomSample(nodes, max, size) {
var randElements = [];
for (var i = 0; i < size; ++i) {
// Stop when no new elements can be found.
if (randElements.size >= nodes.length) {
break;
}
var rand = Math.floor((Math.random() * max));
// If the rand is already in random list or in exclude list
// ignore it and get a new value.
while (randElements.includes(rand)) {
rand = Math.floor((Math.random() * max));
}
randElements.push(nodes[rand]);
}
return randElements;
}

274
examples/nist-images.html Normal file
View File

@@ -0,0 +1,274 @@
<!DOCTYPE html>
<html>
<meta charset="utf-8">
<style>
h1 {
text-align: center;
margin: 10px 0 10px 0;
}
.links line {
stroke: #999;
stroke-opacity: 0.6;
}
/*.nodes circle {
stroke: #fff;
stroke-width: 1.5px;
}*/
circle:hover {
fill: #8B0000;
opacity: 0.8;
}
.notSelected {
fill: #999;
opacity: 0.8;
}
.highlighted {
fill: black;
}
#svg {
border-style: solid;
margin: auto;
}
.left {
float: left;
width: 32%;
}
.controls {
flex-basis: 200px;
padding: 0 5px;
}
.controls input[type="range"] {
margin: 0 5% 0.5em 5%;
width: 90%;
}
.multiplier {
margin: 10px 0 10px 0;
}
.parameters {
margin: 10px 0 10px 30px;
}
.checkbox {
margin: 10px 0 10px 0;
}
.start {
text-align: center;
}
div.tooltip {
position: absolute;
border: 0px;
border-radius: 8px;
pointer-events: none;
}
</style>
<body>
<h1>Evaluation</h1>
<svg id="svg" width="100%" height="600">
<div class="left controls">
<div class="input">
<input type="file" id="csv-file" name="files">
</div>
<div class="multiplier">
<label title="The size of the nodes">
Node size
<output id="nodeSizeSliderOutput">10</output>
<input type="range" min="5" max="200" value="10" step="5" oninput="d3.select('#nodeSizeSliderOutput').text(value); NODE_SIZE=value; d3.selectAll('circle').attr('r', value);">
</label>
<br/>
<label title="The number that distance is multiplied by in order to improve the visibility of the graph">
distanceMultiplier
<output id="distanceMultiplierSliderOutput">50</output>
<input type="range" min="5" max="1000" value="50" step="5" oninput="d3.select('#distanceMultiplierSliderOutput').text(value); MULTIPLIER=value;">
</label>
<br/>
<label title="Number of iterations before the simulation is stopped">
Iterations
<output id="iterationsSliderOutput">300</output>
<input type="range" min="5" max="5000" value="300" step="5" oninput="d3.select('#iterationsSliderOutput').text(value); ITERATIONS=value;">
</label>
<br/>
<label title="Attribute used for coloring nodes">
Color attribute
<select id="color_attr" onchange="COLOR_ATTRIBUTE=value; colorToAttribute();">
</select>
</label>
</div>
<div class="checkbox">
<label title="js/">
Rendering
<input type="checkbox" checked onclick="rendering=!rendering;">
</label>
</div>
<div class="start">
<button id="startSimulation" form="algorithmForm">Start</button>
<button onclick="stopSimulation()">Stop</button>
</div>
</div>
<div class="left">
<p>Select algorithm:</p>
<div id="algorithms">
<input id="HLButton" type="radio" name="algorithm" onclick="d3.select('#startSimulation').on('click', startHybridSimulation)">Hybrid
layout
<br>
<div id="HLParameters" class="parameters" style="display:none">
<div id="pivots">
<input id="BFButton" type="radio" name="pivots" checked="checked" onclick="PIVOTS=false;">Brute-force<br>
<input id="PButton" type="radio" name="pivots" onclick="PIVOTS=true;">Pivots<br>
<div id="numPivots" class="parameters" style="display:none">
<label title="The number of pivots">
Number of Pivots
<output id="numPivotsSliderOutput">3</output><br/>
<input type="range" min="1" max="50" value="3" step="1" oninput="d3.select('#numPivotsSliderOutput').text(value); NUM_PIVOTS=value;">
</label>
</div>
</div>
<br/>
<label title="Number of iterations done at the end">
Full iterations
<output id="fullIterationsSliderOutput">20</output><br/>
<input type="range" min="1" max="100" value="20" step="1" oninput="d3.select('#fullIterationsSliderOutput').text(value); FULL_ITERATIONS=value;">
</label>
<br/>
<label title="NeighbourSize">
Neighbour Set
<output id="hlneighbourSizeSliderOutput">6</output><br/>
<input type="range" min="1" max="100" value="6" step="1" oninput="d3.select('#hlneighbourSizeSliderOutput').text(value); NEIGHBOUR_SIZE=value;">
</label>
<br/>
<label title="SampleSize">
Sample Set
<output id="hlsampleSizeSliderOutput">3</output><br/>
<input type="range" min="1" max="100" value="3" step="1" oninput="d3.select('#hlsampleSizeSliderOutput').text(value); SAMPLE_SIZE=value;">
</label>
</div>
<input id="NSButton" type="radio" name="algorithm" onclick="d3.select('#startSimulation').on('click', startNeighbourSamplingSimulation)">Neighbour
and Sampling<br>
<div id="NSParameters" class="parameters" style="display:none">
<label title="NeighbourSize">
Neighbour Set
<output id="neighbourSizeSliderOutput">6</output><br/>
<input type="range" min="1" max="100" value="6" step="1" oninput="d3.select('#neighbourSizeSliderOutput').text(value); NEIGHBOUR_SIZE=value;">
</label>
<br/>
<label title="SampleSize">
Sample Set
<output id="sampleSizeSliderOutput">3</output><br/>
<input type="range" min="1" max="100" value="3" step="1" oninput="d3.select('#sampleSizeSliderOutput').text(value); SAMPLE_SIZE=value;">
</label>
</div>
<input class="noParameters" type="radio" name="algorithm" onclick="d3.select('#startSimulation').on('click', startLinkSimulation)">Link
force in D3<br>
<input class="noParameters" type="radio" name="algorithm" onclick="d3.select('#startSimulation').on('click', startBarnesHutSimulation)">Barnes-Hut<br>
<input id="tSNEButton" type="radio" name="algorithm" onclick="d3.select('#startSimulation').on('click', starttSNE)">t-SNE<br>
<div id="tSNEParameters" class="parameters" style="display:none">
<label title="Perplexity">
Perplexity
<output id="perplexitySliderOutput">30</output><br/>
<input type="range" min="1" max="500" value="30" step="1" oninput="d3.select('#perplexitySliderOutput').text(value); PERPLEXITY=value;">
</label>
<br/>
<label title="LearningRate">
Learning Rate
<output id="learningRateSliderOutput">10</output><br/>
<input type="range" min="1" max="500" value="10" step="1" oninput="d3.select('#learningRateSliderOutput').text(value); LEARNING_RATE=value;">
</label>
</div>
</div>
</div>
<div class="left">
<p>Select distance function:</p>
<div id="distance">
<input type="radio" name="distance" onclick="distanceFunction=calculateDistance"> General<br>
<input type="radio" name="distance" onclick="distanceFunction=calculateEuclideanDistance"> Euclidean<br>
<input type="radio" name="distance" onclick="distanceFunction=calculateManhattanDistance"> Manhattan<br>
<input type="radio" name="distance" onclick="distanceFunction=calculateJaccardDissimilarity"> Jaccard<br>
<input type="radio" name="distance" onclick="distanceFunction=calculateDiceDissimilarity"> Dice<br>
<input type="radio" name="distance" onclick="distanceFunction=calculateCosineSimilarity"> Cosine<br>
<input type="radio" name="distance" onclick="distanceFunction=calculateDistancePoker"> Poker Hands<br>
</div>
</div>
</svg>
</body>
<!-- Load the files and libraries used. -->
<script src="js/lib/d3.v4.min.js"></script>
<script src="js/lib/papaparse.js"></script>
<script src="js/lib/jquery-3.1.1.js"></script>
<script src="js/lib/intercom.js"></script>
<script src="../build/d3-neighbour-sampling.js"></script>
<script src="js/src/neighbourSampling-papaparsing-nist-images.js"></script>
<script src="js/distances/distancePokerHands.js"></script>
<script src="js/distances/distance.js"></script>
<script src="js/distances/euclideanDistance.js"></script>
<script src="js/distances/euclideanDistanceInTSNE.js"></script>
<script src="js/distances/manhattanDistance.js"></script>
<script src="js/distances/jaccardDissimilarity.js"></script>
<script src="js/distances/diceDissimilarity.js"></script>
<script src="js/distances/cosineSimilarity.js"></script>
<script src="js/distances/normalization.js"></script>
<script src="js/distances/numeric.js"></script>
<script>
$(document).ready(function () {
$("#csv-file").change(function (d) {
parseFile(d);
$("#color_attr option").remove();
});
$("#tSNEButton").click(function () {
$(".parameters").hide();
$("#tSNEParameters").show();
});
$("#NSButton").click(function () {
$(".parameters").hide();
$("#NSParameters").show();
});
$("#HLButton").click(function () {
$(".parameters").hide();
$("#HLParameters").show();
if ($("#PButton").is(":checked")) {
$("#numPivots").show();
}
});
$("#BFButton").click(function () {
$("#numPivots").hide();
});
$("#PButton").click(function () {
$("#numPivots").show();
});
$(".noParameters").click(function () {
$(".parameters").hide();
});
});
</script>
</html>

BIN
img/chalmers-fsmvis.PNG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

BIN
img/chalmers-poker.PNG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

28
index.js Normal file
View File

@@ -0,0 +1,28 @@
export {
default as forceNeighbourSampling
}
from "./src/neighbourSampling";
export {
default as forceNeighbourSamplingPre
}
from "./src/neighbourSamplingPre";
export {
default as forceNeighbourSamplingDistance
}
from "./src/neighbourSamplingDistance";
export {
default as forceBarnesHut
}
from "./src/barnesHut";
export {
default as tSNE
}
from "./src/t-sne";
export {
default as forceLink
}
from "./src/link";
export {
default as hybridSimulation
}
from "./src/hybridSimulation";

36
package.json Normal file
View File

@@ -0,0 +1,36 @@
{
"name": "d3-neighbour-sampling",
"version": "0.0.2",
"description": "A linear time algorithm for visualizing a high-dimensional data.",
"keywords": [
"d3",
"d3-module",
"d3-neighbour-sampling",
"force"
],
"license": "MIT",
"main": "build/d3-neighbour-sampling.js",
"jsnext:main": "index",
"homepage": "https://github.com/sReeper/d3-neighbour-sampling",
"repository": {
"type": "git",
"url": "https://github.com/sReeper/d3-neighbour-sampling.git"
},
"scripts": {
"pretest": "rmdir /s /q build && mkdir build && rollup -g d3-force:d3,d3-dispatch:d3,d3-quadtree:d3,d3-collection:d3 -f umd -n d3 -o build/d3-neighbour-sampling.js -- index.js",
"test": "tape 'test/**/*-test.js'",
"prepublish": "npm run test && uglifyjs build/d3-neighbour-sampling.js -c -m -o build/d3-neighbour-sampling.min.js",
"postpublish": "zip -j build/d3-neighbour-sampling.zip -- LICENSE README.md build/d3-neighbour-sampling.js build/d3-neighbour-sampling.min.js"
},
"devDependencies": {
"rollup": "0.36",
"tape": "4",
"uglify-js": "git+https://github.com/mishoo/UglifyJS2.git#harmony"
},
"dependencies": {
"d3-collection": "^1.0.3",
"d3-force": "^1.0.6",
"d3-dispatch": "^1.0.3",
"d3-quadtree": "^1.0.3"
}
}

158
src/barnesHut.js Normal file
View File

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

8
src/constant.js Normal file
View File

@@ -0,0 +1,8 @@
/**
* @return a constant defined by x.
*/
export default function(x) {
return function() {
return x;
};
}

154
src/hybridSimulation.js Normal file
View File

@@ -0,0 +1,154 @@
import { dispatch } from "d3-dispatch";
import constant from "./constant";
import interpolation from "./interpolation";
import interpolationPivots from "./interpolationPivots";
export default function (nodes) {
var hybrid,
fullSimulation,
distance = constant(300),
MULTIPLIER = 50,
PIVOTS = false,
NUMPIVOTS = 3,
SAMPLE_ITERATIONS = 300,
FULL_ITERATIONS = 20,
neighbourSize = 6,
sampleSize = 3,
event = d3Dispatch.dispatch("sampleTick", "fullTick", "startFull", "end");
var sets = sampleFromNodes(nodes, nodes.length, Math.sqrt(nodes.length));
var sample = sets.sample;
var remainder = sets.remainder;
var interpSubset = sampleFromNodes(sample, sample.length, Math.sqrt(sample.length)).sample;
var sampleSimulation = d3.forceSimulation()
.alphaDecay(1 - Math.pow(0.001, 1 / SAMPLE_ITERATIONS));
sampleSimulation
.force("neighbourSampling", d3.forceNeighbourSampling()
.distance(function (s, t) {
return distance(s, t, props, norm) * MULTIPLIER;
})
.neighbourSize(neighbourSize)
.sampleSize(sampleSize))
.nodes(sample)
.on("tick", function () {
event.call("sampleTick", sampleSimulation);
})
.on("end", ended);
function ended() {
if (PIVOTS) {
interpolationPivots(sample, remainder, interpSubset, NUMPIVOTS, distance);
} else {
interpolation(sample, remainder, interpSubset, distance);
}
fullSimulation = d3.forceSimulation()
.alphaDecay(1 - Math.pow(0.001, 1 / FULL_ITERATIONS));
event.call("startFull", fullSimulation);
fullSimulation
.force("neighbourSampling", d3.forceNeighbourSampling()
.distance(function (s, t) {
return distance(s, t, props, norm) * MULTIPLIER;
})
.neighbourSize(neighbourSize)
.sampleSize(sampleSize))
.nodes(nodes)
.on("tick", function () {
event.call("fullTick", fullSimulation);
})
.on("end", function () {
event.call("end", fullSimulation);
});
}
return hybrid = {
distance: function (_) {
return arguments.length ? (distance = typeof _ === "function" ? _ : constant(+_), hybrid) : distance;
},
stop: function () {
if (typeof sampleSimulation !== 'undefined') {
sampleSimulation.stop();
}
if (typeof fullSimulation !== 'undefined') {
fullSimulation.stop();
}
return hybrid;
},
pivots: function (_) {
return arguments.length ? (PIVOTS = _, hybrid) : PIVOTS;
},
numPivots: function (_) {
return arguments.length ? (NUMPIVOTS = +_, hybrid) : NUMPIVOTS;
},
multiplier: function (_) {
return arguments.length ? (MULTIPLIER = +_, hybrid) : MULTIPLIER;
},
sampleIterations: function (_) {
return arguments.length ? (SAMPLE_ITERATIONS = +_, hybrid) : SAMPLE_ITERATIONS;
},
fullIterations: function (_) {
return arguments.length ? (FULL_ITERATIONS = +_, hybrid) : FULL_ITERATIONS;
},
neighbourSize: function (_) {
return arguments.length ? (neighbourSize = +_, hybrid) : neighbourSize;
},
sampleSize: function (_) {
return arguments.length ? (sampleSize = +_, hybrid) : sampleSize;
},
on: function (name, _) {
return arguments.length > 1 ? (event.on(name, _), hybrid) : event.on(name);
},
sample: function (_) {
return arguments.length ? (sample = _, hybrid) : sample;
},
remainder: function (_) {
return arguments.length ? (remainder = _, hybrid) : remainder;
},
stress: function () {
return fullSimulation.force("neighbourSampling").stress();
}
};
}
function sampleFromNodes(nodes, max, size) {
var randElements = [];
for (var i = 0; i < size; ++i) {
var rand = nodes[Math.floor((Math.random() * max))];
// If the rand is already in random list or in exclude list
// ignore it and get a new value.
while (randElements.includes(rand)) {
rand = nodes[Math.floor((Math.random() * max))];
}
randElements.push(rand);
}
var remainder = nodes.filter(function (node) {
return !randElements.includes(node);
});
return {
sample: randElements,
remainder: remainder
};
}

195
src/interpolation.js Normal file
View File

@@ -0,0 +1,195 @@
export default function(sampleSet, remainderSet, interpSubset, distanceFunction) {
var distance = distanceFunction;
// var distance = calculateEuclideanDistance;
// console.log("Brute-force");
for (var i = 0; i < remainderSet.length; i++) {
var node = remainderSet[i],
minNode = sampleSet[0],
minDist = 0,
sampleCache = [];
minDist = distance(node, minNode, props, norm);
for (var j = 1, sample; j < sampleSet.length; j++) {
sample = sampleSet[j];
if ((sample !== node) && (distance(node, sample, props, norm) < minDist)) {
minDist = distance(node, sample, props, norm);
minNode = sample;
}
}
// console.log()
for (var k = 0; k < interpSubset.length; k++) {
sampleCache[k] = distance(node, interpSubset[k], props, norm);
}
var radius = distance(node, minNode, props, norm);
placeNearToNearestNeighbour(node, minNode, interpSubset, sampleCache, radius);
}
}
function placeNearToNearestNeighbour(node, minNode, sample, sampleCache, radius) {
var
dist0 = 0.0,
dist90 = 0.0,
dist180 = 0.0,
dist270 = 0.0,
lowBound = 0.0,
highBound = 0.0;
dist0 = sumDistToSample(node, centerPoint(0, radius, minNode.x, minNode.y), sample, sampleCache);
dist90 = sumDistToSample(node, centerPoint(90, radius, minNode.x, minNode.y), sample, sampleCache);
dist180 = sumDistToSample(node, centerPoint(180, radius, minNode.x, minNode.y), sample, sampleCache);
dist270 = sumDistToSample(node, centerPoint(270, radius, minNode.x, minNode.y), sample, sampleCache);
// console.log(dist0, dist90, dist180, dist270);
// Determine the closest quadrant
if (dist0 == dist180) {
if (dist90 > dist270)
lowBound = highBound = 270;
else
lowBound = highBound = 90;
} else if (dist90 == dist270) {
if (dist0 > dist180)
lowBound = highBound = 180;
else
lowBound = highBound = 0;
} else if (dist0 > dist180) {
if (dist90 > dist270) {
lowBound = 180;
highBound = 270;
} else {
lowBound = 90;
highBound = 180;
}
} else {
if (dist90 > dist270) {
lowBound = 270;
highBound = 360;
} else {
lowBound = 0;
highBound = 90;
}
}
var angle = binarySearch(lowBound, highBound, minNode.x, minNode.y, radius, node, sample, sampleCache);
var newPoint = centerPoint(angle, radius, minNode.x, minNode.y);
// console.log(newPoint);
node.x = newPoint.x;
node.y = newPoint.y;
// for (var i = 0; i < 20; i++) {
// var forces = sumForcesToSample(node, sample, sampleCache);
// // console.log(forces);
// node.x += forces.x;
// node.y += forces.y;
// }
}
function centerPoint(angle, radius, posX, posY) {
var x = posX + Math.cos(toRadians(angle) * radius);
var y = posY + Math.sin(toRadians(angle) * radius);
return {
x: x,
y: y
};
}
function toRadians(degrees) {
return degrees * (Math.PI / 180);
}
function sumDistToSample(node, point, sample, sampleCache) {
var total = 0.0;
// console.log(total, sample);
for (var i = 0; i < sample.length; i++) {
var s = sample[i];
var realDist = Math.hypot(s.x - point.x, s.y - point.y);
var desDist = sampleCache[i];
total += Math.abs(realDist - desDist);
}
return total;
}
function sumForcesToSample(node, sample, sampleCache) {
var x = 0,
y = 0,
// len = 0,
dist = 0,
force,
SPRING_FORCE = 0.7;
for (var i = 0, unitX, unitY; i < sample.length; i++) {
var s = sample[i];
if (s !== node) {
unitX = s.x - node.x;
unitY = s.y - node.y;
// Normalize coordinates
len = Math.sqrt(unitX * unitX + unitY * unitY);
unitX /= len;
unitY /= len;
console.log(unitX, unitY);
var realDist = Math.sqrt(unitX * unitX + unitY * unitY);
var desDist = sampleCache[i];
dist += realDist - desDist;
force = (SPRING_FORCE * dist);
x += unitX * force;
y += unitY * force;
}
x *= (1.0 / sample.length);
y *= (1.0 / sample.length);
return {
x: x,
y: y
};
}
}
function binarySearch(lb, hb, x, y, r, node, sample, sampleCache) {
while (lb <= hb) {
var mid = Math.round((lb + hb) / 2);
if ((mid === lb) || (mid === hb)) {
if (sumDistToSample(node, centerPoint(lb, r, x, y), sample, sampleCache) >=
sumDistToSample(node, centerPoint(hb, r, x, y), sample, sampleCache)) {
return hb;
} else {
return lb;
}
} else {
var distMidLeft = sumDistToSample(node, centerPoint(mid + 1, r, x, y), sample, sampleCache);
var distMidRight = sumDistToSample(node, centerPoint(mid - 1, r, x, y), sample, sampleCache);
var distMid = sumDistToSample(node, centerPoint(mid, r, x, y), sample, sampleCache);
if (distMid > distMidLeft) {
lb = mid + 1;
} else if (distMid > distMidRight) {
hb = mid - 1;
} else {
return mid;
}
}
}
return -1;
}

287
src/interpolationPivots.js Normal file
View File

@@ -0,0 +1,287 @@
export default function(sampleSet, remainderSet, interpSubset, nPivots, distanceFunction) {
var distance = distanceFunction;
// Pivot based parent finding
var numBuckets = Math.floor(Math.sqrt(sampleSet.length)),
numPivots = nPivots,
parents = [],
maxDists = [],
bucketWidths = [],
pivotsBuckets = [];
console.log("Parents, pivots=", numPivots);
var pivots = createRandomSample(sampleSet.concat(remainderSet), sampleSet.length, numPivots);
for (var i = 0; i < numPivots; i++) {
pivotsBuckets[i] = [];
for (var j = 0; j < numBuckets; j++) {
pivotsBuckets[i][j] = [];
}
}
// Pre-processing
var fullDists = []
for (var i = 0; i < sampleSet.length; i++) {
fullDists[i] = [];
}
for (var j = 0, maxDist = -1; j < numPivots; j++) {
var c1 = pivots[j];
for (var i = 0; i < sampleSet.length; i++) {
var c2 = sampleSet[i];
if (c1 !== c2) {
var dist = distance(c1, c2, props, norm);
// console.log(dist, c1, c2);
if (dist > maxDist) {
maxDist = dist;
}
fullDists[i][j] = dist;
} else {
fullDists[i][j] = 0.0001;
}
}
maxDists.push(maxDist);
bucketWidths.push(maxDist / numBuckets);
}
// console.log(fullDists);
for (var j = 0; j < numPivots; j++) {
var bucketWidth = bucketWidths[j];
for (var i = 0; i < sampleSet.length; i++) {
var tmp = pivotsBuckets[j][Math.floor((fullDists[i][j] - 0.0001) / bucketWidth)];
// pivotsBuckets[j][Math.floor((fullDists[i][j] - 0.0001) / bucketWidth)].push(sampleSet[i]);
// console.log(tmp, i, j, bucketWidth, Math.floor((fullDists[i][j] - 0.0001) / bucketWidth));
tmp.push(sampleSet[i]);
}
}
for (var i = 0; i < remainderSet.length; i++) {
var node = remainderSet[i],
minNode = sampleSet[0],
minDist = 0,
sampleCache = [];
// Pivot based parent search
var node = remainderSet[i];
var clDist = Number.MAX_VALUE;
for (var p = 0; p < numPivots; p++) {
var comp = pivots[p];
var bucketWidth = bucketWidths[p];
if (node !== comp) {
var dist = distance(node, comp, props, norm);
var bNum = Math.floor((dist - 0.0001) / bucketWidth);
if (bNum >= numBuckets) {
bNum = numBuckets - 1;
} else if (bNum < 0) {
bNum = 0;
}
var bucketContents = pivotsBuckets[p][bNum];
for (var w = 0; w < bucketContents.length; w++) {
var c1 = bucketContents[w];
if (c1 != node) {
dist = distance(c1, node, props, norm);
if (dist <= clDist) {
clDist = dist;
minNode = bucketContents[w];
}
}
}
}
}
for (var k = 0; k < interpSubset.length; k++) {
sampleCache[k] = distance(node, interpSubset[k], props, norm);
}
var radius = distance(node, minNode, props, norm);
placeNearToNearestNeighbour(node, minNode, interpSubset, sampleCache, radius);
}
}
function placeNearToNearestNeighbour(node, minNode, sample, sampleCache, radius) {
var
dist0 = 0.0,
dist90 = 0.0,
dist180 = 0.0,
dist270 = 0.0,
lowBound = 0.0,
highBound = 0.0;
dist0 = sumDistToSample(node, centerPoint(0, radius, minNode.x, minNode.y), sample, sampleCache);
dist90 = sumDistToSample(node, centerPoint(90, radius, minNode.x, minNode.y), sample, sampleCache);
dist180 = sumDistToSample(node, centerPoint(180, radius, minNode.x, minNode.y), sample, sampleCache);
dist270 = sumDistToSample(node, centerPoint(270, radius, minNode.x, minNode.y), sample, sampleCache);
// console.log(dist0, dist90, dist180, dist270);
// Determine the closest quadrant
if (dist0 == dist180) {
if (dist90 > dist270)
lowBound = highBound = 270;
else
lowBound = highBound = 90;
} else if (dist90 == dist270) {
if (dist0 > dist180)
lowBound = highBound = 180;
else
lowBound = highBound = 0;
} else if (dist0 > dist180) {
if (dist90 > dist270) {
lowBound = 180;
highBound = 270;
} else {
lowBound = 90;
highBound = 180;
}
} else {
if (dist90 > dist270) {
lowBound = 270;
highBound = 360;
} else {
lowBound = 0;
highBound = 90;
}
}
var angle = binarySearch(lowBound, highBound, minNode.x, minNode.y, radius, node, sample, sampleCache);
var newPoint = centerPoint(angle, radius, minNode.x, minNode.y);
// console.log(newPoint);
node.x = newPoint.x;
node.y = newPoint.y;
// for (var i = 0; i < 20; i++) {
// var forces = sumForcesToSample(node, sample, sampleCache);
// // console.log(forces);
// node.x += forces.x;
// node.y += forces.y;
// }
}
function centerPoint(angle, radius, posX, posY) {
var x = posX + Math.cos(toRadians(angle) * radius);
var y = posY + Math.sin(toRadians(angle) * radius);
return {
x: x,
y: y
};
}
function toRadians(degrees) {
return degrees * (Math.PI / 180);
}
function sumDistToSample(node, point, sample, sampleCache) {
var total = 0.0;
// console.log(total, sample);
for (var i = 0; i < sample.length; i++) {
var s = sample[i];
var realDist = Math.hypot(s.x - point.x, s.y - point.y);
var desDist = sampleCache[i];
total += Math.abs(realDist - desDist);
}
return total;
}
function sumForcesToSample(node, sample, sampleCache) {
var x = 0,
y = 0,
// len = 0,
dist = 0,
force,
SPRING_FORCE = 0.7;
for (var i = 0, unitX, unitY; i < sample.length; i++) {
var s = sample[i];
if (s !== node) {
unitX = s.x - node.x;
unitY = s.y - node.y;
// Normalize coordinates
len = Math.sqrt(unitX * unitX + unitY * unitY);
unitX /= len;
unitY /= len;
console.log(unitX, unitY);
var realDist = Math.sqrt(unitX * unitX + unitY * unitY);
var desDist = sampleCache[i];
dist += realDist - desDist;
force = (SPRING_FORCE * dist);
x += unitX * force;
y += unitY * force;
}
x *= (1.0 / sample.length);
y *= (1.0 / sample.length);
return {
x: x,
y: y
};
}
}
function createRandomSample(nodes, max, size) {
var randElements = [];
for (var i = 0; i < size; ++i) {
// Stop when no new elements can be found.
if (randElements.size >= nodes.length) {
break;
}
var rand = Math.floor((Math.random() * max));
// If the rand is already in random list or in exclude list
// ignore it and get a new value.
while (randElements.includes(rand)) {
rand = Math.floor((Math.random() * max));
}
randElements.push(nodes[rand]);
}
return randElements;
}
function binarySearch(lb, hb, x, y, r, node, sample, sampleCache) {
while (lb <= hb) {
var mid = Math.round((lb + hb) / 2);
if ((mid === lb) || (mid === hb)) {
if (sumDistToSample(node, centerPoint(lb, r, x, y), sample, sampleCache) >=
sumDistToSample(node, centerPoint(hb, r, x, y), sample, sampleCache)) {
return hb;
} else {
return lb;
}
} else {
var distMidLeft = sumDistToSample(node, centerPoint(mid + 1, r, x, y), sample, sampleCache);
var distMidRight = sumDistToSample(node, centerPoint(mid - 1, r, x, y), sample, sampleCache);
var distMid = sumDistToSample(node, centerPoint(mid, r, x, y), sample, sampleCache);
if (distMid > distMidLeft) {
lb = mid + 1;
} else if (distMid > distMidRight) {
hb = mid - 1;
} else {
return mid;
}
}
}
return -1;
}

6
src/jiggle.js Normal file
View File

@@ -0,0 +1,6 @@
/**
* @return {number} a random number.
*/
export default function() {
return (Math.random() - 0.5) * 1e-6;
}

150
src/link.js Normal file
View File

@@ -0,0 +1,150 @@
import constant from "./constant";
import jiggle from "./jiggle";
import {map} from "d3-collection";
/**
* Extended link force algorithm to include the stress metric for
* comparisons between the different algorithms.
* Everything else is the same as in D3 force module.
*/
function index(d, i) {
return i;
}
function find(nodeById, nodeId) {
var node = nodeById.get(nodeId);
if (!node) throw new Error("missing: " + nodeId);
return node;
}
export default function(links) {
var id = index,
strength = defaultStrength,
strengths,
distance = constant(30),
distances,
nodes,
count,
bias,
iterations = 1;
if (links == null) links = [];
function defaultStrength(link) {
return 1 / Math.min(count[link.source.index], count[link.target.index]);
}
function force(alpha) {
for (var k = 0, n = links.length; k < iterations; ++k) {
for (var i = 0, link, source, target, x, y, l, b; i < n; ++i) {
link = links[i], source = link.source, target = link.target;
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 - distances[i]) / l * alpha/* * strengths[i]*/;
x *= l, y *= l;
target.vx -= x * (b = bias[i]);
target.vy -= y * b;
source.vx += x * (b = 1 - b);
source.vy += y * b;
}
}
}
function initialize() {
if (!nodes) return;
var i,
n = nodes.length,
m = links.length,
nodeById = map(nodes, id),
link;
for (i = 0, count = new Array(n); i < n; ++i) {
count[i] = 0;
}
for (i = 0; i < m; ++i) {
link = links[i], link.index = i;
if (typeof link.source !== "object") link.source = find(nodeById, link.source);
if (typeof link.target !== "object") link.target = find(nodeById, link.target);
++count[link.source.index], ++count[link.target.index];
}
for (i = 0, bias = new Array(m); i < m; ++i) {
link = links[i], bias[i] = count[link.source.index] / (count[link.source.index] + count[link.target.index]);
}
strengths = new Array(m), initializeStrength();
distances = new Array(m), initializeDistance();
}
function initializeStrength() {
if (!nodes) return;
for (var i = 0, n = links.length; i < n; ++i) {
strengths[i] = +strength(links[i], i, links);
}
}
function initializeDistance() {
if (!nodes) return;
for (var i = 0, n = links.length; i < n; ++i) {
distances[i] = +distance(links[i], i, links);
}
}
/**
* Calculates the stress. Basically, it computes the difference between
* high dimensional distance and real distance. The lower the stress is,
* the better layout.
* @return {number} - stress of the layout.
*/
function getStress() {
var m = links.length,
totalDiffSq = 0,
totalHighDistSq = 0,
link;
for (var i = 0, source, target, realDist, highDist; i < m; i++) {
link = links[i], source = link.source, target = link.target;
realDist = Math.hypot(target.x-source.x, target.y-source.y);
highDist = distances[i];
totalDiffSq += Math.pow(realDist-highDist, 2);
totalHighDistSq += highDist * highDist;
}
return Math.sqrt(totalDiffSq/totalHighDistSq);
}
force.initialize = function(_) {
nodes = _;
initialize();
};
force.links = function(_) {
return arguments.length ? (links = _, initialize(), force) : links;
};
force.id = function(_) {
return arguments.length ? (id = _, force) : id;
};
force.iterations = function(_) {
return arguments.length ? (iterations = +_, force) : iterations;
};
force.strength = function(_) {
return arguments.length ? (strength = typeof _ === "function" ? _ : constant(+_), initializeStrength(), force) : strength;
};
force.distance = function(_) {
return arguments.length ? (distance = typeof _ === "function" ? _ : constant(+_), initializeDistance(), force) : distance;
};
force.stress = function() {
return getStress();
}
return force;
}

257
src/neighbourSampling.js Normal file
View File

@@ -0,0 +1,257 @@
import constant from "./constant";
import jiggle from "./jiggle";
/**
* Set the node id accessor to the specified i.
* @param {node} d - node.
* @param {accessor} i - id accessor.
* @return {accessor} - node id accessor.
*/
function index(d, i) {
return i;
}
/**
* The implementation of Chalmers' 1996 Neighbour and Sampling algorithm.
* It uses random sampling to find the most suited neighbours from the
* data set.
* @return {force} calculated forces.
*/
export default function () {
var id = index,
neighbours = [],
samples = new Array(),
distance = constant(300),
nodes,
neighbourSize = 6,
sampleSize = 3,
freeness = 0.85,
springForce = 0.7,
dampingFactor = 0.3,
velocity,
multiplier = 50;
/**
* Calculates the forces at each iteration between the node and the
* objects in neighbour and sample sets.
* @param {number} alpha - controls the stopping of the
* particle simulations.
*/
function force(alpha) {
velocity = 0;
for (var i = 0, n = nodes.length; i < n; ++i) {
// Randomize the samples for every node.
samples[i] = randomizeSample(i);
// Calculate the forces between node and its neighbours.
for (var [keyN, valueN] of neighbours[i]) {
setVelocity(i, keyN, valueN, alpha);
}
// Calculate the forces between node and its sample set.
for (var [keyS, valueS] of samples[i]) {
setVelocity(i, keyS, valueS, alpha);
}
// Check if there are a better neighbours in a sample array
// for each node.
findNewNeighbours(i);
}
}
/**
* Set the velocities of the source and target nodes.
* @param {number} sourceId - source node id.
* @param {number} targetId - target node id.
* @param {number} dist - high dimensional distance between
* the two nodes.
* @param {number} alpha - controls the speed of simulation.
*/
function setVelocity(sourceId, targetId, dist, alpha) {
var source, target, x, y, l;
source = nodes[sourceId], target = nodes[targetId];
// If x or y coordinates not defined, add some randomness.
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 * multiplier) / l * alpha;
x *= l, y *= l;
velocity += x + y;
// Set the calculated velocites for both nodes.
target.vx -= x;
target.vy -= y;
source.vx += x;
source.vy += y;
}
/**
* Initialize the neighbour and sample set at the start.
*/
function initialize() {
if (!nodes) return;
// Initialize for each node a neighbour and sample arrays
// with random values.
for (var i = 0, n = nodes.length; i < n; ++i) {
var exclude = []; // Array that keeps the indices of nodes to ignore.
exclude.push(i);
var neighbs = createRandomSample(i, exclude, n, neighbourSize);
// Sort the neighbour set by the distances.
neighbs = new Map([...neighbs.entries()].sort(sortDistances));
neighbours[i] = neighbs;
exclude.concat(neighbs);
samples[i] = createRandomSample(i, exclude, n, sampleSize);
}
}
/**
* Function that compares to map elements by its values.
* @param {object} a
* @param {object} b
* @return {number} - 0, if values are equal, positive number if b > a,
* negative otherwise.
*/
function sortDistances(a, b) {
return b[1] - a[1];
}
/**
* Create an array of random integers, all different, with maximum
* value max and with size elements. No elements from exlucde should
* be included.
* @param {number} index - index of current node.
* @param {array} exclude - indices of the nodes to ignore.
* @param {number} max - maximum value.
* @param {number} size - the number of elements in map to return.
* @return {map} - a created map that contains random elements from
* data set.
*/
function createRandomSample(index, exclude, max, size) {
var randElements = new Map();
for (var i = 0; i < size; ++i) {
// Stop when no new elements can be found.
if (randElements.size + exclude.length >= nodes.length) {
break;
}
var rand = Math.floor((Math.random() * max));
// If the rand is already in random list or in exclude list
// ignore it and get a new value.
while (randElements.has(rand) || exclude.includes(rand)) {
rand = Math.floor((Math.random() * max));
}
randElements.set(rand, +distance(nodes[index], nodes[rand]));
}
return randElements;
}
/**
* Creates a new map of random numbers to be used by the samples list.
* @param {number} index - index of current node.
* @return {map} - map that contains random elements from data set.
*/
function randomizeSample(index) {
// Ignore the current neighbours of the node and itself.
var exclude = [index];
exclude = exclude.concat(Array.from(neighbours[index].keys()));
return createRandomSample(index, exclude, nodes.length, sampleSize);
}
/**
* Compares the elements from sample set to the neighbour set and
* replaces the elements from neighbour set if better neighbours are
* found in sample set.
* @param {number} index - index of current node.
*/
function findNewNeighbours(index) {
var sample = samples[index];
for (var [key, value] of sample) {
var neighbMax = neighbours[index].entries().next().value;
// Check if a value from sample could be a better neighbour
// if so, replace it.
if (value < neighbMax[1]) {
neighbours[index].delete(neighbMax[0]);
neighbours[index].set(key, value)
neighbours[index] = new Map([...neighbours[index].entries()].sort(sortDistances));
}
}
}
/**
* Calculates the stress. Basically, it computes the difference between
* high dimensional distance and real distance. The lower the stress is,
* the better layout.
* @return {number} - stress of the layout.
*/
function getStress() {
var totalDiffSq = 0, totalHighDistSq = 0;
for (var i = 0, source, target, realDist, highDist; i < nodes.length; i++) {
for (var j = 0; j < nodes.length; j++) {
if (i !== j) {
source = nodes[i], target = nodes[j];
realDist = Math.hypot(target.x - source.x, target.y - source.y);
highDist = +distance(source, target);
totalDiffSq += Math.pow(realDist - highDist, 2);
totalHighDistSq += highDist * highDist;
}
}
}
return Math.sqrt(totalDiffSq / totalHighDistSq);
}
/**
* Calculates the average velocity of the force calculation at the
* current iteration.
* @return {number} - average velocity.
*/
function getAvgVelocity() {
return velocity / ((neighbourSize + sampleSize) * nodes.length);
}
// API for initializing the algorithm, setting parameters and querying
// metrics.
force.initialize = function (_) {
nodes = _;
initialize();
};
force.id = function (_) {
return arguments.length ? (id = _, force) : id;
};
force.neighbourSize = function (_) {
return arguments.length ? (neighbourSize = +_, force) : neighbourSize;
};
force.sampleSize = function (_) {
return arguments.length ? (sampleSize = +_, force) : sampleSize;
};
force.distance = function (_) {
return arguments.length ? (distance = typeof _ === "function" ? _ : constant(+_), force) : distance;
};
force.stress = function () {
return getStress();
};
force.velocity = function () {
return getAvgVelocity();
};
force.freeness = function (_) {
return arguments.length ? (freeness = +_, force) : freeness;
};
force.nodeNeighbours = function (_) {
return arguments.length ? neighbours[+_] : [];
};
force.multiplier = function (_) {
return arguments.length ? (multiplier = +_, force) : multiplier;
};
return force;
}

View File

@@ -0,0 +1,300 @@
import constant from "./constant";
import jiggle from "./jiggle";
/**
* Set the node id accessor to the specified i.
* @param {node} d - node.
* @param {accessor} i - id accessor.
* @return {accessor} - node id accessor.
*/
function index(d, i) {
return i;
}
/**
* The implementation of Chalmers' 1996 Neighbour and Sampling algorithm.
* It uses random sampling to find the most suited neighbours from the
* data set.
* @return {force} calculated forces.
*/
export default function () {
var id = index,
neighbours = [],
samples = new Array(),
distance = constant(300),
distanceRange = 10,
nodes,
neighbourSize = 6,
sampleSize = 3,
freeness = 0.85,
springForce = 0.7,
dampingFactor = 0.3,
velocity,
multiplier = 50;
/**
* Calculates the forces at each iteration between the node and the
* objects in neighbour and sample sets.
* @param {number} alpha - controls the stopping of the
* particle simulations.
*/
function force(alpha) {
velocity = 0;
for (var i = 0, n = nodes.length; i < n; ++i) {
// Randomize the samples for every node.
samples[i] = randomizeSample(i);
// Calculate the forces between node and its neighbours.
for (var [keyN, valueN] of neighbours[i]) {
setVelocity(i, keyN, valueN, alpha);
}
// Calculate the forces between node and its sample set.
for (var [keyS, valueS] of samples[i]) {
setVelocity(i, keyS, valueS, alpha);
}
// Check if there are a better neighbours in a sample array
// for each node.
findNewNeighbours(i);
}
}
/**
* Set the velocities of the source and target nodes.
* @param {number} sourceId - source node id.
* @param {number} targetId - target node id.
* @param {number} dist - high dimensional distance between
* the two nodes.
* @param {number} alpha - controls the speed of simulation.
*/
function setVelocity(sourceId, targetId, dist, alpha) {
var source, target, x, y, l;
source = nodes[sourceId], target = nodes[targetId];
// If x or y coordinates not defined, add some randomness.
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 * multiplier) / l * alpha;
x *= l, y *= l;
velocity += x + y;
// Set the calculated velocites for both nodes.
target.vx -= x;
target.vy -= y;
source.vx += x;
source.vy += y;
}
/**
* Initialize the neighbour and sample set at the start.
*/
function initialize() {
if (!nodes) return;
// Initialize for each node a neighbour and sample arrays
// with random values.
for (var i = 0, n = nodes.length; i < n; ++i) {
var exclude = []; // Array that keeps the indices of nodes to ignore.
exclude.push(i);
var neighbs = createRandomNeighbours(i, exclude, n, neighbourSize);
// Sort the neighbour set by the distances.
neighbs = new Map([...neighbs.entries()].sort(sortDistances));
neighbours[i] = neighbs;
exclude.concat(neighbs);
samples[i] = createRandomSample(i, exclude, n, sampleSize);
}
}
/**
* Function that compares to map elements by its values.
* @param {object} a
* @param {object} b
* @return {number} - 0, if values are equal, positive number if b > a,
* negative otherwise.
*/
function sortDistances(a, b) {
return b[1] - a[1];
}
/**
* Create an array of random integers, all different, with maximum
* value max and with size elements. No elements from exlucde should
* be included.
* @param {number} index - index of current node.
* @param {array} exclude - indices of the nodes to ignore.
* @param {number} max - maximum value.
* @param {number} size - the number of elements in map to return.
* @return {map} - a created map that contains random elements from
* data set.
*/
function createRandomNeighbours(index, exclude, max, size) {
var randElements = new Map();
var triedElements = 0;
while ((randElements.size < size) && (randElements.size + exclude.length + triedElements < nodes.length)) {
var rand = Math.floor((Math.random() * max));
// If the rand is already in random list or in exclude list
// ignore it and get a new value.
while (randElements.has(rand) || exclude.includes(rand)) {
rand = Math.floor((Math.random() * max));
}
var dist = +distance(nodes[index], nodes[rand]);
if (dist <= distanceRange) {
randElements.set(rand, dist);
} else {
triedElements++;
}
}
return randElements;
}
function createRandomSample(index, exclude, max, size) {
var randElements = new Map();
for (var i = 0; i < size; ++i) {
// Stop when no new elements can be found.
if (randElements.size + exclude.length >= nodes.length) {
break;
}
var rand = Math.floor((Math.random() * max));
// If the rand is already in random list or in exclude list
// ignore it and get a new value.
while (randElements.has(rand) || exclude.includes(rand)) {
rand = Math.floor((Math.random() * max));
}
randElements.set(rand, +distance(nodes[index], nodes[rand]));
}
return randElements;
}
/**
* Creates a new map of random numbers to be used by the samples list.
* @param {number} index - index of current node.
* @return {map} - map that contains random elements from data set.
*/
function randomizeSample(index) {
// Ignore the current neighbours of the node and itself.
var exclude = [index];
exclude = exclude.concat(Array.from(neighbours[index].keys()));
return createRandomSample(index, exclude, nodes.length, sampleSize);
}
/**
* Compares the elements from sample set to the neighbour set and
* replaces the elements from neighbour set if better neighbours are
* found in sample set.
* @param {number} index - index of current node.
*/
function findNewNeighbours(index) {
var sample = samples[index];
if (neighbours[index].size > 0) {
for (var [key, value] of sample) {
var neighbMax = neighbours[index].entries().next().value;
// Check if a value from sample could be a better neighbour
// if so, replace it.
if (value < neighbMax[1] && value <= distanceRange) {
neighbours[index].delete(neighbMax[0]);
neighbours[index].set(key, value)
neighbours[index] = new Map([...neighbours[index].entries()].sort(sortDistances));
}
}
}
}
/**
* Calculates the stress. Basically, it computes the difference between
* high dimensional distance and real distance. The lower the stress is,
* the better layout.
* @return {number} - stress of the layout.
*/
function getStress() {
var totalDiffSq = 0, totalHighDistSq = 0;
for (var i = 0, source, target, realDist, highDist; i < nodes.length; i++) {
for (var j = 0; j < nodes.length; j++) {
if (i !== j) {
source = nodes[i], target = nodes[j];
realDist = Math.hypot(target.x - source.x, target.y - source.y);
highDist = +distance(source, target) * multiplier;
totalDiffSq += Math.pow(realDist - highDist, 2);
totalHighDistSq += highDist * highDist;
}
}
}
return Math.sqrt(totalDiffSq / totalHighDistSq);
}
/**
* Calculates the average velocity of the force calculation at the
* current iteration.
* @return {number} - average velocity.
*/
function getAvgVelocity() {
return velocity / ((neighbourSize + sampleSize) * nodes.length);
}
function getDistributionData() {
var d = [];
for (var i = 0; i < nodes.length; i++) {
d.push({ "index": i, "size": neighbours[i].size });
}
return { "maxSize": neighbourSize, "l": nodes.length, "distribution": d };
}
// API for initializing the algorithm, setting parameters and querying
// metrics.
force.initialize = function (_) {
nodes = _;
initialize();
};
force.id = function (_) {
return arguments.length ? (id = _, force) : id;
};
force.neighbourSize = function (_) {
return arguments.length ? (neighbourSize = +_, force) : neighbourSize;
};
force.sampleSize = function (_) {
return arguments.length ? (sampleSize = +_, force) : sampleSize;
};
force.distance = function (_) {
return arguments.length ? (distance = typeof _ === "function" ? _ : constant(+_), force) : distance;
};
force.stress = function () {
return getStress();
};
force.velocity = function () {
return getAvgVelocity();
};
force.freeness = function (_) {
return arguments.length ? (freeness = +_, force) : freeness;
};
force.distanceRange = function (_) {
return arguments.length ? (distanceRange = +_, force) : distanceRange;
};
force.multiplier = function (_) {
return arguments.length ? (multiplier = +_, initialize(), force) : multiplier;
};
force.nodeNeighbours = function (_) {
return arguments.length ? neighbours[+_] : neighbours;
};
force.distributionData = function () {
return getDistributionData();
};
return force;
}

197
src/neighbourSamplingPre.js Normal file
View File

@@ -0,0 +1,197 @@
import constant from "./constant";
import jiggle from "./jiggle";
/**
* Set the node id accessor to the specified i.
* @param {node} d - node.
* @param {accessor} i - id accessor.
* @return {accessor} - node id accessor.
*/
function index(d, i) {
return i;
}
/**
* The implementation of Chalmers' 1996 Neighbour and Sampling algorithm.
* It uses random sampling to find the most suited neighbours from the
* data set.
* @return {force} calculated forces.
*/
export default function () {
var id = index,
neighbours = [],
worst = [],
samples = new Array(),
distance = constant(300),
nodes,
neighbourSize = 6,
sampleSize = 3,
freeness = 0.85,
springForce = 0.7,
dampingFactor = 0.3;
/**
* Calculates the forces at each iteration between the node and the
* objects in neighbour and sample sets.
* @param {number} alpha - controls the stopping of the
* particle simulations.
*/
function force(alpha) {
for (var i = 0, n = nodes.length; i < n; ++i) {
// Randomize the samples for every node.
// Calculate the forces between node and its neighbours.
for (var [keyN, valueN] of neighbours[i]) {
setVelocity(i, keyN, valueN, alpha);
}
// Calculate the forces between node and its sample set.
// for (var [keyS, valueS] of worst[i]) {
// setVelocity(i, keyS, valueS, alpha);
// }
}
}
/**
* Set the velocities of the source and target nodes.
* @param {number} sourceId - source node id.
* @param {number} targetId - target node id.
* @param {number} dist - high dimensional distance between
* the two nodes.
* @param {number} alpha - controls the speed of simulation.
*/
function setVelocity(sourceId, targetId, dist, alpha) {
var source, target, x, y, l;
source = nodes[sourceId], target = nodes[targetId];
// If x or y coordinates not defined, add some randomness.
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 * alpha;
x *= l, y *= l;
// Set the calculated velocites for both nodes.
target.vx -= x;
target.vy -= y;
source.vx += x;
source.vy += y;
}
/**
* Initialize the neighbour and sample set at the start.
*/
function initialize() {
if (!nodes) return;
findNeighbours();
}
/**
* Function that compares to map elements by its values.
* @param {object} a
* @param {object} b
* @return {number} - 0, if values are equal, positive number if b > a,
* negative otherwise.
*/
function sortDistances(a, b) {
return b[1] - a[1];
}
function findNeighbours() {
// Initialize for each node a neighbour and sample arrays
// with random values.
for (var i = 0, n = nodes.length; i < n; ++i) {
neighbours[i] = new Map();
for (var j = 0; j < n; j++) {
if (i !== j) {
var dist = +distance(nodes[i], nodes[j]);
if (neighbours[i].size < neighbourSize) {
neighbours[i].set(j, dist);
neighbours[i] = new Map([...neighbours[i].entries()].sort(sortDistances));
} else {
var neighbMax = neighbours[i].entries().next().value;
if (dist < neighbMax[1]) {
neighbours[i].delete(neighbMax[0]);
neighbours[i].set(j, dist);
neighbours[i] = new Map([...neighbours[i].entries()].sort(sortDistances));
}
}
}
}
}
}
/**
* Calculates the stress. Basically, it computes the difference between
* high dimensional distance and real distance. The lower the stress is,
* the better layout.
* @return {number} - stress of the layout.
*/
function getStress() {
var totalDiffSq = 0, totalHighDistSq = 0;
for (var i = 0, source, target, realDist, highDist; i < nodes.length; i++) {
for (var j = 0; j < nodes.length; j++) {
if (i !== j) {
source = nodes[i], target = nodes[j];
realDist = Math.hypot(target.x - source.x, target.y - source.y);
highDist = +distance(source, target);
totalDiffSq += Math.pow(realDist - highDist, 2);
totalHighDistSq += highDist * highDist;
}
}
}
return Math.sqrt(totalDiffSq / totalHighDistSq);
}
/**
* Calculates the average velocity of the force calculation at the
* current iteration.
* @return {number} - average velocity.
*/
function getAvgVelocity() {
return velocity / ((neighbourSize + sampleSize) * nodes.length);
}
// API for initializing the algorithm, setting parameters and querying
// metrics.
force.initialize = function (_) {
nodes = _;
initialize();
};
force.id = function (_) {
return arguments.length ? (id = _, force) : id;
};
force.neighbourSize = function (_) {
return arguments.length ? (neighbourSize = +_, force) : neighbourSize;
};
force.sampleSize = function (_) {
return arguments.length ? (sampleSize = +_, force) : sampleSize;
};
force.distance = function (_) {
return arguments.length ? (distance = typeof _ === "function" ? _ : constant(+_), force) : distance;
};
force.stress = function () {
return getStress();
};
force.velocity = function () {
return getAvgVelocity();
};
force.freeness = function (_) {
return arguments.length ? (freeness = +_, force) : freeness;
};
force.nodeNeighbours = function (_) {
return arguments.length ? neighbours[+_] : [];
};
return force;
}

374
src/t-sne.js Normal file
View File

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

13
src/xy.js Normal file
View File

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