d3.js - evenly spaced bars on a time scale

Matt Parker picture Matt Parker · Aug 29, 2012 · Viewed 12.2k times · Source

I'm building a bar plot in d3.js in which each bar represents total TB cases during a month. The data essentially consists of a date (initially strings in %Y-%m format, but parsed using d3.time.format.parse) and an integer. I'd like the axis labels to be relatively flexible (show just year boundaries, label each month, etc.), but I'd also like the bars to be evenly spaced.

I can get flexible axis labeling when I use a date scale:

var xScaleDate = d3.time.scale()
    .domain(d3.extent(thisstat, function(d) { return d.date; }))
    .range([0, width - margin.left - margin.right]);

... but the bars aren't evenly spaced due to varying numbers of days in each month (e.g., February and March are noticeably closer together than other months). I can get evenly-spaced bars using a linear scale:

var xScaleLinear = d3.scale.linear()
      .domain([0, thisstat.length])
      .range([0, width - margin.left - margin.right]);

... but I can't figure out how to then have date-based axis labels. I've tried using both scales simultaneously and only generating an axis from the xScaleDate, just to see what would happen, but the scales naturally don't align quite right.

Is there a straightforward way to achieve this that I'm missing?

Answer

coquin picture coquin · Jul 2, 2015

You can combine ordinal and time scales:

// Use this to draw x axis
var xScaleDate = d3.time.scale()
    .domain(d3.extent(thisstat, function(d) { return d.date; }))
    .range([0, width - margin.left - margin.right]);

// Add an ordinal scale
var ordinalXScale = d3.scale.ordinal()
    .domain(d3.map(thisstat, function(d) { return d.date; }))
    .rangeBands([0, width], 0.4, 0);

// Now you can use both of them to space columns evenly:
columnGroup.enter()
    .append("rect")
    .attr("class", "column")
    .attr("width", ordinalXScale.rangeBand())
    .attr("height", function (d) {
        return height - yScale(d.value);
    })
    .attr("x", function (d) {
        return xScaleDate(d.date);
    })
    .attr("y", function (d){
        return yScale(d.value);
    });

I've created an example a while ago to demonstrate this approach: http://codepen.io/coquin/pen/BNpQoO