/* 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) ; } });