简体   繁体   中英

d3js force layout on a map

I'm trying to place a force layout node system on a map. SOME of the nodes have lon and lat values in the json file I'm using. Other nodes only need to be connected but not georeferenced. I would like to place in position the nodes which have lon and lat values and the others simply to be connected.

(I found this example which I followed but the nodes without lon and lat values are placed outside the svg: https://bl.ocks.org/cmgiven/4cfa1a95f9b952622280a90138842b79 ) I also tried to filter the nodes with lon and lat values but still no luck.

This is what I'm getting currently:

在此输入图像描述

Here's my code:

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

and some of the 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"
  }

This shouldn't be a problem. However, the approach you have so far will cause some problems. For example:

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

This approach might freeze the circle representing the node, but the node continues to move within the simulation. This will certainly cause visual problems when updating the links - they reference the simulation's position for a given node, not its visual position. This explains some of the odd links that aren't connected to nodes at one end in your image above.

Instead, lets set an fx and fy property for each node that has a latitude and longitude so that the simulation never changes its position, something like:

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 fixes nodes in v3, but d.fx and d.fy fix nodes in v4, see here

Now we can skip the if fixed == true check in the tick:

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

Now we have nodes that are fixed, but we should make sure that any dragging or other function which unfixes nodes doesn't unfix or move these projected nodes. For example with the drag functions:

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

Also, since your visualization is anchored to the ground with geographic coordinates, we don't need to center the nodes with: .force("center", d3.forceCenter(w/2, h/2)) .

Putting that together, with some made up data, I get:

  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 dx; }) .attr('cy', function(d){ return dy; }) ; }) function dragInicia(d){ if (!d.lon || !d.lat) { if (!d3.event.active) force.alphaTarget(0.3).restart(); d.fx = dx; d.fy = dy; } } 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> 

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM