带有国家单击和缩放功能的d3世界地图几乎无法正常工作 [英] d3 world map with country click and zoom almost working not quite

查看:95
本文介绍了带有国家单击和缩放功能的d3世界地图几乎无法正常工作的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I am working on a world map that features a click to zoom feature. When clicking a country the map zooms in but the country is not always centered -- the same happens when you click out and repeat, it never seems to deliver the same result.

注意:如果禁用过渡功能,则只有在添加旋转后,缩放和居中功能才能正常显示.

Note: If you disable the transition function, the zoom and centering does work, only when rotation is added it displays incorrectly.

我的代码有什么问题?

为方便起见,我创建了plunker http://plnkr.co/edit/tgIHG76bM3cbBLktjTX0 ?p =预览

I created a plunker for convenience 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>

推荐答案

这是一个棘手的问题-我很惊讶地看到没有很好的例子(这个问题可能已经提出

This is a difficult question - I was surprised to see that there are not good examples of this (and the issue may have been raised previously without resolution). Based on the problem and what you are trying to achieve, I think you are overly complicating your transitions (and the tween functionality can be made clearer, perhaps). Instead of using both a transform on the g and a modification of the projection, you can achieve this with just a modification of the projection.

当前方法

当前,您平移和缩放g,这会平移和缩放g到预期的目标.单击后,将g放置在适当的位置,以使要素位于中间,然后缩放以展示该要素.因此,g不再位于svg的中心(因为它已缩放和平移),换句话说,将球体移动和拉伸以使特征居中.路径没有改变.

Currently you pan and zoom the g, this pans and zooms the g to the intended destination. After the click, the g is positioned so that the feature is in the middle and then scaled to showcase the feature. Consequently, the g is no longer centered in the svg (as it has been scaled and translated), in other words, the globe is moved and stretched so that the feature is centered. No paths are altered.

这时,旋转投影,这将基于新的旋转重新计算路径.这会将选定的特征移动到g的中心,而不再位于svg的中心-由于特征已经在svg的中心,因此任何移动都会使其偏心.例如,如果删除重新缩放和翻译g的代码,您会发现功能集中在click上.

At this point, you rotate the projection, which recalculates the paths based on the new rotation. This moves the selected features to the center of the g, which is no longer centered within the svg - as the feature was already centered within the svg, any movement will decenter it. For example, if you remove the code that rescales and translates the g, you'll notice your feature is centered on click.

潜在的解决方案

您似乎经历了两次转换:

You appear to be after two transformations:

  1. 旋转
  2. 规模

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

只能使用d3投影进行旋转,并且可以通过对g或在d3投影内进行缩放来进行缩放.因此,仅使用d3投影来处理地图转换可能会更简单.

Rotation can only be done with a d3 projection and scale can be done with either manipulation to the g or within the d3 projection. Therefore, it is probably simpler to just use a d3 projection to handle your map transformations.

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

Also, an issue with the current approach is that by using path.bounds to get a bbox, to derive both scale and translate, you are calculating values which may change as the projection is updated (the type of projection will vary the variance too). For example, if only a portion of a feature is rendered (because it is partly over the horizon), the bounding box will be different than it should, this will cause problems in scaling and translating. To overcome this limitation in my proposed solution, rotate the globe first, calculate the bounds, and scale to that factor. You can calculate the scale without actually updating the rotation of the paths on the globe, just update path and transition the drawn paths later.

解决方案实施

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

I've modified your code slightly, and I think it is cleaner ultimately, to implement the code:

我在此处存储当前的旋转度和缩放比例(以便我们可以从其转换为新值):

I store the current rotation and scale (so we can transition from this to the new values) here:

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

使用变量p来获取我们正在缩放的​​特征质心,我通过应用旋转来找出特征的边界框(但我实际上还没有旋转地图).使用bbox,我可以获得缩放到所选功能所需的比例尺:

Using your variable p to get the feature centroid we are zooming to, I figure out the bounding box of the feature with the applied rotation (but I don't actually rotate the map yet). With the bbox, I get the scale needed to zoom to the selected feature:

  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.

有关此处参数计算的更多信息,请查看此答案.

然后在当前刻度和旋转与目标(下一个)刻度和旋转之间进行补间:

Then I tween between the current scale and rotation and the target (next) scale and rotation:

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

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

Now we are transitioning both properties simultaneously:

柱塞

不仅如此,由于我们仅重绘路径,因此无需修改笔划以适应缩放g.

Not only that, since we are redrawing the paths only, we don't need to modify the stroke to account for scaling the g.

其他改进

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

You can get the centroid of the country/feature with just this:

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

更新后的柱塞 Bl.ock

您还可以使用缓动玩具-而不是仅使用线性插值-例如在 plunker bl.ock .这可能有助于在过渡期间使功能保持可见状态.

You can also toy with the easing - rather than just using a linear interpolation - such as in this plunker or bl.ock. This might help with keeping features in view during the transition.

替代实施

如果您确实希望将缩放作为g的一种操作而不是投影来进行,则可以实现此目的,但是缩放必须在旋转之后进行-因为此功能将在g,它将在svg中居中.请参见此 plunker .您可以在旋转之前计算bbox,但是如果同时进行两个转换(旋转和缩放),则缩放将使地球暂时偏离中心.

If you really want to keep the zoom as a manipulation of the g, rather than the projection, then you can achieve this, but the zoom has to be after the rotation - as the feature will then be centered in the g which will be centered in the svg. See this plunker. You could calculate the bbox prior to the rotation, but then the zoom will temporarily move the globe off center if making both transitions simultaneously (rotation and scale).

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

由于路径的某些部分被隐藏,因此实际路径可能会增加或减少点,完全出现或消失.过渡到最终状态可能并不代表随着旋转超出地球范围而发生的过渡(实际上肯定不会),像这样的路径的简单过渡可能会导致伪影,请参见

Because portions of the paths are hidden, the actual paths can gain or loose points, completely appear or disappear. The transition to its final state might not represent the transition as one rotates beyond the horizon of the globe (in fact it surely won't), a plain transition of paths like this can cause artifacts, see this plunker for a visual demonstration using a modification of your code. To address this, we use the tween method .attrTween.

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

Since the .attrTween method is setting the transition from one path to another, we need to scale at the same time. We cannot use:

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

缩放SVG与缩放投影

可以直接操纵路径/svg来平移和缩放许多圆柱投影,而无需更新投影.由于这不会重新计算带有geoPath的路径,因此要求较低.

Many cylindrical projections can be panned and zoomed by manipulating the paths/svg directly, without updating the projection. As this doesn't recalculate the paths with a geoPath, it should be less demanding.

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

This is not a luxury afforded by the orthographic or conical projections, depending on the circumstances involved. Since you are recalculating the paths anyways when updating the rotation, an update of the scale likely won't lead to extra delay - the geographic path generator needs to re-calculate and re-draw the paths considering both scale and rotation anyways.

这篇关于带有国家单击和缩放功能的d3世界地图几乎无法正常工作的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