|
<!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> |
|
var width = 960, |
|
height = 500, |
|
radius = 4; |
|
|
|
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("x1", 0) |
|
.attr("y1", height/2) |
|
.attr("x2", width) |
|
.attr("y2", height/2) |
|
.style("stroke", "lightgrey"); |
|
|
|
var tooltip = svg.append("g") |
|
.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]+")") |
|
var stem = values.append("text"); |
|
stem.attr("text-anchor", "start"); |
|
var rank = values.append("text"); |
|
rank.attr("text-anchor", "start") |
|
.attr("transform", "translate("+[0,15]+")"); |
|
var value = values.append("text"); |
|
value.attr("text-anchor", "start") |
|
.attr("transform", "translate("+[0,30]+")"); |
|
|
|
function dottype(d) { |
|
d.stem = d.stem; |
|
d.rank = +d.rank; |
|
d.trend = +d.trend; |
|
d.x = width/2+d.trend*6000; |
|
d.y = 0; |
|
return d; |
|
} |
|
|
|
minDistanceBetweenCircles = 2*radius; |
|
minSquareDistanceBetweenCircles = Math.pow(minDistanceBetweenCircles, 2); |
|
|
|
function areCirclesColliding(d0, d1) { |
|
if (d1.y===d0.y && d1.x===d0.x) return true; |
|
var squareDistanceBetweenCircles = Math.pow(d1.y-d0.y, 2) + Math.pow(d1.x-d0.x, 2); |
|
return squareDistanceBetweenCircles < minSquareDistanceBetweenCircles; |
|
} |
|
|
|
function collidesWithOthers (data) { |
|
var collidesWithOthers = false; |
|
AAD.forEach(function(aad) { |
|
if (areCirclesColliding(aad, data)) { |
|
collidesWithOthers = collidesWithOthers || true; |
|
} |
|
}) |
|
return collidesWithOthers; |
|
} |
|
|
|
var AAD = []; //already arranged data; window for collision detection |
|
|
|
function cleanAAD (datum) { |
|
var indexesToRemove = 0; |
|
AAD.forEach(function (aad) { |
|
if (Math.abs(datum.x-aad.x)>minDistanceBetweenCircles) { |
|
indexesToRemove++; |
|
} else { |
|
return |
|
} |
|
}) |
|
AAD.splice(0,indexesToRemove); |
|
} |
|
|
|
function yPosRelativeToAad(aad, d) { |
|
// return 2*radius; //issue: leave extra space when circles are not strictly vertically aligned |
|
|
|
return Math.sqrt(minSquareDistanceBetweenCircles+1E-6-Math.pow(d.x-aad.x,2)); |
|
} |
|
|
|
function placeAbove(aad, d) { |
|
d.y = aad.y + yPosRelativeToAad(aad, d); |
|
} |
|
|
|
function placeBelow(aad, d) { |
|
d.y = aad.y - yPosRelativeToAad(aad, d); |
|
} |
|
|
|
function placeCircles (data) { |
|
data.forEach(function (d) { |
|
cleanAAD(d); |
|
if (AAD.length===0) { |
|
d.y = 0; |
|
AAD.push(d); |
|
} else { |
|
var bestYPosition = -Infinity, |
|
relativeY; |
|
AAD.forEach(function(aad) { |
|
placeBelow(aad, d); |
|
if (!collidesWithOthers(d)) { |
|
if (Math.abs(d.y) < Math.abs(bestYPosition)) { |
|
bestYPosition = d.y; |
|
} |
|
} |
|
placeAbove(aad, d) |
|
if (!collidesWithOthers(d)) { |
|
if (Math.abs(d.y) < Math.abs(bestYPosition)) { |
|
bestYPosition = d.y; |
|
} |
|
} |
|
}) |
|
d.y = bestYPosition; |
|
AAD.push(d); |
|
} |
|
}) |
|
} |
|
|
|
d3.csv("data.csv", dottype, function(error, trendData) { |
|
if (error) throw error; |
|
|
|
//trendData = double(double(trendData)); // test for scaling purpose |
|
|
|
var startTime = Date.now(); |
|
placeCircles(trendData); |
|
console.log("arrangment took (ms): "+(Date.now()-startTime)); |
|
|
|
var node = svg.selectAll("circle") |
|
.data(trendData) |
|
.enter().append("circle") |
|
.attr("r", radius-0.75) |
|
.attr("cx", function(d) { return d.x; }) |
|
.attr("cy", function(d) { return height/2 + 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 double(data) { |
|
// Doubles data while maintaining order |
|
var doubledData = []; |
|
data.forEach(function(d) { |
|
doubledData.push({ |
|
stem: d.stem, |
|
rank: d.rank, |
|
trend: d.trend, |
|
x: d.x+1E-3, |
|
y: d.y |
|
}) |
|
doubledData.push(d); |
|
}) |
|
return doubledData; |
|
} |
|
|
|
</script> |