Creating Population Pyramid with D3.js

2019-03-11 11:50发布

问题:

I need to make a classic looking population pyramid with D3.js. Something similar to this image:

I have found a few examples that look really good (this and this) but they are more complex than I'm looking for. Does anyone know of a good SIMPLE example that I can look at? Any advice for starting this? Should I just be making two bar graphs next to each other, one for each sex group?

回答1:

The key to making a visualization like this is to spend a good amount of time setting up the layout in advance before you get to drawing anything.

There are a few key measurements that you will want to use to make your life much easier when it comes time to make the scales and axes for your chart. Then, you can make use of <g> elements with translations so that in each "section" of your chart, you can work in local coordinates.

The goal is to create two mirrored regions like this:

This will allow you to share the same scales for the x- and y-dimensions between the two regions.

To define these regions, you can first set up some values for the margins of the inside of your svg, and then create a group to sit inside those margins. This is a common practice in d3 visualizations, and if you're not already familiar with it, this is a good time to learn. It usually looks something like this:

var width = 400,
    height = 300;

var margin = {
  top: 20,
  right: 10
  bottom: 40,
  left: 10
};

var svg = d3.select('body').append('svg')
  .attr('width', margin.left + width + margin.right)
  .attr('height', margin.top + height + margin.bottom)
  .append('g')
    .attr('class', 'inner-region')
    .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');

This is a good practice to follow because it allows you to leave room on your svg document for the axis ticks, labels, and titles, without having to account for that space when positioning parts of the visualization itself. Then later, if you find you need more room, you can just adjust one value at the top of your code, rather than re-positioning your entire diagram.

The other points that are important are the x-coordinates of the two y-axes. In other words the points at which population is zero for men and for women (shown as red lines below). These will be a set distance from the center-line of your inner region (shown as a blue line below):

You can add a middle margin property to the margin object created earlier to make reference to this value:

margin.middle = 28;

You will want to create variables to store the positions of the two red lines above. For now, lets call them pointA (the x-coordinate of the zero line for male population) and pointB (the corresponding point for the female population).

var regionWidth = (width/2) - margin.middle;

var pointA = regionWidth,
    pointB = width - regionWidth;

Once you have these points set up, things become much simpler, since you can simply plug these values into an svg transform to translate the objects you create to those positions.

The trickiest part will be the two center y-axes, which share a single set of labels. For this, you will need to have a good handle on how d3.svg.axis() creates its text labels, and what properties can be manipulated. There are four important properties to know for this:

  1. axis.orient(direction) specifies the direction in which the ticks point, coming out from the axis. This will be slightly confusing in this case because we want the ticks to point towards the center, so the left axis will have .orient('right') and the right axis will have .orient('left')

  2. axis.tickSize(inner,outer) sets the length (in svg units) of the tick marks coming out from the axis. The "outer" marks are the ones at the endpoints of the axis, and can be set to 0 in this case because we are using an ordinal scale with range bands, so the endpoints don't represent values. Choose a small value for the inner ticks, for this example I'll use 4.

  3. axis.tickPadding(padding) sets how far away from the end of the tick mark the text-anchor of the label text is placed. We want to place the text-anchor in the center of the image, right between the two axes. Since we already know the middle margin (the distance from the center line to each axis) and we know the tickSize (the length of the ticks), we can place these in the center by taking the difference of these two values: .tickPadding(margin.middle - 4).

  4. axis.tickFormat(format) specifies the format of the tick labels. It can be set to the empty string to remove tick labels from one of the axes, so that no overlapping occurs: .tickFormat('')

So, the two y-axes can be defined like so:

var yAxisLeft = d3.svg.axis()
  .scale(yScale)
  .orient('right')
  .tickSize(4,0)
  .tickPadding(margin.middle - 4);

var yAxisRight = d3.svg.axis()
  .scale(yScale)
  .orient('left')
  .tickSize(4,0)
  .tickFormat('');

Then, when actually drawing the left y-axis, select its <text> elements and set the text-anchor to be middle so they will be centered about the anchor:

.call(yAxisLeft)
  .selectAll('text')
  .style('text-anchor', 'middle');

HERE is a simple example of a whole chart using some contrived data. I've tried to comment as much of the general structure as possible to make it easy to follow. I also made a translation(x,y) function to make the x and y coordinates of each translation easier to spot than they would be when using string concatenation. You can find its definition at the bottom of the script.

I hope this helps, let me know if you have questions about the specifics.