d3世界地图与国家点击和缩放几乎不起作用

时间:2017-08-09 15:53:49

标签: d3.js map-projections

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

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

我的代码出了什么问题?

为方便起见,我创建了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>

1 个答案:

答案 0 :(得分:3)

这是一个棘手的问题 - 我很惊讶地发现没有很好的例子(如果没有解决,可能会提出问题previously)。基于这个问题和你想要实现的目标,我认为你的转换过于复杂(并且补间功能可能会更清晰,也许)。您可以通过修改投影来实现此目的,而不是同时使用g上的变换和投影的修改。

当前方法

目前您平移并缩放g,此平移并将g缩放到预定目的地。单击后,g的位置使得要素位于中间,然后进行缩放以显示要素。因此,g不再以svg为中心(因为它已经缩放和翻译),换句话说,地球被移动和拉伸以使特征居中。没有路径被改变。

此时,您可以旋转投影,根据新旋转重新计算路径。这会将所选要素移动到g的中心,该svg不再位于svg内 - 因为该要素已在g中居中,任何移动都会偏离它。例如,如果您删除重新缩放和翻译g的代码,则会注意到您的功能以点击为中心。

潜在解决方案

您似乎经历了两次转型:

  1. 旋转
  2. 尺度
  3. 平移(/翻译)不是你可能想在这里做的事情,因为当你只是想旋转它时会移动地球。

    旋转只能通过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

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

    Plunker

    不仅如此,由于我们仅重新绘制路径,因此我们不需要修改笔划以考虑缩放 // Clicked on feature: var p = d3.geo.centroid(d);

    其他改进

    您可以通过以下方式获取国家/地区特征的质心:

    g

    Updated Plunker  或Bl.ock

    你也可以玩缓和 - 而不是仅使用线性插值 - 例如在plunkerbl.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重新计算路径,因此要求不高。

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