1

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:

enter image description here

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.

Arash Howaida
  • 2,575
  • 2
  • 19
  • 50
  • Why are you using 2 simulations? It makes no sense. Have a look at my answer to your [previous question](https://stackoverflow.com/q/58006740/5768908) to see how to set the simulation. – Gerardo Furtado Sep 19 '19 at 11:11
  • Apart from @GerardoFurtado's comment about needlessly complicating matters by using two simulations I really like the challenge of giving it a pyramidal shape. Let's see how far we can get from here. – altocumulus Sep 19 '19 at 11:53
  • @altocumulus Ok updated with Gerardo's answer from the previous post. That did simplify things greatly. Now it's the pyramidal shape that remains, not sure if there are any built-in options.. trying to stay hopeful though. – Arash Howaida Sep 20 '19 at 02:42

2 Answers2

2

If it doesn't have to be perfect, you can achieve the effect by imposing hard constraints on the simulation and adjusting forces.

Hard Constraints

To form hard constraints (like having a ceiling or a floor), you can manually move points after every tick.

For instance, if you want to form a floor for the top points at height - 15, and a ceiling for the bottom points at height + 45, you could change:

for (var i = 0; i < 120; ++i) simulation.tick();

to:

for (var i = 0; i < 120; ++i) {
  simulation.tick();
  data.forEach(function (d) {
    if (d.inlf) {
      d.y = Math.min(d.y, height - 15);
    } else {
      d.y = Math.max(d.y, height + 45);
    }    
  });
}

Force Settings

After that, you probably want to play around with the strength of forceX and forceY. I also added .iterations to forceCollide, just in case you need to adjust it later. These settings seemed to work okay for me:

var simulation = d3.forceSimulation(data)
  .force("x", d3.forceX(function(d) {
    return xScale(d.educ);
  }).strength(0.05))
  .force("y", d3.forceY(function(d) {
    return d.inlf ? height - 15 : height + 45
  }).strength(0.1))
  .force("collide", d3.forceCollide(4).iterations(1))
  .stop();

You may need to increase the number of tick iterations for weak forces or for overlapping circles. I increased it to 240 for this example.

for (var i = 0; i < 120; ++i) {
  simulation.tick();
  // ...

to:

for (var i = 0; i < 240; ++i) {
  simulation.tick();
  // ...

Initial positions

As pointed out in the comments, the circles are not where they should be! This is due to the weak forces not moving them far enough from their initial position. To fix this, we can set the initial position to be around where we want them.

data.forEach(function (d) {
  d.x = xScale(d.educ);
  d.y = d.inlf ? height - 15 : height + 45;
});

Result

After moving the points up, all in all, it looks like:

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; }));

  data.forEach(function (d) {
    d.x = xScale(d.educ);
    d.y = d.inlf ? height - 15 : height + 45;
  });

  var simulation = d3.forceSimulation(data)
    .force("x", d3.forceX(function(d) {
      return xScale(d.educ);
    }).strength(0.05))
    .force("y", d3.forceY(function(d) {
      return d.inlf ? height - 15 : height + 45
    }).strength(0.1))
    .force("collide", d3.forceCollide(4).iterations(1))
    .stop();
    
for (var i = 0; i < 240; ++i) {
  simulation.tick();
  data.forEach(function (d) {
    if (d.inlf) {
      d.y = Math.min(d.y, height - 15);
    } else {
      d.y = Math.max(d.y, height + 45);
    }    
  });
}

  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>
Steve
  • 10,435
  • 15
  • 21
  • Looks very promising! Snippet seems to be cut off though – Arash Howaida Sep 26 '19 at 03:08
  • I'm not running into that problem-- looks like it runs for me after I click on `Show code snippet` and scrolling down and clicking on `Run code snippet`. Let me know if you run into any problems – Steve Sep 26 '19 at 03:12
  • 1
    My mistake, refreshing my browser fixed it. The shape is just as I want it, very excited for that. The only thing is the pyramid clusters don't seem to be quite centered on the values. Probably a quick `translate` call could fix it, but I just wanted that brought to your attention -- maybe you know of a better fix. It's more noticeable if you change `height-75` to `height-15` and `height+100` to `height+45`. – Arash Howaida Sep 26 '19 at 03:26
  • Thanks for pointing this out! This looks like it's an issue with weak forces not moving the points far enough. I've updated the snippet and example to set initial positions – Steve Sep 26 '19 at 03:54
2

Little late to the party but if you want a stronger stacked pyramid, I'd abandon the force simulation. Here's a custom implementation with a custom pyramid stacking algorithm:

<!DOCTYPE html>
<html>

<head>
  <script src="https://d3js.org/d3.v5.min.js"></script>
</head>

<body>
  <script>
    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;
    }));

    // bin the data based on top bottom and x-position
    var bins = d3.nest()
      .key(function(d) { return [(d.inlf ? 1 : 0), d.educ] })
      .entries(data);

    // calculate positions (build the pyramid)
    var r = 3;
    bins.forEach( (bin) => {
      var ds = bin.values;
      var br = 1, n = 1;
      while (br < ds.length){
        br += n; n += 1;
      }
      n -= 1
      var c = 0; rn = n; g = 0;
      var s = r * 2 + 3;
      ds.forEach((d)=>{
        d.y = (rn - n) * s;
        d.x = (c * s);
        d.g = g;
        c += 1;
        if (c == n){
          c = 0;
          n -= 1;
          g += 1
        }
      });
    });
    
    // re-bin by age and row so that we can traslate the rows
    var binsAndRow = d3.nest()
      .key(function(d) { return [(d.inlf ? 1 : 0), d.educ]; })
      .key(function(d) { return d.g; })
      .entries(data);
    
    // draw circles
    var x = graphGroup.selectAll("g")
      .data(binsAndRow)
      .enter()
      .append("g")
      .attr("class", (d) => "bin " + "b" + d.key)
      .selectAll("g")
      .data( (d) => {
        return d.values
      })
      .enter()
      .append("g")
      .attr("class", (d) => "row " + "r" + d.key)
      .selectAll("circle")
      .data( (d) => {
        return d.values
      })
      .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)});
    
    // position the pyramids
    graphGroup
      .selectAll(".bin")
      .attr("transform", function(d){
        var bb = this.getBBox(),
            xPos = (xScale(d.key.split(",")[1]) - bb.width/2),
            isFlip = d.key.startsWith("1"),
            yPos = isFlip ? height - 20 : height + 50,
            t = "translate(" + xPos + "," + yPos + ")";
        if (isFlip) t+= "scale(1,-1)";
        return t;
      })
      .selectAll(".row")
      .attr("transform", function(d) {
        var pbb = this.parentNode.getBBox();
        var bb = this.getBBox();
        return "translate(" + ((pbb.width - bb.width)/2) + "," + 0 + ")";
      });

    graphGroup.append("g")
      .attr("class", "axis axis--x")
      .attr("transform", "translate(0," + height + ")")
      .call(d3.axisBottom(xScale));

    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");
      
    
  </script>
</body>

</html>
Mark
  • 106,305
  • 20
  • 172
  • 230