This block compares 2 techniques that both produces a static beeswarm.
The top most beeswarm is the one from mbostock's beeswarm block. It is implemented using d3-force’s collision constraint. The bottom most beeswarm is produced with the d3-beeswarm plugin I made (see the Github project).
This block aims to understand the differences, advantages and drawbacks of the 2 techniques. I'm focusing this comparison on the capability of the beeswarm to exactly reflect the encoded data (ie. the x-coordinate should exactly reflects the encoded data): the more it's red, the less it represents the encoded data. That's the main reason why the d3.force technique doesn't suit my needs, and why I made the d3-beeswarm plugin.
Here are some thoughts:
xxxxxxxxxx
<meta charset="utf-8">
<style>
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.axis text {
font: 10px sans-serif;
cursor: default;
}
.cell .voronoi {
fill: transparent;
//stroke: grey;
}
.path-to-axis {
shape-rendering: crispEdges;
opacity: 0.25;
}
.info {
fill: grey;
}
</style>
<svg width="960" height="500"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://raw.githack.com/kcnarf/d3-beeswarm/master/build/d3-beeswarm.js"></script>
<script>
var svg = d3.select("svg"),
margin = {top: 40, right: 40, bottom: 40, left: 40},
width = svg.attr("width") - margin.left - margin.right,
height = svg.attr("height") - margin.top - margin.bottom;
beeswarmHeight = 100;
axisHeight = 25;
var formatValue = d3.format(",d");
var x = d3.scaleLog()
.rangeRound([0, width]);
var deviation = d3.scaleLinear() //represente position deviation with color
.range(['black','red']);
var g = svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
d3.csv("flare.csv", type, function(error, data) {
if (error) throw error;
x.domain(d3.extent(data, function(d) { return d.value; }));
g.append("g")
.classed("axis axis--x", true)
.attr("transform", "translate(0," + (beeswarmHeight) + ")")
.call(d3.axisBottom(x).ticks(20, ".0s"));
//begin: arrange data with d3-force layout
var simulation = d3.forceSimulation(data)
.force("x", d3.forceX(function(d) { return x(d.value); }).strength(1))
.force("y", d3.forceY(beeswarmHeight/2-axisHeight/2))
.force("collide", d3.forceCollide(4))
.stop();
for (var i = 0; i < 120; ++i) simulation.tick();
//end: arrange data with d3-force layout
//begin: arrange data with d3-beeswarm
var beeswarm = d3.beeswarm()
.data(data.sort(function(a,b) { return b.value-a.value; }))
.radius(4)
.distributeOn(function(d) { return x(d.value);})
.arrange();
//end: arrange data with d3-beeswarm
//represente position deviation with color
deviation.domain(d3.extent(data, function(d) { return Math.abs(d.x-x(d.value)); }));
//reorder data so that highiest position deviation are drawn last/on top
data.sort(function(a,b) { return Math.abs(b.x-x(b.value))-Math.abs(a.x-x(a.value)); });
//begin: draw force-based arrangement
var forceVoronoi = d3.voronoi()
.extent([[-margin.left,0], [width+margin.right, beeswarmHeight-axisHeight]])
.x(function(d) { return d.x; })
.y(function(d) { return d.y; })
.polygons(data);
var forceCellContainer = g.append("g")
.classed("cell-container force", true);
forceCellContainer.append("text")
.classed("info", true)
.attr("transform", "translate("+width+",0)")
.attr("text-anchor", "end")
.text("force-based arrangement");
var forcePathToAxisContainer = forceCellContainer.append("g");
forcePathToAxisContainer.selectAll(".path-to-axis")
.data(forceVoronoi)
.enter()
.append("path")
.classed("path-to-axis", true)
.attr("data-flare-id", function(d) { return d.data.id; })
.attr("d", function(d) { return "M"+[d.data.x, beeswarmHeight-axisHeight]+"V"+(beeswarmHeight); })
.style("stroke", function(d){ return deviation(Math.abs(d.data.x-x(d.data.value))); })
.on("mouseenter", function(d) { highlight(d.data); })
.on("mouseout", function(d) { trivialize(d.data); });
var forceCell = forceCellContainer.selectAll(".cell.force").data(forceVoronoi)
.enter()
.append("g")
.classed("cell force", true)
.attr("data-flare-id", function(d) { return d.data.id; });
forceCell.append("circle")
.attr("r", 3)
.attr("cx", function(d) { return d.data.x; })
.attr("cy", function(d) { return d.data.y; })
.style("fill", function(d){ return deviation(Math.abs(d.data.x-x(d.data.value))); });
forceCell.append("path")
.classed("voronoi", true)
.attr("d", function(d) { return "M" + d.join("L") + "Z"; })
.on("mouseenter", function(d) { highlight(d.data); })
.on("mouseout", function(d) { trivialize(d.data); });
forceCell.append("title")
.text(function(d) { return d.data.id + "\n" + formatValue(d.data.value); });
//end: draw force-based arrangement
//begin: draw d3-beeswarm arrangement
var beeswarmVoronoi = d3.voronoi()
.extent([[-margin.left, -beeswarmHeight/2], [width + margin.right, beeswarmHeight/2]])
.x(function(d) { return d.x; })
.y(function(d) { return d.y; })
.polygons(beeswarm);
var beeswarmCellContainer = g.append("g")
.classed("cell-container beeswarm", true)
.attr("transform", "translate(0,"+(beeswarmHeight)+")");
beeswarmCellContainer.append("text")
.classed("info", true)
.attr("transform", "translate("+[width, beeswarmHeight+axisHeight]+")")
.attr("text-anchor", "end")
.text("d3-beeswarm arrangement");
var beeswarmPathToAxisContainer = beeswarmCellContainer.append("g");
beeswarmPathToAxisContainer.selectAll(".path-to-axis")
.data(beeswarmVoronoi)
.enter()
.append("path")
.classed("path-to-axis", true)
.attr("data-flare-id", function(d) { return d.data.datum.id;})
.attr("d", function(d) { return "M"+[d.data.x, axisHeight]+"V0"; })
.style("stroke", "black")
.on("mouseenter", function(d) { highlight(d.data.datum); })
.on("mouseout", function(d) { trivialize(d.data.datum); });
var beeCell = beeswarmCellContainer.selectAll(".cell.beeswarm").data(beeswarmVoronoi)
.enter()
.append("g")
.classed("cell beeswarm", true)
.attr("data-flare-id", function(d) { return d.data.datum.id; });
beeCell.append("circle")
.attr("r", 3)
.attr("cx", function(d) { return d.data.x; })
.attr("cy", function(d) { return d.data.y+beeswarmHeight/2+axisHeight; })
.style("fill", function(d){ return deviation(Math.abs(d.data.x-x(d.data.value))); });
beeCell.append("path")
.classed("voronoi", true)
.attr("d", function(d) { return "M" + d.join("L") + "Z"; })
.attr("transform", "translate(0,"+(beeswarmHeight/2+axisHeight)+")")
.on("mouseenter", function(d) { highlight(d.data.datum); })
.on("mouseout", function(d) { trivialize(d.data.datum); });
beeCell.append("title")
.text(function(d) { return d.data.datum.id + "\n" + formatValue(d.data.datum.value); });
//end: draw d3-beeswarm arrangement
});
function highlight (datum) {
d3.selectAll("[data-flare-id='"+datum.id+"'].path-to-axis").style("opacity", 1);
d3.selectAll("[data-flare-id='"+datum.id+"'] circle").attr("r", 4);
}
function trivialize (datum) {
d3.selectAll("[data-flare-id='"+datum.id+"'].path-to-axis").style("opacity", 0.25);
d3.selectAll("[data-flare-id='"+datum.id+"'] circle").attr("r", 3);
}
function type(d) {
if (!d.value) return;
d.value = +d.value;
return d;
}
</script>
Updated missing url https://raw.githack.com/Kcnarf/d3-beeswarm/master/build/d3-beeswarm.js to https://raw.githack.com/kcnarf/d3-beeswarm/master/build/d3-beeswarm.js
https://d3js.org/d3.v4.min.js
https://raw.githack.com/Kcnarf/d3-beeswarm/master/build/d3-beeswarm.js