Agregar nuevos nodos al diseño dirigido por la Fuerza

89

Primera pregunta sobre Stack Overflow, ¡así que tengan paciencia conmigo! Soy nuevo en d3.js, pero siempre me ha sorprendido lo que otros pueden lograr con él ... ¡y casi igual de asombrado por los pocos avances que he podido hacer con él! Claramente no estoy asimilando algo, así que espero que las almas amables aquí puedan mostrarme la luz.

Mi intención es crear una función de JavaScript reutilizable que simplemente haga lo siguiente:

  • Crea un gráfico dirigido a la fuerza en blanco en un elemento DOM especificado
  • Le permite agregar y eliminar nodos etiquetados que portan imágenes a ese gráfico, especificando conexiones entre ellos

He tomado http://bl.ocks.org/950642 como punto de partida, ya que ese es esencialmente el tipo de diseño que quiero poder crear:

ingrese la descripción de la imagen aquí

Así es como se ve mi código:

<!DOCTYPE html>
<html>
<head>
    <script type="text/javascript" src="jquery.min.js"></script>
    <script type="text/javascript" src="underscore-min.js"></script>
    <script type="text/javascript" src="d3.v2.min.js"></script>
    <style type="text/css">
        .link { stroke: #ccc; }
        .nodetext { pointer-events: none; font: 10px sans-serif; }
        body { width:100%; height:100%; margin:none; padding:none; }
        #graph { width:500px;height:500px; border:3px solid black;border-radius:12px; margin:auto; }
    </style>
</head>
<body>
<div id="graph"></div>
</body>
<script type="text/javascript">

function myGraph(el) {

    // Initialise the graph object
    var graph = this.graph = {
        "nodes":[{"name":"Cause"},{"name":"Effect"}],
        "links":[{"source":0,"target":1}]
    };

    // Add and remove elements on the graph object
    this.addNode = function (name) {
        graph["nodes"].push({"name":name});
        update();
    }

    this.removeNode = function (name) {
        graph["nodes"] = _.filter(graph["nodes"], function(node) {return (node["name"] != name)});
        graph["links"] = _.filter(graph["links"], function(link) {return ((link["source"]["name"] != name)&&(link["target"]["name"] != name))});
        update();
    }

    var findNode = function (name) {
        for (var i in graph["nodes"]) if (graph["nodes"][i]["name"] === name) return graph["nodes"][i];
    }

    this.addLink = function (source, target) {
        graph["links"].push({"source":findNode(source),"target":findNode(target)});
        update();
    }

    // set up the D3 visualisation in the specified element
    var w = $(el).innerWidth(),
        h = $(el).innerHeight();

    var vis = d3.select(el).append("svg:svg")
        .attr("width", w)
        .attr("height", h);

    var force = d3.layout.force()
        .nodes(graph.nodes)
        .links(graph.links)
        .gravity(.05)
        .distance(100)
        .charge(-100)
        .size([w, h]);

    var update = function () {

        var link = vis.selectAll("line.link")
            .data(graph.links);

        link.enter().insert("line")
            .attr("class", "link")
            .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; });

        link.exit().remove();

        var node = vis.selectAll("g.node")
            .data(graph.nodes);

        node.enter().append("g")
            .attr("class", "node")
            .call(force.drag);

        node.append("image")
            .attr("class", "circle")
            .attr("xlink:href", "https://d3nwyuy0nl342s.cloudfront.net/images/icons/public.png")
            .attr("x", "-8px")
            .attr("y", "-8px")
            .attr("width", "16px")
            .attr("height", "16px");

        node.append("text")
            .attr("class", "nodetext")
            .attr("dx", 12)
            .attr("dy", ".35em")
            .text(function(d) { return d.name });

        node.exit().remove();

        force.on("tick", function() {
          link.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; });

          node.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
        });

        // Restart the force layout.
        force
          .nodes(graph.nodes)
          .links(graph.links)
          .start();
    }

    // Make it all go
    update();
}

graph = new myGraph("#graph");

// These are the sort of commands I want to be able to give the object.
graph.addNode("A");
graph.addNode("B");
graph.addLink("A", "B");

</script>
</html>

Cada vez que agrego un nuevo nodo, vuelve a etiquetar todos los nodos existentes; estos se apilan uno encima del otro y las cosas comienzan a ponerse feas. Entiendo por qué es esto: porque cuando llamo a la update()función función al agregar un nuevo nodo, hace un node.append(...)a todo el conjunto de datos. No puedo averiguar cómo hacer esto solo para el nodo que estoy agregando ... y aparentemente solo puedo usarlo node.enter()para crear un solo elemento nuevo, por lo que eso no funciona para los elementos adicionales que necesito vinculados al nodo . ¿Cómo puedo arreglar esto?

¡Gracias por cualquier orientación que pueda brindar sobre este tema!

Editado porque arreglé rápidamente una fuente de varios otros errores que se mencionaron anteriormente

nkoren
fuente

Respuestas:

152

Después de muchas horas de no poder hacer que esto funcionara, finalmente me encontré con una demostración que no creo que esté vinculada a ninguna de la documentación: http://bl.ocks.org/1095795 :

