//
//Created by Tim on 11/10/2016.
//
// Common variables and functions used by all of the example plots
var width = document.getElementById('plotArea').offsetWidth; //Width of each plot
var height = document.getElementById('plotArea').offsetHeight; //Height of each plot
var padding = 30; //Buffer space to ensure points are adequately
//Initialize the data
var filename = 'data.json';
var locationData = [];
var complexType;
var selectedNodes = [];
var newZscale = 1;
var newxScale, newyScale;
var linew = 4;
var pad = padding;
var cechFaces = [];
var cechEdges = [];
var ripsFaces = [];
var ripsEdges = [];
var allEdges = [];
var dataMin = 0;
var distances = [];
var numSamples = 0; //Number of points to use
var complexRadius = 20; //epsilon ball radius
var dataRadius = 10; //radius of uncertainty
var numPoints = 8; //number of possible data locations per node
var originalDataRadius = 10;
//background grid information
var cellSize = 50;
var gridWidth = Math.ceil( (width+padding*2) / cellSize);
var gridHeight = Math.ceil( (height+padding*2) / cellSize);
var grid = new Array(gridWidth * gridHeight);
var wasDragged = false;
var zoomOn = false;
//Construct the main plot area and add gridlines
var complexSVG = d3.select("#plotArea").append('svg')
.attr("class", "cech")
.attr("id", "complexSVG")
.attr("width", width+padding*2)
.attr("height", height+padding*2)
.style("margin", "auto")
.style("border", "1px solid black");
var xScale = d3.scaleLinear()
.domain([0,100])
.range([0, width]);
var xAxis = d3.axisTop()
.scale(xScale);
var gX = complexSVG.append('g')
.attr('transform','translate('+padding+','+padding+')')
.call(xAxis);
var yScale = d3.scaleLinear()
.domain([0,100])
.range([0, height]);
var yAxis = d3.axisLeft()
.scale(yScale);
var gY = complexSVG.append('g')
.attr('transform','translate('+padding+','+padding+')')
.call(yAxis);
var yellowRedScale = ["#ffffb2", "#fed976", "#feb24c", "#fd8d3c", "#fc4e2a", "#e31a1c", "#b10026"];
var yellowBlueScale = ["#ffffcc", "#c7e9b4", "#7fcdbb", "#41b6c4", "#1d91c0", "#225ea8", "#0c2c84"];
var bluePurpleScale = ["#edf8fb", "#bfd3e6", "#9ebcda", "#8c96c6", "#8c6bb1", "#88419d", "#6e016b"];
var redScale = ["#fef0d9", "#fdd49e", "#fdbb84", "#fc8d59", "#ef6548", "#d7301f", "#990000"];
var greenScale = ["#edf8fb", "#ccece6", "#99d8c9", "#66c2a4", "#41ae76", "#238b45", "#005824"];
var redEdgeScale = ["#fdbb84", "#fc8d59", "#ef6548", "#d7301f", "#990000"];
var blueGreenEdgeScale = ["#a6bddb", "#67a9cf", "#3690c0", "#02818a", "#016450"];
var pinkPurpleEdgeScale = ["#feebe2", "#fcc5c0", "#fa9fb5", "#f768a1", "#dd3497"];
var greenBlueEdgeScale = ["#7fcdbb", "#41b6c4", "#1d91c0", "#225ea8", "#0c2c84"];
var pinkEdgeScale = ["#c994c7", "#df65b0", "#e7298a", "#ce1256", "#91003f"];
var faceYellowBlueScale = d3.scaleOrdinal().range(yellowBlueScale).domain([0.01, 0.17, 0.34, .51, .68, .85, 1]);
var faceYellowRedScale = d3.scaleOrdinal().range(yellowRedScale).domain([0.01, 0.17, 0.34, .51, .68, .85, 1]);
var faceBluePurpleScale = d3.scaleOrdinal().range(bluePurpleScale).domain([0.01, 0.17, 0.34, .51, .68, .85, 1]);
var faceRedScale = d3.scaleOrdinal().range(redScale).domain([0.01, 0.17, 0.34, .51, .68, .85, 1]);
var faceGreenScale = d3.scaleOrdinal().range(greenScale).domain([0.01, 0.17, 0.34, .51, .68, .85, 1]);
var faceColorScale = faceYellowBlueScale;
var edgeRedScale = d3.scaleOrdinal().range(redEdgeScale).domain([0.01, 0.25, 0.5, 0.75, 1]);
var edgeBlueGreenScale = d3.scaleOrdinal().range(blueGreenEdgeScale).domain([0.01, 0.25, 0.5, 0.75, 1]);
var edgePinkPurpleScale = d3.scaleOrdinal().range(pinkPurpleEdgeScale).domain([0.01, 0.25, 0.5, 0.75, 1]);
var edgeGreenBlueScale = d3.scaleOrdinal().range(greenBlueEdgeScale).domain([0.01, 0.25, 0.5, 0.75, 1]);
var edgePinkScale = d3.scaleOrdinal().range(pinkEdgeScale).domain([0.01, 0.25, 0.5, 0.75, 1]);
var edgeColorScale = edgeRedScale;
var edgeWidthScale = d3.scaleLinear().range([6, 6]).domain([0.01, 1]);
var complexCanvas = complexSVG.append('g')
.attr('class','cech')
.attr('id','complexCanvas');
complexSVG.append('rect')
.attr('x', padding)
.attr('y', padding)
.attr('width', width)
.attr('height', height)
.style('fill','none')
.style('stroke','#000')
.style('stroke-opacity',1);
var zoom = d3.zoom()
.scaleExtent([0.1, 10])
.on('zoom', zoomed);
var zoombox = complexSVG.append("rect")
.attr("width", width+padding*2)
.attr("height", height+padding*2)
.attr('id','zoomBox')
.style("fill", "none")
.style("pointer-events", "none")
.style('visibility','off')
.call(zoom);
var tooltip = d3.select("#plotArea").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
window.addEventListener('keydown', function (event) {
if (event.key=='z') {
if (zoomOn) {
d3.select('#zoomBox')
.attr('cursor','auto')
.style('pointer-events','none')
.style('visibility','off');
zoomOn = false;
} else {
d3.select('#zoomBox')
.attr('cursor','move')
.style('pointer-events','all')
.style('visibilty','on')
zoomOn = true;
}
}
});
renderGrid();
dataLoader('data/data.json')
createLegends();
d3.selection.prototype.moveToFront = function() {
return this.each(function(){
this.parentNode.appendChild(this);
});
};
function createLegends() {
/**
* Creates the legends for selecting colors. Calls createFaceLegend() and createEdgeLegend()
*/
createFaceLengend();
createEdgeLegend();
}
function createEdgeLegend() {
/**
* Creates edge legend based on selected color scale
*
*/
var edgeLegend = d3.select('#edge_legend');
edgeLegend.append("g")
.attr("class", "legendSizeLine")
.attr("transform", "translate(0, 20)");
var legendSizeLine = d3.legendSize()
.scale(edgeWidthScale)
.shape("line")
.orient("horizontal").labels(["0.01",
"0.25", "0.50", "0.75", "1.00"])
.labelWrap(30)
.shapeWidth(40)
.labelAlign("start")
.shapePadding(10);
edgeLegend.select(".legendSizeLine")
.call(legendSizeLine);
var lines = edgeLegend.selectAll("line");
lines.attr('stroke', function (d, i) {
if (i == 0) {
return edgeColorScale(0.01);
}
if (i == 1) {
return edgeColorScale(0.25);
}
if (i == 2) {
return edgeColorScale(0.5);
}
if (i == 3) {
return edgeColorScale(0.75);
}
if (i == 4) {
return edgeColorScale(1);
}
});
}
/**
* Create face legend based on selected color scale.
*/
function createFaceLengend() {
var legend = d3.select("#face_legend");
var legendAxis = d3.legendColor()
.shapeWidth(50)
.orient("horizontal")
.scale(faceColorScale);
legend.select(".legendSequential").call(legendAxis);
}
/**
* Change the edge or face color scale
* @param {string} selected Which color scale to use.
*/
function changeColorScale(selected) {
switch (selected) {
case "yellowBlue" :
faceColorScale = faceYellowBlueScale;
edgeColorScale = edgeRedScale;
break;
case "yellowRed" :
faceColorScale = faceYellowRedScale;
edgeColorScale = edgeBlueGreenScale;
break;
case "bluePurple" :
faceColorScale = faceBluePurpleScale;
edgeColorScale = edgePinkPurpleScale;
break;
case "red" :
faceColorScale = faceRedScale;
edgeColorScale = edgeGreenBlueScale;
break;
case "green" :
faceColorScale = faceGreenScale;
edgeColorScale = edgePinkScale;
break;
}
renderEdges();
renderAllEdges();
renderFaces();
renderView();
createFaceLengend();
createEdgeLegend();
}
/**
* Render gridlines for every tick mark on axes.
*/
function renderGrid() {
d3.select('#xlines').remove();
d3.select('#ylines').remove();
var xt = xAxis.scale().ticks();
var xticks = (newxScale) ?
xt.map( function (d) {
return newxScale(d) + padding;
}) :
xt.map( function (d) {
return xScale(d) + padding;
});
var xlines = complexSVG.append('g')
.attr('class','grid')
.attr('id','xlines');
xlines.selectAll('line').data(xticks)
.enter().append('line')
.attr('id','xline')
.attr('class','grid')
.attr('x1', function (d) { return d })
.attr('y1', padding)
.attr('x2', function (d) { return d })
.attr('y2', height+padding);
var yt = yAxis.scale().ticks();
var yticks = (newyScale) ?
yt.map( function (d) {
return newyScale(d) + padding;
}) :
yt.map( function (d) {
return yScale(d) + padding;
});
var ylines = complexSVG.append('g')
.attr('class','grid')
.attr('id','ylines');
ylines.selectAll('line').data(yticks)
.enter().append('line')
.attr('id','yline')
.attr('class','grid')
.attr('y1', function (d) { return d })
.attr('x1', padding)
.attr('y2', function (d) { return d })
.attr('x2', width+padding);
}
/**
* Change scales and zoom plot area.
*/
function zoomed() {
complexCanvas.attr("transform", d3.event.transform)
newxScale = d3.event.transform.rescaleX(xScale);
newyScale = d3.event.transform.rescaleY(yScale);
newZscale = d3.event.transform.k;
linew = 4/newZscale;
pad = padding/newZscale;
gX.call(xAxis.scale(newxScale));
gY.call(yAxis.scale(newyScale));
if (locationData.length != 0) {
if (d3.event.sourceEvent.type == 'wheel') {
d3.select('#complexPoints').selectAll('circle')
.attr('r', 5 / newZscale);
d3.select('#complexEdges').selectAll('line')
.style('stroke-width', 4 / newZscale);
}
if (d3.event.sourceEvent.type == 'wheel') {
renderPoints();
changeComplex();
}
}
renderGrid();
}
/**
* Called whenever the data are changed. Updates the coverage radius and recomputes simplicial complexes.
* @param newValue {number} The new coverage radius.
*/
function updateComplex(newValue) {
//update coverage radius and recompute complexes
//update slider value and/or tex value
complexRadius=+newValue;
d3.select('#complexRadius').node().value = complexRadius;
d3.select('#complexInput').node().value = complexRadius;
//adjust inner and outer coverage disks
var innerRadius = xScale(complexRadius - dataRadius + xScale.domain()[0]);
var outerRadius = xScale(complexRadius + dataRadius + xScale.domain()[0]);
d3.select('#complexCircles').selectAll('circle').attr('r', innerRadius);
d3.select('#complexDataCircle').selectAll('circle').attr('r', outerRadius);
//recompute complexes
var t = Date.now();
constructRips();
var t2 = Date.now() - t;
console.log('compute: ' + t2);
changeComplex();
var t3 = Date.now() - t - t2;
console.log('render: ' + t3)
}
//graphical highlighting
/**
* Highlights a data point on mouseover or when corresponding edge or face is highlighted.
*/
function highlightPoint() {
var pt = (arguments.length == 3) ? arguments[1] : arguments[0]
d3.select('#complex_Point_' + pt)
.transition()
.style('fill', '#c33');
if (document.getElementById('coverCheckbox').checked) {
//highlight the corresponding coverage circle
d3.select('#complex_Circle_' + pt)
.transition()
.style('fill', '#c33')
.style('fill-opacity', 0.25);
d3.select('#complex_Circle_' + pt).moveToFront();
d3.select('#data_Circle_' + pt)
.transition()
.style('fill', '#c33')
.style('fill-opacity', 0.1);
d3.select('#data_Circle_' + pt).moveToFront();
}
}
/**
* Resets data point to default color as defined by current color scale.
*/
function resetPoint() {
var pt = ( arguments.length == 3) ? arguments[1] : arguments[0];
d3.select('#complex_Point_' + pt)
.transition()
.style('fill', '#9370db');
if (document.getElementById('coverCheckbox').checked) {
d3.select('#complex_Circle_' + pt)
.transition()
.style('fill', '#9370db')
.style('fill-opacity', 0.25);
d3.select('#data_Circle_' + pt)
.transition()
.style('fill', '#9370db')
.style('fill-opacity', 0.1);
}
}
/**
* Displays probability of selected edge or face
* @param type {string} Must be 'Edge' or 'Face'
* @param data {number} The probability of the object.
*/
function showToolTip(type, data){
tooltip.transition()
.style("opacity", 0.9);
tooltip.html(type + " probability of " + data.toFixed(3))
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - 28) + "px");
}
/**
* Hides object probability.
*/
function hideToolTip(){
tooltip.transition()
.style("opacity", 0);
}
/**
* Highlight edge and it's corresponding points.
*/
function highlightEdge() {
var data = arguments[0];
if(this.className != "individual_edge" && data.hasOwnProperty("Pedge")){
showToolTip('Edge', data.Pedge);
}
if (arguments.length == 3) {
d3.select(this)
.transition()
.style('stroke','#c33');
highlightPoint(data.Pt1);
highlightPoint(data.Pt2);
} else {
d3.select(arguments[0])
.transition()
.style('stroke','#c33');
}
}
/**
* Reset edge to default color based on selected color scale.
*/
function resetEdge() {
hideToolTip();
var data;
if (arguments.length == 3) {
edge = d3.select(this)
data = arguments[0]
resetPoint(data.Pt1);
resetPoint(data.Pt2);
edge.transition()
.style('stroke', edgeColorScale(arguments[0].Pedge));
} else {
edge = d3.select(arguments[0]);
var points = arguments[0].replace(/#complex_Edge_/, '');
for (i = 0; i < ripsEdges.length; i++){
var possibleEdge = ripsEdges[i];
if(points == possibleEdge.Pt1 + "_" + possibleEdge.Pt2){
edge.transition()
.style('stroke', edgeColorScale(possibleEdge.Pedge));
return;
}
}
}
}
/**
* Highlight face along with it's corresponding edges and points.
*/
function highlightFace() {
var data = arguments[0];
if(data.hasOwnProperty("Pface")){
showToolTip('Face', data["Pface"]);
}
d3.select(this)
.transition()
.style('fill','#969696');
d3.select(this).moveToFront();
//highlight corresponding edges
highlightEdge('#complex_Edge_' + arguments[0].Pt1 + '_' + arguments[0].Pt2);
highlightEdge('#complex_Edge_' + arguments[0].Pt1 + '_' + arguments[0].Pt3);
highlightEdge('#complex_Edge_' + arguments[0].Pt2 + '_' + arguments[0].Pt3);
//highlight corresponding points
highlightPoint(arguments[0].Pt1);
highlightPoint(arguments[0].Pt2);
highlightPoint(arguments[0].Pt3);
}
//reset to default view
/**
* Reset face to default color based on selected color scale.
*/
function resetFace() {
hideToolTip();
if ( arguments.length > 1) {
d3.select(this)
.transition()
.style('fill', faceColorScale(arguments[0].Pface));
resetEdge('#complex_Edge_' + arguments[0].Pt1 + '_' + arguments[0].Pt2);
resetEdge('#complex_Edge_' + arguments[0].Pt1 + '_' + arguments[0].Pt3);
resetEdge('#complex_Edge_' + arguments[0].Pt2 + '_' + arguments[0].Pt3);
resetPoint(arguments[0].Pt1);
resetPoint(arguments[0].Pt2);
resetPoint(arguments[0].Pt3);
} else {
faces = complexType == 'Cech' ? cechFaces : ripsFaces;
d3.select('#complex_Face_'+faces[arguments[0]].Pt1+'_'+faces[arguments[0]].Pt2+'_'+faces[arguments[0]].Pt3)
.transition()
.style('fill', faceColorScale(faces[arguments[0]].Pface));
faces.forEach( function (d) {
d3.select('#complex_Face_'+d.Pt1+'_'+d.Pt2+'_'+d.Pt3)
.moveToFront()
})
}
}
/**
* Compute probability of edge (lines) for all n choose 2 permutations of locations.
* @returns {{edges: Array, edgeProb: Array}}
*/
function constructEdges() {
/**
*
* Test distance to determine if edge exists, squared to save computation time.
* @type {number}
*/
var sqDiameter = 4 * Math.pow(complexRadius, 2);
/**
*
* Minimum distance between nodes for which coverage can be guaranteed equal to the coverage radius minus the radius of uncertainty.
* @type {number}
*/
var sqDiameterMin = 4 * Math.pow(complexRadius-dataRadius, 2);
/**
*
* Maximum distance between nodes for which coverage is possible, equal to the coverage radius plus the radius of uncertainty.
* @type {number}
*/
var sqDiameterMax = 4 * Math.pow(complexRadius+dataRadius, 2);
var edgeProb = [];
var tempEdges = [];
var count, p, pFlag;
locationData.forEach( function (d) {
d.star = {edges: [], faces: []};
d.link = {points: [], edges: []}
})
for (i = 0; i < numSamples - 1; i++) {
x1 = locationData[i].anchor.x;
y1 = locationData[i].anchor.y;
edgeProb.push([0]);
for (j = i + 1; j < numSamples; j++) {
x2 = locationData[j].anchor.x;
y2 = locationData[j].anchor.y;
d12 = sqEuclidDist([x1, y1], [x2, y2]);
if (d12 <= sqDiameterMin) {
edgeProb[i].push({p: 1, edgeInd: tempEdges.length})
locationData[i].star.edges.push(tempEdges.length)
locationData[i].link.points.push([j])
locationData[j].star.edges.push(tempEdges.length)
locationData[j].link.points.push([i])
tempEdges.push({Pt1: i, Pt2: j, Pedge: 1})
} else if (d12 > sqDiameterMax){
edgeProb[i].push({p: 0})
} else {
count = 0;
pFlag = [];
iEdges = [];
for (m=0; m<numPoints; m++) {
x1 = locationData[i].points[m].x;
y1 = locationData[i].points[m].y;
for (n=0; n<numPoints; n++) {
x2 = locationData[j].points[n].x;
y2 = locationData[j].points[n].y;
d12 = sqEuclidDist([x1, y1],[x2,y2]);
if (d12 <= sqDiameter) {
count++
pFlag.push(true)
iEdges.push({Pt1: m, Pt2: n})
} else {
pFlag.push(false)
}
}
}
p = count/(numPoints*numPoints);
edgeProb[i].push({p: p, pFlag: pFlag, edgeInd: tempEdges.length})
if (p>0) {
locationData[i].star.edges.push(tempEdges.length)
locationData[i].link.points.push([j])
locationData[j].star.edges.push(tempEdges.length)
locationData[j].link.points.push([i])
tempEdges.push({Pt1: i, Pt2: j, Pedge: p, iEdges: iEdges})
}
}
}
}
/**
*
* Stores all individual edges for ease of rendering
* @type {Array}
*/
allEdges = [];
tempEdges.forEach( function(d) {
if (d.Pedge == 1) {
for (i=0; i<numPoints; i++) {
for (j=0; j<numPoints; j++) {
x1 = locationData[d.Pt1].points[i].x;
y1 = locationData[d.Pt1].points[i].y;
x2 = locationData[d.Pt2].points[j].x;
y2 = locationData[d.Pt2].points[j].y;
allEdges.push({x1: x1, y1: y1, x2: x2, y2: y2})
}
}
} else {
for (i=0; i<d.iEdges.length; i++) {
x1 = locationData[d.Pt1].points[d.iEdges[i].Pt1].x;
y1 = locationData[d.Pt1].points[d.iEdges[i].Pt1].y;
x2 = locationData[d.Pt2].points[d.iEdges[i].Pt2].x;
y2 = locationData[d.Pt2].points[d.iEdges[i].Pt2].y;
allEdges.push({x1: x1, y1: y1, x2: x2, y2: y2})
}
}
})
return {edges: tempEdges, edgeProb: edgeProb}
}
/**
* Compute Vietoris-Rips complex. Determine 2-faces (triangles) from location data. Computes a probability for each face.
* A face exists for each n choose 3 permutation of points for which each point is pairwise connected by an edge.
*/
function constructRips() {
var tempFaces = [];
ripsFaces = [];
var tmp = constructEdges();
ripsEdges = tmp.edges.slice();
var edgeProb = tmp.edgeProb.slice();
var faceProb = [];
for (i=0; i<numSamples-2; i++) {
for (j=i+1; j<numSamples-1; j++) {
if (edgeProb[i][j-i].p > 0) {
for (k=j+1; k<numSamples; k++) {
if (edgeProb[j][k-j].p == 1 && edgeProb[i][k-i].p == 1 && edgeProb[i][j-i].p == 1){
tempFaces.push({Pt1: i, Pt2: j, Pt3: k, Pface: 1})
faceProb.push(1)
} else if (edgeProb[j][k-j].p > 0 && edgeProb[i][k-i].p > 0){
tempFaces.push({Pt1: i, Pt2: j, Pt3: k, p12: edgeProb[i][j-i], p13: edgeProb[i][k-i], p23: edgeProb[j][k-j]})
faceProb.push(0)
}
}
}
}
}
tempFaces.forEach( function (d, ind) {
var count = 0;
var allFaces = [];
for (i=0; i<numPoints; i++) {
for (j=0; j<numPoints; j++) {
for (k=0; k<numPoints; k++) {
isEdge = [true, true, true];
if (faceProb[ind] == 0) {
if (d.p12.p < 1) {
isEdge[0] = d.p12.pFlag[i * numPoints + j] ? true : false;
}
if (d.p13.p < 1) {
isEdge[1] = d.p13.pFlag[i * numPoints + k] ? true : false;
}
if (d.p23.p < 1) {
isEdge[2] = d.p23.pFlag[j * numPoints + k] ? true : false;
}
}
if (isEdge[0] && isEdge[1] && isEdge[2]) {
allFaces.push([i, j, k])
count++
}
}
}
}
p = count/Math.pow(numPoints,3);
d.allFaces = allFaces;
if (p>0) {
d.Pface = p;
ripsFaces.push(d);
}
})
constructCech()
}
/**
* Construct the Čech complex. For each face determined from the Vietoris-Rips complex, test whether all three points
* have a common intersection. Computes a probability for each face.
*/
function constructCech() {
cechEdges = ripsEdges.slice();
var tempFaces = JSON.parse(JSON.stringify(ripsFaces));
cechFaces = [];
var sqDist;
//calculate the squared diameter to compare each pair to. Use square diameter to compare to squared euclidean distanct
//of each pair so save computation.
sqDiameter = 4 * Math.pow(complexRadius, 2);
tempFaces.forEach( function(d, i) {
d.Pface = 0;
count = 0;
for (j=0; j<d.allFaces.length; j++) {
x1 = locationData[d.Pt1].points[d.allFaces[j][0]].x;
y1 = locationData[d.Pt1].points[d.allFaces[j][0]].y;
x2 = locationData[d.Pt2].points[d.allFaces[j][1]].x;
y2 = locationData[d.Pt2].points[d.allFaces[j][1]].y;
x3 = locationData[d.Pt3].points[d.allFaces[j][2]].x;
y3 = locationData[d.Pt3].points[d.allFaces[j][2]].y;
d12 = sqEuclidDist([x1, y1], [x2, y2]);
d23 = sqEuclidDist([x2, y2], [x3, y3]);
d13 = sqEuclidDist([x1, y1], [x3, y3]);
//determine longest edge
if (d12 >= d13 && d12 >= d23) {
xc = (x2 + x1) / 2;
yc = (y2 + y1) / 2;
dist = Math.sqrt(sqEuclidDist([x3, y3], [xc, yc]));
testRadius = Math.sqrt(d12) / 2;
} else if (d13 >= d12 && d13 >= d23) {
xc = (x3 + x1) / 2;
yc = (y3 + y1) / 2;
dist = Math.sqrt(sqEuclidDist([x2, y2], [xc, yc]));
testRadius = Math.sqrt(d13) / 2;
} else {
xc = (x3 + x2) / 2;
yc = (y3 + y2) / 2;
dist = Math.sqrt(sqEuclidDist([x1, y1], [xc, yc]));
testRadius = Math.sqrt(d23) / 2;
}
if (dist <= testRadius) {
//determine if third point is within circumcircle of longest edge
count++
} else {
//otherwise determine if circumcircle radius is smaller than the coverage radius
a = Math.sqrt(d12);
b = Math.sqrt(d13);
c = Math.sqrt(d23);
testRadius = (a * b * c) / Math.sqrt((a + b + c) * (b + c - a) * (a + c - b) * (a + b - c));
if (testRadius <= complexRadius) {
count++
}
}
}
p = count/Math.pow(numPoints,3);
if (p > 0) {
d.Pface = p;
cechFaces.push(d)
}
})
}
/**
* Renders the probabilistic simplicial complexes generated by constructCech() or constructRips
* @param {array} edges - An object array containing the edges, must contain the following fields: Pt1, Pt2, Pedge
* @param {array} faces - An object array containing the faces, must contain the following fields: Pt1, Pt2, Pt3, Pface
*/
function renderComplex(edges,faces) {
if (edges.length==0) {
constructRips();
};
// edges.forEach( function (d,i) {
// d.star = {points: [], faces: []};
// d.link = {points: [], edges: [], faces: []}
// t = locationData[d.Pt1].star.faces.forEach( function (e) {
// var testArray = [faces[e].Pt1, faces[e].Pt2, faces[e].Pt3];
// if (testArray.indexOf(d.Pt2) != -1) {
// d.star.faces.push(e)
// } else {
// d.link.faces.push(e)
// }
// return
// })
//
// locationData[d.Pt2].star.faces.forEach( function (e) {
// if (d.star.faces.indexOf(e) == -1 && d.link.faces.indexOf(e) == -1) {
// d.link.faces.push(e)
// }
// })
// })
//remove existing canvas elements
complexCanvas.select('#complexFaces').remove();
complexCanvas.append('g')
.attr('id','complexFaces')
.style('visibility','hidden');
renderFaces();
complexCanvas.selectAll('#complexEdges').remove();
complexCanvas.append('g')
.attr('id','complexEdges')
.style('visibility','hidden');
renderEdges();
//render faces, give each an id with corresponding vertex indices. This makes it easier to find and highlight the corresponding
//points and edges, do the same for each edge. Start with everything hidden then render view according to what the user
//has selected
//Make sure points stay on top
pts = d3.select('#complexPoints').node();
pts.parentNode.appendChild(pts);
renderAllEdges();
renderView();
}
/**
* Renders edges for selected simplicial complex. Colors bundled edges based on probability.
*/
function renderEdges(){
var edges;
if (complexType=='Cech') {
edges = cechEdges;
} else if (complexType=='Vietoris-Rips') {
edges = ripsEdges;
}
complexCanvas.selectAll('.edge').remove();
var complexEdges = complexCanvas.select('g#complexEdges');
complexEdges.selectAll('line').data(edges)
.enter().append('line')
.attr('class', 'edge')
.style('stroke-width', function(d){
return edgeWidthScale(d.Pedge)/newZscale;
})
.attr('x1', function (d) {
return xScale(locationData[d.Pt1].anchor.x) + pad;
})
.attr('y1', function (d) {
return yScale(locationData[d.Pt1].anchor.y) + pad;
})
.attr('x2', function (d) {
return xScale(locationData[d.Pt2].anchor.x) + pad;
})
.attr('y2', function (d) {
return yScale(locationData[d.Pt2].anchor.y) + pad;
})
.attr('id', function (d) {
return 'complex_Edge_'+d.Pt1+'_'+d.Pt2;
})
.attr('stroke', function (d) {
return edgeColorScale(d.Pedge);
})
.on('mouseover', highlightEdge)
.on('mouseout', resetEdge);
}
/**
* Renders faces for selected simplicial complex. Colors faces based on probability.
*/
function renderFaces(){
var faces;
if (complexType=='Cech') {
faces = cechFaces;
} else if (complexType=='Vietoris-Rips') {
faces = ripsFaces;
}
faces.sort( function (a, b) { return a.Pface - b.Pface } )
faces.forEach( function (d, i) {
locationData[d.Pt1].link.edges.push([d.Pt2, d.Pt3])
locationData[d.Pt1].star.faces.push(i)
locationData[d.Pt2].link.edges.push([d.Pt1, d.Pt3])
locationData[d.Pt2].star.faces.push(i)
locationData[d.Pt3].link.edges.push([d.Pt1, d.Pt2])
locationData[d.Pt3].star.faces.push(i)
})
complexCanvas.selectAll('.face').remove();
var complexFaces = complexCanvas.select('g#complexFaces');
complexFaces.selectAll('polygon').data(faces)
.enter().append('polygon')
.attr('class','face')
.attr('points',function (d, i) {
return (xScale(locationData[d.Pt1].anchor.x)+padding/newZscale)+','+(yScale(locationData[d.Pt1].anchor.y)+padding/newZscale)+
' '+(xScale(locationData[d.Pt2].anchor.x)+padding/newZscale)+','+(yScale(locationData[d.Pt2].anchor.y)+padding/newZscale)+
' '+(xScale(locationData[d.Pt3].anchor.x)+padding/newZscale)+','+(yScale(locationData[d.Pt3].anchor.y)+padding/newZscale);
}
)
.attr('id', function (d, i) {
return 'complex_Face_'+d.Pt1+'_'+d.Pt2+'_'+d.Pt3;
})
.attr('fill', function (d) {
return faceColorScale(d.Pface);
})
.on('mouseover',highlightFace)
.on('mouseout', resetFace);
}
/**
* Render individual edges. These are the actual data points for each location that meet the coverage criteria (i.e. have overlapping coverage circles).
*/
function renderAllEdges(){
complexCanvas.selectAll('#allEdges').remove();
var allEdgesGroup = complexCanvas.append('g')
.attr('id','allEdges')
.attr('class', 'all_edges');
allEdgesGroup.selectAll('line').data(allEdges)
.enter().append('line')
.attr('class', 'individual_edge')
.attr('x1', function (d) {
return xScale(d.x1) + pad;
})
.attr('y1', function (d) {
return yScale(d.y1) + pad;
})
.attr('x2', function (d) {
return xScale(d.x2) + pad;
})
.attr('y2', function (d) {
return yScale(d.y2) + pad;
})
.attr('id', function (d) {
return 'complex_individual_Edge_'+d.x1+'_'+d.x2+d.y1+'_'+d.y2;
})
.attr('stroke', 'black');
}
/**
* Render all anchor points, individual points, coverage circles and radius of uncertainty.
*/
function renderPoints() {
//render each point and coverage circle. The id simply corresponds to its index within locationData
complexCanvas.selectAll('.circle').remove();
complexCanvas.selectAll('.point').remove();
var complexCircles = complexCanvas.append('g')
.attr('class','circle')
.attr('id','complexCircles')
var complexPoints = complexCanvas.append('g')
.attr('class', 'point')
.attr('id','complexPoints');
var complexAndDataCircle = complexCanvas.append('g')
.attr('class', 'circle')
.attr('id', 'complexDataCircle');
var pts = complexPoints.selectAll('circle').data(locationData)
.enter()
.append('circle')
.style('visibility','hidden')
.attr('class', 'point')
.attr('cx', function (d) {
if (newxScale && newyScale) {
return xScale(d.anchor.x) + padding/newZscale;
}
else {
return xScale(d.anchor.x) + padding;
}
})
.attr('cy', function (d) {
if (newxScale && newyScale) {
return yScale(d.anchor.y) + padding/newZscale;
}
else {
return yScale(d.anchor.y) + padding / newZscale;
}
})
.attr('id', function (d, i) {
return 'complex_Point_' + i.toString();
})
.attr('r', xScale(dataRadius + xScale.domain()[0]))
.on('click', selectNode)
.on('mouseover', highlightPoint)
.on('mouseout', resetPoint)
.call(d3.drag()
.on('drag', dragNode)
.on('end', dragEnd))
.each(function(d, j){
complexPoints.selectAll('small_circle').data(d.points)
.enter()
.append('circle')
.attr('class', 'small_circle')
.attr('cx', function (d) {
if (newxScale && newyScale) {
return xScale(d.x) + padding/newZscale;
}
else {
return xScale(d.x) + padding;
}
})
.attr('cy', function (d) {
if (newxScale && newyScale) {
return yScale(d.y) + padding/newZscale;
}
else {
return yScale(d.y) + padding / newZscale;
}
})
.attr('id', function (d, i) {5
return 'complex_small_Point_' + j.toString() + '_' + i.toString();
})
.attr('r', 2/newZscale);
});
complexCircles.selectAll('circle').data(locationData)
.enter()
.append('circle')
.style('visibility','hidden')
.attr('class', 'circle')
.attr('cx', function (d) {
return xScale(d.anchor.x) + padding/newZscale;
})
.attr('cy', function (d) {
return yScale(d.anchor.y) + padding/newZscale;
})
.attr('id', function (d, i) {
return 'complex_Circle_' + i.toString();
})
.attr('r', xScale(complexRadius-dataRadius + xScale.domain()[0]));
complexAndDataCircle.selectAll('circle').data(locationData)
.enter()
.append('circle')
.attr('class', 'circle')
.attr('cx', function (d) {
return xScale(d.anchor.x) + padding/newZscale;
})
.attr('cy', function (d) {
return yScale(d.anchor.y) + padding/newZscale;
})
.attr('id', function (d, i) {
return 'data_Circle_' + i.toString();
})
.attr('fill', '#9370db')
.attr('fill-opacity', 0.1)
.attr('r', xScale(dataRadius + complexRadius + xScale.domain()[0]));
// For plotting node labels (disable, only for troubleshooting)
//
// r = xScale(dataRadius + xScale.domain()[0])+5;
// textOffset = -r * Math.cos( 3*Math.PI/4 );
//
// complexPoints.selectAll('text')
// .data(locationData)
// .enter().append('text')
// .text( function (d, i) {
// return i.toString();
// })
// .attr('x', function (d) {
// return xScale(d.anchor.x) + padding/newZscale;
// })
// .attr('y', function (d) {
// return yScale(d.anchor.y) + padding/newZscale;
// })
// .attr('dx',textOffset)
// .attr('dy',textOffset);
renderView()
}
/**
* Render data. Checks the interface for which layers are to be displayed and calls the appropriate rendering functions.
*/
function renderView() {
//query the various view options toggle visibility of each "g" element accordingly
f = document.getElementById('coverCheckbox');
showCoverage(f.checked);
f = document.getElementById('nodeCheckbox');
show(f.checked,'.small_circle');
f = document.getElementById('nodeRadiusCheckbox');
show(f.checked,'.point');
f = document.getElementById('edgeCheckbox');
show(f.checked,'.edge');
f = document.getElementById('allEdgeCheckbox');
show(f.checked,'.individual_edge');
f = document.getElementById('faceCheckbox');
show(f.checked,'.face');
}
/**
* Update the display limits.
* @param xMin {number} The minimum x-value to display.
* @param xMax {number} The maximum x-value to display.
* @param yMin {number} The minimum y-value to display.
* @param yMax {number} The maximum y-value to display.
*/
function updateScales(xMin, xMax, yMin, yMax){
var aspectMin = Math.min(width, height);
var aspect, yScaleMax, xScaleMax;
if(aspectMin == height){
aspect = width / height;
yScaleMax = yMax;
xScaleMax = yScaleMax * aspect;
xScale.domain([xMin, xScaleMax]);
yScale.domain([yMin, yScaleMax]);
} else {
aspect = height / width;
xScaleMax = xMax;
yScaleMax = xScaleMax * aspect;
xScale.domain([xMin, xScaleMax]);
yScale.domain([yMin, yScaleMax]);
}
}
/**
* Import anchor locations from a CSV file.
* <br><br>
* Files must contain, at a minimum, a column labeled "xf" and a column labeled "yf" with scalar values corresponding
* to the anchor coordinates.
*/
function importData() {
//allow user to select file
var selectedFile = document.getElementById('fileSelector');
var fReader = new FileReader();
fReader.readAsDataURL(selectedFile.files[0]);
fReader.onloadend = function(event) {
d3.csv(event.target.result, function (csv) {
//read data into locationData array and update number of samples
locationData = [];
csv.forEach(function (d) {
// Convert numeric values to 'numbers'
locationData.push({anchor: {x: +d.xf, y: +d.yf} });
});
numSamples = locationData.length;
perturbData();
//set data scale
xMin = d3.min(locationData.map( function (d) {
return d.anchor.x;
}));
xMax = d3.max(locationData.map( function (d) {
return d.anchor.x;
}));
xRange = xMax-xMin;
yMin = d3.min(locationData.map( function (d) {
return d.anchor.y;
}));
yMax = d3.max(locationData.map( function (d) {
return d.anchor.y;
}));
yRange = yMax-yMin;
dataRange = d3.max([xRange, yRange]);
updateScales(xMin, xMax, yMin, yMax);
d3.select('#complexInput')
.attr('min', 0.05*dataRange)
.attr('max', 0.5*dataRange)
.attr('value', 0.2*dataRange);
complexCanvas.attr("transform", d3.zoomIdentity)
newxScale = false;
newyScale = false;
newZscale = 1;
gX.call(xAxis.scale(xScale));
gY.call(yAxis.scale(yScale));
renderGrid()
//reset to default view and calculate complexes
resetCheckboxes();
renderPoints();
updateComplex(document.getElementById('complexInput').value);
});
}
}
/**
* Resets display checkboxes to their default values.
*/
function resetCheckboxes(){
c = document.getElementById('coverCheckbox');
c.disabled = false;
c.checked = true;
r = document.getElementById('nodeRadiusCheckbox');
r.disabled = false;
r.checked = true;
n = document.getElementById('nodeCheckbox');
n.disabled = false;
n.checked = true;
document.getElementById('edgeCheckbox').disabled = 0;
document.getElementById('faceCheckbox').disabled = 0;
}
/**
* Generate uniform random anchor locations.
*/
function randomData() {
//generate uniform random data points
var xd = (newxScale) ? newxScale.domain() : xScale.domain();
var xmin = xd[0] + 0.1*(xd[1]-xd[0]);
var xmax = xd[1] - 0.1*(xd[1]-xd[0]);
var yd = (newyScale) ? newyScale.domain() : yScale.domain();
var ymin = yd[0] + 0.1*(yd[1]-yd[0]);
var ymax = yd[1] - 0.1*(yd[1]-yd[0]);
numSamples = +document.getElementById('numSensors').value;
locationData = [];
for (i=0; i<numSamples; i++) {
var xi = Math.random() * (xmax - xmin + 1) + xmin;
var yi = Math.random() * (ymax - ymin + 1) + ymin;
locationData.push({ anchor: {x: xi, y: yi}});
};
perturbData();
dataRange = d3.max([xd[1]-xd[0], yd[1]-yd[0]]);
dataPadding = 0.1*dataRange;
d3.select('#complexInput')
.attr('min', 0.05*dataRange)
.attr('max', 0.5*dataRange)
.attr('value', 0.2*dataRange);
resetCheckboxes();
renderPoints();
updateComplex(document.getElementById('complexInput').value);
}
/**
* Save data to a JSON file.
*/
function saveData() {
var data = {n: numSamples, k: numPoints, r: complexRadius, eps: dataRadius, sensors: locationData,
allEdges: allEdges, cechComplex: [cechEdges, cechFaces], ripsComplex: [ripsEdges, ripsFaces]};
var tempData = JSON.stringify(data, null, 2);
var blob = new Blob([tempData], { type: 'text/plain;charset=utf-8;' });
if (navigator.msSaveBlob) { // IE 10+
navigator.msSaveBlob(blob, filename);
} else {
var link = document.createElement("a");
if (link.download !== undefined) { // feature detection
// Browsers that support HTML5 download attribute
var url = URL.createObjectURL(blob);
link.setAttribute("href", url);
link.setAttribute("download", filename);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
}
}
/**
* Allow user to select file from local system and load data.
*/
function loadData() {
//allow user to select file
var selectedFile = document.getElementById('openButton');
var fReader = new FileReader();
fReader.readAsDataURL(selectedFile.files[0]);
fReader.onloadend = function(event) {
dataLoader(event.target.result)
}
}
/**
* Load formatted JSON data from local file system
*
* @param file {string} The path to the local file
*/
function dataLoader(file){
d3.json(file, function(data) {
dataRadius = data.eps;
numPoints = data.k;
numSamples = data.n;
complexRadius = data.r;
locationData = data.sensors;
allEdges = data.allEdges;
ripsEdges = data.ripsComplex[0];
ripsFaces = data.ripsComplex[1];
cechEdges = data.cechComplex[0];
cechFaces = data.cechComplex[1];
//set data scale
var dataPadding = complexRadius+dataRadius;
var xMin = d3.min(locationData.map( function (d) {
return d.anchor.x;
})) - dataPadding;
var xMax = d3.max(locationData.map( function (d) {
return d.anchor.x;
})) + dataPadding;
var yMin = d3.min(locationData.map( function (d) {
return d.anchor.y;
})) - dataPadding;
var yMax = d3.max(locationData.map( function (d) {
return d.anchor.y;
})) + dataPadding;
var yRange = yMax-yMin;
var rmax = d3.max([complexRadius, Math.ceil(0.5*yRange)]);
updateScales(xMin, xMax, yMin, yMax);
complexCanvas.attr("transform", d3.zoomIdentity)
newxScale = false;
newyScale = false;
newZscale = 1;
gX.call(xAxis.scale(xScale));
gY.call(yAxis.scale(yScale));
renderGrid()
//adjust radius slider
d3.select('#complexInput')
.attr('min', 1)
.attr('max', rmax);
d3.select('#complexRadius')
.attr('min', 1)
.attr('max', rmax);
d3.select('#complexInput').node().value = complexRadius;
d3.select('#complexRadius').node().value = complexRadius;
d3.select('#numSensors').node().value = numSamples;
d3.select('#numSampleSensors').node().value = numPoints;
d3.select('#complexDataRadius').node().value = dataRadius;
resetCheckboxes();
addSampleSensors();
})
}
/**
* Allow user to change the number of sensors.
*/
function changeNumberSampleSensors(){
var numSamplesSensors = parseInt(document.getElementById('numSampleSensors').value);
if(numPoints != numSamplesSensors) {
numPoints = +numSamplesSensors;
}
perturbData();
addSampleSensors();
}
/**
* Change the radius of uncertainty and update display. Also update coverage circles and update display.
* @param val {number} The new radius of uncertainty.
*/
function changeDataRadius(val){
dataRadius = parseInt(val);
d3.select('#complexRadiusInput').node().value = dataRadius;
d3.select('#complexDataRadius').node().value = dataRadius;
if(dataRadius < originalDataRadius) {
perturbData();
originalDataRadius = dataRadius;
addSampleSensors();
} else {
d3.select('#complexDataCircle').selectAll('circle')
.attr('r', xScale(dataRadius + complexRadius + xScale.domain()[0]));
d3.select('#complexPoints').selectAll('.point')
.attr('r', xScale(dataRadius + xScale.domain()[0]));
}
}
/**
* Add additional sensors to data.
*/
function addSampleSensors(){
renderPoints();
if (complexType=='Cech') {
constructCech();
} else if (complexType=='Vietoris-Rips') {
constructRips();
}
changeComplex();
}
/**
* Create random points for each data location. Points are uniform random within the radius of uncertainty from anchor.
* @param d {Object} An object with the fields x and y containing the anchor location.
* @returns {Array} An array of objects with the fields x and y containing the location of each random point.
*/
function createRandomPoints(d){
var r, theta, xj, yj;
var tmp = [];
for (j=0; j<numPoints; j++) {
r = math.random(dataRadius);
theta = math.random(2*math.pi);
xj = d.anchor.x + r * math.cos(theta);
yj = d.anchor.y + r * math.sin(theta);
tmp.push({x: xj, y: yj})
}
return tmp;
}
/**
* Calls the createRandomPoints function on each anchor location in locationData.
*/
function perturbData() {
if (arguments.length == 0) {
locationData.forEach( function (d) {
d.points = createRandomPoints(d);
})
} else {
data = arguments[0]
data.points = createRandomPoints(data);
}
}
/**
* Toggle between Vietoris-Rips and Čech complexes.
*/
function changeComplex() {
d = document.getElementsByName('complexType');
if (d[0].checked) {
complexType = 'Cech'
renderComplex(cechEdges, cechFaces);
} else {
complexType = 'Vietoris-Rips'
renderComplex(ripsEdges, ripsFaces);
}
}
/**
* Adds a node and generates random points around it. Creates an event listener for the plot area. If event is left-click,
* add node. If event is "escape", cancel.
*
*/
function addNode() {
complexSVG.attr('cursor','crosshair')
.on('click',function () {
coords = d3.mouse(d3.select('#complexSVG').node());
updateNode(coords);
});
window.addEventListener('keydown', function(event) {
if (event.code=='Escape') {
complexSVG.attr('cursor', null)
.on('click', null);
}
});
}
/**
* Update node location after drag and drop. Update anchor location and individual points.
* @param coords {array} 2-element array with new node coordinates.
*/
function updateNode(coords) {
if (locationData.length==0) {
resetCheckboxes();
};
i = locationData.length;
var x,y;
if (newxScale && newyScale) {
x = newxScale.invert(coords[0] - padding);
y = newyScale.invert(coords[1] - padding);
} else {
x = xScale.invert(coords[0] - padding);
y = yScale.invert(coords[1] - padding);
};
var newPoint = {anchor: {x: x, y: y}};
newpoint = perturbData(newPoint)
locationData.push(newPoint);
numSamples++;
renderPoints();
updateComplex(document.getElementById('complexInput').value);
}
// function queryNode() {
// complexSVG.attr('cursor','crosshair')
// .on('click', function () {
//
//
// })
// }
// function updateLocation(coords) {
// locationData[selectedNode].anchor.x = coords[0];
// locationData[selectedNode].anchor.y = coords[1];
// updateCech(document.getElementById('complexInput').value);
// window.addEventListener('keypress', function (evt) {
// complexCanvas.attr('cursor',null)
// .on('click',null);
// });
// }
//
// function myMap() {
// var mapCanvas = document.getElementById('map');
// var mapOptions = {
// center: new google.maps.LatLng(40.762,-111.839),
// zoom: 16
// };
// var map = new google.maps.Map(mapCanvas, mapOptions);
// }
/**
* Display or hide coverage circles.
* @param d {Boolean} Show if true, hide if false.
*/
function showCoverage(d) {
if (d) {
fillColor = '#9370db';
fillOpacity = '0.1';
d3.select('#complexCircles').selectAll('circle')
.transition()
.style('visibility','visible')
.style('fill', fillColor)
.style('fill-opacity', 0.2);
d3.select('#complexDataCircle').selectAll('circle')
.transition()
.style('visibility','visible')
.style('fill', fillColor)
.style('fill-opacity', 0.1);
} else {
d3.select('#complexCircles').selectAll('circle')
.transition()
.style('fill', 'none');
d3.select('#complexDataCircle').selectAll('circle')
.transition()
.style('fill', 'none');
}
}
/**
* Toggle display
* @param state {Boolean} Show if true, hide if false.
* @param type {String} Which type of object to display: circle, line, or polygon
*/
function show(state, type) {
if (state) {str='visible'} else {str='hidden'};
complexCanvas.selectAll(type)
.style('visibility', str);
}
/**
* Drag a node location. Individual points, if displayed, will move along with anchor.
*/
function dragNode() {
coords = d3.mouse(this)
i = this.id.match(/\d+/g);
str = '#complex_Circle_'+i;
str2 = '#data_Circle_'+i;
dx = locationData[i].anchor.x - coords[0];
dy = locationData[i].anchor.y - coords[1];
d3.selectAll(".small_circle").filter( function () {
var re = new RegExp('complex_small_Point_'+i+'_\d*');
return re.test(this.id)
})
.attr('cx', function(d) {
return d.x - dx
})
.attr('cy', function(d) {
return d.y - dy
})
d3.select(str)
.attr('cx', coords[0])
.attr('cy', coords[1]);
d3.select(str2)
.attr('cx', coords[0])
.attr('cy', coords[1]);
d3.select(this)
.attr('cx', coords[0])
.attr('cy', coords[1]);
wasDragged = true;
}
/**
* Called after a point is dragged. Updates locations and recomputes simplicial complex.
*/
function dragEnd() {
if (wasDragged) {
coords = d3.mouse(d3.select('#complexSVG').node());
i = this.id.match(/\d+/g);
var x,y;
if (newxScale && newyScale) {
x = newxScale.invert(coords[0] - padding);
y = newyScale.invert(coords[1] - padding);
} else {
x = xScale.invert(coords[0] - padding);
y = yScale.invert(coords[1] - padding);
};
dx = locationData[i].anchor.x - x;
dy = locationData[i].anchor.y - y;
locationData[i].anchor.x = x;
locationData[i].anchor.y = y;
locationData[i].points.forEach( function (d) {
d.x = d.x - dx;
d.y = d.y - dy;
})
renderPoints();
updateComplex(document.getElementById('complexInput').value);
}
wasDragged = false;
for (i=0; i<selectedNodes.length; i++) {
highlightPoint([],selectedNodes[i])
}
}
/**
* Select and highlight a node for possible deletion.
*/
function selectNode() {
if (d3.event.defaultPrevented) {
return;
}
i = +this.id.match(/\d+/g);
selectedNodes.push(i);
highlightPoint([],i);
d3.select('#complex_Point_'+i)
.on('mouseover',null)
.on('mouseout',null)
.on('click',null);
highlightPoint([],i);
if (selectedNodes.length==1) {
window.addEventListener('keydown', nodeSelector);
}
}
/**
* Delete selected nodes, recompute simplicial complexes and re-render or, if escape pressed, unselect points.
*/
function nodeSelector() {
if (event.code=='Delete' || event.code=='Backspace') {
window.removeEventListener('keydown', nodeSelector)
selectedNodes = selectedNodes.sort(function(a, b){return a-b});
for (j = 0; j < selectedNodes.length; j++) {
locationData.splice(selectedNodes[j]-j, 1);
}
numSamples = locationData.length;
selectedNodes = [];
renderPoints();
updateComplex(document.getElementById('complexInput').value);
} else if (event.code == 'Escape') {
window.removeEventListener('keydown', nodeSelector)
selectedNodes = [];
renderPoints();
changeComplex();
}
}
/**
* Calculate squared euclidean distance between 2 data points.
* @param pt1 {Array} 2-element array with coordinates of first data point.
* @param pt2 {Array} 2-element array with coordinates of second data point.
* @returns {number}
*/
function sqEuclidDist(pt1, pt2) {
return Math.pow(pt2[0]-pt1[0],2) + Math.pow(pt2[1]-pt1[1],2);
}
/**
* Clear canvas and reset all variables to their default values.
*/
function clearScreen() {
complexCanvas.selectAll('.face').remove();
complexCanvas.selectAll('.edge').remove();
complexCanvas.selectAll('.circle').remove();
complexCanvas.selectAll('.point').remove();
locationData = [];
selectedNodes = [];
newZscale = 1;
updateScales(0, 100, 0, 100);
gX.call(xAxis.scale(xScale));
gY.call(yAxis.scale(yScale));
newxScale = false;
newyScale = false;
renderGrid();
complexRadius = 5;
numSamples = 0;
d3.select('#complexInput')
.attr('min', 1)
.attr('max', 50);
d3.select('#complexInput').node().value = complexRadius;
d3.select('#complexRadius').node().value = complexRadius;
}
/**
* Allows user to manually set maximum coverage radius.
*/
function setMax() {
var rmax = d3.select('#complexInput').node().max.toString();
var maxval = prompt('Enter maximum radius value',rmax)
d3.select('#complexInput').attr('max', maxval);
d3.select('#complexRadius').attr('max', maxval);
}