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:
Compared to the previous block:
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 src="sorted_direct_access_doubly_linked_list.js"></script>
<script src="beeswarm.js"></script>
<script>
var width = 960,
height = 500;
var csvData = []; // data retrieve from CSV
var memorizedSortedData = [] // allow to maintain sort when changing radius
var availableSortings = ['min -> max', 'max -> min', 'shuffled'];
var config = {
radius: 4,
manyPoints: false,
sorting: "shuffled",
sortOptions: {
shuffled: function() {
config.sorting = "shuffled";
renewData();
drawBeeswarm();
},
minToMax: function() {
config.sorting = "minToMax";
renewData();
drawBeeswarm();
},
maxToMin: function() {
config.sorting = "maxToMin";
renewData();
drawBeeswarm();
}
}
};
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 tooltip, stem, rank, value;
prepareTooltip();
//-->for metrics purpose
var informationPanel, computationTimeInfo, dataLengthInfo, posibleCollidersInfo, placementInfo, visitedCollidersInfo;
prepareInformationPanel();
var totalPossibleColliders, maxPossibleColliders,
totalTestedPlacements,
visitedColliderCount, totalVisitedColliders, maxVisitedColliders;
//<--for metrics purpose
////////////////////////
// Custom Arrangement //
////////////////////////
var minDistanceBetweenCircles,
minSquareDistanceBetweenCircles,
xBasedDataManager, // for collision detection, x-based sorted direct-access doubly-linked list of data, used to find nearest already arranged data
xBasedColliderManager, // for collision detection, x-based sorted direct-access doubly-linked list of already arranged data, limit collision detection to already arranged neighbours
yBasedColliderManager; // for collision detection, y-based sorted direct-access doubly-linked list of already arranged data, limit collision detection to already arranged neighbours
function initArrangement(originalData, radius) {
minDistanceBetweenCircles = 2*radius;
minSquareDistanceBetweenCircles = Math.pow(minDistanceBetweenCircles, 2);
xBasedDataManager = new SortedDirectAccessDoublyLinkedList()
.valueAccessor(function(d){return d.x;})
.addMany(originalData);
xBasedColliderManager = new SortedDirectAccessDoublyLinkedList()
.valueAccessor(function(d){return d.x;});
yBasedColliderManager = new SortedDirectAccessDoublyLinkedList()
.valueAccessor(function(d){return d.y;});
//-->for metrics purpose
totalPossibleColliders = maxPossibleColliders = 0;
totalTestedPlacements = 0;
visitedColliderCount = totalVisitedColliders = maxVisitedColliders =0;
//<--for metrics purpose
};
function findNearestBelowPossibleCollider(dln, visitedDln, xBasedDataManager) {
if (visitedDln === null) { // special case: min reached
return null;
} else if ((dln.value - visitedDln.value) > minDistanceBetweenCircles) {
// stop visit, remaining data are too far away
return null;
} else {// visitedDln is close enought
if (isFinite(visitedDln.datum.y)) { // visitedDln is already arranged, and hence is the nearest possible x-based collider
return(visitedDln.datum);
}
// continue finding
return findNearestBelowPossibleCollider(dln, visitedDln.below, xBasedDataManager);
}
};
function findNearestAbovePossibleCollider(dln, visitedDln, xBasedDataManager) {
if (visitedDln === null) { // special case: max reached
return null;
} else if ((visitedDln.value - dln.value) > minDistanceBetweenCircles) {
// stop visit, remaining data are too far away
return null;
} else {// visitedDln is close enought
if (isFinite(visitedDln.datum.y)) { // visitedDln is already arranged, and hence is the nearest possible x-based collider
return(visitedDln.datum);
}
// continue finding
return findNearestAbovePossibleCollider(dln, visitedDln.above, xBasedDataManager);
}
};
function gatherXBelowPossibleColliders(dln, visitedDln, xBasedColliderManager, xBasedPossibleColliders) {
if (visitedDln === null) { // special case: min reached
return;
} else if ((dln.value - visitedDln.value) > minDistanceBetweenCircles) {
// stop visit, remaining data are too far away
return;
} else {// visitedDln is close enought
// visitedDln is already arranged, and hence is a possible x-based collider
xBasedPossibleColliders.push(visitedDln.datum);
// continue gathering
gatherXBelowPossibleColliders(dln, visitedDln.below, xBasedColliderManager, xBasedPossibleColliders);
}
};
function gatherXAbovePossibleColliders(dln, visitedDln, xBasedColliderManager, xBasedPossibleColliders) {
if (visitedDln === null) { // special case: max reached
return;
} else if ((visitedDln.value - dln.value) > minDistanceBetweenCircles) {
// stop visit, remaining data are too far away
return;
} else {// visitedDln is close enought
// visitedDln is already arranged, and hence is a possible x-based collider
xBasedPossibleColliders.push(visitedDln.datum);
// continue gathering
gatherXAbovePossibleColliders(dln, visitedDln.above, xBasedColliderManager, xBasedPossibleColliders);
}
};
function gatherXBasedPossibleColliders (datum) {
var xBasedPossibleColliders = [];
var dln = xBasedDataManager.dln(datum);
//use xBasedDataManager to retrieve nearest already arranged data
var nearestXBelowAlreadyArrangedData = findNearestBelowPossibleCollider(dln, dln.below, xBasedDataManager);
var nearestXAboveAlreadyArrangedData = findNearestAbovePossibleCollider(dln, dln.above, xBasedDataManager);
//use xBasedColliderManager to retrieve already arranged data that may collide with datum (ie, close enought to datum considering x position)
if (nearestXBelowAlreadyArrangedData != null) {
//visit x-below already arranged nodes
dln = xBasedColliderManager.dln(nearestXBelowAlreadyArrangedData);
gatherXBelowPossibleColliders(dln, dln, xBasedColliderManager, xBasedPossibleColliders);
}
if (nearestXAboveAlreadyArrangedData != null) {
//visit x-above already arranged nodes
dln = xBasedColliderManager.dln(nearestXAboveAlreadyArrangedData);
gatherXAbovePossibleColliders(dln, dln, xBasedColliderManager, xBasedPossibleColliders);
}
//-->for metrics purpose
totalPossibleColliders += xBasedPossibleColliders.length;
if (xBasedPossibleColliders.length > maxPossibleColliders) {
maxPossibleColliders = xBasedPossibleColliders.length;
}
//<--for metrics purpose
return xBasedPossibleColliders;
};
function isBetterPlacement(datum, bestYPosition) {
return Math.abs(datum.y) < Math.abs(bestYPosition);
};
function yPosRelativeToXbpc(xbpc, d) {
// handle Float approximation with +1E-6
return Math.sqrt(minSquareDistanceBetweenCircles-Math.pow(d.x-xbpc.x,2))+1E-6;
};
function placeBelow(d, aad, relativeYPos) {
d.y = aad.y - relativeYPos;
//showOnTheFlyCircleArrangement(d, "test");
};
function placeAbove(d, aad, relativeYPos) {
d.y = aad.y + relativeYPos;
//showOnTheFlyCircleArrangement(d, "test");
};
function areCirclesColliding(d0, d1) {
visitedColliderCount++; //for metrics prupose
return (Math.pow(d1.y-d0.y, 2) + Math.pow(d1.x-d0.x, 2)) < minSquareDistanceBetweenCircles;
};
function collidesWithBelow(datum, visitedDln, yBasedColliderManager, visitCount) {
if (visitedDln === null) { // special case: y_min reached, no collision detected
return false;
} else if ((datum.y - visitedDln.datum.y) > minDistanceBetweenCircles) {
// stop visit, no collision detected, remaining data are too far away
return false;
} else if (areCirclesColliding(datum, visitedDln.datum)) {
return true;
} else {
// continue visit
return collidesWithBelow(datum, visitedDln.below, yBasedColliderManager, visitCount++)
}
};
function collidesWithAbove(datum, visitedDln, yBasedColliderManager, visitCount) {
if (visitedDln === null) { // special case: y_max reached, no collision detected
return false;
} else if ((visitedDln.datum.y - datum.y) > minDistanceBetweenCircles) {
// stop visit, no collision detected, remaining data are too far away
return false;
} else if (areCirclesColliding(datum, visitedDln.datum)) {
return true;
} else {
// continue visit
return collidesWithAbove(datum, visitedDln.above, yBasedColliderManager, visitCount++)
}
};
function collidesWithOther (datum, visitedDln, yBasedColliderManager) {
var visitCount = 0;
//visit below dlns for collision check
if (collidesWithBelow(datum, visitedDln.below, yBasedColliderManager, visitCount++)) {
return true;
} else {
//visit above dlns for collision check
return collidesWithAbove(datum, visitedDln.above, yBasedColliderManager, visitCount++);
}
};
function arrangeCircles (data) {
initArrangement(data, config.radius);
data.forEach(function (d) {
var bestYPosition = -Infinity,
relativeYPos,
xBasedPossibleColliders = gatherXBasedPossibleColliders(d);
if (xBasedPossibleColliders.length===0) {
bestYPosition = 0;
} else {
yBasedColliderManager.clear();
yBasedColliderManager.addMany(xBasedPossibleColliders);
xBasedPossibleColliders.forEach(function(xbpc) {
relativeYPos = yPosRelativeToXbpc(xbpc, d);
placeBelow(d, xbpc, relativeYPos);
if (isBetterPlacement(d, bestYPosition) &&
!collidesWithOther(d, yBasedColliderManager.dln(xbpc), yBasedColliderManager)) {
bestYPosition = d.y;
}
//-->for metrics purpose
totalVisitedColliders += visitedColliderCount;
if (visitedColliderCount > maxVisitedColliders) {
maxVisitedColliders = visitedColliderCount;
}
visitedColliderCount = 0;
//<--for metrics purpose
placeAbove(d, xbpc, relativeYPos);
if (isBetterPlacement(d, bestYPosition) &&
!collidesWithOther(d, yBasedColliderManager.dln(xbpc), yBasedColliderManager)) {
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;
xBasedColliderManager.add(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 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 drawBeeswarm() {
var data = copyData(memorizedSortedData);
var startTime = Date.now();
/* target Beeswarm API
var beeswarm = new Beeswarm()
.data(data)
.x(function(d) { return d.x; })
.arrange();
*/arrangeCircles(data);
showMetrics(data, (Date.now()-startTime));
showCircles(data);
};
d3.csv("data.csv", dottype, function(error, data) {
if (error) throw error;
renewData();
drawBeeswarm();
});
////////////////////////
// bl.ocks' utilities //
////////////////////////
function dottype(d) {
d.id = d.stem;
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 = -Infinity;
csvData.push(d);
return d;
};
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=1; i<4; 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 renewData () {
var newData = copyData(csvData);
if (config.manyPoints) {
newData = quadruple(newData);
}
if (config.sorting === "maxToMin" ) {
memorizedSortedData = newData;
} else if (config.sorting === "minToMax" ) {
memorizedSortedData = newData.reverse();
} else {
memorizedSortedData = d3.shuffle(newData);
}
};
function insertControls () {
var ctrls = new dat.GUI({width: 200});
radiusCtrl = ctrls.add(config, "radius", 1, 20);
radiusCtrl.onChange(function(value) {
drawBeeswarm();
});
manyPointsCtrl = ctrls.add(config, "manyPoints");
manyPointsCtrl.onChange(function(value) {
renewData();
drawBeeswarm();
});
sortingFolder = ctrls.addFolder("Sorting");
sortingFolder.open();
sortingCtrl = sortingFolder.add(config.sortOptions, "shuffled");
sortingCtrl = sortingFolder.add(config.sortOptions, "minToMax");
sortingCtrl = sortingFolder.add(config.sortOptions, "maxToMin");
};
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) {
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: ~"+Math.round(totalTestedPlacements/data.length)+" per data ("+(maxPossibleColliders*2)+" max, "+totalTestedPlacements+" total)");
visitedCollidersInfo.text("# collision checks: ~"+Math.round(totalVisitedColliders/totalTestedPlacements)+" per placement ("+maxVisitedColliders+" max, "+totalVisitedColliders+" total)")
};
function showOnTheFlyCircleArrangement(d, type) {
nodeContainer.selectAll("circle.test").remove();
nodeContainer.append("circle")
.datum(d)
.classed(type, true)
.attr("r", config.radius)
.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(); })
};
</script>
</body>
https://d3js.org/d3.v3.min.js
https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.5.1/dat.gui.min.js