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.
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.
<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>
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.
<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>
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…
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.
<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>
And if you go on and open up the web inspector to check it out…
Success! No longer runs over the edge.
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.
<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>
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.
<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>
Looking good!
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.
<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>
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.
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…
<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>
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!