Добавить связанную графику в фиксированные местоположения на географическом графике d3 - PullRequest
0 голосов
/ 07 июня 2018

Я пытаюсь добавить дерево связанной графики к вращающемуся глобусу, используя d3 geo.Я адаптировал демонстрации вращающегося земного шара, видимые здесь (без перетаскивания) и здесь , и сумел добавить принудительно ориентированную компоновку узлов / ссылок, которые я нашел здесь .

Это - скрипка того, что я имею до сих пор.График сил появляется около южного полюса, извиняюсь за прыгающие ссылки. Я думаю, что это просто проблема 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 голосов
/ 10 июня 2018

Предполагая, что вы использовали силу, чтобы вы могли добавлять точки и ссылки, давайте немного отступим назад, давайте отбросим все, что связано с силой, без узлов и без ссылок.Расположение сил не является необходимым ни в этой ситуации.Давайте начнем с вашего земного шара с анимации и перетаскивания (и перейдем к 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, я сделал несколько небольших изменений, чтобы оптимизировать просмотр фрагмента (например, размер) или краткость (например, жесткое кодирование клипа).угол), но код по сути тот же минус сила / узлы / ссылки

Я думаю, что проверяет половину вашего первого требования «удалить макет силы, но сохранить узлы / ссылки».Это также дает нам более простой код, с которым можно работать для удовлетворения остальных требований.

Хорошо, теперь, когда у нас есть базовая карта, мы можем добавлять точки, а затем мы можем добавлять линии.Но давайте разберемся с этим, сначала мы добавим точки, затем добавим ссылки.

Добавление точек с географической привязкой

Давайте возьмем формат данных, я пойдус помощью словаря точек / узлов, которые мы хотим показать:

  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, так как это автоматически обрежет те точки, которыенаходятся на противоположной стороне земного шара.Точка геойсона выглядит следующим образом (как вы создали в своей скрипке):

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

Итак, мы можем получить массив точек геойсона с:

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

d3.entries возвращает массив при кормлении объекта.Каждый элемент в массиве представляет пару значений ключа исходного объекта {key: key, value: value}, для получения дополнительной информации см. документы

Теперь мы можем добавить наши точки геоджона в 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 градусов.Поэтому для простоты я использовал геоджон и использовал проекцию и путь, чтобы скрыть эти точки на противоположной стороне земного шара.

Пути

Причина, по которой я предпочитаю приведенный выше формат для точек, заключается в том, что он дает нам удобочитаемый список ссылок:

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

Теперь, как и выше, нам нужно преобразовать его в геойсон.Линия геойсона выглядит так (как вы создали в скрипте):

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

Итак, мы можем создать массив линий геоджона с:

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 тоже многоуровневые.

...