在d3地理图上的固定位置附加链接图形

时间:2018-06-07 00:51:54

标签: javascript d3.js svg topojson

我正在尝试使用d3 geo将一个链接图形树附加到旋转的地球上。我已经调整了here (sans drag and drop)here的旋转地球仪演示,并设法添加了我发现here的节点/链接的强制定向布局。

This是我到目前为止的一个小提琴。力图出现在南极附近,为跳跃链接道歉我认为这只是一个css问题,因为它在我的模拟中正确显示(我现在已经离开了样式表)。

由于我希望节点固定在特定的纬度/经度,我想完全摆脱力模拟。但是,所有在保留节点和链接的同时删除它的尝试都会导致它们完全消失。我也在努力修复他们的位置并将节点覆盖在地图图形上(你可以看到节点落在陆地后面)

总结一下,我想:

  • 删除强制布局但保留节点/链接
  • 在旋转期间修复特定纬度/经度的节点
  • 在地理地图要素之上叠加节点/链接

非常感谢对这些要点的任何帮助。

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));
  }
})();

1 个答案:

答案 0 :(得分:1)

假设你使用了力量以便你可以添加点和链接,让我们退一步,让任何与力相关的东西,没有节点和没有链接。在这种情况下,不需要力布局。让我们从你的地球开始动画和拖动(并在我们到达时移动到d3v5):

&#13;
&#13;
  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);
    });

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

除了转到v5之外,我做了一些小修改以优化片段视图(例如:大小)或简洁(例如:硬编码剪辑角度),但代码实质上相同减去力/节点/链接

我认为检查你的第一个要求的一半&#34;删除强制布局但保留节点/链接&#34;。这也为我们提供了更简单的代码,以满足其他要求。

好的,现在我们有了一个基本地图,我们可以添加点,然后我们可以添加行。但是,让我们分解它,首先我们添加点,然后我们添加链接。

添加地理配准点

让我们采用数据格式,我将使用我们想要显示的点/节点字典:

  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点看起来像这样(就像你在小提琴中创建的那样):

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

所以,我们可以得到一个geojson点数组:

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

d3.entries在输入对象时返回一个数组。数组中的每个项目代表原始对象{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);

最后,由于我删除了您在计时器功能中应用的过滤器,因此每次轮换时所有路径都会一起更新,这样可以简化代码。

好的,总而言之,这给了我们:

&#13;
&#13;
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);
    });

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

我们可以添加圆圈,但这会引入一个新问题:我们需要通过查看当前旋转中心和点之间的角度是否检查每个圆圈是否应该在地球的每个运动中都可见。大于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线看起来像(就像你在小提琴中创建的那样):

{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")

与积分一样,每个计时器滴答都会更新:

&#13;
&#13;
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);
    });

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

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