在缩放期间在时间刻度上设置刻度 [英] Setting ticks on a time scale during zoom

查看:41
本文介绍了在缩放期间在时间刻度上设置刻度的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

下面的代码段创建了一个x轴,开始的 ticks 为10.在缩放过程中,我使用以下方法更新了重新缩放的轴上的刻度:

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

使用 .scaleExtent([1,50]),我可以从几年到3小时的区块平稳地降级(除了在这里和那里有一点标签重叠).

但是,当我请求在刻度上应用的刻度数( xScale.ticks().length )时,我得到的数字与刚刚分配的数字不同.

此外,当我获得标签( xScale.ticks().map(xScale.tickFormat()))时,它们与呈现的标签有所不同,因为我对变焦的了解越来越深.

在此处阅读:

可选的count参数请求更多或更少的滴答声.的数量但是,返回的滴答并不一定等于请求的滴答声数数.刻度线仅限于四舍五入的值(1的倍数)2、5和10的幂),并且标度的域不能总是细分为精确计算此类间隔.有关更多信息,请参见d3.ticks.详细信息.

我了解我可能无法获得我要求的 ticks 的数量,但这与直觉相反:

  • 我要求越来越多的滴答声(每个 k )-在10到500之间
  • 然后返回的 ticks 在5到19之间波动.

这是为什么?在缩放 scaleTime scaleUtc 时,是否有更好的或标准"的方式来更新 ticks ?

  var margin = {顶部:0,右侧:25,底部:20,左侧:25}var width = 600-margin.left-margin.right;var height = 40-margin.top-margin.bottom;//x域var x = d3.timeDays(new Date(2020,00,01),new Date(2025,00,01));//以10个刻度开始var startTicks = 10;//缩放功能var zoom = d3.zoom().on("zoom",(event)=> {var t = event.transform;比例尺.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 *宽度;var kw = kAppliedToWidth.toFixed(4);var zoomTicks = zt = startTicks * Math.floor(t.k);svg.select(.x轴").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比例var xScale = d3.scaleTime().domain(d3.extent(x)).range([0,width]);//x缩放副本var xScale2 = xScale.copy();//svgvar svg = d3.select(#scale").append("svg").attr("width",width + margin.left + margin.right).attr("height",高度+ margin.top + margin.bottom).call(缩放).append("g").attr("transform",`translate($ {margin.left},$ {margin.top})`);//剪切路径svg.append("defs").append("clipPath").attr("id","clip").append("rect").attr("x",0).attr("width",width).attr("height",height);//x轴svg.append("g").attr("class","x轴").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>  

解决方案

问题在于缩放时如何更新xScale.

示例中的当前方法是:

  xScale.domain(t.rescaleX(xScale2).domain()).range([0,width] .map(d => t.applyX(d))); 

这是在做两件事:

  1. 创建重新缩放的xScale2副本,但只能获得其域.
  2. 根据变换扩展xScale的范围.

由于步骤2,缩放范围正在屏幕外扩大.当您请求500个滴答声但只看到10个滴答声时,是因为视口中有490个.

解决方案是连续缩放不需要在缩放时更新范围,因为rescaleX方法足以完成转换过程.

重新缩放缩放比例的合适方法是:

  xScale = t.rescaleX(xScale2) 

仅更改域并保持范围不变.

请考虑以下示例,说明为什么仅更改域就足够了:如果刻度从域 [0,1] 映射到范围 [0,100] ,并使用rescaleX进行转换,新的比例现在将从另一个域(例如 [0.4,0.6] )映射到相同的范围 [0,100] .这就是缩放的概念:它在100宽度的视口中显示的是从0到1的数据,但是现在在同一视口中显示的是从0.4到0.6的数据;它放大"了分别为0.4和0.6.

xScale.tickFormat()返回的格式错误是范围扩展的结果,也是显示的报价和计算的报价之间不匹配的结果.如果该方法还考虑了相同数量的刻度,则该方法仅返回显示的相同刻度,这在第一个参数中得到通知(在您的示例中,该刻度为 xScale.tickFormat(zt)).由于没有参数,因此默认值为10,与显示的 zt 滴答滴答声相比,在时间尺度上计算的10个滴答滴答可能不同或具有不同的时间粒度.

总而言之,此代码段需要进行三处更改:

  • 更改1:直接使用rescaleX仅更新域.
  • 更改2:将缩放刻度固定为一个数字,例如10.
  • 更改3:使用 tickFormat 方法时,应考虑刻度数.

下面的代码段通过这些更改进行了更新:

  var margin = {顶部:0,右侧:25,底部:20,左侧:25}var width = 600-margin.left-margin.right;var height = 40-margin.top-margin.bottom;//x域var x = d3.timeDays(new Date(2020,00,01),new Date(2025,00,01));//以10个刻度开始var startTicks = 10;//缩放功能var zoom = d3.zoom().on("zoom",(event)=> {var t = event.transform;//更改1:直接使用rescaleX仅更新域xScale = t.rescaleX(xScale2);var zoomedRangeWidth = xScale.range()[1]-xScale.range()[0];var zrw = zoomedRangeWidth.toFixed(4);var kAppliedToWidth = kw = t.k *宽度;var kw = kAppliedToWidth.toFixed(4);//更改2:将缩放刻度固定为一个数字,例如10var zoomTicks = zt = 10svg.select(.x轴").call(d3.axisBottom(xScale).ticks(zt));var realTicks = rt = xScale.ticks().length;console.log(`zrw:$ {zrw},kw:$ {kw},zt:$ {zt},rt:$ {rt}`);//变更3:使用tickFormat方法时请考虑ztconsole.log(`labels:$ {xScale.ticks().map(xScale.tickFormat(zt))}`);}).scaleExtent([1,50]);//x比例var xScale = d3.scaleTime().domain(d3.extent(x)).range([0,width]);//x缩放副本var xScale2 = xScale.copy();//svgvar svg = d3.select(#scale").append("svg").attr("width",width + margin.left + margin.right).attr("height",高度+ margin.top + margin.bottom).call(缩放).append("g").attr("transform",`translate($ {margin.left},$ {margin.top})`);;//剪切路径svg.append("defs").append("clipPath").attr("id","clip").append("rect").attr("x",0).attr("width",width).attr("height",height);//x轴svg.append("g").attr("class","x轴").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>  

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>

解决方案

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:

  1. Creating a rescaled copy of xScale2, but only to get its domain.
  2. 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>

这篇关于在缩放期间在时间刻度上设置刻度的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