问题
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:
The issue is in how the xScale is being updated on zoom.
The current approach in the example is:
xScale
.domain(t.rescaleX(xScale2).domain())
.range([0, width].map(d => t.applyX(d)));
This is doing two things:
- Creating a rescaled copy of xScale2, but only to get its domain.
- Extending the range of the xScale depending on the transform.
Because of step 2, the scale range is growing outside of the screen. When you request 500 ticks but only see 10, it is because there are 490 out of the viewport.
The solution is that continuous scales don't need to have the range updated on zoom, because the rescaleX method is enough for the transformation process.
The appropriate way to rescale a continuous scale on zoom is:
xScale = t.rescaleX(xScale2)
Which changes only the domain and keeps the range intact.
Consider this example to illustrate why only changing the domain is enough: If a scale maps from a domain [0,1]
to a range [0, 100]
, and it is transformed with rescaleX, the new scale will now map from another domain (say, [0.4, 0.6]
) to the same range [0, 100]
. This is the zoom concept: it was showing data from 0 to 1 in a 100 width viewport, but now it is showing data from 0.4 to 0.6 in the same viewport; it "zoomed in" to 0.4 and 0.6.
The incorrect format returned from xScale.tickFormat()
was a consequence of the range extension, but also of a mismatch between the displayed ticks and the computed ticks. The method only return the same ticks that are displayed if it also consideres the same amount of ticks, which is informed in the first parameter (in your example, it would be xScale.tickFormat(zt)
). Since it had no arguments, it defaults to 10, and the 10 ticks computed in the time scale could be different or be in a different time granularity than the zt
ticks that are displayed.
In summary, the snippet needs three changes:
- Change 1: Update only the domain directly with rescaleX.
- Change 2: Fix zoom ticks to a number, such as 10.
- Change 3: Consider the number of ticks when using the
tickFormat
method.
The snippet below is updated with those changes:
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;
// Change 1: Update only the domain directly with rescaleX
xScale = t.rescaleX(xScale2);
var zoomedRangeWidth = xScale.range()[1] - xScale.range()[0];
var zrw = zoomedRangeWidth.toFixed(4);
var kAppliedToWidth = kw = t.k * width;
var kw = kAppliedToWidth.toFixed(4);
// Change 2: Fix zoom ticks to a number, such as 10
var zoomTicks = zt = 10
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}`);
// Change 3: Consider zt when using the tickFormat method
console.log(`labels: ${xScale.ticks().map(xScale.tickFormat(zt))}`);
})
.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>
来源:https://stackoverflow.com/questions/65916866/setting-ticks-on-a-time-scale-during-zoom