ingrese la descripción de la imagen aquí

Esta demostración contenía las claves que finalmente me ayudaron a resolver el problema.

Se pueden agregar varios objetos en una enter()asignando elenter() a una variable y luego agregando a eso. Esto tiene sentido. La segunda parte crítica es que los arreglos de nodos y enlaces deben basarse en el force(); de lo contrario, el gráfico y el modelo se desincronizarán a medida que se eliminen y agreguen nodos.

Esto se debe a que si se construye una nueva matriz en su lugar, carecerá de los siguientes atributos :

  • índice: el índice de base cero del nodo dentro de la matriz de nodos.
  • x: la coordenada x de la posición actual del nodo.
  • y: la coordenada y de la posición actual del nodo.
  • px: la coordenada x de la posición anterior del nodo.
  • py: la coordenada y de la posición del nodo anterior.
  • fijo: un valor booleano que indica si la posición del nodo está bloqueada.
  • peso - el peso del nodo; el número de enlaces asociados.

Estos atributos no son estrictamente necesarios para la llamada a force.nodes(), pero si no están presentes, se inicializarán aleatoriamenteforce.start() en la primera llamada.

Si alguien tiene curiosidad, el código de trabajo se ve así:

<script type="text/javascript">

function myGraph(el) {

    // Add and remove elements on the graph object
    this.addNode = function (id) {
        nodes.push({"id":id});
        update();
    }

    this.removeNode = function (id) {
        var i = 0;
        var n = findNode(id);
        while (i < links.length) {
            if ((links[i]['source'] === n)||(links[i]['target'] == n)) links.splice(i,1);
            else i++;
        }
        var index = findNodeIndex(id);
        if(index !== undefined) {
            nodes.splice(index, 1);
            update();
        }
    }

    this.addLink = function (sourceId, targetId) {
        var sourceNode = findNode(sourceId);
        var targetNode = findNode(targetId);

        if((sourceNode !== undefined) && (targetNode !== undefined)) {
            links.push({"source": sourceNode, "target": targetNode});
            update();
        }
    }

    var findNode = function (id) {
        for (var i=0; i < nodes.length; i++) {
            if (nodes[i].id === id)
                return nodes[i]
        };
    }

    var findNodeIndex = function (id) {
        for (var i=0; i < nodes.length; i++) {
            if (nodes[i].id === id)
                return i
        };
    }

    // set up the D3 visualisation in the specified element
    var w = $(el).innerWidth(),
        h = $(el).innerHeight();

    var vis = this.vis = d3.select(el).append("svg:svg")
        .attr("width", w)
        .attr("height", h);

    var force = d3.layout.force()
        .gravity(.05)
        .distance(100)
        .charge(-100)
        .size([w, h]);

    var nodes = force.nodes(),
        links = force.links();

    var update = function () {

        var link = vis.selectAll("line.link")
            .data(links, function(d) { return d.source.id + "-" + d.target.id; });

        link.enter().insert("line")
            .attr("class", "link");

        link.exit().remove();

        var node = vis.selectAll("g.node")
            .data(nodes, function(d) { return d.id;});

        var nodeEnter = node.enter().append("g")
            .attr("class", "node")
            .call(force.drag);

        nodeEnter.append("image")
            .attr("class", "circle")
            .attr("xlink:href", "https://d3nwyuy0nl342s.cloudfront.net/images/icons/public.png")
            .attr("x", "-8px")
            .attr("y", "-8px")
            .attr("width", "16px")
            .attr("height", "16px");

        nodeEnter.append("text")
            .attr("class", "nodetext")
            .attr("dx", 12)
            .attr("dy", ".35em")
            .text(function(d) {return d.id});

        node.exit().remove();

        force.on("tick", function() {
          link.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; });

          node.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
        });

        // Restart the force layout.
        force.start();
    }

    // Make it all go
    update();
}

graph = new myGraph("#graph");

// You can do this from the console as much as you like...
graph.addNode("Cause");
graph.addNode("Effect");
graph.addLink("Cause", "Effect");
graph.addNode("A");
graph.addNode("B");
graph.addLink("A", "B");

</script>
nkoren
fuente
1
Usar en force.start()lugar de force.resume()cuando se agregan nuevos datos fue la clave. ¡Muchas gracias!
Mouagip
Esto es asombroso. Sería genial si ajustara automáticamente el nivel de zoom (¿tal vez reduciendo la carga hasta que todo encaje?) Para que todo encajara en el tamaño de la caja que estaba dibujando.
Rob Grant
1
+1 para el ejemplo de código limpio. Me gusta más que el ejemplo del Sr. Bostock porque muestra cómo encapsular el comportamiento en un objeto. Bien hecho. (¿Considerar agregarlo a la biblioteca de ejemplo D3?)
fearless_fool
¡Eso es hermoso! Estoy aprendiendo a usar forceGraph con d3 desde hace un par de días, y esta es la forma más hermosa de hacerlo que he visto. ¡Muchas gracias!
Lucas Azevedo