d3js 在地图上强制布局

d3js force layout on a map

我正在尝试在地图上放置力布局节点系统。我正在使用的 json 文件中的某些节点具有经度和纬度值。其他节点只需要连接但不需要地理参考。我想将具有 lon 和 lat 值的节点放置到位,而其他节点只需连接。

(我找到了我遵循的这个示例,但是没有 lon 和 lat 值的节点被放置在 svg 之外:https://bl.ocks.org/cmgiven/4cfa1a95f9b952622280a90138842b79)我还尝试过滤具有 lon 和 lat 值的节点,但仍然没有运气。

这是我目前得到的:

这是我的代码:

var w = 1340;
var h = 620;

//Zoom del mapa porque panamá es muy peque en la aproyección
var zoomOffset = 75000;
var wOffset = 103300;
var hOffset = 11500;
var escala = 0.50;

//Tipo de proyección del mapa escalado y transladado
//posicion del mapa
var projection = d3.geoMercator()
                   .translate([w + wOffset, h + hOffset])
                   .scale([zoomOffset])
;

//Los paths que toman el tipo de proyección
var path = d3.geoPath().projection(projection);

//El "centro" del pais
var center = projection([9.018, -79.500])
;

//Esquema de colores
var color = d3.scaleOrdinal(d3.schemeCategory20);

//Define la siulación de fuerza
var fuerza = d3.forceSimulation()
                .force("link", d3.forceLink()
                    .id(function(d){
                        return d.id;
                    })
                .distance(40))
                .force("charge", d3.forceManyBody().strength(-5))
                .force("center", d3.forceCenter(w/2, h/2))
;

//Leer datos de ambos json y llamar la funcion que dibuja todo
d3.queue()
    .defer(d3.json, 'proyectos_v5.json')
    .defer(d3.json, 'panama.json')
    .awaitAll(dibujar)
;

//Leer los datos y dibujar los assets y el mapa
function dibujar (error, data){
    if (error) {throw error}

    //Leer los datos de los json y ponerlos en arreglos distintos
    var graph = data[0];
    var features = data[1].features;

    //Printea los datos para verificar
    console.log(graph);
    console.log(features);

    //Le dice a la simulación cuales son los nodos y los links
    fuerza.nodes(graph.nodes);
    fuerza.force("link").links(graph.edges);

    //svg en donde dibujar
    var svg = d3.selectAll("body")
            .append("svg")
            .attr('width', w)
            .attr('height', h)
    ;

    //grupo en donde esten todos los objetos draggeables
    var mapa = svg.append("g")
        .attr('id', "mapa") //para luego dibujar los circulos y el mapa
        //dibuja el mapa, sin zoom porque no se necesita
        .selectAll("path")
        .data(features)
        .enter()
            .append("path")
            .attr("d", path)
            .style('fill', "#EDEDED")
    ;

        //crea las lineas con un svg y los datos de "edges"
    var lineas = svg.append('g')
        .selectAll("line")
        .data(graph.edges)
        .enter()
            .append("line")
            .style("stroke", "black")
            .style('stroke-width', 1)
    ;

    //crea los nodos de acuerdo a los nombres
    var nodos = svg.append('g')
        .selectAll("circle")
        .data(graph.nodes)
        .enter()
            .append("circle")
            .style('fill', function(d, i){
            return color(i);
            })
            .attr('r',5 )
            .call(d3.drag()
            .on("start", dragInicia)
            .on("drag", dragging)
            .on("end", dragTermina)) //llama la el metodo de nodos dragg y le dice que hacer en cada momento
    ;

    nodos.append("title")
        .text(function(d){
            return d.id;
    });

    //simulación y actualizacion de la posicion de los nodos en cada "tick"
    fuerza.on("tick", function (){
        lineas
            .attr('x1', function(d){
                return d.source.x;
            })
            .attr('y1', function(d){
                return d.source.y;
            })
            .attr('x2', function(d){
                return d.target.x;
            })
            .attr('y2', function(d){
                return d.target.y;
            })
        ;

        nodos
        .attr('cx', function(d){
            if(d.fixed== true"){
                return projection([d.lon, d.lat])[0];
            } else {
                return d.x;
            }
        })
        .attr('cy', function(d){
            if(d.fixed== "true"){
                return projection([d.lon, d.lat])[1];
            } else {
                return d.y;
            }
        })
        ;       
    })

    //crea las funciones para saber qué hacer en cada momento del dragging
    function dragInicia(d){
        if (!d3.event.active) fuerza.alphaTarget(0.3).restart();
        d.fx = d.x;
        d.fy = d.y;
    }

    function dragging(d){
        d.fx = d3.event.x;
        d.fy = d3.event.y;
    }

    function dragTermina(d){
        if(!d3.event.active) fuerza.alphaTarget(0);
        d.fx = null;
        d.fy = null;
    }
};

和一些 json:

    {
    "id": "Urbanicación La Marina",
    "lat": 9.0463,
    "lon": -79.4204,
    "año": 2019,
    "tipo": "proyecto",
    "area": "urbano",
    "extension": "",
    "estado": "",
    "publico": "",
    "fixed": "true"
  },
  {
    "id": "Zona Logística del aeropuerto de Tocumen",
    "lat": 9.0567,
    "lon": -79.4191,
    "año": 2019,
    "tipo": "proyecto",
    "area": "urbano",
    "extension": "",
    "estado": "",
    "publico": "",
    "fixed": "true"
  },
  {
    "id": "100 ciudades resilentes",
    "lat": "",
    "lon": "",
    "año": "",
    "tipo": "actor",
    "area": "",
    "extension": "",
    "estado": "",
    "publico": "",
    "fixed": "false"
  },
  {
    "id": "ACOBIR",
    "lat": "",
    "lon": "",
    "año": "",
    "tipo": "actor",
    "area": "",
    "extension": "",
    "estado": "",
    "publico": "",
    "fixed": "false"
  }

这应该不是问题。但是,您目前采用的方法会导致一些问题。例如:

.attr('cy', function(d){
  if(d.fixed== "true"){
    return projection([d.lon, d.lat])[1];
  } else {
    return d.y;
  }
})

这种方法可能会冻结代表节点的圆,但节点会继续在模拟中移动。这肯定会在更新链接时导致视觉问题 - 它们引用给定节点的模拟位置,而不是其视觉位置。这解释了一些奇怪的链接,这些链接没有连接到上图中一端的节点。

相反,让我们为每个具有纬度和经度的节点设置一个 fxfy 属性,以便模拟永远不会改变其位置,例如:

graph.nodes.forEach(function(d) {
    if(d.lon && d.lat) { 
        var p = projection([d.lon,d.lat]);
        d.fx = p[0];
        d.fy = p[1];
    }
})

d.fixed = true 修复了 v3 中的节点,但是 d.fxd.fy 修复了 v4 中的节点,参见 here

现在我们可以跳过if fixed == true勾选:

  .attr('cy', function(d){
      return d.y;  // d.y == d.fy if d.fy is set
   })

现在我们有固定的节点,但我们应该确保任何拖动或其他取消固定节点的功能不会取消固定或移动这些投影节点。例如使用拖动功能:

function dragTermina(d){
    if (!d.lon ||!d.lat) {  // don't move nodes with geographic data
        if(!d3.event.active) force.alphaTarget(0);
        d.fx = null;
        d.fy = null;
    }
}

此外,由于您的可视化是通过地理坐标固定在地面上的,因此我们不需要将节点居中:.force("center", d3.forceCenter(w/2, h/2))

结合一些虚构的数据,我得到:

 var width = 960;
 var height = 500;
 
 
 var graph = { nodes : [
  {id: "New York", lat: 40.706109,lon:-74.01194 },
  {id: "London", lat: 51.508070, lon: -0.126432 },
  {id: "Montevideo", lat: -34.901776, lon: -56.163983 },
  {id: "London-NewYork1" },
  {id: "London-NewYork2" },
  {id: "London-NewYork3" },
  {id: "Montevideo-London"}
   ],
      links : [
  { source: "New York", target: "London-NewYork1" },
  { source: "New York", target: "London-NewYork2" },
  { source: "New York", target: "London-NewYork3" },
  { source: "London-NewYork1", target: "London" },
  { source: "London-NewYork2", target: "London" },
  { source: "London-NewYork3", target: "London" } , 
  { source: "London", target: "Montevideo-London" },
  { source: "Montevideo-London", target: "Montevideo" }
   ]
 }
 
 
    var force = d3.forceSimulation()
        .force("link", d3.forceLink()
            .id(function(d){
                return d.id;
        })
        .distance(10))
        .force("charge", d3.forceManyBody().strength(-200));

  
 var svg = d3.select("body")
   .append("svg")
   .attr("width",width)
   .attr("height",height);
   
 var projection = d3.geoMercator()
   .center([0,10])
   .translate([width/2,height/2]);
   
 var path = d3.geoPath().projection(projection);
 
 var g = svg.append("g");
 
    d3.json("https://unpkg.com/world-atlas@1/world/110m.json").then(function(data) {
        g.selectAll("path")
          .data(topojson.object(data, data.objects.countries).geometries)
          .enter()
          .append("path")
          .attr("d", path)
    .attr("fill","lightgreen");
 
  var links = svg.append('g')
    .selectAll("line")
    .data(graph.links)
    .enter()
       .append("line")
    .attr("stroke-width", 2)
    .attr("stroke", "black");


     var nodes = svg.append('g')
          .selectAll("circle")
          .data(graph.nodes)
          .enter()
            .append("circle")
            .attr('r',5 )          
   .call(d3.drag()
            .on("start", dragInicia)
            .on("drag", dragging)
            .on("end", dragTermina));
   

 
    force.nodes(graph.nodes);
    force.force("link").links(graph.links);
 
 graph.nodes.forEach(function(d) {
  if(d.lon && d.lat) { 
   var p = projection([d.lon,d.lat]);
   d.fx = p[0];
   d.fy = p[1];
  }
 })
 
 //simulación y actualizacion de la posicion de los nodos en cada "tick"
    force.on("tick", function (){
        links
            .attr('x1', function(d){
                return d.source.x;
            })
            .attr('y1', function(d){
                return d.source.y;
            })
            .attr('x2', function(d){
                return d.target.x;
            })
            .attr('y2', function(d){
                return d.target.y;
            })
        ;

        nodes
        .attr('cx', function(d){
                return d.x;
        })
        .attr('cy', function(d){
                 return d.y;
        })
        ;       
    })
 
 
    function dragInicia(d){
        if (!d.lon || !d.lat) {
   if (!d3.event.active) force.alphaTarget(0.3).restart();
   d.fx = d.x;
   d.fy = d.y;
  }
    }

    function dragging(d){
  if (!d.lon || !d.lat) {
   d.fx = d3.event.x;
   d.fy = d3.event.y;
  }
    }

    function dragTermina(d){
        if (!d.lon ||!d.lat) {
   if(!d3.event.active) force.alphaTarget(0);
   d.fx = null;
   d.fy = null;
  }
    }
 
    
    });
  
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="https://d3js.org/topojson.v0.min.js"></script>