【问题标题】:d3 world map with country click and zoom almost working not quited3世界地图与国家点击和缩放几乎工作不完全
【发布时间】:2018-01-17 14:57:40
【问题描述】:

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

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

我的代码有什么问题?

为了方便http://plnkr.co/edit/tgIHG76bM3cbBLktjTX0?p=preview,我创建了一个plunker

<!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>

【问题讨论】:

    标签: d3.js map-projections


    【解决方案1】:

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

    当前方法

    当前您平移和缩放g,这会将g 平移和缩放到预期目标。点击后,g 被定位,使该功能位于中间,然后缩放以展示该功能。因此,g 不再在svg 中居中(因为它已被缩放和平移),换句话说,地球被移动和拉伸以使要素居中。没有改变任何路径。

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

    可能的解决方案

    你似乎经历了两次转变:

    1. 旋转
    2. 规模

    平移(/translating) 可能不是您想要在此处执行的操作,因为当您只想旋转地球时,它会移动地球。

    旋转只能通过 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 与缩放投影

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

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

    【讨论】:

    • 不错的答案。我今天早上看了一会儿,找不到好的解决方案。
    • 谢谢,我之前也看到了。它整天都在我的脑海里 - 结果对我来说比我最初预期的要困难一些,但这使得挑战变得更加有趣。也很高兴见到另一个北方人。
    猜你喜欢
    • 2015-04-25
    • 2013-01-13
    • 2014-11-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-01-12
    • 1970-01-01
    相关资源
    最近更新 更多