/* Credits to:
- David Braun for the solution on How to arrange the nodes of a force layout in a circle:
https://stackoverflow.com/questions/22439832/d3-js-how-do-i-arrange-nodes-of-a-force-layout-to-be-on-a-circle
- altocumulus, Hansi Schmidt and Tim B for the solution on how to highlight neighbor nodes in the force layout:
https://stackoverflow.com/questions/39964582/highlight-neighbor-in-the-force-layout
- rioV8 for the solution on how to place the text of each node correctly around the circle:
https://stackoverflow.com/questions/51529999/nodes-title-around-a-circle
*/
// width, height, radio y radio de los circulos
var w = 1920;
var h = 950;
var RAD2DEG = 180.0/Math.PI;
var gCircleRadius = 230;
var gSmallCircleRadius = 7;
// declarar la fuerza y la union de los nodos por id, ahora sin charge ni centro porque no se van a correr
var fuerza = d3.forceSimulation()
.force("link", d3.forceLink().id(function(d){
return d.id;
}))
;
// insertar los datos y ponerlos en consola
d3.json("actores_v5.json", function(error, data){
if (error) throw error;
//verificar los datos
console.log("Número de Nodos: " + data.nodes.length);
console.log(data.nodes);
console.log("Número de Links: " + data.edges.length);
console.log(data.edges);
//svg en donde dibujar
var svg = d3.selectAll("body")
.append("svg")
.attr('width', w)
.attr('height', h)
;
var tooltip = d3.select("body").append("div")
.attr("class", "tooltip")
. style("opacity", 0);
//circulo invisible para dibujar los nodos
var dim = w/2;
var circle = svg.append("circle")
.attr('cx', w/2)
.attr('cy', h/2)
.attr('r', gCircleRadius)
.style("fill", "#ffffff")
;
//crea las lineas con un svg y los datos de "edges"
var lineas = svg.append('g')
.selectAll("line")
.data(data.edges)
.enter()
.append("path")
.attr("class", function(d) {
return "link " + d.relacion;
})
.attr('stroke-width', function(d){
return d.vinculo * 1.5;
})
;
//crea los nodos de acuerdo a los nombres
var nodos = svg.append("g")
.selectAll("circle")
.data(data.nodes)
.enter()
.append("circle")
.attr('class', function (d) {
return "nodos" + (d.categoria ? " " + d.categoria: "");
})
.on("mouseover.1", mouseEncima)
.on("mouseover.2", function(d){
tooltip
.html(function(){
return "
" + d.id +
"
" + "
" +
d.categoria +
"
" + "
" +
"AQUI VA LA DESCRIP: "
;
})
.style("top", svg.node().parentNode.offsetTop + 50 + "px")
.style("left", svg.node().parentNode.offsetLeft + 50 + "px")
.style("opacity", .9);
})
.on("mouseout.1", mouseAfuera)
.on("mouseout.2", function(){
tooltip.style("opacity", 0)
})
.attr('r', 5)
.style("opacity", 1)
;
//Crea el texto de cada nodo
var text = svg.append("g").selectAll("text")
.data(data.nodes)
.attr('class', "text")
.enter()
.append("text")
.attr("x", 8)
.attr("y", ".31em")
.text(function(d) {
return d.id;
})
.on("mouseover", mouseEncima)
.on("mouseout", mouseAfuera)
;
//define los nodos y los links de la simulacion
fuerza.nodes(data.nodes);
fuerza.force("link").links(data.edges);
// calcula los espacios de los circulos en el circulo
var circleCoord = function(node, index, num_nodes){
var circumference = circle.node().getTotalLength();
var pointAtLength = function(l){
return circle.node().getPointAtLength(l)};
var sectionLength = (circumference)/num_nodes;
var position = sectionLength*index+sectionLength/2;
return pointAtLength(circumference-position);
}
// define la posicion de los nodos segun el calculo anterior
data.nodes.forEach(function(d, i) {
var coord = circleCoord(d, i, data.nodes.length);
d.fx = coord.x;
d.fy = coord.y;
});
//Calcula el radio y la posicion del texto
var radiusScale = (gCircleRadius+1.5*gSmallCircleRadius) / gCircleRadius;
var textPosition = d => {
var circX = w*0.5, circY = h*0.5;
var dX = (d.fx - circX)*radiusScale, dY = (d.fy - circY)*radiusScale;
return { x: circX + dX, y: circY + dY};
};
//simulación y actualizacion de la posicion de los nodos en cada "tick"
fuerza.on("tick", function(){
lineas.attr("d", function(d) {
var dx = d.target.x - d.source.x,
dy = d.target.y - d.source.y,
dr = Math.sqrt(dx * dx + dy * dy);
return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y;
});
nodos.attr("cx", function(d){
return d.x = d.fx;
})
.attr('cy', function(d){
return dy = d.fy;
})
;
//Transforma y rota la posicion del texto segun el radio y su X y Y
text.each( (d, i, nodes) => {
var textPos = textPosition(d);
var angle = Math.atan2(textPos.y-h*0.5, textPos.x-w*0.5)*RAD2DEG;
d3.select(nodes[i])
.attr('x', textPos.x)
.attr('y', textPos.y)
.attr('dy', (angle>90 || angle<-90) ? "0.3em" : "0.4em")
.style("text-anchor", (angle>90 || angle<-90) ? "end" : "start")
.attr("transform", `rotate(${ (angle>90 || angle<-90) ? angle+180 : angle}, ${textPos.x}, ${textPos.y})` )
;
});
});
//Create an array logging what is connected to what
var linkedByIndex = {};
for (i = 0; i < data.nodes.length; i++) {
linkedByIndex[i + "," + i] = 1;
}
;
data.edges.forEach(function (d) {
linkedByIndex[d.source.index + "," + d.target.index] = 1;
});
//This function looks up whether a pair are neighbours
function neighboring(a, b) {
return linkedByIndex[a.index + "," + b.index];
}
//Cuando se pasa encima de los nodos o del texto
function mouseEncima() {
//Reduce la opacidad de los nodos vecinos, sus links y sus textos
d = d3.select(this).node().__data__;
nodos
.transition()
.style("opacity", function (o) {
return neighboring(d, o) | neighboring(o, d) ? 1 : 0.1;
})
.attr('r', function(o){
return neighboring(d, o) | neighboring(o, d) ? 7 : 5;
})
;
lineas
.transition()
.style("stroke-opacity", function (o) {
return d.index==o.source.index | d.index==o.target.index ? 0.5 : 0;
})
;
text
.transition()
.style("opacity", function (o) {
return neighboring(d, o) | neighboring(o, d) ? 1 : 0.1;
})
;
}
//Cuando el mouse está afuera de los nodos
function mouseAfuera() {
//devuelve la opacidad de los nodos, sus links y sus textos
nodos
.transition()
.style("opacity", 1)
.attr('r', 5)
;
// y las lineas a 0
lineas
.transition()
.style("stroke-opacity", 0.05)
;
text
.transition()
.style("opacity", 0.5)
;
}
});