|
<!DOCTYPE html> |
|
<meta charset="utf-8"> |
|
<style> |
|
|
|
circle { |
|
stroke-width: 1.5px; |
|
} |
|
|
|
line { |
|
stroke: #999; |
|
} |
|
|
|
</style> |
|
<body> |
|
<script src="https://d3js.org/d3.v4.0.0-alpha.35.min.js"></script> |
|
<script> |
|
|
|
var width = 960, |
|
height = 500, |
|
radius = 4; |
|
|
|
var fill = d3.scaleLinear().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.originalX = width/2+d.trend*6000; |
|
d.x = d.originalX; |
|
d.y = height/2; |
|
return d; |
|
} |
|
|
|
d3.csv("data.csv", dottype, function(error, trendData) { |
|
if (error) throw error; |
|
|
|
var node = svg.selectAll("circle") |
|
.data(trendData) |
|
.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(); }) |
|
.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 iterationCount = 0; |
|
var force = d3.forceSimulation() |
|
.nodes(trendData) |
|
//.force("collide", d3.forceCollide(radius)) //handles nodes's overlapping; radius is constantly increased in 'tick' function in order to handle jitter effect |
|
//.force("maintainXPosition", d3.forceX(function(d) { return d.x; }).strength(1)) //does not replaces first line of code in 'tick' function because it can NOT garanty x-position with regards to other forces |
|
.force("groupOnXAxis", d3.forceY(function(d) { return height/2; }).strength(0.5)) //allows to regroup nodes if somes are ejected too far away by 'forceCollide' |
|
.on("tick", tick); |
|
|
|
force.restart(); |
|
|
|
function tick() { |
|
//begin: handle jitter effect by constantly increase collision radius |
|
var jitterHandlingPhase = 150; |
|
var newCollideRadius; |
|
iterationCount++; |
|
if (iterationCount<jitterHandlingPhase) { |
|
newCollideRadius = 1+(radius-1)*Math.pow((iterationCount/jitterHandlingPhase),2); |
|
force.force("collide", d3.forceCollide(newCollideRadius)) //handles nodes's overlapping |
|
} |
|
//end: handle jitter effect by constantly update increase radius |
|
|
|
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)); }); |
|
} |
|
}); |
|
|
|
</script> |