sveti petar
sveti petar

Reputation: 3787

Show yearly X labels for June instead of January on d3.js chart

I have a line chart in d3.js where I have labels on the X-axis every year (showing 20 years of data). The labels are created with:

g.append("g")
    .attr("transform", "translate(0," + height + ")")
    .call(d3.axisBottom(x).tickFormat(d3.timeFormat("%b/%d/%Y")).ticks(d3.timeYear))
    .selectAll("text")
    .style("text-anchor", "end")
    .attr("dy", ".25em")
    .attr("transform", "rotate(-45)");

The outcome looks like this:

X-axis

Now, the thing is, I need the labels to not be placed on January 1st of each year - I need them on June 30th. How can I accomplish that?

See the Fiddle here to try for yourself.

Upvotes: 2

Views: 175

Answers (2)

Andrew Reid
Andrew Reid

Reputation: 38151

You can specify an interval with axis.ticks(), D3 provides a number of built in intervals which we can use and then filter for the appropriate day/month/time.

If we wanted June 1 every year we could use:

var axis = d3.axisBottom(x)
    .tickFormat(d3.timeFormat("%b/%d/%Y"))
    .ticks(d3.timeMonth.filter(function(d) { return d.getMonth() == 5; })))

If we want June 30 we can specify with a bit more specificity:

 var axis = d3.axisBottom(x)
   .tickFormat(d3.timeFormat("%b/%d/%Y"))
   .ticks(d3.timeDay.filter(function(d) { return d.getMonth() == 5 && d.getDate() == 30 }))

The d3-time docs have some more description of d3 intervals and the d3-scale documentation time scale details have some example implementations of this method here.

Here's an updated fiddle

Upvotes: 1

Mehdi
Mehdi

Reputation: 7403

One way of doing this is to specify explicitly each desired axis tick's value. The function axis.tickValues is designed for this.

The following function generates an array of dates, starting from the min date (june 28th, 2000 in your case), and adding a year until the max date (june 28th, 2020) is reached. It is necessary to go through this generation step because the dataset does not contain data for all years.

    function generateTickvalues(min, max) {
        let res = []
        , currentExtent = new Date(min.valueOf())

        while(currentExtent <= max) {
            res.push(new Date(currentExtent.valueOf()))
            currentExtent.setFullYear(currentExtent.getFullYear() + 1);
        }

      return res

    }

Remark: new Date(date.valueOf()) is necessary in this function so that the date values from the original dataset are not overwritten.

The min and max dates from the dataset can conveniently be found using d3.extent. This array can also be used when calling x.domain.

let dateExtent = d3.extent(data, function(d) { return d.date})

let tickValues = generateTickvalues(dateExtent[0], dateExtent[1])

x.domain(dateExtent);

Then, when generating the axis, call the function axis.tickValues, passing the array of years starting from June just generated:

d3.axisBottom(x)
  .tickFormat(d3.timeFormat("%b/%d/%Y"))
  .ticks(d3.timeYear)
  .tickValues(tickValues)

Demo in the snippet below:

