This block is a continuation of a previous one.
This sequel 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:
This block produces a beeswarm in 2 stages:
The ForceLayout is used only few times (5 iterations by default) in order to handle computation time. For comparison, with 600 points:
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 src="https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.5.1/dat.gui.min.js"></script>
<script>
var width = 960,
height = 500;
var csvData = [];
var config = {
radius: 4,
use_it: true,
iterations: 5,
gravity: 0.4,
manyPoints: true
};
insertControls();
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("id", "x-axis")
.attr("x1", 0)
.attr("y1", height/2)
.attr("x2", width)
.attr("y2", height/2)
.style("stroke", "lightgrey");
var nodeContainer = svg.append("g").attr("id", "node-container");
var informationPanel, computationTimeInfo, dataLengthInfo, posibleCollidersInfo, placementInfo, visitedCollidersInfo;
prepareInformationPanel();
var tooltip, stem, rank, value;
prepareTooltip();
var minDistanceBetweenCircles,
minSquareDistanceBetweenCircles,
AAD,
totalPossibleCollders, maxPossibleColliders,
totalTestedPlacements,
visitedColliderCount, totalVisitedColliders, maxVisitedColliders;
function initPlacement() {
minDistanceBetweenCircles = 2*config.radius;
minSquareDistanceBetweenCircles = Math.pow(minDistanceBetweenCircles, 2);
AAD = []; //already arranged data; window for collision detection
//-->for metrics purpose
totalPossibleColliders = maxPossibleColliders = 0;
totalTestedPlacements = 0;
visitedColliderCount = totalVisitedColliders = maxVisitedColliders =0;
//<--for metrics purpose
};
function findPossibleColliders (datum) {
//remove circles from AAD that are far away from datum
var indexesToRemove = 0;
AAD.every(function (aad) {
if (Math.abs(datum.x-aad.x)>minDistanceBetweenCircles) {
indexesToRemove++;
return true;
}
return false;
});
AAD.splice(0,indexesToRemove);
//-->for metrics purpose
totalPossibleColliders += AAD.length;
if (AAD.length > maxPossibleColliders) {
maxPossibleColliders = AAD.length;
}
//<--for metrics purpose
}
function isBetterPlacement(datum, bestYPosition) {
return Math.abs(datum.y) < Math.abs(bestYPosition);
}
function yPosRelativeToAad(aad, d) {
// handle Float approximation with +1E-6
return Math.sqrt(minSquareDistanceBetweenCircles-Math.pow(d.x-aad.x,2))+1E-6;
}
function placeBelow(d, aad, relativeYPos) {
d.y = aad.y - relativeYPos;
}
function placeAbove(d, aad, relativeYPos) {
d.y = aad.y + relativeYPos;
}
function areCirclesColliding(d0, d1) {
visitedColliderCount++ //for metrics prupose
//first simple check (vertical positions)
if (Math.abs(d1.y - d0.y) > minDistanceBetweenCircles) return false;
//more advanced check
var squareDistanceBetweenCircles = Math.pow(d1.y-d0.y, 2) + Math.pow(d1.x-d0.x, 2);
return squareDistanceBetweenCircles < minSquareDistanceBetweenCircles;
}
function collidesWithOther (data) {
return AAD.some(function(aad) {
return areCirclesColliding(aad, data);
});
}
function placeCircles (data) {
initPlacement();
data.forEach(function (d) {
var bestYPosition = -Infinity,
relativeYPos;
findPossibleColliders(d);
if (AAD.length===0) {
bestYPosition = 0;
} else {
AAD.forEach(function(aad) {
relativeYPos = yPosRelativeToAad(aad, d);
placeBelow(d, aad, relativeYPos);
if (isBetterPlacement(d, bestYPosition) && !collidesWithOther(d)) {
bestYPosition = d.y;
}
//-->for metrics purpose
totalVisitedColliders += visitedColliderCount;
if (visitedColliderCount > maxVisitedColliders) {
maxVisitedColliders = visitedColliderCount;
}
visitedColliderCount = 0;
//<--for metrics purpose
placeAbove(d, aad, relativeYPos);
if (isBetterPlacement(d, bestYPosition) && !collidesWithOther(d)) {
bestYPosition = d.y;
}
//-->for metrics purpose
totalVisitedColliders += visitedColliderCount;
if (visitedColliderCount > maxVisitedColliders) {
maxVisitedColliders = visitedColliderCount;
}
visitedColliderCount = 0;
//<--for metrics purpose
totalTestedPlacements += 2; //for metrics purpose
})
};
d.y = bestYPosition;
AAD.push(d);
});
}
function showCircles (data) {
nodeContainer.selectAll("circle").remove();
var node = nodeContainer.selectAll("circle")
.data(data)
.enter().append("circle")
.attr("r", config.radius-0.75)
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return 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 applyForceLayout (data) {
//prepare data for ForceLayout
data.forEach(function(d) {
d.y += height/2;
})
var force = d3.layout.force()
.gravity(config.gravity)
.charge(-(config.radius*config.radius-config.radius))
.size([width, height])
.friction(0.7)
function tick() {
data.forEach(function(d){ d.x = d.originalX; }) //constrains x-position
}
force.nodes(data)
.on("tick", tick)
.stop();
force.start();
for (var i = 0; i < config.iterations; i++) { force.tick(); }
force.stop();
}
function drawBeeswarm() {
var data = config.manyPoints? double(double(copyCsvData())) : copyCsvData();
var startTime = Date.now();
if (config.use_it) {
placeCircles(data);
//showMetrics(data, (Date.now()-startTime));
}
applyForceLayout(data);
showCircles(data);
}
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 = 0;
csvData.push(d);
return d;
}
d3.csv("data.csv", dottype, function(error, foo) {
if (error) throw error;
drawBeeswarm()
});
function copyCsvData() {
return csvData.map(function(d) {
return {
stem: d.stem,
rank: d.rank,
trend: d.trend,
originalX: d.originalX,
x: d.originalX,
y: d.y
}
});
}
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,
originalX: d.originalX+1E-3,
x: d.originalX+1E-3,
y: d.y
})
doubledData.push(d);
})
return doubledData;
}
function insertControls () {
var ctrls = new dat.GUI({width: 200});
var customArrangementCtrl = ctrls.addFolder("Step1 - Custom Arrangement");
customArrangementCtrl.open();
var applyCustomArrangementCtrl = customArrangementCtrl.add(config, "use_it");
applyCustomArrangementCtrl.onChange(function(value) {
drawBeeswarm();
});
var forceCtrl = ctrls.addFolder("Step2 - Force Layout");
forceCtrl.open();
var iterationCountCtrl = forceCtrl.add(config, "iterations", 0, 50);
iterationCountCtrl.step(1).onChange(function(value) {
drawBeeswarm();
});
var gravityCtrl = forceCtrl.add(config, "gravity", 0, 1);
gravityCtrl.onChange(function(value) {
drawBeeswarm();
});
var manyPointsCtrl = ctrls.add(config, "manyPoints");
manyPointsCtrl.onChange(function(value) {
drawBeeswarm();
});
}
function prepareTooltip() {
tooltip = svg.append("g")
.attr("id", "tooltip")
.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]+")")
stem = values.append("text")
.attr("text-anchor", "start");
rank = values.append("text")
.attr("text-anchor", "start")
.attr("transform", "translate("+[0,15]+")");
value = values.append("text")
.attr("text-anchor", "start")
.attr("transform", "translate("+[0,30]+")");
}
function prepareInformationPanel() {
var i=4;
informationPanel = svg.append("g")
.attr("id", "infomation-panel")
.attr("transform", "translate("+[width-20, height-20]+")");
computationTimeInfo = informationPanel.append("text")
.attr("text-anchor", "end")
.attr("transform", "translate("+[0,-15*i--]+")");
dataLengthInfo = informationPanel.append("text")
.attr("text-anchor", "end")
.attr("transform", "translate("+[0,-15*i--]+")");
possibleCollidersInfo = informationPanel.append("text")
.attr("text-anchor", "end")
.attr("transform", "translate("+[0,-15*i--]+")");
placementInfo = informationPanel.append("text")
.attr("text-anchor", "end")
.attr("transform", "translate("+[0,-15*i--]+")");
visitedCollidersInfo = informationPanel.append("text")
.attr("text-anchor", "end");
}
function showMetrics (data, elapsed) {
//-->for metrics purpose
computationTimeInfo.text("Arrangement took: "+elapsed+" ms");
dataLengthInfo.text("# data: "+data.length);
possibleCollidersInfo.text("# possible colliders: ~"+Math.round(totalPossibleColliders/data.length)+" per data ("+maxPossibleColliders+" max, "+totalPossibleColliders+" total)");
placementInfo.text("# tested placements: "+totalTestedPlacements);
visitedCollidersInfo.text("# collision checks: "+
totalVisitedColliders);
//>--for metrics purpose
}
</script>
</body>
https://d3js.org/d3.v3.min.js
https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.5.1/dat.gui.min.js