D3世界地图与国家点击和缩放几乎不完全工作



我正在制作一张具有单击缩放功能的世界地图。当单击一个国家时,地图会放大,但国家并不总是居中 - 当您单击并重复时也会发生同样的情况,它似乎永远不会提供相同的结果。

注意:如果禁用过渡功能,缩放和居中确实有效,只有在添加旋转时才会显示不正确。

我的代码有什么问题?

为了方便起见,我创建了一个plunkerhttp://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>

这是一个困难的问题 - 我很惊讶地看到没有很好的例子(而且这个问题以前可能已经提出而没有解决)。根据问题和您要实现的目标,我认为您将过渡过于复杂(也许补间功能可以更清晰)。无需同时使用g上的变换和投影的修改,只需修改投影即可实现此目的。

当前的方法

当前,您可以平移和缩放g,这会将g平移和缩放到预期目标。单击后,g的位置使要素位于中间,然后缩放以显示要素。因此,g不再以svg为中心(因为它已被缩放和平移),换句话说,地球被移动和拉伸以使要素居中。不会更改任何路径。

此时,旋转投影,这将根据新旋转重新计算路径。这会将选定的特征移动到g的中心,该中心不再在svg内居中 - 因为该特征已经在svg内居中,任何移动都会使它偏离中心。例如,如果删除重新缩放和转换g的代码,则会注意到功能以单击为中心。

潜在解决方案

您似乎正在经历两次转换:

  1. 旋转
  2. 规模

平移(/翻译)不是您可能想在这里做的事情,因为当您只想旋转地球时,这会移动地球。

旋转只能通过 d3 投影完成,比例可以通过对g的操作或在 d3 投影内完成。因此,仅使用 d3 投影来处理地图变换可能更简单。

此外,当前方法的一个问题是,通过使用 path.bounds 获取 bbox,派生比例和平移,您正在计算的值可能会随着投影的更新而变化(投影的类型也会改变方差)。 例如,如果仅渲染要素的一部分(因为它部分位于地平线上),则边界框将不同于应有的范围,这将导致缩放和平移问题。为了克服我提出的解决方案中的这一限制,请先旋转地球,计算边界,然后缩放到该因子。您可以在不实际更新地球上路径旋转的情况下计算比例,只需更新path并在以后转换绘制的路径即可。

解决方案实施

我稍微修改了您的代码,我认为最终实现代码更干净:

我将当前的旋转和缩放(以便我们可以从此值过渡到新值)存储在此处:

// Store the current rotation and scale:
var currentRotate = projection.rotate();
var currentScale = projection.scale();

使用您的变量p来获取我们正在缩放到的特征质心,我找出了应用旋转的特征的边界框(但我实际上还没有旋转地图)。使用 bbox,我获得了缩放到所选要素所需的比例:

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.

有关此处参数计算的更多信息,请参阅此答案。

然后,我在当前缩放和旋转与目标(下一个)缩放和旋转之间补间:

// 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);

现在我们正在同时转换这两个属性:

普伦克

不仅如此,由于我们仅重绘路径,因此无需修改笔画以考虑缩放g

其他改进

您可以通过以下方式获取国家/功能的质心:

// Clicked on feature:
var p = d3.geo.centroid(d);

更新的普伦克 或 Bl.ock

您还可以玩弄缓动 - 而不仅仅是使用线性插值 - 例如在此 plunker 或 bl.ock 中。这可能有助于在过渡期间保持功能视图。

替代实施

如果您真的想将缩放保留为g的操作,而不是投影,那么您可以实现这一点,但缩放必须在旋转之后 - 因为特征将在g中居中,该将在svg中居中。看到这个褶皱。您可以在旋转之前计算 bbox,但如果同时进行两个过渡(旋转和缩放),缩放将暂时将地球移离中心。

为什么需要使用补间函数来旋转和缩放?

由于部分路径是隐藏的,因此实际路径可能会获得或松散点,完全出现或消失。过渡到其最终状态可能不代表一个人旋转到地球地平线之外的过渡(事实上它肯定不会),像这样的路径的简单过渡可能会导致伪影,请参阅此 plunker 以获取使用代码修改的可视化演示。为了解决这个问题,我们使用补间方法.attrTween.

由于.attrTween方法是设置从一条路径到另一条路径的转换,因此我们需要同时进行缩放。我们不能使用:

path.transition()
.attrTween("d", function()...) // set rotation
.attr("d", path) // set scale

缩放 SVG 与缩放投影

许多圆柱投影可以通过直接操作路径/svg 来平移和缩放,而无需更新投影。由于这不会使用 geoPath 重新计算路径,因此要求应该不高。

这不是正交或圆锥投影提供的奢侈品,这取决于所涉及的情况。由于您在更新旋转时无论如何都要重新计算路径,因此比例的更新可能不会导致额外的延迟 - 地理路径生成器无论如何都需要重新计算和重新绘制路径,同时考虑比例和旋转。

最新更新