1

I created a force layout around a circle in d3js (D3.js how do I arrange nodes of a force layout to be on a circle) and now I want to put the labels around like this:

enter image description here

but right now this is what I'm getting: You can take a look here too: https://bl.ocks.org/pierreee1/07eb3b07ba876011419168d60c587090

enter image description here

What could I do to get the results I want? I searched some questions but none of the solutions have helped me.

Here's my code:

// width y height
var w = 1350;
var h = 600;

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

    //circulo invisible para dibujar los nodos
    // it's actually two arcs so we can use the getPointAtLength() and getTotalLength() methods
    var dim = w/2;
    var circle = svg.append("circle")
        //.attr("d", "M 40, "+(dim/2+40)+" a "+dim/2+","+dim/2+" 0 1,0 "+dim+",0 a "+dim/2+","+dim/2+" 0 1,0 "+dim*-1+",0")
        .attr('cx', w/2)
        .attr('cy', h/2)
        .attr('r', 250)
        .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.tipo; 
            })          
    ;

    //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){
                if (d.categoria == "gobierno"){
                    return "nodos " + d.categoria;
                }   
                if (d.categoria == "patrimonio"){
                    return "nodos " + d.categoria;
                }
                if (d.categoria == "planeacion"){
                    return "nodos " + d.categoria;
                }
                if (d.categoria == "ong"){
                    return "nodos " + d.categoria;
                }
                if (d.categoria == "gremios"){
                    return "nodos " + d.categoria;
                }
                if (d.categoria == "academia"){
                    return "nodos " + d.categoria;
                }
                if (d.categoria == "comunidad"){
                    return "nodos " + d.categoria;
                }
                if (d.categoria == "privado"){
                    return "nodos " + d.categoria;
                }
                if (d.categoria == "medios"){
                    return "nodos " + d.categoria;
                }
                if (d.categoria == "otros"){
                    return "nodos " + d.categoria;
                }
            })
            .on("mouseover", mouseEncima)
            .on("mouseout", mouseAfuera)
            .attr('r', 5)
    ;

    nodos
        .filter(function(d){
            return d.categoria == "gobierno"
                || d.categoria == "patrimonio"
                || d.categoria == "planeacion"
                || d.categoria == "ong"
                || d.categoria == "gremios"
                || d.categoria == "academia" 
                || d.categoria == "comunidad"
                || d.categoria == "privado"
                || d.categoria == "medios"
                || d.categoria == "otros"
            ;
        })
        .style("opacity", 1)
    ;

    //titulos de los nodos
    nodos.append("title")
        .text(function(d){
            return d.id;
        })
    ;

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

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

    for (i = 0; i < data.nodes.length; i++) {
        var angle = (i / (data.nodes.length / 2)) * Math.PI;
        //data.nodes.push({ 'angle': angle });
    }   

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

        text.attr("x", function(d){
                return dx = d.fx;
            })
            .attr('y', function(d){
                return dy = d.fy;
            })
            .style("text-anchor", "start")
    });

    //saber si las conexiones se ven o no
    var toggle = 0;

    //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];
    }

    function mouseEncima() {

        if (toggle == 0) {

            //Reduce the opacity of all but the neighbouring nodes
            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) ? 10 : 5;
                })
            ;  

            lineas
                .transition()
                .style("stroke-opacity", function (o) {
                    return d.index==o.source.index | d.index==o.target.index ? 1 : 0.1;
                })
            ;

            // text
            //  .transition()
            //  .style("opacity", function (o) {
            //      return neighboring(d, o) | neighboring(o, d) ? 1 : 0.1;
            //  })
            // ;

            //Reduce the opacity        
            toggle = 1;
        }
    }

    function mouseAfuera() {

            nodos
                .filter(function(d){
                    return d.categoria == "gobierno"
                        || d.categoria == "patrimonio"
                        || d.categoria == "planeacion"
                        || d.categoria == "ong"
                        || d.categoria == "gremios"
                        || d.categoria == "academia" 
                        || d.categoria == "comunidad"
                        || d.categoria == "privado"
                        || d.categoria == "medios"
                        || d.categoria == "otros"
                    ;
                })
                .transition()
                .style("opacity", 1)
                .attr('r', 5)
            ;

            // y las lineas a 0        
            lineas
                .transition()
                .style("stroke-opacity", 0.1)
            ;

            // text
            //  .transition()
            //  .style("opacity", 0.1)
            // ;

            toggle = 0;
        }   
});
Bejuco
  • 133
  • 12
  • Where is the file: actores_v5.json or a smaller version of the dataset – rioV8 Jul 26 '18 at 03:55
  • @PierrePuentes Instead of asking **us** to download the JSON, why don't **you** prepare a running code, with HTML, JS and the JSON, so we just have to adjust it? You can use Blockbuilder, for instance: http://blockbuilder.org. Click the plus sign at the right-hand side to create the JSON file and the JS file, the HTML you can edit at the starting page. – Gerardo Furtado Jul 26 '18 at 04:18
  • @GerardoFurtado You are right. give me a sec – Bejuco Jul 26 '18 at 04:19
  • 1
    Ok, here it is.https://bl.ocks.org/pierreee1/07eb3b07ba876011419168d60c587090 – Bejuco Jul 26 '18 at 04:30

