Positioning axes

The problem

The worst thing about using axes is they just don’t seem to fit on the page. Let’s say we have two scales:

  • One for the x-axis with a range from 0 to width
  • One for the y-axis with a range from height to 0 (it’s inverted, remember!)

That works fine when we’re just displaying the data, but as soon as we add in axes they flop off the bottom and the left-hand sides.

Open example in new window

<style>
.axis path,
.axis line {
    fill: none;
    stroke: black;
    shape-rendering: crispEdges;
}

.axis text {
    font-family: sans-serif;
    font-size: 11px;
}
</style>
<svg></svg>
<script>
var datapoints = [
  { 'title': 'Pride and Prejudice', 'author': 'Jane Austen', 'words': 120000, 'published': 1813 },
  { 'title': 'Cryptonomicon', 'author': 'Neal Stephenson', 'words': 415000, 'published': 1999 },
  { 'title': 'Great Gatsby', 'author': 'F. Scott Fitzgerald', 'words': 47094, 'published': 1925 },
  { 'title': 'Song of Solomon', 'author': 'Toni Morrison', 'words': 92400, 'published': 1977 },
  { 'title': 'White Teeth', 'author': 'Zadie Smith', 'words': 169000, 'published': 2000 }
];

var height = 300, width = 600;

var svg = d3.select("svg").attr('height', height).attr('width', width);

var x_scale = d3.scale.linear().domain([0, 500000]).range([0, width]);

// and ORDINAL scale is for categorical data
var titles = datapoints.map( function(d) { return d['title'] });

// range bands to the rescue for non-numeric domains
// https://github.com/mbostock/d3/wiki/Ordinal-Scales#ordinal_rangeBands
// http://jaketrent.com/post/use-d3-rangebands/
var y_scale = d3.scale.ordinal().domain(titles).rangeBands([height, 0], 0.5, 0.2);

var xAxis = d3.svg.axis()
  .scale(x_scale)
  .orient("bottom");

var yAxis = d3.svg.axis()
  .scale(y_scale)
  .orient("left");

svg.append('g').attr('class','axis').call(xAxis).attr("transform", "translate(0," + height + ")");
svg.append('g').attr('class','axis').call(yAxis).attr("transform", "translate(0,0)");;

svg.selectAll('rect')
    .data(datapoints)
    .enter()
    .append('rect')
    .attr('y', function(d) {
      return y_scale(d['title']);
    })
    .attr('x', 0)
    .attr('height', y_scale.rangeBand())
    .attr('width', function(d) {
      return x_scale(d['words']);
    })

</script>

Open example in new window

The thing is, the axes are still there, they’re just off the sides. If you use the web inspector…

Disappearing axes

Web inspector

The solution

Remember our friend g, the element that just holds miscellaneous stuff? One of the benefits of the g element it acts like an svg element - suddenly (0,0) is the top left of the g, even if you move the g halfway across the page.

  1. Move the g into the middle of the svg and put padding around it.
  2. Put the chart inside of the g. The 0,0 is no longer the top of the svg, but rather the top-left of the g.
  3. The overflow goes outside of the g, but it’s still inside of the svg, thanks to the margin.
  4. Partytime!

Confused? Take a look at this image from this margin convention piece.

Web inspector

First, we need to establish our margin:

var margin = 50;

Now you need to juggle the difference between the height/width of the svg, and the height/width of the g element. There are a lot of ways to do this, and you’ll see many options out there, so this is just one particular method - making separate heights and widths for your chart and your svg.

var svg_height = 300,
    svg_width = 600;

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

// subtract margin for the top AND the bottom
var height = svg_height - margin * 2;
// subtract margin for the left AND the right
var width = svg_width - margin * 2;

And then add a g to your svg, and use translate/transform to shift the top-left-hand corner of the g on over.

var chart = svg.append("g").attr('translate', 'transform(' + margin + ',' + margin + ')');

Now instead of using selectAll/etc on the svg, append to the g!

chart.append('g').attr('class','axis').call(xAxis).attr("transform", "translate(0," + height + ")");
chart.append('g').attr('class','axis').call(yAxis).attr("transform", "translate(0,0)");;

chart.selectAll('rect')
    .data(datapoints)
    .enter()
    ....

Open example in new window

<style>
.axis path,
.axis line {
    fill: none;
    stroke: black;
    shape-rendering: crispEdges;
}

.axis text {
    font-family: sans-serif;
    font-size: 11px;
}
</style>
<svg></svg>
<script>
var datapoints = [
  { 'title': 'Pride and Prejudice', 'author': 'Jane Austen', 'words': 120000, 'published': 1813 },
  { 'title': 'Cryptonomicon', 'author': 'Neal Stephenson', 'words': 415000, 'published': 1999 },
  { 'title': 'Great Gatsby', 'author': 'F. Scott Fitzgerald', 'words': 47094, 'published': 1925 },
  { 'title': 'Song of Solomon', 'author': 'Toni Morrison', 'words': 92400, 'published': 1977 },
  { 'title': 'White Teeth', 'author': 'Zadie Smith', 'words': 169000, 'published': 2000 }
];

