|
<!DOCTYPE html> |
|
<meta charset="utf-8"> |
|
<style> |
|
|
|
#under-construction { |
|
display: none; |
|
position: absolute; |
|
top: 200px; |
|
left: 300px; |
|
font-size: 40px; |
|
} |
|
|
|
circle { |
|
stroke-width: 1.5px; |
|
} |
|
|
|
line { |
|
stroke: #999; |
|
} |
|
|
|
</style> |
|
<body> |
|
<div id="under-construction"> |
|
UNDER CONSTRUCTION |
|
</div> |
|
<script src="https://d3js.org/d3.v3.min.js"></script> |
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.5.1/dat.gui.min.js"></script> |
|
<script> |
|
var width = 960, |
|
height = 500; |
|
var csvData = []; |
|
var config = { |
|
radius: 4, |
|
use_it: true, |
|
iterations: 5, |
|
gravity: 0.4, |
|
manyPoints: true |
|
}; |
|
|
|
insertControls(); |
|
|
|
var fill = d3.scale.linear().domain([1,150]).range(['lightgreen', 'pink']); |
|
|
|
var svg = d3.select("body").append("svg") |
|
.attr("width", width) |
|
.attr("height", height); |
|
|
|
svg.append("line") |
|
.attr("id", "x-axis") |
|
.attr("x1", 0) |
|
.attr("y1", height/2) |
|
.attr("x2", width) |
|
.attr("y2", height/2) |
|
.style("stroke", "lightgrey"); |
|
|
|
var nodeContainer = svg.append("g").attr("id", "node-container"); |
|
|
|
var informationPanel, computationTimeInfo, dataLengthInfo, posibleCollidersInfo, placementInfo, visitedCollidersInfo; |
|
prepareInformationPanel(); |
|
|
|
var tooltip, stem, rank, value; |
|
prepareTooltip(); |
|
|
|
var minDistanceBetweenCircles, |
|
minSquareDistanceBetweenCircles, |
|
AAD, |
|
totalPossibleCollders, maxPossibleColliders, |
|
totalTestedPlacements, |
|
visitedColliderCount, totalVisitedColliders, maxVisitedColliders; |
|
|
|
function initPlacement() { |
|
minDistanceBetweenCircles = 2*config.radius; |
|
minSquareDistanceBetweenCircles = Math.pow(minDistanceBetweenCircles, 2); |
|
AAD = []; //already arranged data; window for collision detection |
|
//-->for metrics purpose |
|
totalPossibleColliders = maxPossibleColliders = 0; |
|
totalTestedPlacements = 0; |
|
visitedColliderCount = totalVisitedColliders = maxVisitedColliders =0; |
|
//<--for metrics purpose |
|
}; |
|
|
|
function findPossibleColliders (datum) { |
|
//remove circles from AAD that are far away from datum |
|
var indexesToRemove = 0; |
|
AAD.every(function (aad) { |
|
if (Math.abs(datum.x-aad.x)>minDistanceBetweenCircles) { |
|
indexesToRemove++; |
|
return true; |
|
} |
|
return false; |
|
}); |
|
AAD.splice(0,indexesToRemove); |
|
|
|
//-->for metrics purpose |
|
totalPossibleColliders += AAD.length; |
|
if (AAD.length > maxPossibleColliders) { |
|
maxPossibleColliders = AAD.length; |
|
} |
|
//<--for metrics purpose |
|
} |
|
|
|
function isBetterPlacement(datum, bestYPosition) { |
|
return Math.abs(datum.y) < Math.abs(bestYPosition); |
|
} |
|
|
|
function yPosRelativeToAad(aad, d) { |
|
// handle Float approximation with +1E-6 |
|
return Math.sqrt(minSquareDistanceBetweenCircles-Math.pow(d.x-aad.x,2))+1E-6; |
|
} |
|
|
|
function placeBelow(d, aad, relativeYPos) { |
|
d.y = aad.y - relativeYPos; |
|
} |
|
|
|
function placeAbove(d, aad, relativeYPos) { |
|
d.y = aad.y + relativeYPos; |
|
} |
|
|
|
function areCirclesColliding(d0, d1) { |
|
visitedColliderCount++ //for metrics prupose |
|
|
|
//first simple check (vertical positions) |
|
if (Math.abs(d1.y - d0.y) > minDistanceBetweenCircles) return false; |
|
|
|
//more advanced check |
|
var squareDistanceBetweenCircles = Math.pow(d1.y-d0.y, 2) + Math.pow(d1.x-d0.x, 2); |
|
return squareDistanceBetweenCircles < minSquareDistanceBetweenCircles; |
|
} |
|
|
|
function collidesWithOther (data) { |
|
return AAD.some(function(aad) { |
|
return areCirclesColliding(aad, data); |
|
}); |
|
} |
|
|
|
function placeCircles (data) { |
|
initPlacement(); |
|
data.forEach(function (d) { |
|
var bestYPosition = -Infinity, |
|
relativeYPos; |
|
findPossibleColliders(d); |
|
if (AAD.length===0) { |
|
bestYPosition = 0; |
|
} else { |
|
AAD.forEach(function(aad) { |
|
relativeYPos = yPosRelativeToAad(aad, d); |
|
placeBelow(d, aad, relativeYPos); |
|
if (isBetterPlacement(d, bestYPosition) && !collidesWithOther(d)) { |
|
bestYPosition = d.y; |
|
} |
|
//-->for metrics purpose |
|
totalVisitedColliders += visitedColliderCount; |
|
if (visitedColliderCount > maxVisitedColliders) { |
|
maxVisitedColliders = visitedColliderCount; |
|
} |
|
visitedColliderCount = 0; |
|
//<--for metrics purpose |
|
placeAbove(d, aad, relativeYPos); |
|
if (isBetterPlacement(d, bestYPosition) && !collidesWithOther(d)) { |
|
bestYPosition = d.y; |
|
} |
|
//-->for metrics purpose |
|
totalVisitedColliders += visitedColliderCount; |
|
if (visitedColliderCount > maxVisitedColliders) { |
|
maxVisitedColliders = visitedColliderCount; |
|
} |
|
visitedColliderCount = 0; |
|
//<--for metrics purpose |
|
totalTestedPlacements += 2; //for metrics purpose |
|
}) |
|
}; |
|
d.y = bestYPosition; |
|
AAD.push(d); |
|
}); |
|
} |
|
|
|
function showCircles (data) { |
|
nodeContainer.selectAll("circle").remove(); |
|
var node = nodeContainer.selectAll("circle") |
|
.data(data) |
|
.enter().append("circle") |
|
.attr("r", config.radius-0.75) |
|
.attr("cx", function(d) { return d.x; }) |
|
.attr("cy", function(d) { return d.y; }) |
|
.style("fill", function(d) { return fill(d.rank); }) |
|
.style("stroke", function(d) { return d3.rgb(fill(d.rank)).darker(); }) |
|
.on("mouseenter", function(d) { |
|
stem.text(d.stem); |
|
rank.text(d.rank); |
|
value.text(d.trend); |
|
tooltip.transition().duration(0).style("opacity", 1); // remove fade out transition on mouseleave |
|
}) |
|
.on("mouseleave", function(d) { |
|
tooltip.transition().duration(1000).style("opacity", 0); |
|
}); |
|
} |
|
|
|
function applyForceLayout (data) { |
|
//prepare data for ForceLayout |
|
data.forEach(function(d) { |
|
d.y += height/2; |
|
}) |
|
|
|
var force = d3.layout.force() |
|
.gravity(config.gravity) |
|
.charge(-(config.radius*config.radius-config.radius)) |
|
.size([width, height]) |
|
.friction(0.7) |
|
|
|
function tick() { |
|
data.forEach(function(d){ d.x = d.originalX; }) //constrains x-position |
|
} |
|
|
|
force.nodes(data) |
|
.on("tick", tick) |
|
.stop(); |
|
|
|
force.start(); |
|
for (var i = 0; i < config.iterations; i++) { force.tick(); } |
|
force.stop(); |
|
} |
|
|
|
function drawBeeswarm() { |
|
var data = config.manyPoints? double(double(copyCsvData())) : copyCsvData(); |
|
|
|
var startTime = Date.now(); |
|
if (config.use_it) { |
|
placeCircles(data); |
|
//showMetrics(data, (Date.now()-startTime)); |
|
} |
|
|
|
applyForceLayout(data); |
|
|
|
showCircles(data); |
|
} |
|
|
|
function dottype(d) { |
|
d.stem = d.stem; |
|
d.rank = +d.rank; |
|
d.trend = +d.trend; |
|
d.originalX = width/2+d.trend*6000; |
|
d.x = d.originalX; |
|
d.y = 0; |
|
csvData.push(d); |
|
return d; |
|
} |
|
|
|
d3.csv("data.csv", dottype, function(error, foo) { |
|
if (error) throw error; |
|
drawBeeswarm() |
|
}); |
|
|
|
function copyCsvData() { |
|
return csvData.map(function(d) { |
|
return { |
|
stem: d.stem, |
|
rank: d.rank, |
|
trend: d.trend, |
|
originalX: d.originalX, |
|
x: d.originalX, |
|
y: d.y |
|
} |
|
}); |
|
} |
|
|
|
function double(data) { |
|
// Doubles data while maintaining order |
|
var doubledData = []; |
|
data.forEach(function(d) { |
|
doubledData.push({ |
|
stem: d.stem, |
|
rank: d.rank, |
|
trend: d.trend, |
|
originalX: d.originalX+1E-3, |
|
x: d.originalX+1E-3, |
|
y: d.y |
|
}) |
|
doubledData.push(d); |
|
}) |
|
return doubledData; |
|
} |
|
|
|
function insertControls () { |
|
var ctrls = new dat.GUI({width: 200}); |
|
var customArrangementCtrl = ctrls.addFolder("Step1 - Custom Arrangement"); |
|
customArrangementCtrl.open(); |
|
var applyCustomArrangementCtrl = customArrangementCtrl.add(config, "use_it"); |
|
applyCustomArrangementCtrl.onChange(function(value) { |
|
drawBeeswarm(); |
|
}); |
|
var forceCtrl = ctrls.addFolder("Step2 - Force Layout"); |
|
forceCtrl.open(); |
|
var iterationCountCtrl = forceCtrl.add(config, "iterations", 0, 50); |
|
iterationCountCtrl.step(1).onChange(function(value) { |
|
drawBeeswarm(); |
|
}); |
|
var gravityCtrl = forceCtrl.add(config, "gravity", 0, 1); |
|
gravityCtrl.onChange(function(value) { |
|
drawBeeswarm(); |
|
}); |
|
var manyPointsCtrl = ctrls.add(config, "manyPoints"); |
|
manyPointsCtrl.onChange(function(value) { |
|
drawBeeswarm(); |
|
}); |
|
} |
|
|
|
function prepareTooltip() { |
|
tooltip = svg.append("g") |
|
.attr("id", "tooltip") |
|
.attr("transform", "translate("+[width/2, 50]+")") |
|
.style("opacity", 0); |
|
var titles = tooltip.append("g").attr("transform", "translate("+[-5,0]+")") |
|
titles.append("text").attr("text-anchor", "end").text("stem(fr):"); |
|
titles.append("text") |
|
.attr("text-anchor", "end") |
|
.attr("transform", "translate("+[0,15]+")") |
|
.text("rank:"); |
|
titles.append("text") |
|
.attr("text-anchor", "end") |
|
.attr("transform", "translate("+[0,30]+")") |
|
.text("x-value:"); |
|
var values = tooltip.append("g").attr("transform", "translate("+[5,0]+")") |
|
stem = values.append("text") |
|
.attr("text-anchor", "start"); |
|
rank = values.append("text") |
|
.attr("text-anchor", "start") |
|
.attr("transform", "translate("+[0,15]+")"); |
|
value = values.append("text") |
|
.attr("text-anchor", "start") |
|
.attr("transform", "translate("+[0,30]+")"); |
|
} |
|
|
|
function prepareInformationPanel() { |
|
var i=4; |
|
informationPanel = svg.append("g") |
|
.attr("id", "infomation-panel") |
|
.attr("transform", "translate("+[width-20, height-20]+")"); |
|
computationTimeInfo = informationPanel.append("text") |
|
.attr("text-anchor", "end") |
|
.attr("transform", "translate("+[0,-15*i--]+")"); |
|
dataLengthInfo = informationPanel.append("text") |
|
.attr("text-anchor", "end") |
|
.attr("transform", "translate("+[0,-15*i--]+")"); |
|
possibleCollidersInfo = informationPanel.append("text") |
|
.attr("text-anchor", "end") |
|
.attr("transform", "translate("+[0,-15*i--]+")"); |
|
placementInfo = informationPanel.append("text") |
|
.attr("text-anchor", "end") |
|
.attr("transform", "translate("+[0,-15*i--]+")"); |
|
visitedCollidersInfo = informationPanel.append("text") |
|
.attr("text-anchor", "end"); |
|
} |
|
|
|
function showMetrics (data, elapsed) { |
|
//-->for metrics purpose |
|
computationTimeInfo.text("Arrangement took: "+elapsed+" ms"); |
|
dataLengthInfo.text("# data: "+data.length); |
|
possibleCollidersInfo.text("# possible colliders: ~"+Math.round(totalPossibleColliders/data.length)+" per data ("+maxPossibleColliders+" max, "+totalPossibleColliders+" total)"); |
|
placementInfo.text("# tested placements: "+totalTestedPlacements); |
|
visitedCollidersInfo.text("# collision checks: "+ |
|
totalVisitedColliders); |
|
//>--for metrics purpose |
|
} |
|
</script> |
|
</body> |