d3js 在地图上强制布局

Posted

技术标签:

【中文标题】d3js 在地图上强制布局【英文标题】:d3js force layout on a map 【发布时间】:2018-12-26 22:36:42 【问题描述】:

我正在尝试在地图上放置一个强制布局节点系统。一些节点在我使用的 json 文件中有 lon 和 lat 值。其他节点只需要连接但不需要地理参考。我想将具有 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"
  

【问题讨论】:

你能分享几行数据吗(包括有和没有纬度/经度的点)? 是的,我用一段 json 更新了问题! 单独在某个已知节点周围画一个圆圈,然后将相关节点放在圆圈上;没有坐标,难怪他们是禁区…… 我可以通过在 .on("click" 函数上写这个来解决部分问题:graph.nodes.forEach(function (d) if(d.fixed=="true") var pos = projection([d.lon, d.lat]) dx = pos[0] dy = pos[1] else dx = dx; dy = dy; ) 【参考方案1】:

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

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

【讨论】:

你是救生员!它完美地工作。非常感谢。唯一的问题是未连接的节点会离开屏幕,但我认为这是一个强制问题。无论如何,再次感谢您。 @PierrePuentes,很高兴我能帮上忙。是的,不受链接约束的节点可能会逃逸。您可以尝试再次应用 d3.forceCenter,但这可能无法达到预期的效果,您可以尝试使用positioning force。或者,您可以限制节点,所有这些都应该在堆栈溢出的某个地方有一些问题/答案。另一种选择,我认为我没有在其他地方看到过,将定位力仅应用于断开的节点。 我应用了 forceX 和 force Y 去模拟和无线电,所以模拟有边界。它工作得很好:)

以上是关于d3js 在地图上强制布局的主要内容,如果未能解决你的问题,请参考以下文章

无法将标签添加到 d3js 强制布局网络可视化

D3js:自动放置标签以避免重叠? (强制排斥)

D3强制布局:如何设置每个节点的大小?

消息“d3.js TypeError: n is undefined”(对于具有强制布局的 D3 世界地图)

在d3js中寻找具有特定投影的世界地图

如何使用按钮在 ScrollView Controller 中强制“滚动”?