|
<!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) //higher value mean higher attraction to the center of the force layout; can be replace with 'chargeDistance' (cf. below) |
|
.charge(-(radius*radius-radius)) //higher positive/negative value means higher attraction/repulsion of each circle |
|
//.chargeDistance(radius) //can be used instead of 'gravity' in order to maintain nodes grouped; if not set, chargeDistance is infinite, leading to escaping nodes if there is no gravity and/or friction; but does not re-group escaping nodes, as 'gravity' does |
|
.size([width, height]) |
|
.friction(0.7) //higher positive value means easier repositionning |
|
|
|
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.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(); }) |
|
//.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); |
|
}); |
|
|
|
force.nodes(data) |
|
.on("tick", tick) |
|
.stop() |
|
.start(); |
|
|
|
function tick() { |
|
node.each(function(d){d.x = d.originalX; }) //constrains/fixes x-position |
|
|
|
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)); }); |
|
} |
|
}; |
|
|
|
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> |