2017-08-09 38 views
2

我正在研究一个世界地图,其功能点击缩放功能。点击一个国家时,地图会放大,但国家并不总是居中 - 当您点击并重复时发生的情况也是如此,但似乎从未出现过相同的结果。d3世界地图与国家点击和缩放几乎不起作用

注意:如果禁用了转换功能,则只有在添加了旋转功能后,缩放和居中功能才能正常工作。

我的代码有什么问题?我很惊讶地看到,有没有很好的例证(并且该问题可能已经提出previously无分辨率) -

我为了方便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> 

回答

2

这是一个很难回答的问题创造了一个plunker。基于这个问题以及你想要达到的目标,我认为你过度复杂化了你的过渡(而且补间功能也许会变得更加清晰)。除了在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. 

有关此处,see this answer参数的计算更多信息。

然后我目前的缩放和旋转和目标(下)尺度和旋转之间吐温:

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

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

Plunker

不仅由于我们只重绘路径,所以我们不需要修改笔画来计算缩放g

其他改进

你可以得到国家的心/功能只有这个:

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

Updated PlunkerBl.ock

你也可以用宽松的玩具 - 而而不仅仅是使用线性插值 - 比如在这个plunkerbl.ock中。这可能有助于在过渡期间保留功能。

替代实施

如果你真的想保持变焦为g的操作,而不用投影,那么你就可以做到这一点,但变焦必须是旋转后 - 作为功能将集中在g,这将集中在svg。看到这个plunker。您可以在旋转之前计算bbox,但如果同时进行两个转换(旋转和缩放),则缩放将暂时将地球移离中心。

为什么我需要使用补间功能进行旋转和缩放?

由于部分路径是隐藏的,实际路径可能增加或消失,完全出现或消失。向最终状态的过渡可能并不代表当一个人转过地球以外的地方(事实上肯定不会),像这样的路径的平稳过渡可能会导致人为因素,请参阅this plunker进行使用修改的视觉演示的代码。为了解决这个问题,我们使用补间法.attrTween

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

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

缩放SVG VS缩放投影

许多圆柱形凸起物可以通过直接操纵的路径/ SVG被平移和缩放,而不更新投影。由于这不会使用地理路径重新计算路径,因此应该不那么苛刻。

根据所涉及的情况,这不是由正字法或圆锥形投影提供的奢侈品。由于在更新旋转时无论如何都要重新计算路径,所以缩放的更新可能不会导致额外的延迟 - 地理路径生成器需要重新计算并重新绘制路径,无论如何都要考虑缩放和旋转。

+0

不错的答案。今天早上我看了一会儿,找不到一个好的解决方案。 –

+0

谢谢,我之前也看到了这一点。它整天留在我的脑海 - 对我来说比我一开始预料的要困难一些,但这使得它更加有趣的挑战。总是很高兴看到另一位北方人。 –

相关问题