我正在制作一个具有点击缩放功能的世界地图。单击某个国家/地区时,地图会放大但国家/地区并不总是居中 - 当您单击并重复时也会发生同样的情况,它似乎永远不会产生相同的结果。
注意:如果禁用过渡功能,缩放和居中确实有效,只有在添加旋转时它才会显示错误。
我的代码出了什么问题?
为方便起见,我创建了plunker
http://plnkr.co/edit/tgIHG76bM3cbBLktjTX0?p=preview
<!DOCTYPE html>
<meta charset="utf-8">
<style>
.background {
fill: none;
pointer-events: all;
stroke:grey;
}
.feature, {
fill: #ccc;
cursor: pointer;
}
.feature.active {
fill: orange;
}
.mesh,.land {
fill: black;
stroke: #ddd;
stroke-linecap: round;
stroke-linejoin: round;
}
.water {
fill: #00248F;
}
</style>
<body>
<script src="//d3js.org/d3.v3.min.js"></script>
<script src="//d3js.org/topojson.v1.min.js"></script>
<script src="//d3js.org/queue.v1.min.js"></script>
<script>
var width = 960,
height = 600,
active = d3.select(null);
var projection = d3.geo.orthographic()
.scale(250)
.translate([width / 2, height / 2])
.clipAngle(90);
var path = d3.geo.path()
.projection(projection);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
svg.append("rect")
.attr("class", "background")
.attr("width", width)
.attr("height", height)
.on("click", reset);
var g = svg.append("g")
.style("stroke-width", "1.5px");
var countries;
var countryIDs;
queue()
.defer(d3.json, "js/world-110m.json")
.defer(d3.tsv, "js/world-110m-country-names.tsv")
.await(ready)
function ready(error, world, countryData) {
if (error) throw error;
countries = topojson.feature(world, world.objects.countries).features;
countryIDs = countryData;
//Adding water
g.append("path")
.datum({type: "Sphere"})
.attr("class", "water")
.attr("d", path);
var world = g.selectAll("path.land")
.data(countries)
.enter().append("path")
.attr("class", "land")
.attr("d", path)
.on("click", clicked)
};
function clicked(d) {
if (active.node() === this) return reset();
active.classed("active", false);
active = d3.select(this).classed("active", true);
var bounds = path.bounds(d),
dx = bounds[1][0] - bounds[0][0],
dy = bounds[1][1] - bounds[0][1],
x = (bounds[0][0] + bounds[1][0]) / 2,
y = (bounds[0][1] + bounds[1][1]) / 2,
scale = 0.5 / Math.max(dx / width, dy / height),
translate = [width / 2 - scale * x, height / 2 - scale * y];
g.transition()
.duration(750)
.style("stroke-width", 1.5 / scale + "px")
.attr("transform", "translate(" + translate + ")scale(" + scale + ")");
var countryCode;
for (i=0;i<countryIDs.length;i++) {
if(countryIDs[i].id==d.id) {
countryCode = countryIDs[i];
}
}
var rotate = projection.rotate();
var focusedCountry = country(countries, countryCode);
var p = d3.geo.centroid(focusedCountry);
(function transition() {
d3.transition()
.duration(2500)
.tween("rotate", function() {
var r = d3.interpolate(projection.rotate(), [-p[0], -p[1]]);
return function(t) {
projection.rotate(r(t));
g.selectAll("path").attr("d", path)
//.classed("focused", function(d, i) { return d.id == focusedCountry.id ? focused = d : false; });
};
})
})();
function country(cnt, sel) {
for(var i = 0, l = cnt.length; i < l; i++) {
console.log(sel.id)
if(cnt[i].id == sel.id) {
return cnt[i];
}
}
};
}
function reset() {
active.classed("active", false);
active = d3.select(null);
g.transition()
.duration(750)
.style("stroke-width", "1.5px")
.attr("transform", "");
}
</script>
答案 0 :(得分:3)
这是一个棘手的问题 - 我很惊讶地发现没有很好的例子(如果没有解决,可能会提出问题previously)。基于这个问题和你想要实现的目标,我认为你的转换过于复杂(并且补间功能可能会更清晰,也许)。您可以通过修改投影来实现此目的,而不是同时使用g
上的变换和投影的修改。
当前方法
目前您平移并缩放g
,此平移并将g
缩放到预定目的地。单击后,g
的位置使得要素位于中间,然后进行缩放以显示要素。因此,g
不再以svg
为中心(因为它已经缩放和翻译),换句话说,地球被移动和拉伸以使特征居中。没有路径被改变。
此时,您可以旋转投影,根据新旋转重新计算路径。这会将所选要素移动到g
的中心,该svg
不再位于svg
内 - 因为该要素已在g
中居中,任何移动都会偏离它。例如,如果您删除重新缩放和翻译g
的代码,则会注意到您的功能以点击为中心。
潜在解决方案
您似乎经历了两次转型:
平移(/翻译)不是你可能想在这里做的事情,因为当你只是想旋转它时会移动地球。
旋转只能通过d3投影完成,并且可以通过操纵path
或d3投影来完成缩放。因此,使用d3投影来处理地图转换可能更简单。
此外,当前方法的一个问题是,通过使用path.bounds获取bbox,导出缩放和平移,您计算的值可能会随着投影的更新而改变(投影的类型会改变差异)。
例如,如果仅渲染要素的一部分(因为它部分在地平线上),则边界框将与应有的不同,这将导致缩放和翻译中的问题。要在我提出的解决方案中克服此限制,首先旋转地球,计算边界,然后缩放到该因子。 您可以在不实际更新地球上路径旋转的情况下计算比例,只需更新 // Store the current rotation and scale:
var currentRotate = projection.rotate();
var currentScale = projection.scale();
并稍后转换绘制的路径。
解决方案实施
我已经稍微修改了您的代码,并且我认为最终实现代码更清晰:
我存储当前的旋转和比例(所以我们可以从这里转换为新值):
p
使用变量 projection.rotate([-p[0], -p[1]]);
path.projection(projection);
// calculate the scale and translate required:
var b = path.bounds(d);
var nextScale = currentScale * 1 / Math.max((b[1][0] - b[0][0]) / (width/2), (b[1][1] - b[0][1]) / (height/2));
var nextRotate = projection.rotate(); // as projection has already been updated.
来获取我们要缩放的要素质心,我用应用的旋转找出要素的边界框(但我还没有实际旋转地图)。使用bbox,我得到缩放到所选特征所需的比例:
// Update the map:
d3.selectAll("path")
.transition()
.attrTween("d", function(d) {
var r = d3.interpolate(currentRotate, nextRotate);
var s = d3.interpolate(currentScale, nextScale);
return function(t) {
projection
.rotate(r(t))
.scale(s(t));
path.projection(projection);
return path(d);
}
})
.duration(1000);
有关此处参数计算的详情,请see this answer。
然后我在当前比例和旋转以及目标(下一个)比例和旋转之间补间:
g
现在我们正在同时转换这两个属性:
不仅如此,由于我们仅重新绘制路径,因此我们不需要修改笔划以考虑缩放 // Clicked on feature:
var p = d3.geo.centroid(d);
。
其他改进
您可以通过以下方式获取国家/地区特征的质心:
g
你也可以玩缓和 - 而不是仅使用线性插值 - 例如在plunker或bl.ock中。这可能有助于在过渡期间保持视图中的特征。
替代实施
如果你真的想将变焦保持为g
的操纵,而不是投影,那么你可以实现这一点,但缩放必须在旋转之后 - 因为该特征将被居中svg
中.attrTween
的中心位置.attrTween
。见plunker。您可以在旋转之前计算bbox,但如果同时进行两个过渡(旋转和缩放),则缩放将暂时将地球移离中心。
为什么我需要使用补间功能来旋转和缩放?
由于路径的某些部分是隐藏的,因此实际路径可以获得或松散点,完全出现或消失。过渡到最终状态可能并不代表过渡,因为一个旋转超出了地球的视野(事实上它肯定会赢得),这样的路径的简单过渡会导致伪影,请参阅this plunker使用代码修改的可视化演示。为解决此问题,我们使用补间方法path.transition()
.attrTween("d", function()...) // set rotation
.attr("d", path) // set scale
。
由于{{1}}方法正在设置从一个路径到另一个路径的转换,因此我们需要同时进行缩放。我们不能使用:
{{1}}
扩展SVG与缩放投影
可以通过直接操作路径/ svg来平移和缩放许多圆柱投影,而无需更新投影。由于这不会使用geoPath重新计算路径,因此要求不高。
根据所涉及的情况,这不是正视或圆锥投影所提供的奢侈品。由于您在更新旋转时无论如何都要重新计算路径,因此更新比例可能不会导致额外延迟 - 地理路径生成器需要重新计算并重新绘制路径,同时考虑到比例和旋转。