1
votes

I have a graph which has data points for each day, but I'm dissatisfied with the default setup of the x axis.

The default x-axis picks out a selection of points to label, e.g. with xAxis.ticks(d3.time.month); it will label the 1st of each month with the name of that month:

  |-------|-------|-------|-------|-------|-------|
 nov     dec     jan     feb     mar     apr     may

This works fine for a bar graph, where there is only one bar at each of the months above, but it is misleading for an area graph, as e.g. the 'd' in 'dec' appears under the data points for the 28th, 29th & 30th of november.

I want it to label the range that the month covers (the gaps between the ticks):

  |-------|-------|-------|-------|-------|-------|
     nov     dec     jan     feb     mar     apr  

Any easy way to specify this at a high level?

3
Something like this? You can also adjust dx if you want to move the labels even further. - Lars Kotthoff
@LarsKotthoff those appear just to the right of their tick, rather than centred over their range. - EoghanM
Yes, this is the only way you can do it without modifying the source. So to make them appear in the middle of the range, you would need to add an offset (dx) to the elements. It might be easier to leave the text anchor in the middle and offset them by half the width of an interval. - Lars Kotthoff
dx will vary depending on the number of days in the month - EoghanM
Sure, but you can compute the width of an interval dynamically. - Lars Kotthoff

3 Answers

1
votes

(2019) Using latest d3v5, other stackoverflow responses, 1, and mbostock trick to select and edit the labels text property, axis styling.

First giving a x_scale and number of ticks you can calculate the distance in rendered pixels of all ticks, including non linear scales. The function below returns an array with ticks distances / spaces.

// ticksDistance is constant for a specific x_scale
// apply scale to ticks and calcule distance
const getTicksDistance = (scale) => {
      const ticks = scale.ticks(); // return array with ticks
      const spaces = []
      for(let i=0; i < ticks.length - 1; i++){
        spaces.push(scale(ticks[i+1]) - scale(ticks[i]))
      }
      return spaces;
};
const ticksSpacingTime = getTicksDistance(x_scale_time);

Then use that distance to shift the labels from its current position, the index i gives you the position d3 is iterating over the text labels.

svg.append("g")
    .attr("class", "x-axis-shifted")
    .attr("transform", "translate(0,100)")
    .call(x_axis)
  .selectAll("text")
    .attr("x", (d,i) => ticksSpacingTime[i]/2)

const x_scale_time = d3.scaleTime()
    .domain([new Date(2017,12,1),new Date()])
    .range([0, 960]);
    
const x_axis_time = d3.axisBottom()
    .scale(x_scale_time)
    .ticks(d3.timeMonth.every(1))
    

const x_scale_pow = d3.scalePow().exponent(2)
    .domain([0,20000])
    .range([0, 960]);
    
const x_axis_pow = d3.axisBottom()
    .scale(x_scale_pow)
    .ticks(10)
    
// ticksDistance is constant for a specific x_scale
const getTicksDistance = (scale) => {
      const ticks = scale.ticks();
      const spaces = []
      for(let i=0; i < ticks.length - 1; i++){
        spaces.push(scale(ticks[i+1]) - scale(ticks[i]))
      }
      return spaces;
};

//you have to recalculate when x_scale or ticks change
const ticksSpacingTime = getTicksDistance(x_scale_time);
const ticksSpacingPow = getTicksDistance(x_scale_pow);

const svg = d3.select("body").append("svg")
    .attr("width", "500px")
    .attr("height","350px")
    .style("width", "100%")
    .style("height", "auto");

// normal 
svg.append("g")
    .attr("class", "x-axis-time")
    .attr("transform", "translate(0,0)")
    .call(x_axis_time)
    
// shift labels to half of the ticks distance
svg.append("g")
    .attr("class", "x-axis-time-shifted")
    .attr("transform", "translate(0,40)")
    .call(x_axis_time)
  .selectAll("text")
    .attr("x", (d,i) => ticksSpacingTime[i]/2)


// normal 
svg.append("g")
    .attr("class", "x-axis")
    .attr("transform", "translate(0,110)")
    .call(x_axis_pow)
    
// shift labels to half of the ticks distance
svg.append("g")
    .attr("class", "x-axis-shifted")
    .attr("transform", "translate(0,150)")
    .call(x_axis_pow)
  .selectAll("text")
    .attr("x", (d,i) => ticksSpacingPow[i]/2)
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
0
votes

To make them appear in the middle of the range, you would need to add an offset (dx) to the text elements. This offset would be equal to half the size of an interval, which you can compute dynamically.

Here's an example that does something very similar.

0
votes

I had the same problem and solved it by using another scale for the X-axis that is shifted 15 days.

// scale is your usual x scale. change dates on the domain by 15 days
var d0 = scale.domain()[0];
d0.setDate(d0.getDate() - 15);
var d1 = scale.domain()[1];
d1.setDate(d1.getDate() - 15);

// use this new x scale for the axis only
new_scale = d3.time.scale().domain([d0, d1]).range(scale.range());
d3.svg.axis().scale(new_scale);

This way you have no awkwardly shifted texts and still leave all the heavy lifting to d3. For showing ticks you'd use a second x-axis with the normal scale.