在 d3 geo plot 的固定位置附加链接图形

Append linked graphics at fixed locations on d3 geo plot

我正在尝试使用 d3 geo 将链接图形树附加到旋转的地球上。我改编了看到的旋转地球演示 here (sans drag and drop) and here, and have managed to append a force directed layout of nodes/links that I found here

This 是我目前所拥有的 fiddle。力图出现在南极附近,为跳跃链接道歉 我认为这只是一个 css 问题,因为它在我的模拟中正确显示(我暂时离开了样式表)。

因为我希望节点固定在特定的 latitude/longitudes,所以我想完全摆脱力模拟。然而,所有在保留节点和链接的同时删除它的尝试都会导致它们完全消失。我也一直在努力修复它们的位置并将节点 覆盖在 地图图形上(你可以看到节点在陆地后面)

总而言之,我想:

如能就以上任何一点提供帮助,我们将不胜感激。

HTML

<!doctype html>
<html lang="en">
<body>
<script src="//d3js.org/d3.v3.min.js"></script>
<script src="//d3js.org/topojson.v1.min.js"></script>
<div id="vis"></div>
</body>
</html>

脚本

(function (){
  var config = {
    "projection": "Orthographic",
    "clip": true, "friction": 1,
    "linkStrength": 1,
    "linkDistance": 20,
    "charge": 50,
    "gravity": 1,
    "theta": .8 };

  var width = window.innerWidth,
      height = window.innerHeight - 5,
      fill = d3.scale.category20(),
      feature,
      origin = [0, -90],
      velocity = [0.01, 0],
      t0 = Date.now(),
      nodes = [{x: width/2, y: height/2}],
      links = [];

  var projection = d3.geo.orthographic()
      .scale(height/2)
      .translate([(width/2)-125, height/2])
      .clipAngle(config.clip ? 90 : null)

  var path = d3.geo.path()
      .projection(projection);

  var force = d3.layout.force()
     .linkDistance(config.linkDistance)
     .linkStrength(config.linkStrength)
     .gravity(config.gravity)
     .size([width, height])
     .charge(-config.charge);

  var svg = d3.select("#vis").append("svg")
      .attr("width", width)
      .attr("height", height)
      .call(d3.behavior.drag()
        .origin(function() { var r = projection.rotate(); return {x: 2 * r[0], y: -2 * r[1]}; })
        .on("drag", function() { force.start(); var r = [d3.event.x / 2, -d3.event.y / 2, projection.rotate()[2]]; t0 = Date.now(); origin = r; projection.rotate(r); }))

  for(x=0;x<20;x++){
    source = nodes[~~(Math.random() * nodes.length)]
    target = {x: source.x + Math.random(), y: source.y + Math.random(), group: Math.random()}
    links.push({source: source, target: target})
    nodes.push(target)
  }

  var node = svg.selectAll("path.node")
      .data(nodes)
      .enter().append("path").attr("class", "node")
      .style("fill", function(d) { return fill(d.group); })
      .style("stroke", function(d) { return d3.rgb(fill(d.group)).darker(); })
      .call(force.drag);
  console.log(node)
  var link = svg.selectAll("path.link")
      .data(links)
      .enter().append("path").attr("class", "link")

  force
     .nodes(nodes)
     .links(links)
     .on("tick", tick)
     .start();

  var url = "https://raw.githubusercontent.com/d3/d3.github.com/master/world-110m.v1.json";
  d3.json(url, function(error, topo) {
    if (error) throw error;

    var land = topojson.feature(topo, topo.objects.land);

    svg.append("path")
     .datum(land)
     .attr("class", "land")
     .attr("d", path)

    d3.timer(function() {
      force.start();
      var dt = Date.now() - t0;
      projection.rotate([velocity[0] * dt + origin[0], velocity[1] * dt + origin[1]]);
      svg.selectAll("path")
        .filter(function(d) {
          return d.type == "FeatureCollection";})
        .attr("d", path);
    });
  });

  function tick() {
    node.attr("d", function(d) { var p = path({"type":"Feature","geometry":{"type":"Point","coordinates":[d.x, d.y]}}); return p ? p : 'M 0 0' });
    link.attr("d", function(d) { var p = path({"type":"Feature","geometry":{"type":"LineString","coordinates":[[d.source.x, d.source.y],[d.target.x, d.target.y]]}}); return p ? p : 'M 0 0' });
  }

  function clip(d) {
    return path(circle.clip(d));
  }
})();

