When x-axes become ugly!

D3 Dabbler Series: Creating multiple x-axes for a neat date display

One difficulty faced when drawing plots is often in making them 100% visually appealing and informative. It is easy to play with colours and fonts to make certain parts of the plots stand out and, thanks to the power of D3, it is also easy enough to make the plot interactive and informative for the person viewing the plot. The challenge, however, sometimes comes when trying to draw the axes in such a way that they display all the information necessary to be able to interpret the plot while not becoming over-crowded and, for want of a better word, ugly!  

A common x-axis label used when dealing with data spread over time is a date string. When the data only spreads over a few days, we can get away with using the whole date string. 


 
 

However, when the data spreads over many days or even months or years, using the whole date string becomes a problem. Showing the dates horizontally is a definite no-no and showing them vertically is not ideal as the axis starts to feel a little bit crowded.

 
 
 
 

An alternative to this is to use multiple x-axes, with each one showing a separate and often necessary-to-show “date grouping”.

 
 

Let’s see how we got there... 

An essential first part of drawing any plot is preparing the data and getting it into a format that is easy for the plot drawing functions or language to use. One key field required in this example is the “monthyear” field – a field containing both the month and the year of the date. This is particularly important if the data is spread over more than 12 months to allow us to differentiate between, for example, May 2018 and May 2019.  

One of the first steps in this example is to extract a list of the months (or “monthyears”) and years represented in the data: 

let months = []
for (var i = 0; i < data.length; i++) {
    if (months.indexOf(data[i].monthyear)==-1) {
        months.push(data[i].monthyear)
    } else {
        months = months
    }
}

let years = []
for (var i = 0; i < data.length; i++) {
    if (years.indexOf(data[i].year)==-1) {
        years.push(data[i].year)
    } else {
        years = years
    }
}

Each part of the date (day, month, year) is then drawn as a separate x-axis:

let xScale = d3.scaleBand()
    .domain(data.map(d => d.date))
    .range([0, bodyWidth])
    
chart.append("g").call(d3.axisBottom(xScale).tickFormat((_,i)=>data[i].day))
    .attr("transform", `translate(0, ${bodyHeight})`)

Note that the primary x-axis is drawn using the “date” field but labelled with the “day” field.

let month_ranges = [0]
let monthcount = 0

for (var i = 1; i <= months.length; i++) {
    let totalcount = data.length
    monthcount += data.filter(options => options.monthyear === months[i-1]).length

    month_ranges[i] = bodyWidth/totalcount*monthcount
        
    let xScale2 = d3.scaleBand()
        .range([month_ranges[i-1], month_ranges[i]])

    chart.append("g")
        .attr('transform', `translate(0, ${bodyHeight+30})`)
        .call(d3.axisBottom(xScale2).tickSize(-5))

    chart.append("text")
        .attr("class", "axis")
        .data(data)
        .attr("y", bodyHeight + 50)
        .attr("x", month_ranges[i-1] + (month_ranges[i] - month_ranges[i-1])/2)
        .style("text-anchor", "middle")
        .text(months[i-1].split(" ")[0])
}
let year_ranges = [0]
let yearcount = 0
    
for (var i = 1; i <= years.length; i++) {
    let totalcount = data.length
    yearcount += data.filter(options => options.year === years[i-1]).length

    year_ranges[i] = bodyWidth/totalcount*yearcount
            
    let xScale2 = d3.scaleBand()
        .range([year_ranges[i-1], year_ranges[i]])
    
    chart.append("g")
        .attr('transform', `translate(0, ${bodyHeight+65})`)
        .call(d3.axisBottom(xScale2).tickSize(-5))
    
    chart.append("text")
        .attr("class", "axis")
        .data(data)
        .attr("y", bodyHeight + 85)
        .attr("x", year_ranges[i-1] + (year_ranges[i] - year_ranges[i-1])/2)
        .style("text-anchor", "middle")
        .text(years[i-1])
}


The date can be further broken down into the week or quarter and the same process can be followed, with the addition of an extra field in the data. As each new x-axis is added, you might need to make slight adjustments to the position of the x-axis label. 

The result provides the viewer with a much better “user-experience” - he/she can obtain a fair amount of important information quickly and without the need for an in-depth study of the plot. More importantly though, it looks a whole lot prettier too!   

All files needed to draw the above plot can be found here

Written By: Liesl Hendry