This block experiments a way to vizualise the distribution of things (whatever it is) in a horizontal way (ie. along the x-axis), where constraints/objectives are:
In previous blocks (1, 2), I tried the Force Layout.
Now I try to implement my own arrangement algorithm (with the secret objective that it will be faster than using the Force O_° ... and, hopefully, it is).
The resulting arrangement can be used as-is (shown in this example), or can be a good starting point before applying the Force. In this later case, final arrangement with the Force will be obtained faster than with the random default arrangment.
The algorithm is:
xxxxxxxxxx
<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>
https://d3js.org/d3.v3.min.js