假设您使用力来添加点和链接,让我们退后一步,让我们放弃任何与力相关的东西,没有节点也没有链接。在这种情况下,两者都不需要部队布局。让我们从带有动画和拖动的地球开始(并在我们进行时移动到 d3v5):

  var width = 500,
      height = 500,
   t0 = Date.now(),
   velocity = [0.01, 0],
   origin = [0, -45];
  
  var projection = d3.geoOrthographic()
      .scale(height/2.1)
      .translate([width/2, height/2])
      .clipAngle(90)

  var path = d3.geoPath()
      .projection(projection);
  
  var svg = d3.select("body").append("svg")
      .attr("width", width)
      .attr("height", height)
      .call(d3.drag()
      .subject(function() { var r = projection.rotate(); return {x: 2 * r[0], y: -2 * r[1]}; })
      .on("drag", function() { var r = [d3.event.x / 2, -d3.event.y / 2, projection.rotate()[2]]; t0 = Date.now(); origin = r; projection.rotate(r); }))

  d3.json("https://unpkg.com/world-atlas@1/world/110m.json").then(function(topo) {
    var land = topojson.feature(topo, topo.objects.land);
    
    svg.append("path")
     .datum(land)
     .attr("class", "land")
     .attr("d", path);

    d3.timer(function() {
      var dt = Date.now() - t0;
      projection.rotate([velocity[0] * dt + origin[0], velocity[1] * dt + origin[1]]);
      svg.selectAll("path")
         .attr("d", path);
    });

 
  });
<script type="text/javascript" src="https://d3js.org/d3.v5.js"></script>
<script src="https://d3js.org/topojson.v1.min.js"></script>

除了移动到 v5 之外,我还做了一些小修改以优化代码片段视图(例如:大小)或简洁性(例如:对剪辑角度进行硬编码),但代码基本相同减去force/nodes/links

我认为这满足了您第一个要求的一半 "remove force layout but keep nodes/links"。这也为我们提供了更简单的代码来满足其余要求。

好了,底图有了,我们就可以加点了,然后就可以加线了。但是,让我们分解一下,首先我们添加点,然后我们添加链接。

添加地理参考点

让我们采用一种数据格式,我将使用我们想要显示的 points/nodes 的字典:

  var points = {
     "Vancouver":[-123,49.25],
     "Tokyo":[139.73,35.68],
     "Honolulu":[-157.86,21.3],
     "London":[0,50.5],
     "Kampala":[32.58,0.3]
  }

由于我们处理的是正交投影,明智的做法是使用带有 d3.geoPath 的 geojson 点,因为这会自动裁剪地球远端的那些点。 geojson 点看起来像这样(正如您在 fiddle 中创建的):

{ type: "Point", geometry: [long,lat] }

因此,我们可以通过以下方式获得一组 geojson 点:

var geojsonPoints = d3.entries(points).map(function(d) {
    return {type: "Point", coordinates: d.value}
})

d3.entries returns 输入对象时的数组。数组中的每一项代表原始对象的键值对 {key: key, value: value},请参阅 the docs 了解更多信息

现在我们可以将我们的 geojson 点添加到 svg 中:

svg.selectAll()
  .data(geojsonPoints)
  .enter()
  .append("path")
  .attr("d",path)
  .attr("fill","white")
  .attr("stroke-width",2)
  .attr("stroke","steelblue");

由于这些是点,我们需要设置路径的点半径:

var path = d3.geoPath()
  .projection(projection)
  .pointRadius(5);

最后,由于我删除了您在定时器函数中应用的过滤器,所有路径将在每次旋转时一起更新,这稍微简化了代码。

好的,总的来说,这给了我们:

var width = 500,
    height = 500,
   t0 = Date.now(),
   velocity = [0.01, 0],
   origin = [0, -45];
    
  var points = {
   "Vancouver":[-123,49.25],
  "Tokyo":[139.73,35.68],
  "Honolulu":[-157.86,21.3],
  "London":[0,50.5],
  "Kampala":[32.58,0.3]
  }    

  
  var projection = d3.geoOrthographic()
      .scale(height/2.1)
      .translate([width/2, height/2])
      .clipAngle(90)

  var path = d3.geoPath()
      .projection(projection)
      .pointRadius(5);
  
  var svg = d3.select("body").append("svg")
      .attr("width", width)
      .attr("height", height)
      .call(d3.drag()
      .subject(function() { var r = projection.rotate(); return {x: 2 * r[0], y: -2 * r[1]}; })
      .on("drag", function() { var r = [d3.event.x / 2, -d3.event.y / 2, projection.rotate()[2]]; t0 = Date.now(); origin = r; projection.rotate(r); }))

  d3.json("https://unpkg.com/world-atlas@1/world/110m.json").then(function(topo) {
    var land = topojson.feature(topo, topo.objects.land);
    
    svg.append("path")
     .datum(land)
     .attr("class", "land")
     .attr("d", path);
     
   var geojsonPoints = d3.entries(points).map(function(d) {
    return {type: "Point", coordinates: d.value}
   });
    
   svg.selectAll(null)
   .data(geojsonPoints)
   .enter()
   .append("path")
   .attr("d",path)
   .attr("fill","white")
   .attr("stroke-width",2)
   .attr("stroke","steelblue");
   

    d3.timer(function() {
      var dt = Date.now() - t0;
      projection.rotate([velocity[0] * dt + origin[0], velocity[1] * dt + origin[1]]);
      svg.selectAll("path")
         .attr("d", path);
    });

 
  });
