D3 v5轴比例改变平移方式太多

2022-07-01 00:00:00 zooming reactjs d3.js javascript

我有一个以时间为X轴的简单图表。预期的行为是在图表中拖动时,X轴将仅平移以显示数据的其他部分。

为方便起见,由于我的X轴位于Reaction组件中,因此创建我的图表的函数将X标度、x轴及其附加的元素分别设置为this.xScalethis.xAxisthis.gX

如果我将其设置为我的Zoom方法的内容,则一切正常:

        this.gX.call(this.xAxis.scale(d3.event.transform.rescaleX(this.xScale)))

使用触摸输入,X轴移动顺畅。但是,这对我不起作用,因为稍后当我更新图表(移动数据点以响应轴的更改)时,我需要更改this.xAxis,以便这些点将映射到不同的位置。

因此,我随后将我的Zoom方法的内容设置为:

        this.xScale = d3.event.transform.rescaleX(this.xScale);
        this.xAxis = this.xAxis.scale(this.xScale);
        this.gX.call(this.xAxis);

据我所知,这应该以完全相同的方式起作用。然而,当我使用这段代码时,即使没有运行我的updateChart()函数(更新数据点),平移时X轴也会不规律地缩放,远远超过正常情况。我的X轴是基于时间的,所以从2014年到2018年的时间域突然包括了20世纪20年代初。

我做错了什么?


解决方案

问题

当您使用scale.rescaleX时,您正在基于当前的缩放转换(基于平移和缩放)修改比例的域。

但是,从d3.event.transfrom返回的转换不是对上一个缩放转换的更改,而是表示累积转换。我们希望在原始比例上应用此转换,因为该转换代表与原始状态的变化。但是,您要在以前的缩放变换修改的比例上应用此累积变换:

     this.xScale = d3.event.transform.rescaleX(this.xScale);

让我们来了解一下在平移等翻译事件期间它会做些什么:

  1. 向右平移10个单位
  2. 将刻度范围移位10个单位。

有效,但如果我们再次平移:

  1. 再向右平移10个单位
  2. 将刻度域额外移动20个单位。

为什么?因为缩放变换跟踪相对于初始状态的缩放状态,但您希望仅使用状态更改而不是缩放变换的累计更改来更新比例。因此,在这一点上,域已经移动了30个单位,但用户只平移了20个单位。

同样的事情也发生在比例上:

  1. 在图形中心放大2倍(缩放变换比例=2)
  2. 重新调整比例,使其具有一半的域(详细程度加倍)
  3. 再次在图形中心放大2倍(缩放变换比例=4)
  4. 重新缩放比例,使其具有当前域的四分之一(已是原始域的一半,因此我们现在放大8x:2x4)。

在第四步,d3.event.transform.k == 4,而rescaleX现在将比例调整为四倍,它不"知道"该比例已经调整为二倍。

如果我们继续应用缩放,情况会变得更糟,例如,如果我们从k=4缩小到k=2,d3.event.transform.k == 2,尽管我们试图缩小,但仍然放大2倍,现在我们处于16x:2x4x2。相反,如果我们放大,我们会得到64x(2x4x8)

此效果在平移上尤其糟糕-缩放甚至会在整个平移事件中不断触发,因此会在已累计应用缩放变换的比例上累加重新应用缩放。平移可以很容易地触发数十个缩放事件。在下面的比较片段中,稍微平移一下就可以很容易地将你带入20世纪20年代,尽管你的起始域是2014-2018年。

解决方案

纠正此错误的最简单方法(以及规范方法)与您在代码中用于平移(但不更新)的方法非常相似:

 this.gX.call(this.xAxis.scale(d3.event.transform.rescaleX(this.xScale)))

我们在这里做什么?我们正在创建一个新的比例,同时保持原来的比例不变-d3.event.transform.rescaleX(this.xScale)。我们向轴提供新的刻度。但是,正如您注意到的,在更新遇到问题的图表时,xScale不是轴使用的比例,因为我们现在有两个完全不同的比例。

那么解决方案就是使用我所说的参考标尺和工作标尺。参考比例将用于基于当前缩放变换更新工作比例。无论何时创建/更新轴或点,都将使用工作比例。开始时,两个比例可能是相同的,因此我们可以这样创建比例:

var xScale = d3.scaleLinear().domain(...).range(...) // working
var xScaleReference = xScale.copy();                 // reference

我们可以像往常一样使用XScale更新或放置元素。

缩放时,我们可以使用:

更新XScale(和轴)
xScale = d3.event.transform.rescaleX(xScaleReference)
xAxis.scale(xScale);
selection.call(xAxis);

这里有一个比较,它的范围与你所提到的相同,但不需要很长时间就可以达到20世纪20年代的较高等级(使用一个等级)。底部更符合预期(并使用工作和参考标尺):

数据-lang="js"数据-隐藏="真"数据-控制台="真"数据-巴贝尔="假">
var svg = d3.select("body")
  .append("svg")
  .attr("width", 400)
  .attr("height", 200);

var parseTime = d3.timeParse("%Y")
 
 
var start = parseTime("2014");
var end = parseTime("2018");
 
///////////////////
// Single scale updated by zoom transform:
var a = d3.scaleTime()
  .domain([start,end])
  .range([20,380])
  
var aAxis = d3.axisBottom(a).ticks(5);

var aAxisG = svg.append("g")
  .attr("transform","translate(0,30)")
  .call(aAxis);

/////////////////
// Reference and working scale:
var b = d3.scaleTime()
  .domain([start,end])
  .range([20,380])
  
var bReference = b.copy();

var bAxis = d3.axisBottom(b).ticks(5);
  
var bAxisG = svg.append("g")
  .attr("transform","translate(0,80)")
  .call(bAxis);

/////////////////
// Zoom:
var zoom = d3.zoom()
  .on("zoom", function() {
    a = d3.event.transform.rescaleX(a);
    b = d3.event.transform.rescaleX(bReference);
    
    aAxisG.call(aAxis.scale(a));
    bAxisG.call(bAxis.scale(b));


})

svg.call(zoom);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

我们可以看到Mike Bostock的例子采用了相同的方法,例如brush and zoom,其中x2和y2表示参考刻度,x和y表示工作刻度。

相关文章