const data = [
    { value: 46, date: '2000-06-28', formatted_date: '06/28/2000' },
    { value: 48, date: '2003-06-28', formatted_date: '06/28/2003' },
    { value: 26, date: '2004-06-28', formatted_date: '06/28/2004' },
    { value: 36, date: '2006-06-28', formatted_date: '06/28/2006' },
    { value: 40, date: '2010-06-28', formatted_date: '06/28/2010' },
    { value: 48, date: '2012-06-28', formatted_date: '06/28/2012' },
    { value: 34, date: '2018-06-28', formatted_date: '06/28/2018' },
    { value: 33, date: '2020-06-28', formatted_date: '06/28/2020' }
  ];
  
  create_area_chart(data, 'history-chart-main');
  
  function generateTickvalues(min, max) {
      let res = []
        , currentExtent = new Date(min.valueOf())

        while(currentExtent <= max) {
          res.push(new Date(currentExtent.valueOf()))
          currentExtent.setFullYear(currentExtent.getFullYear() + 1);
        }

      return res

  }
  
  function create_area_chart(data, target){
    document.getElementById(target).innerHTML = '';
    var parentw = document.getElementById(target).offsetWidth;
    var parenth = 0.6*parentw;
    var svg = d3.select('#'+target).append("svg").attr("width", parentw).attr("height", parenth),
        margin = {top: 20, right: 20, bottom: 40, left: 50},
        width = +svg.attr("width") - margin.left - margin.right,
        height = +svg.attr("height") - margin.top - margin.bottom,
        g = svg.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")");

    var parseTime = d3.timeParse("%Y-%m-%d");
    bisectDate = d3.bisector(function(d) { return d.date; }).left;

    var x = d3.scaleTime()
        .rangeRound([0, width]);

    var y = d3.scaleLinear()
        .rangeRound([height, 0]);

    var area = d3.area()
        .x(function (d) { return x(d.date); })
        .y1(function (d) { return y(d.value); });

    data.forEach(function (d) {
        //only parse time if not already parsed (i.e. when using time period filters)
        if(parseTime(d.date))
            d.date = parseTime(d.date);
        d.value = +d.value;
    });

    let dateExtent = d3.extent(data, function(d) { return d.date})
    
    
    let tickValues = generateTickvalues(dateExtent[0], dateExtent[1])
 
		x.domain(dateExtent);
    
    y.domain([0, 1.05 * d3.max(data, function (d) { return d.value; })]);
    area.y0(y(0));

    g.append("rect")
        .attr("transform", "translate(" + -margin.left + "," + -margin.top + ")")
        .attr("width", svg.attr("width"))
        .attr('class', 'overlay')
        .attr("height", svg.attr("height"))
        .on("mouseover", function () {
            d3.selectAll(".eps-tooltip").remove();
            d3.selectAll(".eps-remove-trigger").remove();
            focus.style("display", "none");
        });

    g.append("path")
        .datum(data)
        .attr("fill", "#f6f6f6")
        .attr("d", area);

    //create line
    var valueline = d3.line()
        .x(function (d) { return x(d.date); })
        .y(function (d) { return y(d.value); });

    g.append("path")
        .data([data])
        .attr('fill', 'none')
        .attr('stroke', '#068d46')
        .attr("class", "line")
        .attr("d", valueline);

    g.append("g")
        .attr("transform", "translate(0," + height + ")")
        .call(d3.axisBottom(x).tickFormat(d3.timeFormat("%b/%d/%Y")).ticks(d3.timeYear).tickValues(tickValues))
        .selectAll("text")
        .style("text-anchor", "end")
        .attr("dy", ".25em")
        .attr("transform", "rotate(-45)");

    g.append("g")
        .call(d3.axisLeft(y)
            .tickFormat(function (d) { return "$" + d }))
        .append("text")
        .attr("fill", "#068d46")
        .attr("transform", "rotate(-90)")
        .attr("y", 6)
        .attr("dy", "0.71em")
        .attr("text-anchor", "end");

    var focus = g.append("g")
        .attr("class", "focus")
        .style("display", "none");

    focus.append("line")
        .attr("class", "x-hover-line hover-line")
        .attr("y1", 0)
        .attr("y2", height);

    focus.append("line")
        .attr("class", "y-hover-line hover-line")
        .attr("x1", width)
        .attr("x2", width);

    focus.append("circle")
        .attr("fill", "#068d46")
        .attr("r", 4);

    focus.append("text")
        .attr("class", "text-date focus-text")
        .attr("x", 0)
        .attr("y", -20)
        .attr("dy", ".31em")
        .style("text-anchor", "middle");

    focus.append("text")
        .attr("class", "text-val focus-text")
        .attr("x", 0)
        .attr("y", -30)
        .attr("dy", ".31em")
        .style("text-anchor", "middle");

    g.append("rect")
        .attr("class", "overlay")
        .attr("width", width)
        .attr("height", height)
        .on("mouseover", function () { focus.style("display", null); })
        .on("mousemove", function () {
            var x0 = x.invert(d3.mouse(this)[0]),
                    i = bisectDate(data, x0, 1),
                    d0 = data[i - 1],
                    d1 = data[i],
                    d = x0 - d0.year > d1.year - x0 ? d1 : d0;
            focus.attr("transform", "translate(" + x(d.date) + "," + y(d.value) + ")");
            focus.select(".text-date").text(function () { return d.formatted_date; });
            focus.select(".text-val").text(function () { return '$' + d.value; });
            focus.select(".x-hover-line").attr("y2", height - y(d.value));
            focus.select(".y-hover-line").attr("x2", width + width);
        });
}
.chart {
    text-align: center;
    padding: 10px 10px 25px 10px;
    background: #f6f6f6;
  }

  .chart svg {
    overflow: visible;
  }

  .chart .overlay {
    fill: none;
    pointer-events: all;
  }
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.8.0/d3.min.js"></script>
<div class="chart" id="history-chart-main"></div>

Upvotes: 1

Related Questions