1 Answers1

1

The Text is positioned a bit outside the big circle so we can read the first character (textPosition()).

Based on the position/angle of the text relative to the circle center you have to rotate it, change the text-anchor (start, end) and a small baseline shift (dy) to make the text centered vertical relative to the colored circle.

For the transparency you don't need the toggle variable because the mouseover event is only called once for every circle.

Removed the redundancy of adding the circle class, it is identical to the d.categoria.

What is the purpose of the large OR compare of d.categoria in the filter() calls? Is this a test if d.categoria is defined?

// width y height
var w = 1350;
var h = 800;
var RAD2DEG = 180.0/Math.PI;
var gCircleRadius = 250;
var gSmallCircleRadius = 5;

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

    //circulo invisible para dibujar los nodos
    // it's actually two arcs so we can use the getPointAtLength() and getTotalLength() methods
    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')
        .attr('class', "links")
        .selectAll("line")
        .data(data.edges)
        .enter()
            .append("path")
            .attr("class", function(d) {
                return "link " + d.tipo; 
            });

    //crea los nodos de acuerdo a los nombres
    var nodos = svg.append("g")
        .attr('class', "nodes")
        .selectAll("circle")
        .data(data.nodes)
        .enter()
            .append("circle")
            .attr('class', function (d) { return "nodos" + (d.categoria ? " " + d.categoria: ""); })
            .on("mouseover", mouseEncima)
            .on("mouseout", mouseAfuera)
            .attr('r', gSmallCircleRadius);

    nodos
        .filter(function(d){
            return d.categoria == "gobierno"
                || d.categoria == "patrimonio"
                || d.categoria == "planeacion"
                || d.categoria == "ong"
                || d.categoria == "gremios"
                || d.categoria == "academia" 
                || d.categoria == "comunidad"
                || d.categoria == "privado"
                || d.categoria == "medios"
                || d.categoria == "otros"
            ;
        })
        .style("opacity", 1)
    ;

    //titulos de los nodos
    nodos.append("title")
        .text(function(d) { return d.id; });

    var text = svg.append("g")
        .attr('class', "text")
        .selectAll("text")
        .data(data.nodes)
        .enter()
        .append("text")
        .text(function(d) { return d.id; });

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

    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 d.y = d.fy; });

        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];
    }

    function mouseEncima() {
        //Reduce the opacity of all but the neighbouring nodes
        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) ? 2*gSmallCircleRadius : gSmallCircleRadius;
            })
        ;  

        lineas
            .transition()
            .style("stroke-opacity", function (o) {
                return d.index==o.source.index | d.index==o.target.index ? 1 : 0.1;
            })
        ;
    }

    function mouseAfuera() {

        nodos
            .filter(function(d){
                return d.categoria == "gobierno"
                    || d.categoria == "patrimonio"
                    || d.categoria == "planeacion"
                    || d.categoria == "ong"
                    || d.categoria == "gremios"
                    || d.categoria == "academia" 
                    || d.categoria == "comunidad"
                    || d.categoria == "privado"
                    || d.categoria == "medios"
                    || d.categoria == "otros"
                ;
            })
            .transition()
            .style("opacity", 1)
            .attr('r', 5)
        ;

        // y las lineas a 0
        lineas
            .transition()
            .style("stroke-opacity", 0.1)
        ;
    }
});

Edit

After a look at the complete fiddle.

Suggestion of a change for the link path. Some paths are following the circumference of the circle. The following prevents this and makes sure all paths start in the direction of the center.

lineas.attr("d", function(d) {
  return `M${d.source.x},${d.source.y}Q${w*0.5},${h*0.5} ${d.target.x},${d.target.y}`;
});
rioV8
  • 24,506
  • 3
  • 32
  • 49
  • 1
    Thank you very much! It works perfectly. Yes, it's something I did in a old code and I forgot to change it. You can check out the new version here: https://bl.ocks.org/pierreee1/afa24dd63bb06caaa77dae7ae85bdb4f (w| credits for the help!) Thank you again. – Bejuco Jul 26 '18 at 19:03
  • @Pierre: I have a suggestion to change the link paths so you can always see them all. – rioV8 Jul 26 '18 at 22:35
  • how could I do that? – Bejuco Jul 29 '18 at 22:21
  • replace the first line in the `tick()` function where you update the link path. See my edit of the answer. – rioV8 Jul 30 '18 at 00:17
  • It looks and works perfect! Thank you very much. Take a look: https://i.imgur.com/LWOM545.png – Bejuco Jul 30 '18 at 15:03