Reputation: 145
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"
}
Upvotes: 3
Views: 1521
Reputation: 38171
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>
Upvotes: 2