Positioning Tricks

Having a nice, well-spaced graph is important, but it can sometimes be a little tricky.

A lot of d3 (and programmatic graphics work in general) is figuring out how position coordinates, canvas sizes and height/width relate. It’s simple algebra, but it can be tough to wrap your head around.

The Case of the Too Small Canvas

Let’s say we have a nice simple bar chart that spans the width of the container SVG. I’ve outlined the SVG to help us see where its bounds are.

Open example in new window

<svg class='fruits'></svg>
<style> .fruits { border: 1px solid #333; } rect { fill: red; }</style>
<script>
var datapoints = [ { 'product': 'Apples', 'sales': 40 }, { 'product': 'Oranges', 'sales': 20 }, { 'product': 'Bananas', 'sales': 80 }];

// We'll manually set the height/width of the svg
var svg_height = 50;
var svg_width = 500;

var svg = d3.select('.fruits').attr('height', svg_height).attr('width', svg_width);

// building our scale
// our largest input is 80 (bananas), and our largest output
// should be the width of the svg
var scale = d3.scale.linear().domain([0, 80]).range([0, svg_width]);

svg.selectAll('rect')
    .data(datapoints) // bind your data
    .enter() // grab the "new" ones (all of them)
    .append('rect') // and add in rectangles
    .attr('x', 0)
    .attr('y', function(d, i) { return i * 15 + 5 }) // space them evenly vertically
    .attr('height', 10)
    .attr('width', function(d) { return scale(d['sales']); }) // use the scale!
</script>

Open example in new window

But hey! We live in the future, and in the future we have labeled charts. So first thing we have to do is make a little room on the left hand side, by increasing the x value.

  .attr('x', 60)

It seems like it’s okay, but it’s really not.

Open example in new window

<svg class='fruits'></svg>
<style> .fruits { border: 1px solid #333; } rect { fill: red; }</style>
<script>
var datapoints = [ { 'product': 'Apples', 'sales': 40 }, { 'product': 'Oranges', 'sales': 20 }, { 'product': 'Bananas', 'sales': 80 }];
var svg_height = 50;
var svg_width = 500;
var svg = d3.select('.fruits').attr('height', svg_height).attr('width', svg_width);
var scale = d3.scale.linear().domain([0, 80]).range([0, svg_width]);

svg.selectAll('rect')
    .data(datapoints)
    .enter()
    .append('rect')
    .attr('x', 60)
    .attr('y', function(d, i) { return i * 15 + 5 }) // space them evenly vertically
    .attr('height', 10)
    .attr('width', function(d) { return scale(d['sales']); }) // use the scale!
</script>

Open example in new window

You can see the ‘Bananas’ bar go all the way to the right, and you just assume it ends at the end. Wrong! If you open up the web inspector

Bar overflowing the bounds of the canvas

Hey, look at that! It’s going over the edge, right off the svg canvas!

But let’s think about it: We added 60 pixels of padding to the left, but our bar still wants to be svg_width pixels wide. If the bar is starting 60 pixels in, it needs to be 60 pixels shorter than the width of the SVG. You can make that happen by adjusting the .range of the scale.

var scale = d3.scale.linear().domain([0, 80]).range([0, svg_width - 60]);

You could also create a variable called left_padding and use it in the scale and in the .attr('x', 60) section, but we’ll stick with that for now.

Open example in new window

<svg class='fruits'></svg>
<style> .fruits { border: 1px solid #333; } rect { fill: red; }</style>
<script>
var datapoints = [ { 'product': 'Apples', 'sales': 40 }, { 'product': 'Oranges', 'sales': 20 }, { 'product': 'Bananas', 'sales': 80 }];
var svg_height = 50;
var svg_width = 500;
var svg = d3.select('.fruits').attr('height', svg_height).attr('width', svg_width);
var scale = d3.scale.linear().domain([0, 80]).range([0, svg_width - 60]);

svg.selectAll('rect')
    .data(datapoints)
    .enter()
    .append('rect')
    .attr('x', 60)
    .attr('y', function(d, i) { return i * 15 + 5 }) // space them evenly vertically
    .attr('height', 10)
    .attr('width', function(d) { return scale(d['sales']); }) // use the scale!
</script>

Open example in new window

And if you go on and open up the web inspector to check it out…

No more overflow

Success! No longer runs over the edge.

The Case Of Axis Overflow

Let’s say we’re working with the same thing as above, but we’re also adding in an axis. Most of the time the labels are going to go riiiight off the end of the page.

Open example in new window

<svg class='fruits'></svg>
<style> .fruits { border: 1px solid #333; } rect { fill: red; } .axis path, .axis line { fill: none; stroke: black; shape-rendering: crispEdges; } .axis text { font-family: sans-serif; font-size: 11px; }</style>
<script>
var datapoints = [ { 'product': 'Apples', 'sales': 40 }, { 'product': 'Oranges', 'sales': 20 }, { 'product': 'Bananas', 'sales': 80 }];
var svg_height = 75;
var svg_width = 500;
var svg = d3.select('.fruits').attr('height', svg_height).attr('width', svg_width);
var scale = d3.scale.linear().domain([0, 80]).range([0, svg_width - 60]);

svg.selectAll('rect')
    .data(datapoints)
    .enter()
    .append('rect')
    .attr('x', 60)
    .attr('y', function(d, i) { return i * 15 + 5 }) // space them evenly vertically
    .attr('height', 10)
    .attr('width', function(d) { return scale(d['sales']); }) // use the scale!

var axis = d3.svg.axis().orient('bottom').scale(scale);
svg.append('g').attr('transform', 'translate(60, 50)').attr('class', 'axis').call(axis);
</script>

Open example in new window

We gotta get rid of that clipped 80! It’s super easy, though.

So, the length of the bar and the length of the axis are both determined by the same variable - scale. It’s going to stretch the banana bar and the axis out to the maximum range (80 sales, svg_width - 60 pixels).

All we gotta do is shorten the range to shorten the bar! Let’s chop another 20 pixels off.

var scale = d3.scale.linear().domain([0, 80]).range([0, svg_width - 60 - 20]);

Yes, svg_width - 60 - 20 is the same thing as svg_width - 80, but keeping them separate helps me remember what both numers are for! The 60 is to keep the bar from going off of the svg canvas from when we shifted the x, and the 20 is to pull it back a little from the right hand side.

Open example in new window

<svg class='fruits'></svg>
<style> .fruits { border: 1px solid #333; } rect { fill: red; } .axis path, .axis line { fill: none; stroke: black; shape-rendering: crispEdges; } .axis text { font-family: sans-serif; font-size: 11px; }</style>
<script>
var datapoints = [ { 'product': 'Apples', 'sales': 40 }, { 'product': 'Oranges', 'sales': 20 }, { 'product': 'Bananas', 'sales': 80 }];
var svg_height = 75;
var svg_width = 500;
var svg = d3.select('.fruits').attr('height', svg_height).attr('width', svg_width);
var scale = d3.scale.linear().domain([0, 80]).range([0, svg_width - 60 - 20]);

svg.selectAll('rect')
    .data(datapoints)
    .enter()
    .append('rect')
    .attr('x', 60)
    .attr('y', function(d, i) { return i * 15 + 5 }) // space them evenly vertically
    .attr('height', 10)
    .attr('width', function(d) { return scale(d['sales']); }) // use the scale!

var axis = d3.svg.axis().orient('bottom').scale(scale);
svg.append('g').attr('transform', 'translate(60, 50)').attr('class', 'axis').call(axis);
</script>

Open example in new window

Looking good!

The magic of vertical bars

Horizontal bars are easy! Your x stays at 0 (left hand side), and your y hops down the page to space the bars out, and your width is how big the bar is.

Vertical bars are another story! It isn’t tough, it’s just one of those things you need to realize before you Get It.

Let’s start with two bars. At the very least we know they need to be spaced out evenly left-to-right using x and their height is going to be how long they are.

Open example in new window

<style> .vertical-bars { border: 1px solid #333; } rect { fill: red; }</style>
<svg class='vertical-bars'></svg>
<script>
var datapoints = [3, 1, 2];
var svg_height = 80, 
    svg_width = 100;

// Set the size of the svg through D3
var svg = d3.select('.vertical-bars').attr('height', svg_height).attr('width', svg_width);
var scale = d3.scale.linear().domain([0,3]).range([0, svg_height]);

svg.selectAll('rect')
    .data(datapoints)
    .enter()
    .append('rect')
    .attr('y', 0)
    .attr('x', function(d, i) { return i * 20 })
    .attr('width', 15)
    .attr('height', function(d) { 
      return scale(d);
    });
</script>

Open example in new window

As we know, we can only change the top left of the rect using x and y. For example, maybe we keep pushing it down until the first bar is grounded on the bottom of the svg.

.attr('y', 20)

Which lines up one of them, but not the others. In order to get the next bar down, we need to increase the y even further…

.attr('y', 40)

Which seems passable until you use the web inspector, and notice the longest bar is going outside of the canvas.

Bar overflowing the bounds of the canvas

You probably already knew this, but it looks like you have to set each bar’s y individually to make things work. Every time you have to do something to each and every unit in a visualization, there’s always a formula waiting for you.

We just found out that a 60-pixel-long bar needs an offset of 20, and a 40-pixel-long bar needs 40. Let’s look at this for a second!

bar height required y offset added together
60 20 80
40 40 80
20 60 80

Hmmm… looks like “bar height” + “required y offset” always equals 80 (I cheated when I added that third column). If we take a closer look, 80 is the height of our svg!

It makes sense when you think about it - in order to draw a rectangles that touches the bottom of the svg, you need to start drawing from the height of the rectangle. If we want to get mathy, this means

bar height + required y offset = svg_size

which, since we’re looking to set y, means

required y offset = svg_size - bar height

which means *we need to use the formula for height in the formula for the y offset. If we cut out just that part…

.attr('y', function(d) {
  return svg_height - scale(d);
})
.attr('height', function(d) { 
  return scale(d);
});

And to see it all together…

Open example in new window

<style> .vertical-bars { border: 1px solid #333; } rect { fill: red; }</style>
<svg class='vertical-bars'></svg>
<script>
var datapoints = [3, 1, 2];
var svg_height = 80, 
    svg_width = 100;

// Set the size of the svg through D3
var svg = d3.select('.vertical-bars').attr('height', svg_height).attr('width', svg_width);
var scale = d3.scale.linear().domain([0,3]).range([0, svg_height]);

svg.selectAll('rect')
    .data(datapoints)
    .enter()
    .append('rect')
    .attr('y', function(d) {
      return svg_height - scale(d);
    })
    .attr('x', function(d, i) { return i * 20 })
    .attr('width', 15)
    .attr('height', function(d) { 
      return scale(d);
    });
</script>

Open example in new window

And there we have it! Sometime we’ll need to size elements based on elements other than the containing SVG - maybe other elements or groupings or whatnot - so keep your eye out for these things!

Want to hear when I release new things?
My infrequent and sporadic newsletter can help with that.