Skip to content Skip to sidebar Skip to footer

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 georefe

Solution 1:

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

Post a Comment for "D3js Force Layout On A Map"