|
<!DOCTYPE html> |
|
<meta charset="utf-8"> |
|
<style> |
|
|
|
circle { |
|
stroke-width: 1.5px; |
|
} |
|
|
|
line { |
|
stroke: #999; |
|
} |
|
|
|
</style> |
|
<body> |
|
<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, |
|
radius = 4 |
|
csvData = [], |
|
config = { |
|
manyPoints: false |
|
}; |
|
|
|
insertControls(); |
|
|
|
var fill = d3.scale.linear().domain([1,150]).range(['lightgreen', 'pink']); |
|
|
|
var force = d3.layout.force() |
|
.gravity(0.2) |
|
.charge(-(radius*radius-radius)) |
|
.size([width, height]) |
|
.friction(0.7) |
|
|
|
var svg = d3.select("body").append("svg") |
|
.attr("width", width) |
|
.attr("height", height); |
|
|
|
svg.append("line") |
|
.attr({x1: 0, y1: height/2, x2: width, 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", |
|
"transform": "translate("+[0,15]+")" |
|
}).text("rank:"); |
|
titles.append("text").attr({ |
|
"text-anchor": "end", |
|
"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", |
|
"transform": "translate("+[0,15]+")" |
|
}); |
|
var value = values.append("text"); |
|
value.attr({ |
|
"text-anchor": "start", |
|
"transform": "translate("+[0,30]+")" |
|
}); |
|
|
|
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 = height/2; |
|
csvData.push(d); |
|
return d; |
|
} |
|
|
|
d3.csv("data.csv", dottype, function(error, foo) { |
|
if (error) throw error; |
|
|
|
draw(); |
|
}); |
|
|
|
function draw() { |
|
var data = copyData(csvData); |
|
if (config.manyPoints) { data = quadruple(data); } |
|
|
|
svg.selectAll("circle").remove(); |
|
var node = svg.selectAll("circle") |
|
.data(data) |
|
.enter().append("circle") |
|
.attr("r", radius - .75) |
|
.attr("cx", function(d) { return d.x; }) |
|
.attr("cy", height/2) |
|
.style("fill", function(d) { return fill(d.rank); }) |
|
.style("stroke", function(d) { return d3.rgb(fill(d.rank)).darker(); }) |
|
.style("opacity", 0) |
|
//.call(force.drag) don't need drag behaviour |
|
.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); |
|
}); |
|
|
|
var iterations=0; |
|
var startTime = Date.now(); |
|
|
|
force |
|
.nodes(data) |
|
.on("tick", tick) |
|
.on("end", function() { |
|
console.log("iterations: "+iterations); |
|
console.log("time: "+(Date.now()-startTime)); |
|
}); |
|
|
|
|
|
// Use the d3's Force Layout to compute final position of nodes: |
|
// usually, after calling force.start(), an internal Timer calls each 'tick()'; this Timer lets enought time for the tick() function to redraw each node's position; the drawback is that it takes some time (~5s) to have the final arrangement |
|
// Instead, we explicitly call the tick() function (and no longer rely on the Timer); the tick() function no longer updates each node's position at each tick; this will be done once, when the force layout ends |
|
|
|
force.stop().start(); |
|
for (var i = 0; i < 20; ++i) force.tick(); |
|
force.stop(); |
|
function tick() { |
|
iterations++; |
|
node.each(function(d){d.x = d.originalX; }) //constrains/fixes x-position |
|
|
|
// The code below is no longer used |
|
/* |
|
node.attr("cx", function(d) {return d.x = Math.max(radius, Math.min(width - radius, d.x)); }) |
|
.attr("cy", function(d) {return d.y = Math.max(radius, Math.min(height - radius, d.y)); }); |
|
*/ |
|
} |
|
|
|
//Now that we know the final position of each node, we can update each node's position in any way we want |
|
node.transition() |
|
.delay(function(d,i) {return 2*i}) |
|
.style("opacity", 1) |
|
.attr("cx", function(d) {return d.x = Math.max(radius, Math.min(width - radius, d.x)); }) |
|
.attr("cy", function(d) {return d.y = Math.max(radius, Math.min(height - radius, d.y)); }); |
|
}; |
|
|
|
function copyData(data) { |
|
return data.map(function(d) { |
|
return { |
|
id: d.id, |
|
stem: d.stem, |
|
rank: d.rank, |
|
trend: d.trend, |
|
originalX: d.originalX, |
|
x: d.originalX, |
|
y: d.y |
|
} |
|
}); |
|
}; |
|
|
|
function quadruple(data) { |
|
// Quadruples data while maintaining order and uniq id |
|
var quadrupledData = [], |
|
i; |
|
data.forEach(function(d) { |
|
for (i=0; i<3; i++) { |
|
quadrupledData.push({ |
|
id: d.id+"_"+i, |
|
stem: d.stem, |
|
rank: d.rank, |
|
trend: d.trend, |
|
originalX: d.originalX+i*1E-3, |
|
x: d.x+i*1E-3, |
|
y: d.y |
|
}) |
|
} |
|
quadrupledData.push(d); |
|
}) |
|
return quadrupledData; |
|
}; |
|
|
|
function insertControls () { |
|
var ctrls = new dat.GUI({width: 200}); |
|
manyPointsCtrl = ctrls.add(config, "manyPoints"); |
|
manyPointsCtrl.onChange(function(value) { |
|
draw(); |
|
}); |
|
}; |
|
</script> |
|
</body> |