<script type="text/javascript" src="https://d3js.org/d3.v5.js"></script>
<script src="https://d3js.org/topojson.v1.min.js"></script>

我们可以附加圆圈,但这引入了一个新问题:我们需要通过查看当前旋转中心之间的角度来检查每个圆圈是否应该在地球的每次移动中可见并且该点大于90度。因此,为方便起见,我使用了 geojson 并依靠投影和路径来隐藏地球远端的那些点。

路径

我更喜欢上述格式的原因是它允许我们提供一个人类可读的链接列表:

var links = [
  { source: "Vancouver",    target: "Tokyo" },
  { source: "Tokyo",        target: "Honolulu" },
  { source: "Honolulu",     target: "Vancouver" },
  { source: "Tokyo",        target: "London" },
  { source: "London",       target: "Kampala" }
]

现在,如上所述,我们需要将其转换为 geojson。 geojson 行看起来像(如您在 fiddle 中创建的那样):

{type:"LineString", coordinates: [[long,lat],[long,lat], ... ]

因此,我们可以创建一个 geojson 行数组:

var geojsonLinks = links.map(function(d) {
    return {type: "LineString", coordinates: [points[d.source],points[d.target]] }
})

这利用了点的字典数据结构。

现在您可以像这样附加它们:

svg.selectAll(null)
  .data(geojsonLinks)
  .enter()
  .append("path")
  .attr("d", path)
  .attr("stroke-width", 2)
  .attr("stroke", "steelblue")
  .attr("fill","none")

与积分一样,它们在每个计时器滴答声中更新:

var width = 500,
    height = 500,
   t0 = Date.now(),
   velocity = [0.01, 0],
   origin = [0, -45];
    
  var points = {
   "Vancouver":[-123,49.25],
  "Tokyo":[139.73,35.68],
  "Honolulu":[-157.86,21.3],
  "London":[0,50.5],
  "Kampala":[32.58,0.3]
  }    
  
  var links = [
 { source: "Vancouver",target: "Tokyo" },
    { source: "Tokyo",   target: "Honolulu" },
 { source: "Honolulu", target: "Vancouver" },
 { source: "Tokyo",   target: "London" },
 { source: "London",  target: "Kampala" }
  ]  

  
  var projection = d3.geoOrthographic()
      .scale(height/2.1)
      .translate([width/2, height/2])
      .clipAngle(90)

  var path = d3.geoPath()
      .projection(projection)
      .pointRadius(5);
  
  var svg = d3.select("body").append("svg")
      .attr("width", width)
      .attr("height", height)
      .call(d3.drag()
      .subject(function() { var r = projection.rotate(); return {x: 2 * r[0], y: -2 * r[1]}; })
      .on("drag", function() { var r = [d3.event.x / 2, -d3.event.y / 2, projection.rotate()[2]]; t0 = Date.now(); origin = r; projection.rotate(r); }))

  d3.json("https://unpkg.com/world-atlas@1/world/110m.json").then(function(topo) {
    var land = topojson.feature(topo, topo.objects.land);
    
    svg.append("path")
     .datum(land)
     .attr("class", "land")
     .attr("d", path);
     
   var geojsonPoints = d3.entries(points).map(function(d) {
    return {type: "Point", coordinates: d.value}
   });
    
   var geojsonLinks = links.map(function(d) {
    return {type: "LineString", coordinates: [points[d.source],points[d.target]] }
   })
    
    svg.selectAll(null)
   .data(geojsonLinks)
   .enter()
   .append("path")
   .attr("d",path)
   .attr("fill","none")
   .attr("stroke-width",2)
   .attr("stroke","steelblue");
    
   svg.selectAll(null)
   .data(geojsonPoints)
   .enter()
   .append("path")
   .attr("d",path)
   .attr("fill","white")
   .attr("stroke-width",2)
   .attr("stroke","steelblue");
   

    d3.timer(function() {
      var dt = Date.now() - t0;
      projection.rotate([velocity[0] * dt + origin[0], velocity[1] * dt + origin[1]]);
      svg.selectAll("path")
         .attr("d", path);
    });

 
  });
<script type="text/javascript" src="https://d3js.org/d3.v5.js"></script>
<script src="https://d3js.org/topojson.v1.min.js"></script>

现在请记住,svg 路径的分层是按照它们附加的顺序完成的 - 第一个附加的将在第二个附加的后面。因此,如果您希望链接位于点之上,只需交换它们的附加顺序即可。您也可以使用 g 组来管理排序 - g 也是分层的。