d3 v5 轴刻度改变平移方式太多
d3 v5 Axis Scale Change Panning Way Too Much
我有一个以时间为 X 轴的简单图表。预期的行为是在图表中拖动时,X 轴只会平移以显示数据的其他部分。
为方便起见,由于我的 X 轴在 React 组件中,因此创建我的图表的函数将 X 刻度、x 轴及其附加的元素设置为 this.xScale
、this.xAxis
,和 this.gX
。
如果我将其设置为缩放方法的内容,一切正常:
this.gX.call(this.xAxis.scale(d3.event.transform.rescaleX(this.xScale)))
X轴随触摸输入平滑移动。然而,这对我不起作用,因为稍后当我更新图表(移动数据点以响应轴的变化)时,我需要更改 this.xAxis
以便这些点映射到不同的位置。
因此,我将缩放方法的内容设置为:
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 年代初期。
我做错了什么?
问题
当您使用 scale.rescaleX 时,您正在修改基于当前缩放变换(基于平移和缩放)的缩放域。
但是,从d3.event.transfrom
返回的变换不是之前缩放变换的变化,它代表了累积变换。我们想在我们的原始尺度上应用这个变换,因为变换代表从原始状态的变化。但是,您正在将此累积变换应用于由先前缩放变换修改的比例:
this.xScale = d3.event.transform.rescaleX(this.xScale);
让我们研究一下它在平移等翻译事件中的作用:
- 向右平移 10 个单位
- 将比例域移动 10 个单位。
可行,但如果我们再次平移:
- 再向右平移 10 个单位
- 将比例域再移动 20 个单位。
为什么?因为缩放变换会跟踪相对于初始状态的缩放状态,但您只想使用状态变化来更新比例,而不是缩放变换的累积变化。因此,此时域已经移动了 30 个单位,但用户只平移了 20 个单位。
规模也发生了同样的事情:
- 在图形中心放大 2 倍(缩放变换比例 = 2)
- 重新缩放比例,使其具有一半的域(详细程度是原来的两倍)
- 在图形中心再次放大 2 倍(缩放变换比例 = 4)
- 重新缩放比例,使其具有当前域的四分之一(已经是原始域的二分之一,因此我们现在放大 8 倍:2x4)。
在第四步,d3.event.transform.k == 4
,rescaleX 现在将比例缩放四倍,"know" 比例已经[=87] =] 被缩放了两倍。
如果我们继续应用缩放,情况会变得更糟,例如,如果我们从 k=4 缩小到 k=2,d3.event.transform.k == 2
,尽管尝试缩小,但我们仍在放大 2 倍,现在我们是 16x:2x4x2。相反,如果我们放大,我们会得到 64x (2x4x8)
此效果在平移时特别糟糕 - 缩放甚至在整个平移事件中不断触发,因此缩放会在已经累积应用缩放变换的缩放上累积重新应用。平移可以轻松触发数十个缩放事件。在下面的比较片段中,尽管起始域为 2014-2018,但稍微平移一下就可以轻松将您带入 1920 年代。
解决方案
更正此问题的最简单方法(以及规范方法)与您在用于平移(但不更新)的代码中使用的方法非常相似:
this.gX.call(this.xAxis.scale(d3.event.transform.rescaleX(this.xScale)))
我们在这里做什么?我们正在创建一个新的比例,同时保持原来的比例 - d3.event.transform.rescaleX(this.xScale)
。我们为轴提供新的比例。但是,正如您所注意到的,在更新图表时 运行 遇到问题,xScale
不是轴使用的比例,因为我们现在有两个不同的比例。
那么解决方案就是使用我所说的参考比例和工作比例。参考比例将用于根据当前缩放变换更新工作比例。只要 creating/updating 轴或点,就会使用工作比例。一开始,两个比例可能是一样的,所以我们可以这样创建比例:
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);
这是一个比较,它与您记下的域相同,但用不了多久就可以达到 1920 年代的较高比例(使用一个比例)。底部更符合预期(并使用工作和参考比例):
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 代表工作比例。
我有一个以时间为 X 轴的简单图表。预期的行为是在图表中拖动时,X 轴只会平移以显示数据的其他部分。
为方便起见,由于我的 X 轴在 React 组件中,因此创建我的图表的函数将 X 刻度、x 轴及其附加的元素设置为 this.xScale
、this.xAxis
,和 this.gX
。
如果我将其设置为缩放方法的内容,一切正常:
this.gX.call(this.xAxis.scale(d3.event.transform.rescaleX(this.xScale)))
X轴随触摸输入平滑移动。然而,这对我不起作用,因为稍后当我更新图表(移动数据点以响应轴的变化)时,我需要更改 this.xAxis
以便这些点映射到不同的位置。
因此,我将缩放方法的内容设置为:
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 年代初期。
我做错了什么?
问题
当您使用 scale.rescaleX 时,您正在修改基于当前缩放变换(基于平移和缩放)的缩放域。
但是,从d3.event.transfrom
返回的变换不是之前缩放变换的变化,它代表了累积变换。我们想在我们的原始尺度上应用这个变换,因为变换代表从原始状态的变化。但是,您正在将此累积变换应用于由先前缩放变换修改的比例:
this.xScale = d3.event.transform.rescaleX(this.xScale);
让我们研究一下它在平移等翻译事件中的作用:
- 向右平移 10 个单位
- 将比例域移动 10 个单位。
可行,但如果我们再次平移:
- 再向右平移 10 个单位
- 将比例域再移动 20 个单位。
为什么?因为缩放变换会跟踪相对于初始状态的缩放状态,但您只想使用状态变化来更新比例,而不是缩放变换的累积变化。因此,此时域已经移动了 30 个单位,但用户只平移了 20 个单位。
规模也发生了同样的事情:
- 在图形中心放大 2 倍(缩放变换比例 = 2)
- 重新缩放比例,使其具有一半的域(详细程度是原来的两倍)
- 在图形中心再次放大 2 倍(缩放变换比例 = 4)
- 重新缩放比例,使其具有当前域的四分之一(已经是原始域的二分之一,因此我们现在放大 8 倍:2x4)。
在第四步,d3.event.transform.k == 4
,rescaleX 现在将比例缩放四倍,"know" 比例已经[=87] =] 被缩放了两倍。
如果我们继续应用缩放,情况会变得更糟,例如,如果我们从 k=4 缩小到 k=2,d3.event.transform.k == 2
,尽管尝试缩小,但我们仍在放大 2 倍,现在我们是 16x:2x4x2。相反,如果我们放大,我们会得到 64x (2x4x8)
此效果在平移时特别糟糕 - 缩放甚至在整个平移事件中不断触发,因此缩放会在已经累积应用缩放变换的缩放上累积重新应用。平移可以轻松触发数十个缩放事件。在下面的比较片段中,尽管起始域为 2014-2018,但稍微平移一下就可以轻松将您带入 1920 年代。
解决方案
更正此问题的最简单方法(以及规范方法)与您在用于平移(但不更新)的代码中使用的方法非常相似:
this.gX.call(this.xAxis.scale(d3.event.transform.rescaleX(this.xScale)))
我们在这里做什么?我们正在创建一个新的比例,同时保持原来的比例 - d3.event.transform.rescaleX(this.xScale)
。我们为轴提供新的比例。但是,正如您所注意到的,在更新图表时 运行 遇到问题,xScale
不是轴使用的比例,因为我们现在有两个不同的比例。
那么解决方案就是使用我所说的参考比例和工作比例。参考比例将用于根据当前缩放变换更新工作比例。只要 creating/updating 轴或点,就会使用工作比例。一开始,两个比例可能是一样的,所以我们可以这样创建比例:
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);
这是一个比较,它与您记下的域相同,但用不了多久就可以达到 1920 年代的较高比例(使用一个比例)。底部更符合预期(并使用工作和参考比例):
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 代表工作比例。