I have made considerable progress in replicating a descriptive statistics beeswarm-style visual that can be seen here. I'll also include the picture here for extra convenience:
From my code snippet you will see that I have all the trappings of that visual except for the pyramid style stacking.
var margins = {top:20, bottom:300, left:30, right:100};
var height = 150;
var width = 900;
var totalWidth = width+margins.left+margins.right;
var totalHeight = height+margins.top+margins.bottom;
var svg = d3.select('body')
.append('svg')
.attr('width', totalWidth)
.attr('height', totalHeight);
var graphGroup = svg.append('g')
.attr('transform', "translate("+margins.left+","+margins.top+")");
var xScale = d3.scaleLinear()
.range([0, width]);
var data = [{'age': 32.0, 'educ': 12.0, 'inlf': 1},
{'age': 30.0, 'educ': 12.0, 'inlf': 1},
{'age': 35.0, 'educ': 12.0, 'inlf': 1},
{'age': 34.0, 'educ': 12.0, 'inlf': 1},
{'age': 31, 'educ': 14.0, 'inlf': 1},
{'age': 54.0, 'educ': 12.0, 'inlf': 1},
{'age': 37.0, 'educ': 16.0, 'inlf': 1},
{'age': 54.0, 'educ': 12.0, 'inlf': 1},
{'age': 48.0, 'educ': 12.0, 'inlf': 1},
{'age': 39.0, 'educ': 12.0, 'inlf': 1},
{'age': 33.0, 'educ': 12.0, 'inlf': 1},
{'age': 42.0, 'educ': 11, 'inlf': 1},
{'age': 30.0, 'educ': 12.0, 'inlf': 1},
{'age': 43.0, 'educ': 12.0, 'inlf': 1},
{'age': 43.0, 'educ': 10.0, 'inlf': 1},
{'age': 35.0, 'educ': 11, 'inlf': 1},
{'age': 43.0, 'educ': 12.0, 'inlf': 1},
{'age': 39.0, 'educ': 12.0, 'inlf': 1},
{'age': 45.0, 'educ': 12.0, 'inlf': 1},
{'age': 35.0, 'educ': 12.0, 'inlf': 1},
{'age': 42.0, 'educ': 16.0, 'inlf': 1},
{'age': 30.0, 'educ': 12.0, 'inlf': 1},
{'age': 48.0, 'educ': 13.0, 'inlf': 1},
{'age': 45.0, 'educ': 12.0, 'inlf': 1},
{'age': 31, 'educ': 12.0, 'inlf': 1},
{'age': 43.0, 'educ': 17.0, 'inlf': 1},
{'age': 59.0, 'educ': 12.0, 'inlf': 1},
{'age': 32.0, 'educ': 12.0, 'inlf': 1},
{'age': 31, 'educ': 17.0, 'inlf': 1},
{'age': 42.0, 'educ': 12.0, 'inlf': 1},
{'age': 50.0, 'educ': 11, 'inlf': 1},
{'age': 59.0, 'educ': 16.0, 'inlf': 1},
{'age': 36.0, 'educ': 13.0, 'inlf': 1},
{'age': 51, 'educ': 12.0, 'inlf': 1},
{'age': 45.0, 'educ': 16.0, 'inlf': 1},
{'age': 42.0, 'educ': 11, 'inlf': 1},
{'age': 46.0, 'educ': 12.0, 'inlf': 1},
{'age': 46.0, 'educ': 10.0, 'inlf': 1},
{'age': 51, 'educ': 14.0, 'inlf': 1},
{'age': 30.0, 'educ': 17.0, 'inlf': 1},
{'age': 30.0, 'educ': 12.0, 'inlf': 1},
{'age': 57.0, 'educ': 12.0, 'inlf': 1},
{'age': 31, 'educ': 16.0, 'inlf': 1},
{'age': 48.0, 'educ': 12.0, 'inlf': 1},
{'age': 30.0, 'educ': 12.0, 'inlf': 1},
{'age': 34.0, 'educ': 12.0, 'inlf': 1},
{'age': 48.0, 'educ': 16.0, 'inlf': 1},
{'age': 45.0, 'educ': 12.0, 'inlf': 1},
{'age': 51, 'educ': 12.0, 'inlf': 1},
{'age': 30.0, 'educ': 12.0, 'inlf': 1},
{'age': 46.0, 'educ': 12.0, 'inlf': 1},
{'age': 58.0, 'educ': 12.0, 'inlf': 1},
{'age': 37.0, 'educ': 12.0, 'inlf': 1},
{'age': 52.0, 'educ': 8.0, 'inlf': 1},
{'age': 52.0, 'educ': 10.0, 'inlf': 1},
{'age': 31, 'educ': 16.0, 'inlf': 1},
{'age': 55.0, 'educ': 14.0, 'inlf': 1},
{'age': 34.0, 'educ': 17.0, 'inlf': 1},
{'age': 55.0, 'educ': 14.0, 'inlf': 1},
{'age': 39.0, 'educ': 12.0, 'inlf': 1},
{'age': 40.0, 'educ': 14.0, 'inlf': 1},
{'age': 43.0, 'educ': 12.0, 'inlf': 1},
{'age': 48.0, 'educ': 8.0, 'inlf': 1},
{'age': 47.0, 'educ': 12.0, 'inlf': 1},
{'age': 41, 'educ': 12.0, 'inlf': 1},
{'age': 36.0, 'educ': 8.0, 'inlf': 1},
{'age': 46.0, 'educ': 17.0, 'inlf': 1},
{'age': 34.0, 'educ': 12.0, 'inlf': 1},
{'age': 41, 'educ': 12.0, 'inlf': 1},
{'age': 51, 'educ': 12.0, 'inlf': 1},
{'age': 33.0, 'educ': 12.0, 'inlf': 1},
{'age': 52.0, 'educ': 12.0, 'inlf': 1},
{'age': 58.0, 'educ': 9.0, 'inlf': 1},
{'age': 34.0, 'educ': 10.0, 'inlf': 1},
{'age': 31, 'educ': 12.0, 'inlf': 1},
{'age': 48.0, 'educ': 12.0, 'inlf': 1},
{'age': 32.0, 'educ': 12.0, 'inlf': 1},
{'age': 49.0, 'educ': 17.0, 'inlf': 1},
{'age': 32.0, 'educ': 15.0, 'inlf': 1},
{'age': 58.0, 'educ': 12.0, 'inlf': 1},
{'age': 50.0, 'educ': 6.0, 'inlf': 1},
{'age': 60.0, 'educ': 14.0, 'inlf': 1},
{'age': 50.0, 'educ': 12.0, 'inlf': 1},
{'age': 56.0, 'educ': 14.0, 'inlf': 1},
{'age': 51, 'educ': 9.0, 'inlf': 1},
{'age': 54.0, 'educ': 17.0, 'inlf': 1},
{'age': 59.0, 'educ': 13.0, 'inlf': 1},
{'age': 46.0, 'educ': 9.0, 'inlf': 1},
{'age': 46.0, 'educ': 15.0, 'inlf': 1},
{'age': 39.0, 'educ': 12.0, 'inlf': 0},
{'age': 44.0, 'educ': 12.0, 'inlf': 0},
{'age': 33.0, 'educ': 12.0, 'inlf': 0},
{'age': 33.0, 'educ': 12.0, 'inlf': 0},
{'age': 48.0, 'educ': 12.0, 'inlf': 0},
{'age': 30, 'educ': 12.0, 'inlf': 0},
{'age': 45.0, 'educ': 12.0, 'inlf': 0},
{'age': 45.0, 'educ': 12.0, 'inlf': 0},
{'age': 32.0, 'educ': 13.0, 'inlf': 0},
{'age': 47.0, 'educ': 12.0, 'inlf': 0},
{'age': 34.0, 'educ': 13.0, 'inlf': 0},
{'age': 37.0, 'educ': 12.0, 'inlf': 0},
{'age': 36.0, 'educ': 12.0, 'inlf': 0},
{'age': 47.0, 'educ': 12.0, 'inlf': 0},
{'age': 48.0, 'educ': 16.0, 'inlf': 0},
{'age': 42.0, 'educ': 12.0, 'inlf': 0},
{'age': 33.0, 'educ': 13.0, 'inlf': 0},
{'age': 46.0, 'educ': 10, 'inlf': 0},
{'age': 47.0, 'educ': 12.0, 'inlf': 0},
{'age': 44.0, 'educ': 12.0, 'inlf': 0},
{'age': 36.0, 'educ': 12.0, 'inlf': 0},
{'age': 30, 'educ': 17.0, 'inlf': 0},
{'age': 55.0, 'educ': 14.0, 'inlf': 0},
{'age': 45.0, 'educ': 16.0, 'inlf': 0},
{'age': 47.0, 'educ': 17.0, 'inlf': 0},
{'age': 46.0, 'educ': 12.0, 'inlf': 0},
{'age': 49.0, 'educ': 10, 'inlf': 0},
{'age': 49.0, 'educ': 12.0, 'inlf': 0},
{'age': 45.0, 'educ': 12.0, 'inlf': 0},
{'age': 38.0, 'educ': 17.0, 'inlf': 0},
{'age': 47.0, 'educ': 10.0, 'inlf': 0},
{'age': 54.0, 'educ': 13.0, 'inlf': 0},
{'age': 40, 'educ': 10, 'inlf': 0},
{'age': 43.0, 'educ': 12.0, 'inlf': 0},
{'age': 30, 'educ': 16.0, 'inlf': 0},
{'age': 47.0, 'educ': 17.0, 'inlf': 0},
{'age': 35.0, 'educ': 12.0, 'inlf': 0},
{'age': 45.0, 'educ': 16.0, 'inlf': 0},
{'age': 33.0, 'educ': 12.0, 'inlf': 0},
{'age': 54.0, 'educ': 16.0, 'inlf': 0},
{'age': 35.0, 'educ': 8.0, 'inlf': 0},
{'age': 30, 'educ': 12.0, 'inlf': 0},
{'age': 55.0, 'educ': 12.0, 'inlf': 0},
{'age': 34.0, 'educ': 12.0, 'inlf': 0},
{'age': 38.0, 'educ': 13.0, 'inlf': 0},
{'age': 45.0, 'educ': 10, 'inlf': 0},
{'age': 47.0, 'educ': 12.0, 'inlf': 0},
{'age': 39.0, 'educ': 12.0, 'inlf': 0},
{'age': 36.0, 'educ': 14.0, 'inlf': 0},
{'age': 33.0, 'educ': 12.0, 'inlf': 0},
{'age': 50.0, 'educ': 12.0, 'inlf': 0},
{'age': 58.0, 'educ': 12.0, 'inlf': 0},
{'age': 49.0, 'educ': 17.0, 'inlf': 0},
{'age': 40, 'educ': 14.0, 'inlf': 0},
{'age': 50, 'educ': 12.0, 'inlf': 0},
{'age': 53.0, 'educ': 9.0, 'inlf': 0},
{'age': 36.0, 'educ': 12.0, 'inlf': 0},
{'age': 46.0, 'educ': 12.0, 'inlf': 0},
{'age': 36.0, 'educ': 12.0, 'inlf': 0},
{'age': 53.0, 'educ': 14.0, 'inlf': 0},
{'age': 40.0, 'educ': 16.0, 'inlf': 0}];
var colorScale = d3.scaleLinear()
.range(["#e7eef8","#003366"]);
xScale.domain(d3.extent(data, function(d) { return d.educ; }));
colorScale.domain(d3.extent(data, function(d) {return d.age; }));
var simulation = d3.forceSimulation(data)
.force("x", d3.forceX(function(d) {
return xScale(d.educ);
}).strength(1))
.force("y", d3.forceY(function(d) {
return d.inlf ? height - 75 : height + 100
}))
.force("collide", d3.forceCollide(4))
.stop();
for (var i = 0; i < 120; ++i) simulation.tick();
graphGroup.append("g")
.attr("class", "axis axis--x")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(xScale));
var circles = graphGroup.selectAll(null)
.data(data)
.enter()
.append("circle")
.attr("r", 3)
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
})
.style('fill', function(d) {return colorScale(d.age)});
graphGroup.append('line')
.attr('x1', xScale(5))
.attr('x2', xScale(17))
.attr('y1', height-5)
.attr('y2', height-5)
.style('stroke',"#000");
graphGroup.append('line')
.attr('x1', xScale(5))
.attr('x2', xScale(17))
.attr('y1', height+35)
.attr('y2', height+35)
.style('stroke',"#000");
graphGroup.append('line')
.attr('x1', xScale(5))
.attr('x2', xScale(12))
.attr('y1', height-5)
.attr('y2', height-5)
.style('stroke',"#b8cce4")
.style('stroke-width',"5px");
graphGroup.append('line')
.attr('x1', xScale(12))
.attr('x2', xScale(14))
.attr('y1', height-5)
.attr('y2', height-5)
.style('stroke',"#4f81b9")
.style('stroke-width',"5px");
graphGroup.append('rect')
.attr('x',xScale(12))
.attr('y', height-10)
.attr('width', 5)
.attr('height',10)
.style('fill', "#f6d18b");
graphGroup.append('line')
.attr('x1', xScale(5))
.attr('x2', xScale(11))
.attr('y1', height+35)
.attr('y2', height+35)
.style('stroke',"#b8cce4")
.style('stroke-width',"5px");
graphGroup.append('line')
.attr('x1', xScale(11))
.attr('x2', xScale(12))
.attr('y1', height+35)
.attr('y2', height+35)
.style('stroke',"#4f81b9")
.style('stroke-width',"5px");
graphGroup.append('rect')
.attr('x',xScale(11.7))
.attr('y', height+30)
.attr('width', 5)
.attr('height',10)
.style('fill', "#f6d18b");
.cells path {
fill: none;
pointer-events: all;
}
.cells :hover circle {
fill: red;
}
text {
font-size: 17px;
font-family: TW Cen MT;
}
.axis path, .axis line {
fill: none;
stroke: none;
}
<script src="https://d3js.org/d3.v5.min.js"></script>
As it stands, the stacking shape seems to be a kind of ellipse. I have not found a way to alter the default shape.
Question
How can I modify the default shape of the circle piling to resemble a pyramid? It doesn't have to be a perfect pyramid, just gunning for more at the bottom less at the top. I can foresee things might be especially tricky for data points where d.educ=12
, as that is highly saturated.