4
votes

Background

I'm using D3 4.x to build a multi-series line chart (see an example) to compare multiple years of annual data, where the Y-axis is the range of possible values throughout the years and the X-axis represents the time interval between the dates of 01/01-12/31.

Issues

While building this chart, it appears as though time intervals are year specific preventing data from different years to be rendered on the same chart. To reiterate, the current approach is to overlaying each year on the same chart; a line for each year. And to do this with time intervals being year specific, requires significant (and erroneous) data manipulation.

In the example chart below, one can see that the year 2014 approaches from the left (color: blue) and 2015 continues on to the right (color: orange). What I'd like to see is both of these to overlay on top of each other.

enter image description here

Acknowledgement / Caveat

Some of the complexities of comparing multiple years worth of data by overlaying the data on top of each other (ie. multiple lines, each representing a year) are acknowledged. Such as how to deal with years with leap days compared with those that do not. This complicates thing programmatically but visually should not be an issue.

Questions

1) What is the best approach to using time intervals when the year just gets in the way?

2) Is there a better, more elegant, approach to creating multi-series line charts that represent multiple years layered on top of each other, instead of hacking together the pieces?

3) Is there a way (not statically) to remove the year (ie. 2015) from the axis and use the month (January) instead?

4) Is it possible to shift the text on the X-axis to the right (programmatically), so that the month appears between the tick marks (IMO, it's visually more accurate)?

1

1 Answers

3
votes

There are several ways to solve this. Actually, so many of them that this question qualifies as too broad. Every approach, of course, has its pros and cons. For instance, a simple one is dropping the time scale altogether, and using a point scale for the x axis.

I'll suggest a different approach, a little bit more complex, but still using time scales: create several different time scales, one for each year. Each of them has the same range: that way, the lines for each year can overlay on top of each other.

For instance, from 2013 to 2017:

var years = [2013, 2014, 2015, 2016, 2017];

var scales = {};

years.forEach(d => {
    scales["scale" + d] = d3.scaleTime()
        .domain([new Date(+d, 0, 1), new Date(+d, 11, 31)])
        .range([30, 470]);
});

That way, you can keep using a time scale for the x axis. The problem is this: time scales deal with each year as they are: different. Some years have more days, some of them have less days. Even the days can have more hours or less hours.

So, I chose one year (2017) and displayed only the months for that year in the x axis. The result may not be extremely precise, but it's good enough if you have an entire year in the x axis.

Here is the demo. The data is randomly generated, each line is a different year (from 2013 to 2017), going from Jan 1st to Dec 31st:

var svg = d3.select("body").append("svg")
  .attr("width", 500)
  .attr("height", 200);

var color = d3.scaleOrdinal(d3.schemeCategory10);

var thisYear;

var line = d3.line()
  .x(d => scales[thisYear](d.x))
  .y(d => yScale(d.y))
  .curve(d3.curveMonotoneX);

var years = [2013, 2014, 2015, 2016, 2017];

var data = [];

years.forEach((d, i) => {
  data.push({
    year: d,
    points: []
  });
  for (var j = 0; j < 12; j++) {
    data[i].points.push({
      y: Math.random() * 80 + 20,
      x: new Date(d, j)
    })
  }
});

var scales = {};

var yScale = d3.scaleLinear()
  .domain([0, 100])
  .range([170, 30]);

years.forEach(d => {
  scales["scale" + d] = d3.scaleTime()
    .domain([new Date(+d, 0, 1), new Date(+d, 11, 31)])
    .range([30, 470]);
});

var paths = svg.selectAll("foo")
  .data(data)
  .enter()
  .append("path");

paths.attr("stroke", (d, i) => color(i))
  .attr("d", d =>{
  thisYear = "scale" + d.year;
  return line(d.points);
  })
  .attr("fill", "none");
  
var xAxis = d3.axisBottom(scales.scale2017).tickFormat(d=>d3.timeFormat("%b")(d));
var yAxis = d3.axisLeft(yScale);

var gX = svg.append("g").attr("transform", "translate(0,170)").call(xAxis);

var gY = svg.append("g").attr("transform", "translate(30,0)").call(yAxis);
<script src="https://d3js.org/d3.v4.js"></script>