颠倒 d3.zoom 缩放和平移的顺序
reversing order of d3.zoom scale and translate
如果您单击此示例中的红色按钮:
https://bl.ocks.org/interwebjill/fe782e6f195b17f6fe6798a24c390d90
您可以看到图表平移,使圆圈位于中心,然后放大到指定级别(再次单击按钮会缩小)。以这种方式平移然后缩放会在左侧留下一个我不希望有的空隙。我该如何更改代码,以便图表先缩放然后转换到中心,这样图表中就没有这个间隙?
我已尝试在缩放定义和 zoomToExtent 函数中颠倒缩放顺序并进行转换,但效果并无不同。
问题的最终根源是d3.interpolateZoom。这个插值器的比例插值比平移快——尽管它们大多同时转换。使用 d3.interpolateZoom
实现的模式基于此 paper。
因为缩放和平移两者在 d3.interpolateZoom
中的插值不同,您会在图表的一侧出现间隙,因为缩放 decreases/increases 比平移值更快。
d3.interpolateZoom
用于对过渡调用缩放。
但是,如果您使用 .attr()
直接在转换上应用转换,d3 转换将使用 d3.interpolateString
,这将在开始和结束字符串中搜索相应的数字并使用 d3.interpolateNumber
那些。这将对缩放和平移应用相同的插值。
使用这两种方法,我们可以比较 d3.interpolateZoom 和 d3.interpolateString 之间的差异。下面的黑色矩形使用 d3.interpolateString
,而橙色矩形使用 d3.interpolateZoom
。单击矩形开始过渡:
var svg = d3.select("body").append("svg")
.attr("width", 500)
.attr("height", 300);
var g1 = svg.append("g"), g2 = svg.append("g");
var zoom1 = d3.zoom().on("zoom", function() {
g1.attr("transform", d3.event.transform);
});
var zoom2 = d3.zoom().on("zoom", function() {
g2.attr("transform", d3.event.transform);
});
g1.call(zoom1.transform, d3.zoomIdentity
.translate(150, 100)
.scale(2));
g2.call(zoom2.transform, d3.zoomIdentity
.translate(150,100)
.scale(2));
g1.append("rect")
.attr("x", 20)
.attr("y", 20)
.attr("width", 50)
.attr("height", 50);
g2.append("rect")
.attr("x", 22)
.attr("y", 22)
.attr("width", 46)
.attr("height",46)
.attr("fill","orange");
d3.selectAll("rect").on("click", function() {
g1.transition()
.duration(6000)
.attr("transform", d3.zoomIdentity)
.on("end", function() {
d3.select(this).call(zoom1.transform, d3.zoomIdentity);
})
g2.transition()
.duration(6000)
.call(zoom2.transform, d3.zoomIdentity)
});
<script type="text/javascript" src="https://d3js.org/d3.v5.js"></script>
第一个矩形使用 .attr() 转换变换,我们需要在之后调用缩放以确保缩放具有当前变换,在本例中我们不需要,但是如果你想在转换后使用缩放,你需要这样做
比较这两个我们得到:
(Y轴表示从开始属性到结束属性的过渡剩余百分比)
您希望缩放和平移在过渡时以相同的速率同时移动。如果我们使用补间函数,我们可以做到这一点。与上面不同,我们不能只使用 transition().attr("transform",newTransfrom)
,因为您还绘制了 canvas 并更新了轴。所以我们需要创建自己的补间函数,它可以使用当前的变换和缩放,将其应用于轴、canvas 和标记。
例如,而不是调用缩放(将使用 d3.interpolateZoom):
function zoomToExtent(d0, d1) {
zoomRect.call(zoom).transition()
.duration(1500)
.call(zoom.transform, d3.zoomIdentity
.translate(-xSVG(d0), 0)
.scale(width / (xSVG(d1) - xSVG(d0))));
}
相反,我们可以使用补间函数来控制元素的变换并应用相同的插值器来缩放和平移:
function zoomToExtent(d0, d1) {
//get transition start and end values:
var startScale = d3.zoomTransform(zoomRect.node()).k;
var startTranslate = d3.zoomTransform(zoomRect.node()).x;
var endTranslate = -xSVG(d0);
var endScale = width / (xSVG(d1) - xSVG(d0));
zoomRect.call(zoom).transition()
.duration(1500)
.tween("transform", function() {
var interpolateScale = d3.interpolateNumber(startScale,endScale);
var interpolateTranslate = d3.interpolateNumber(startTranslate,endTranslate);
return function(t) {
var t = d3.zoomIdentity.translate(interpolateTranslate(t),0).scale(interpolateScale(t));
zoomed(t);
}
})
.on("end", function() { // update the zoom identity on end:
d3.select(this).call(zoom.transform, d3.zoomIdentity
.translate(endTranslate, 0)
.scale(endScale));
})
}
你可能会注意到我正在将一个变换值传递给缩放函数,因为没有 d3.event.transform 用于此,我们需要修改缩放函数以使用传递的参数(如果可用),否则会下降回到事件转换:
function zoomed(transform) {
var t = transform || d3.event.transform;
...
总而言之,这可能看起来像 like this。
对于两种过渡方法之间的另一个比较,我创建了一个可以在两个缩放标识之间切换的网格比较:
var svg = d3.select("body").append("svg")
.attr("width", 510)
.attr("height", 310);
var g1 = svg.append("g");
var g2 = svg.append("g");
var rectangles1 = g1.selectAll()
.data(d3.range(750))
.enter()
.append("rect")
.attr("x", function(d) { return d%25*20; })
.attr("y", function(d) { return Math.floor(d/25)*20; })
.attr("width", 20)
.attr("height", 20)
.attr("fill","#ccc")
.attr("stroke","white")
.attr("stroke-width", 2);
var rectangles2 = g2.selectAll()
.data(d3.range(750))
.enter()
.append("rect")
.attr("x", function(d) { return d%25*20; })
.attr("y", function(d) { return Math.floor(d/25)*20; })
.attr("width", 20)
.attr("height", 20)
.attr("fill","none")
.attr("stroke","#444")
.attr("stroke-width", 1);
var startZoom = d3.zoomIdentity
.translate(-250,-200)
.scale(4);
var endZoom = d3.zoomIdentity
.translate(-100,-100)
.scale(5);
var zoom1 = d3.zoom().on("zoom", function() { g1.attr("transform", d3.event.transform); });
var zoom2 = d3.zoom().on("zoom", function() { g2.attr("transform", d3.event.transform); });
g1.call(zoom1.transform, startZoom);
g2.call(zoom2.transform, startZoom);
var toggle = true;
svg.on("click", function() {
toggle = !toggle;
g1.transition()
.duration(5000)
.call(zoom1.transform, toggle ? startZoom: endZoom)
g2.transition()
.duration(5000)
.attr("transform", toggle ? startZoom: endZoom)
.on("end", function() {
d3.select(this).call(zoom2.transform, toggle ? startZoom: endZoom);
})
})
rect {
opacity: 0.5;
}
<script type="text/javascript" src="https://d3js.org/d3.v5.js"></script>
如果您单击此示例中的红色按钮:
https://bl.ocks.org/interwebjill/fe782e6f195b17f6fe6798a24c390d90
您可以看到图表平移,使圆圈位于中心,然后放大到指定级别(再次单击按钮会缩小)。以这种方式平移然后缩放会在左侧留下一个我不希望有的空隙。我该如何更改代码,以便图表先缩放然后转换到中心,这样图表中就没有这个间隙?
我已尝试在缩放定义和 zoomToExtent 函数中颠倒缩放顺序并进行转换,但效果并无不同。
问题的最终根源是d3.interpolateZoom。这个插值器的比例插值比平移快——尽管它们大多同时转换。使用 d3.interpolateZoom
实现的模式基于此 paper。
因为缩放和平移两者在 d3.interpolateZoom
中的插值不同,您会在图表的一侧出现间隙,因为缩放 decreases/increases 比平移值更快。
d3.interpolateZoom
用于对过渡调用缩放。
但是,如果您使用 .attr()
直接在转换上应用转换,d3 转换将使用 d3.interpolateString
,这将在开始和结束字符串中搜索相应的数字并使用 d3.interpolateNumber
那些。这将对缩放和平移应用相同的插值。
使用这两种方法,我们可以比较 d3.interpolateZoom 和 d3.interpolateString 之间的差异。下面的黑色矩形使用 d3.interpolateString
,而橙色矩形使用 d3.interpolateZoom
。单击矩形开始过渡:
var svg = d3.select("body").append("svg")
.attr("width", 500)
.attr("height", 300);
var g1 = svg.append("g"), g2 = svg.append("g");
var zoom1 = d3.zoom().on("zoom", function() {
g1.attr("transform", d3.event.transform);
});
var zoom2 = d3.zoom().on("zoom", function() {
g2.attr("transform", d3.event.transform);
});
g1.call(zoom1.transform, d3.zoomIdentity
.translate(150, 100)
.scale(2));
g2.call(zoom2.transform, d3.zoomIdentity
.translate(150,100)
.scale(2));
g1.append("rect")
.attr("x", 20)
.attr("y", 20)
.attr("width", 50)
.attr("height", 50);
g2.append("rect")
.attr("x", 22)
.attr("y", 22)
.attr("width", 46)
.attr("height",46)
.attr("fill","orange");
d3.selectAll("rect").on("click", function() {
g1.transition()
.duration(6000)
.attr("transform", d3.zoomIdentity)
.on("end", function() {
d3.select(this).call(zoom1.transform, d3.zoomIdentity);
})
g2.transition()
.duration(6000)
.call(zoom2.transform, d3.zoomIdentity)
});
<script type="text/javascript" src="https://d3js.org/d3.v5.js"></script>
第一个矩形使用 .attr() 转换变换,我们需要在之后调用缩放以确保缩放具有当前变换,在本例中我们不需要,但是如果你想在转换后使用缩放,你需要这样做
比较这两个我们得到:
(Y轴表示从开始属性到结束属性的过渡剩余百分比)
您希望缩放和平移在过渡时以相同的速率同时移动。如果我们使用补间函数,我们可以做到这一点。与上面不同,我们不能只使用 transition().attr("transform",newTransfrom)
,因为您还绘制了 canvas 并更新了轴。所以我们需要创建自己的补间函数,它可以使用当前的变换和缩放,将其应用于轴、canvas 和标记。
例如,而不是调用缩放(将使用 d3.interpolateZoom):
function zoomToExtent(d0, d1) {
zoomRect.call(zoom).transition()
.duration(1500)
.call(zoom.transform, d3.zoomIdentity
.translate(-xSVG(d0), 0)
.scale(width / (xSVG(d1) - xSVG(d0))));
}
相反,我们可以使用补间函数来控制元素的变换并应用相同的插值器来缩放和平移:
function zoomToExtent(d0, d1) {
//get transition start and end values:
var startScale = d3.zoomTransform(zoomRect.node()).k;
var startTranslate = d3.zoomTransform(zoomRect.node()).x;
var endTranslate = -xSVG(d0);
var endScale = width / (xSVG(d1) - xSVG(d0));
zoomRect.call(zoom).transition()
.duration(1500)
.tween("transform", function() {
var interpolateScale = d3.interpolateNumber(startScale,endScale);
var interpolateTranslate = d3.interpolateNumber(startTranslate,endTranslate);
return function(t) {
var t = d3.zoomIdentity.translate(interpolateTranslate(t),0).scale(interpolateScale(t));
zoomed(t);
}
})
.on("end", function() { // update the zoom identity on end:
d3.select(this).call(zoom.transform, d3.zoomIdentity
.translate(endTranslate, 0)
.scale(endScale));
})
}
你可能会注意到我正在将一个变换值传递给缩放函数,因为没有 d3.event.transform 用于此,我们需要修改缩放函数以使用传递的参数(如果可用),否则会下降回到事件转换:
function zoomed(transform) {
var t = transform || d3.event.transform;
...
总而言之,这可能看起来像 like this。
对于两种过渡方法之间的另一个比较,我创建了一个可以在两个缩放标识之间切换的网格比较:
var svg = d3.select("body").append("svg")
.attr("width", 510)
.attr("height", 310);
var g1 = svg.append("g");
var g2 = svg.append("g");
var rectangles1 = g1.selectAll()
.data(d3.range(750))
.enter()
.append("rect")
.attr("x", function(d) { return d%25*20; })
.attr("y", function(d) { return Math.floor(d/25)*20; })
.attr("width", 20)
.attr("height", 20)
.attr("fill","#ccc")
.attr("stroke","white")
.attr("stroke-width", 2);
var rectangles2 = g2.selectAll()
.data(d3.range(750))
.enter()
.append("rect")
.attr("x", function(d) { return d%25*20; })
.attr("y", function(d) { return Math.floor(d/25)*20; })
.attr("width", 20)
.attr("height", 20)
.attr("fill","none")
.attr("stroke","#444")
.attr("stroke-width", 1);
var startZoom = d3.zoomIdentity
.translate(-250,-200)
.scale(4);
var endZoom = d3.zoomIdentity
.translate(-100,-100)
.scale(5);
var zoom1 = d3.zoom().on("zoom", function() { g1.attr("transform", d3.event.transform); });
var zoom2 = d3.zoom().on("zoom", function() { g2.attr("transform", d3.event.transform); });
g1.call(zoom1.transform, startZoom);
g2.call(zoom2.transform, startZoom);
var toggle = true;
svg.on("click", function() {
toggle = !toggle;
g1.transition()
.duration(5000)
.call(zoom1.transform, toggle ? startZoom: endZoom)
g2.transition()
.duration(5000)
.attr("transform", toggle ? startZoom: endZoom)
.on("end", function() {
d3.select(this).call(zoom2.transform, toggle ? startZoom: endZoom);
})
})
rect {
opacity: 0.5;
}
<script type="text/javascript" src="https://d3js.org/d3.v5.js"></script>