var margin = 50;
var svg_height = 300,
    svg_width = 600;

var svg = d3.select("svg").attr('height', svg_height).attr('width', svg_width);
var chart = svg.append("g").attr('transform', 'translate(' + margin + ',' + margin + ')');

// subtract margin for the top AND the bottom
var height = svg_height - margin * 2;
// subtract margin for the left AND the right
var width = svg_width - margin * 2;

var x_scale = d3.scale.linear().domain([0, 500000]).range([0, width]);

// and ORDINAL scale is for categorical data
var titles = datapoints.map( function(d) { return d['title'] });

// range bands to the rescue for non-numeric domains
// https://github.com/mbostock/d3/wiki/Ordinal-Scales#ordinal_rangeBands
// http://jaketrent.com/post/use-d3-rangebands/
var y_scale = d3.scale.ordinal().domain(titles).rangeBands([height, 0], 0.5, 0.2);

var xAxis = d3.svg.axis()
  .scale(x_scale)
  .orient("bottom");

var yAxis = d3.svg.axis()
  .scale(y_scale)
  .orient("left");

chart.append('g').attr('class','axis').call(xAxis).attr("transform", "translate(0," + height + ")");
chart.append('g').attr('class','axis').call(yAxis).attr("transform", "translate(0,0)");;

chart.selectAll('rect')
    .data(datapoints)
    .enter()
    .append('rect')
    .attr('y', function(d) {
      return y_scale(d['title']);
    })
    .attr('x', 0)
    .attr('height', y_scale.rangeBand())
    .attr('width', function(d) {
      return x_scale(d['words']);
    })

</script>

Open example in new window

Okay, that’s better, but we’re going to have to push in a ton on the left hand side in order to make this work. Instead of making a huge margin on all sides, instead we can set the left, right, top and bottom margins individually.

var margins = { 'top': 20, 'left': 125, 'bottom': 20, 'left': 20 };

Now we just have to make sure that when we specify our margins we pick the right ones between left and right and top and bottom.

var chart = svg.append("g").attr('transform', 'translate(' + margin['left'] + ',' + margin['top'] + ')');

var height = svg_height - margin['top'] - margin['bottom'];
var width = svg_width - margin['left'] - margin['right'];

Open example in new window

<style>
.axis path,
.axis line {
    fill: none;
    stroke: black;
    shape-rendering: crispEdges;
}

.axis text {
    font-family: sans-serif;
    font-size: 11px;
}
</style>
<svg></svg>
<script>
var datapoints = [
  { 'title': 'Pride and Prejudice', 'author': 'Jane Austen', 'words': 120000, 'published': 1813 },
  { 'title': 'Cryptonomicon', 'author': 'Neal Stephenson', 'words': 415000, 'published': 1999 },
  { 'title': 'Great Gatsby', 'author': 'F. Scott Fitzgerald', 'words': 47094, 'published': 1925 },
  { 'title': 'Song of Solomon', 'author': 'Toni Morrison', 'words': 92400, 'published': 1977 },
  { 'title': 'White Teeth', 'author': 'Zadie Smith', 'words': 169000, 'published': 2000 }
];

var margin = { 'top': 20, 'left': 125, 'bottom': 20, 'right': 20 };
var svg_height = 300,
    svg_width = 600;

var svg = d3.select("svg").attr('height', svg_height).attr('width', svg_width);
var chart = svg.append("g").attr('transform', 'translate(' + margin['left'] + ',' + margin['right'] + ')');

// subtract margin for the top AND the bottom
var height = svg_height - margin['top'] - margin['bottom'];
// subtract margin for the left AND the right
var width = svg_width - margin['left'] - margin['right'];

var x_scale = d3.scale.linear().domain([0, 500000]).range([0, width]);

// and ORDINAL scale is for categorical data
var titles = datapoints.map( function(d) { return d['title'] });

// range bands to the rescue for non-numeric domains
// https://github.com/mbostock/d3/wiki/Ordinal-Scales#ordinal_rangeBands
// http://jaketrent.com/post/use-d3-rangebands/
var y_scale = d3.scale.ordinal().domain(titles).rangeBands([height, 0], 0.5, 0.2);

var xAxis = d3.svg.axis()
  .scale(x_scale)
  .orient("bottom");

var yAxis = d3.svg.axis()
  .scale(y_scale)
  .orient("left");

chart.append('g').attr('class','axis').call(xAxis).attr("transform", "translate(0," + height + ")");
chart.append('g').attr('class','axis').call(yAxis).attr("transform", "translate(0,0)");;

chart.selectAll('rect')
    .data(datapoints)
    .enter()
    .append('rect')
    .attr('y', function(d) {
      return y_scale(d['title']);
    })
    .attr('x', 0)
    .attr('height', y_scale.rangeBand())
    .attr('width', function(d) {
      return x_scale(d['words']);
    })

</script>

Open example in new window

Now that you can adjust each margin individually, you’re all set! If you’d like a little more info, I highly highly recommend this piece.

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