Question

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?

Was it helpful?

Solution 2

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.

OTHER TIPS

(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>

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.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top