Search code examples
d3.jsscatter-plot

d3 scatterplot Labels


i'm new to d3 and frontend Could you look and tell on my axis max values aren't shown? like no year 2015 or no 39.50 on Y scale even tho d3.max sees them in array.

https://codepen.io/DeanWinchester88/pen/QWgGMja

const xScale = d3.scaleLinear()
                      .domain([ d3.min(years), d3.max(years)  ])
                      .range([paddings.paddingLeft,w - paddings.paddingRight - paddings.paddingLeft]);
      const xAxis = d3.axisBottom(xScale).tickFormat(d3.format("d"));
      svg.append("g")
                     .attr("transform", "translate(0," + (h - paddings.paddingLeft) + ")")
                      .attr("id","x-axis")
                      .call(xAxis);
  const yScale = d3.scaleLinear()
                      .domain([ parseInt(d3.min(timings)), parseInt(d3.max(timings))])
                      .range([h - paddings.paddingBottom, paddings.paddingTop]);
  const yAxis = d3.axisLeft(yScale).tickFormat( (d) =>  `(${d.toFixed(2)}`);
      svg.append("g")
       .attr("transform", "translate(" + paddings.paddingLeft + ",0)")
        .attr("id","y-axis")
       .call(yAxis)

Solution

  • As noted in the comments there is an issue with how you parse the domain for the y axis: by using parseInt you don't capture the full domain. This only affects how far the axis line extends, not whether ticks capture the entire domain.

    Default Ticks

    As for the ticks, a D3 axis prioritizes clean numbers and an ideal number of ticks (by default 10 if I remember right - though the number is more of a suggestion than constraint). Also, the axis generator does not consider the rendered size of the axis.

    If we have a domain from 1.999 through 9.999 we'll see that our ticks are 2,3,4,5,6,7,8,9, prioritizing both count and roundness as noted above:

    var scale = d3.scaleLinear()
      .domain([1.999,9.999])
      .range([20,290])
      
    console.log(scale.ticks());
    
    d3.select("body")
      .append("svg")
      .attr("transform","translate(0,50)")
      .call(d3.axisBottom(scale));
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

    The axis extends to 9.999, just that this is beyond the last tick, which does look a bit odd

    While this is cleaner than ticks of 1.999, 2.999, 3.999... in both terms of number and amount of text, it doesn't show the extent of the domain.

    In some cases increasing the number of ticks (axis.ticks(idealNumberOfTicks)) will result in the desired outcome, this may be true when dealing with domains that consist of something like integers representing years, as in your case: .ticks(d3.max(years)-d3.min(years));, which should create ticks for every year since the integers are considered suitable round numbers given the desired count of ticks.

    Specifying Specific Ticks

    We can specify values to be used as ticks with axis.tickValues([values]), which will set the ticks used in the axis. In the case of your years axis, this is fairly straightforward, we'd want a tick for every unique year, so we'd only need to create an array that contains each year:

     .tickValues(d3.range(d3.min(years),d3.max(years))); 
    

    It isn't always this straight forward. Using the example scale in the snippet above (more analogous to your y scale), we could use the ticks provided by scale.ticks() and add two new ticks but this means there is a high likelihood of overlap or irregular spacing when combining manual ticks with the automatically generated ticks:

    var scale = d3.scaleLinear()
      .domain([1.999,9.999])
      .range([20,290])
     
    var ticks = [1.999,...scale.ticks(),9.999];
    
    var axis = d3.axisBottom(scale)
      .tickValues(ticks)
      .tickFormat(d3.format(".3f"))
      
    var svg  = d3.select("body")
      .append("svg")
      .attr("transform","translate(0,60)")
      .call(axis);
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

    We could make this more complicated to better set the tick values, but it'll be very data dependent, often result in non-round tick values, and is likely better resolved by using scale.nice(), described below.

    One other option if discarding the automatically generated ticks, especially when dealing with times or dates, is to use d3.time to require a tick every specified interval, as noted in the d3-axis docs with the example: axis.ticks(d3.timeMinute.every(15));. However, this doesn't set ticks so that they contain the entire domain.

    scale.nice()

    There is a better option than manually adding or overriding the automatic ticks however, and this is using scale.nice(), which:

    Extends the domain so that it starts and ends on nice round values. This method typically modifies the scale’s domain, and may only extend the bounds to the nearest round value. An optional tick count argument allows greater control over the step size used to extend the bounds, guaranteeing that the returned ticks will exactly cover the domain. Nicing is useful if the domain is computed from data, say using extent, and may be irregular. For example, for a domain of [0.201479…, 0.996679…], a nice domain might be [0.2, 1.0]. If the domain has more than two values, nicing the domain only affects the first and last value. (docs)

    Taking our example from above we get:

    var scale = d3.scaleLinear()
      .domain([1.999,9.999])
      .range([20,290])
      .nice()
      
    console.log(scale.ticks());
    
    d3.select("body")
      .append("svg")
      .attr("transform","translate(0,50)")
      .call(d3.axisBottom(scale));
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

    This is the solution I'd offer for your y scale, while specifying specific ticks or using axis.ticks() to specify a number of ticks is ideal for your x axis.