|
<!DOCTYPE html> |
|
<meta charset="utf-8"> |
|
<style> |
|
circle { |
|
fill: steelblue; |
|
opacity: 0.4; |
|
} |
|
|
|
circle:hover { |
|
opacity: 1; |
|
} |
|
|
|
body { |
|
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; |
|
} |
|
|
|
.hidden { |
|
display: none; |
|
} |
|
|
|
.actionableText:hover { |
|
cursor: pointer; |
|
text-decoration: underline; |
|
} |
|
</style> |
|
|
|
<body> |
|
<!-- D3 --> |
|
<script src="https://d3js.org/d3.v4.min.js"></script> |
|
|
|
<!-- MathJax --> |
|
<script type="text/x-mathjax-config">MathJax.Hub.Config({tex2jax: {inlineMath: [['$','$'], ['\\(','\\)']]}});</script> |
|
<script type="text/javascript" async src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS_CHTML"></script> |
|
|
|
<!-- Stats computations --> |
|
<script type="text/javascript" src="computeStats.js"></script> |
|
|
|
<div id="scatterplotDiv"></div> |
|
<div id="buttonsDiv"> |
|
<button id="clearDataButton" class="actionableText">Clear all</button> | |
|
<button id="downloadDataButton" class="actionableText">Download data</button> | |
|
<button> |
|
<label for="file-upload" class="custom-file-upload">Browse</label> |
|
<input id="file-upload" type="file" style="display:none;"/> |
|
</button> |
|
</div> |
|
|
|
<script> |
|
var obs = [], // main data object |
|
datCounter = 0; // for giving points unique IDs |
|
|
|
// statistics reported |
|
var stats = [{ |
|
name: "Avg Tradeoff Angle", |
|
type: "conflict", |
|
value: null, |
|
update: function() { |
|
this.value = computeTEST(obs.map(function(e) { |
|
return e.x; |
|
}), obs.map(function(e) { |
|
return e.y; |
|
})) |
|
} |
|
}, { |
|
name: "Our 1", |
|
type: "conflict", |
|
value: null, |
|
update: function() { |
|
this.value = computeOur1(obs.map(function(e) { |
|
return e.x; |
|
}), obs.map(function(e) { |
|
return e.y; |
|
})) |
|
} |
|
}, { |
|
name: "Spearman's", |
|
type: "correlation", |
|
value: null, |
|
update: function() { |
|
this.value = computeSpearmans(obs.map(function(e) { |
|
return e.x; |
|
}), obs.map(function(e) { |
|
return e.y; |
|
})) |
|
} |
|
}, { |
|
name: "Pearson's", |
|
type: "correlation", |
|
value: null, |
|
update: function() { |
|
this.value = computePearsons(obs.map(function(e) { |
|
return e.x; |
|
}), obs.map(function(e) { |
|
return e.y; |
|
})) |
|
} |
|
}, { |
|
name: "Kendall's", |
|
type: "conflict", |
|
value: null, |
|
update: function() { |
|
this.value = computeKendalls(obs.map(function(e) { |
|
return e.x; |
|
}), obs.map(function(e) { |
|
return e.y; |
|
})) |
|
} |
|
}]; |
|
|
|
// setting dimensions and margins |
|
var margin = { |
|
top: 20, |
|
right: 200, |
|
bottom: 70, |
|
left: 70 |
|
}, |
|
width = 660 - margin.left - margin.right, |
|
height = 400 - margin.top - margin.bottom; |
|
|
|
|
|
// defining scales |
|
var x = d3.scaleLinear().domain([0, 1]).range([0, width]).nice(); |
|
var y = d3.scaleLinear().domain([0, 1]).range([height, 0]).nice(); |
|
|
|
// append svg object |
|
var svg = d3.select("#scatterplotDiv").append("svg") |
|
.attr("width", width + margin.left + margin.right) |
|
.attr("height", height + margin.top + margin.bottom) |
|
.append("g") |
|
.attr("transform", |
|
"translate(" + margin.left + "," + margin.top + ")"); |
|
|
|
// define and add droplines |
|
var droplineX = svg.append("line") |
|
.attr("class","hidden") |
|
.attr("x1",0) |
|
.attr("x2",width) |
|
.style("stroke","#7e7e7e"); |
|
var droplineY = svg.append("line") |
|
.attr("class","hidden") |
|
.attr("y1",0) |
|
.attr("y2",height) |
|
.style("stroke","#7e7e7e"); |
|
|
|
// add rectangle overlay to catch pointer events |
|
svg.append('rect') |
|
.attr('width', width) |
|
.attr('height', height) |
|
.attr('fill', 'none') |
|
.style('pointer-events', 'all') |
|
.style('cursor','crosshair') |
|
.on("mouseover",function(){ // show the droplines |
|
droplineX.classed("hidden",false); |
|
droplineY.classed("hidden",false); |
|
}) |
|
.on("mousemove",function(){ // update droplines |
|
var mx = d3.mouse(this)[0], |
|
my = d3.mouse(this)[1]; |
|
droplineX |
|
.attr("y1",my) |
|
.attr("y2",my); |
|
droplineY |
|
.attr("x1",mx) |
|
.attr("x2",mx); |
|
}) |
|
.on("mouseout",function(){ // hide droplines |
|
droplineX.classed("hidden",true); |
|
droplineY.classed("hidden",true); |
|
}) |
|
.on('click', function() { // add circle on click |
|
var locx = d3.mouse(this)[0], |
|
locy = d3.mouse(this)[1]; |
|
|
|
var datx = x.invert(d3.mouse(this)[0]), |
|
daty = y.invert(d3.mouse(this)[1]); |
|
|
|
addNewDatum(datCounter, [datx, daty]); |
|
drawCircle(datCounter, [locx, locy]); |
|
datCounter++; |
|
}); |
|
|
|
// Add x Axis |
|
svg.append("g") |
|
.attr("transform", "translate(0," + height + ")") |
|
.call(d3.axisBottom(x)); |
|
|
|
// text label for x axis |
|
svg.append("text") |
|
.attr("transform", |
|
"translate(" + (width) + " ," + |
|
(height + margin.top + 20) + ")") |
|
.style("text-anchor", "end") |
|
.text("Objective 1 (max)"); |
|
|
|
// Add y Axis |
|
svg.append("g") |
|
.call(d3.axisLeft(y)); |
|
|
|
// text label for y axis |
|
svg.append("text") |
|
.attr("transform", "rotate(-90)") |
|
.attr("y", 0 - margin.left) |
|
.attr("x", 0 ) |
|
.attr("dy", "1em") |
|
.style("text-anchor", "end") |
|
.text("Objective 2 (max)"); |
|
|
|
// add area for statistics |
|
var statsG = d3.select('svg').append("g") |
|
.attr("transform", "translate(" + (margin.left + width) + "," + (margin.top + 0.4 * height) + ")"); |
|
statsG.selectAll(".statsField") |
|
.data(stats) |
|
.enter().append("text") |
|
.attr("y", function(d, i) { |
|
return (i * 1.5) + "em"; |
|
}) |
|
.attr("x", 5) |
|
.attr("class","statsField") |
|
.style("text-anchor", "left") |
|
.text(function(d) { |
|
return d.name + ": " + d.value; |
|
}); |
|
|
|
// function for clear all button |
|
d3.select("#clearDataButton") |
|
.on("click",function(){ |
|
clearAllSolutions(); |
|
}); |
|
|
|
// function for download data button |
|
d3.select("#downloadDataButton") |
|
.on("click",function(){ |
|
downloadData(); |
|
}); |
|
|
|
// function for upload data button |
|
// uploaded data replaces existing data |
|
d3.select("#file-upload").on("change", function(){ |
|
if (window.File && window.FileReader && window.FileList && window.Blob) { |
|
var filereader = new window.FileReader(); |
|
filereader.onload = function(){ |
|
var newObs = prep(filereader.result); |
|
if (newObs[0]) { |
|
clearAllSolutions(); |
|
obs = newObs[1]; |
|
for (var i=0;i<obs.length;i++){ |
|
drawCircle(obs[i].id,[x(obs[i].x),y(obs[i].y)]); |
|
} |
|
recalculateStatistics(); |
|
refreshStatsDisplay(); |
|
datCounter = d3.max(obs,function(e){return e.id;})+1; // so next ID is unique |
|
}; |
|
} |
|
filereader.readAsText(this.files[0]); |
|
} else { console.log("Error with file upload. Please try again."); } |
|
}); |
|
|
|
/** Given a CSV as text, returns an array whose |
|
* 0th component is true for parsing success, false for failure |
|
* 1st component is an obs-like data object of points |
|
*/ |
|
var prep = function (csvAsText) { |
|
|
|
var arr2d = csvAsText.split("\n").map(function(str){return str.split(",",2).map(Number);}); |
|
// if last row empty, remove it. |
|
// all others assumed to be well formatted |
|
if (arr2d[arr2d.length-1].length <2) arr2d.pop(); |
|
|
|
var result = arr2d.map(function(d,i){ |
|
return {id:i,x:d[0],y:d[1]}; |
|
}) |
|
|
|
// in case error-handling added later... |
|
if (true) return [true,result] |
|
else return [false,null]; |
|
} |
|
|
|
/** Removes all current solutions from the viz */ |
|
var clearAllSolutions = function() { |
|
var currData = obs.map(function(e){return e.id;}); |
|
d3.selectAll("circle.datapoint").remove(); |
|
for (var i=0;i<currData.length;i++){ |
|
removeDatum(currData[i]); |
|
} |
|
} |
|
|
|
/** Adds a new point to the obs array and updates the corr/conflict statistics */ |
|
var addNewDatum = function(id, datapoint) { |
|
obs.push({ |
|
id: id, |
|
x: datapoint[0], |
|
y: datapoint[1] |
|
}); |
|
recalculateStatistics(); |
|
refreshStatsDisplay(); |
|
} |
|
|
|
/** Removes a data point from obs and updates corr/conflict statistics */ |
|
var removeDatum = function(id) { |
|
for (var i = 0; i < obs.length; i++) { |
|
if (obs[i].id === id) { |
|
obs.splice(i, 1); |
|
break; |
|
} |
|
} |
|
recalculateStatistics(); |
|
refreshStatsDisplay(); |
|
} |
|
|
|
/** Adds a circle to the svg at the location given by coords */ |
|
var drawCircle = function(id, coords) { |
|
svg.append("circle") |
|
.attr('class', 'datapoint') |
|
.attr('id', 'datum-' + id) |
|
.attr("cx", coords[0]) |
|
.attr("cy", coords[1]) |
|
.attr("r", 5) |
|
.on("click", function() { |
|
removeDatum(id); |
|
d3.select(this).remove(); |
|
}); |
|
} |
|
|
|
/** Recalculates the values for each of the corr/conflict statistics */ |
|
var recalculateStatistics = function() { |
|
for (var stat = 0; stat < stats.length; stat++) { |
|
stats[stat].update(); |
|
} |
|
} |
|
|
|
/** Updates the values displayed for the statistics */ |
|
var refreshStatsDisplay = function() { |
|
statsG.selectAll(".statsField") |
|
.data(stats) |
|
.text(function(d) { |
|
return d.name + ": " + d.value; |
|
}); |
|
} |
|
|
|
/** Opens a file dialog to let user save the current data in CSV format */ |
|
var downloadData = function() { |
|
var csvContent = "data:text/csv;charset=utf-8,"; |
|
obs.forEach(function(dat, index){ |
|
dataString = dat.x+","+dat.y; |
|
csvContent += index < obs.length ? dataString+ "\n" : dataString; |
|
}); |
|
|
|
var encodedUri = encodeURI(csvContent); |
|
window.open(encodedUri); |
|
} |
|
|
|
</script> |
|
</body> |