Setting ticks on a time scale during zoom

The snippet below creates a single x axis with starting ticks of 10. During zoom I’m updating ticks on the rescaled axis with:

.ticks(startTicks * Math.floor(event.transform.k))

With .scaleExtent([1, 50]) I can get down from years to 3-hourly blocks fairly smoothly (besides a little label overlap here and there).

But, when I request the number of ticks applied on the scale (xScale.ticks().length) I get a different number to the one I just assigned.

Also, when I get the labels (xScale.ticks().map(xScale.tickFormat())) they differ from the ones rendered as I get deeper into the zoom.

Reading here:

An optional count argument requests more or fewer ticks. The number of
ticks returned, however, is not necessarily equal to the requested
count. Ticks are restricted to nicely-rounded values (multiples of 1,
2, 5 and powers of 10), and the scale’s domain can not always be
subdivided in exactly count such intervals. See d3.ticks for more
details.

I understand I might not get the number of ticks I request, but it’s counter-intuitive that:

  • I request more and more ticks (per k) – between 10 and 500
  • Then the returned ticks fluctuates between 5 and 19.

Why is this ? Is there a better or ‘standard’ way to update ticks whilst zooming for scaleTime or scaleUtc ?

var margin = {top: 0, right: 25, bottom: 20, left: 25}
var width = 600 - margin.left - margin.right;
var height = 40 - margin.top - margin.bottom;

// x domain
var x = d3.timeDays(new Date(2020, 00, 01), new Date(2025, 00, 01));

// start with 10 ticks
var startTicks = 10;

// zoom function 
var zoom = d3.zoom()
  .on("zoom", (event) => {
  
    var t = event.transform;

    xScale
      .domain(t.rescaleX(xScale2).domain())
      .range([0, width].map(d => t.applyX(d))); 
      
    var zoomedRangeWidth = xScale.range()[1] - xScale.range()[0];
    var zrw = zoomedRangeWidth.toFixed(4);
    var kAppliedToWidth = kw = t.k * width;
    var kw = kAppliedToWidth.toFixed(4);
    var zoomTicks = zt = startTicks * Math.floor(t.k);
      
    svg.select(".x-axis")
      .call(d3.axisBottom(xScale)
        .ticks(zt) 
    );

    var realTicks = rt = xScale.ticks().length;
    console.log(`zrw: ${zrw}, kw: ${kw}, zt: ${zt}, rt: ${rt}`);
    console.log(`labels: ${xScale.ticks().map(xScale.tickFormat())}`);
    
  })
  .scaleExtent([1, 50]); 


// x scale
var xScale = d3.scaleTime()
  .domain(d3.extent(x))
  .range([0, width]); 

// x scale copy
var xScale2 = xScale.copy();

// svg
var svg = d3.select("#scale")
  .append("svg")
  .attr("width", width + margin.left + margin.right)
  .attr("height", height + margin.top + margin.bottom)
  .call(zoom) 
  .append("g")
  .attr("transform", `translate(${margin.left},${margin.top})`);

// clippath 
svg.append("defs").append("clipPath")
  .attr("id", "clip")
  .append("rect")
  .attr("x", 0)
  .attr("width", width)
  .attr("height", height);
    
// x-axis
svg.append("g")
  .attr("class", "x-axis")
  .attr("clip-path", "url(#clip)") 
  .attr("transform", "translate(0," + height + ")")
  .call(d3.axisBottom(xScale)
    .ticks(startTicks));
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.3.1/d3.min.js"></script>
<div id="scale"></div>

1 thought on “Setting ticks on a time scale during zoom”

Leave a Comment