sveti petar
sveti petar

Reputation: 3797

Add custom text below each X-axis date label in d3.js

I have a line chart in d3.js.

The X-axis has dates, as defined by:

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)");

I would like to show additional information next to each date. Specifically, a calculation based on the value of the line for that date. For the sake of simplicity, let's say I just want to display the value of the line at that point, multiplied by 3. So, if on 04/19/2020 the value of the line is 150, then the X-axis should read:

04/19/2020
450

If I try to use the axis transform to insert the text into the axis label directly, I get an error, presumably because it's no longer a valid date/time format then. So I think I have to add the text as a separate element, but I don't understand how I can iterate over every point on the graph and calculate the proper text to display. Calculating the correct Y position should be easy for each individual case (y is the full height of the graph plus some fixed offset) so the problem is to iterate and set the correct X and value for each point.

Fiddle

Upvotes: 4

Views: 1645

Answers (1)

D Malan
D Malan

Reputation: 11414

You can define create a custom tickFormat method. For example:

function customFormat(e) {
    // do whatever calculation you like with e here
    return d3.timeFormat("%b/%d/%Y")(e) + "-" + e.getDay();
}

Then you can pass it to tickFormat like this:

d3.axisBottom(x).tickFormat(customFormat).ticks(d3.timeYear)

This is what it will look like:

Screenshot of x-axis with extra data

If the custom text needs to be below the existing axis, you can add another x-axis. You can also use a custom tickFormat for it and make its lines invisible. For example:

function customFormat(e) {
  return e.getDay();
}

let anotheraxis = g.append("g")
  .attr("transform", "translate(0," + (height + 60) + ")")
  .call(d3.axisBottom(x).tickFormat(customFormat).ticks(d3.timeYear));

// Hide the new axis' lines
anotheraxis
  .selectAll("line")
  .style('opacity', 0);

anotheraxis
  .selectAll("path")
  .style('opacity', 0);

This will look like:

Screenshot of an extra x-axis with extra data

You mention "a calculation based on the value of the line for that date." Of course, if your data was generated by some kind of function, you could just pass the tick value in your custom format to that function. However, if you have a set of data points, like in your JSFiddle example, you would have to use interpolation to calculate the value of your curve at that tick value. D3 doesn't seem to provide an easy way to do this, but these questions might be starting points for you.

It's also possible to use the tickValues method to set which specific ticks the axis should have. If you set it to your data's x-values, then you won't need to do any interpolation because you already have the y-values.

For example, you can create an array containing only your data's dates:

let dates = data.map((x) => x.date);

And then construct an object that contains the values of your data for the dates:

let date_values = data.reduce((o, x) => { o[x.date] = x.value; return o;}, {});

Then you can create a custom format that uses the date_values object to get the value for a date and multiplies it by 3:

function customFormat(e) {
   return d3.timeFormat("%b/%d/%Y")(e) + "-" + (3 * date_values[e]);
}

And then finally, use the custom format and dates array for the axis:

.call(d3.axisBottom(x).tickFormat(customFormat).tickValues(dates))

I've created a demo here. This is what it looks like:

Screenshot of axis containing only known data points for ticks

Another way you can add values to the bottom of the axis, is to select all of the .tick elements and add a new text element for each of them. For example:

let axis_bottom = g.append("g")
  .attr("transform", "translate(0," + height + ")")
  .call(d3.axisBottom(x).tickFormat(d3.timeFormat("%b/%d/%Y")).ticks(d3.timeYear).ticks(5));

axis_bottom
  .selectAll("text")
  .attr("dy", ".5em");

axis_bottom
  .selectAll('.tick')
  .append('text')
  .attr("dy", "2.5em")
  .attr("fill", "currentColor")
  .text((e) => {
      // Do whatever calculation on e here.
      return 'abcd';
  });

This is what it will look like:

Screenshot of extra text elements for every .tick element

Upvotes: 2

Related Questions