Editing Dynamic Bar Graphs in D3

Taylor McGinnis
NYC Planning Tech
Published in
6 min readMar 14, 2019

Our Population Factfinder application includes a number of graphs and population pyramids that help users better visualize important American Community Survey (ACS) and census statistics. In order to make these nifty bar graphs, we use the popular data visualization library, D3.js. We build all of the bars, points, and lines that appear on our charts using Scalable Vector Graphics (SVG), which allow web developers to build, style, and organize a variety of different shapes. Because ACS and census statistics tend to have significant levels of uncertainty, we include margins of error in our bar graphs, which are the SVG grey rectangles shown below.

The Population Division noticed a bug where the MOE bar was extending all the way to 125%

The margin of error (MOE) represents the range of possible values above and below our estimated statistic. A larger margin of error means more possible values for that variable, which demonstrates a higher level of uncertainty. What this means for our graphs is that the center of the MOE bar will intersect with our estimate bar, and extend symmetrically in both directions. Recently, demographers from the Population Division noticed a small bug on a graph representing newly-arrived residents in a Manhattan census tract. In this case, with a percentage of movers from another borough reaching 70%, and the margin of error reaching 55%, our graph was being stretched beyond 120%, something we definitely didn’t want.

My first instinct when solving this problem was to create a clipping path, a special SVG that only displays other graphics that are within its boundaries. The plan was to build a rectangle that covered only the part of the graph that reached to 100%, cutting off that extra 25%. I would append this rectangle to the moebars, meaning it would only affect the visibility of that particular graphic. While this solution does get the job done, it produces a choppy image and feels more like covering up the problem than fixing it. So instead, I’ll demonstrate how we edited these dynamic D3 bar graphs using a series of fun formulas :).

An example of a shape mimicking the clipping path that would be appended to the moebar graphic. This method was not used.

Dynamic Functions for our SVG Shapes

Population Factfinder allows users to visualize data for numerous geographic areas and census variables. Each of these graphs will display different data and have different maximum values, which means they require a dynamic setup. When building these MOE rectangles with .append(‘rect’), we use a dynamic x coordinate, xFunctionMOE, and a dynamic bar width, widthFunctionMOE.

const moebars = svg.selectAll('.moebar')
.data(rawData, d => get(d, 'group'));
moebars.enter()
.append('rect')
.attr('class', d => `moebar ${get(d, 'classValue')}`)
.attr('fill', (d) => {
if (get(d, 'color')) return get(d, 'color');
return '#2e6472';
})
.attr('opacity', 0.4)
.attr('x', xFunctionMOE)
.attr('y', d => y(get(d, 'group')) + (y.bandwidth() / 2) + -3)
.attr('height', 6)
.attr('width', widthFunctionMOE);

Adding a new condition to widthFunctionMOE allows us to assure that large margins of error have limited widths. But before we jump into this, I’ll give you a little background on how these D3 graphs are built.

Margins

Margins represent the space between the outer boundary of our full chart shape and the start of the actual axes (including axes titles). The 0,0 mark starts at the top left corner of the chart. We define the margins early on in our code:

const margin = {
top: 10,
right: 10,
bottom: 60,
left: 10,
};
This is a simple example of a D3 graph with margins. Our graphs have titles on the vertical axis, meaning that the beginning of the x axis, x(0), doesn’t start until further to the right of the left margin.

This way, we get to define the width of our graph, width, and the width of our vertical axis titles, textWidth using the margins.

const height = 800
const el = this.$();
const elWidth = el.width();
const height = this.get('height') - margin.top - margin.bottom;
const width = elWidth - margin.left - margin.right;
const textWidth = width * 0.35;

Scales

In D3, scales help us to easily translate raw data values, called the domain, into new pixel values, called the range, that situate our graphics correctly on the axes. In our case, we set the domain as 0 to our maximum x value, xMax (0 to 1 displays as 0% to 100%). xMax is equal to the estimate percentage plus the MOE percentage. In our buggy census tract, the maximum x value ended up being1.255, which explains why it was extending the axes past 100%.

We set our range as textWidth (~137px) to width (~392px). So if we had a maximum x value of 0.5 , for example, our set of values in our domain: [0, 0.1, 0.2, 0.3, 0.4, 0.5] would translate to this in our range: [137, 188, 239, 290, 341, 392]. This way we can easily deal with our data in its original form and our linear scale will situate them on the graph automatically 🎉.

const xMax = max([
max(rawData, d => get(d, 'percent') + get(d, 'percent_m')),
]);
const x = scaleLinear()
.domain([0, xMax])
.range([textWidth, width]);

Dynamic Maximums, Coordinates and Widths

The first step in fixing this bug was limiting the maximum x axis value to never go beyond 100%. We did this through a simple function called limitMax that checked for when xMax was greater than 1, and if so, return 1 instead of the original xMax.

function limitMax(newMax) {
if (newMax > 1) return 1;
return newMax;
}
const xMax = max([
max(rawData, d => get(d, 'percent') + get(d, 'percent_m')),
]);
const x = scaleLinear()
.domain([0, limitMax(xMax)])
.range([textWidth, width]);

Now our axis was capped at 100% but our MOE bar was still extending to the full 125%.

In order to start tackling the MOE bar, I had to consider how we were determining the x coordinate for this shape. This is where the domain and range come in handy. x(get(d, ‘percent') grabs the percent value from the original data and translates it into the pixel values that we need to organize shapes on our graph. x(0) in our case would be 137, which is equal to the textWidth, and the start of our axes. After translating the percent and percent_m (moe percent) values into pixels, we then add textWidth to get the MOE bar where it needs to be.

const xFunctionMOE = (d) => {
if (get(d, 'percent_m') > get(d, 'percent')) return x(0);
return x(get(d, 'percent')) - x(get(d, 'percent_m')) - -textWidth;
};

Now I had to consider how we were determining the width of the MOE bar. Margins of error exist symmetrically above and below the estimate percentage value (for this example, 0.55 below and above). We multiply this MOE percentage width by 2 to make sure it extends beyond the end of the estimate bar (look at figure above).

const widthFunctionMOE = (d) => {
const moeDefaultWidth = (x(get(d, 'percent_m')) - textWidth) * 2;
return moeDefaultWidth;
};

In order to limit the width of the MOE bar, I had to check for the condition where the barWidth + (moeDefaultWidth * 0.5) were greater than the width of the x axis, axesWidth. Remember that we already limited the maximum value of the axis to be 1, so width does not measure beyond 100%. I then measured the width of the “gap” between the estimate bar and the end of the axis as gapWidth.

Instead of multiplying by 2 like we do for moeDefaultWidth, I simply added the first half of the bar width, moeDefaultWidth * 0.5, to the gapWidth, meaning the MOE bar would only ever reach to 100% under this condition.

const widthFunctionMOE = (d) => {
const moeDefaultWidth = (x(get(d, 'percent_m')) - textWidth) * 2;
const axesWidth = width - textWidth;
const barWidth = x(get(d, 'percent')) - textWidth;
if (barWidth + (moeDefaultWidth * 0.5) > axesWidth) {
const gapWidth = axesWidth - barWidth;
const shortenedWidth = (moeDefaultWidth * 0.5) + gapWidth;
return shortenedWidth;
}
return moeDefaultWidth;
};

And there you have it! No extra clipping path needed. Check out more of our D3 code here.

--

--

Taylor McGinnis
NYC Planning Tech

Mapper, web developer, and data analyst focused on city planning issues