Skip to content

Instantly share code, notes, and snippets.

@Kcnarf
Last active December 3, 2018 08:31
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Kcnarf/17e82d3a3afe4a8606e48a3bff8fa50e to your computer and use it in GitHub Desktop.
Save Kcnarf/17e82d3a3afe4a8606e48a3bff8fa50e to your computer and use it in GitHub Desktop.
Custom Beeswarm & ForceLayout
license: mit

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:

  • to maintain the exact position of each datum (represented by a circle) along the x-axis
  • to be able to hover each circle to show related datum

This block produces a beeswarm in 2 stages:

  • define a first arrangement using a custom algorythm (cf. previous block)
  • then, use the ForceLayout in order to group extreme accumulations, or constraint the arrangement to a certain size

The ForceLayout is used only few times (5 iterations by default) in order to handle computation time. For comparison, with 600 points:

  • ~1200 ms <== Force only with ~300 iterations
  • ~ 100 ms <== custom only | extreme acc., no overlap, fast
  • ~ 150 ms <== custom (100ms) + force with 5 iterations (50ms) |some overlap, no extreme acc., fast
  • ~ 150ms <== Force only with ~20 iterations | cons: not so beautiful, some overlap, no extreme acc., fast

Acknowledgments to:

stem rank trend
jean 1 0.0221834557
excelent 2 0.0172573247
bon 3 0.0152706367
conseil 4 0.0142771343
part 5 0.0139849763
ecout 6 0.0125657668
promotion 7 0.0116647465
disponibilit 8 0.0107923086
serviabl 9 0.0099251458
acesoir 10 0.0098934332
haut 11 0.009431776
pantalon 12 0.0093578819
bele 13 0.0092871115
aceuil 14 0.0092121588
nouveaut 15 0.0090155204
achat 16 0.008722844
promo 17 0.0086278463
tendanc 18 0.0085356217
manqu 19 0.0084932708
parfait 20 0.0083623534
done 21 0.008355304
feme 22 0.0083248405
vent 23 0.0083132016
avoi 24 0.0080424169
chaleureu 25 0.0078822066
comand 26 0.0075604839
quelqu 27 0.0074075752
tenu 28 0.0072492987
chausur 29 0.00711808
disponibl 30 0.0068948326
person 31 0.0068073546
originalit 32 0.0067217905
vendeu 33 0.0067063822
rien 34 0.0067047717
esay 35 0.0067006641
satisfait 36 0.006684763
cher 37 0.0066836778
acueil 38 0.0066314869
colection 39 0.0064813458
metr 40 0.0064580388
tre 41 0.0064376867
servic 42 0.0063193197
sympath 43 0.0061779954
être 44 0.0061047823
sup 45 0.0060575513
dispos 46 0.0060483871
goût 47 0.0059637097
ador 48 0.0059467335
boutiqu 49 0.0059089954
beau 50 0.0058717403
regulier 51 0.0057329804
certain 52 0.005686449
ofre 53 0.005585669
gentil 54 0.0055481446
styl 55 0.005431152
fidelit 56 0.005398658
foi 57 0.0053355415
propos 58 0.00518158
profesion 59 0.0051523297
game 60 0.0051314774
cart 61 0.0050913638
chos 62 0.0050861437
coup 63 0.0050283014
souriant 64 0.0049009439
tail 65 0.004843215
raport 66 0.0048075105
autr 67 0.0046451013
general 68 0.0046430266
reduction 69 0.0046182266
mode 70 0.004570462
achet 71 0.0045116366
amelior 72 0.0045047883
present 73 0.004487011
elev 74 0.0044661415
rest 75 0.0044256491
jol 76 0.0043042359
acueilant 77 0.0041950808
marqu 78 0.0040932108
produit 79 0.0039764739
diferent 80 0.0039120302
modern 81 0.0038882488
enseign 82 0.0038190136
agreabl 83 0.0037894
trouv 84 0.0037702036
parfoi 85 0.0036276761
port 86 0.0035256142
magasin 87 0.0034050179
chang 88 0.0031922043
agenc 89 0.003190358
clai 90 0.0031812945
ainsi 91 0.0031549695
choi 92 0.0030694137
rayon 93 0.0030580465
client 94 0.0029360714
vête 95 0.0028722762
dire 96 0.0027721774
qualit 97 0.0027403058
var 98 0.0026126169
personel 99 0.0025779703
aimabl 100 0.0025749765
internet 101 0.0025134409
pri 102 0.00238414
sai 103 0.0023536115
abordabl 104 0.0022652921
bone 105 0.0022487593
matier 106 0.0021982583
promod 107 0.0020265214
larg 108 0.0018647201
stock 109 0.0018290033
sympa 110 0.0018003201
corect 111 0.0017615927
vraiment 112 0.0016633065
petit 113 0.0016439206
aime 114 0.0015219369
model 115 0.0014867544
equip 116 0.0011670831
couleu 117 0.0011177858
seul 118 0.0010356827
bais 119 0.0010036041
renouvel 120 0.0009602366
nouvel 121 0.0009557945
niveau 122 0.0004725302
articl 123 0.0003851409
vete 124 0.000353736
domag 125 0.0002193117
raisonabl 126 -0.0001944124
esayag 127 -0.00025102
valeu 128 -0.0002974617
site 129 -0.00040451
amabilit 130 -0.0005457864
grand 131 -0.0008885675
original 132 -0.0009822167
acesibl 133 -0.0010942806
sourir 134 -0.0011092602
diversit 135 -0.001424439
tisu 136 -0.0016639137
cabin 137 -0.0025446106
ane 138 -0.0032602282
propr 139 -0.0039869126
joli 140 -0.0051917043
clas 141 -0.0052463184
variet 142 -0.007044379
atractif 143 -0.0072967461
cais 144 -0.0080716152
rang 145 -0.0081059257
robe 146 -0.0084245392
espac 147 -0.0087132705
atent 148 -0.0109695748
interesant 149 -0.0113475782
sold 150 -0.0611010533
<!DOCTYPE html>
<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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment