Adicionando novos nós ao layout direcionado à força

89

Primeira pergunta no Stack Overflow, então tenha paciência comigo! Eu sou novo no d3.js, mas sempre fico surpreso com o que os outros são capazes de realizar com ele ... e quase tão surpreso com o pouco progresso que tenho conseguido fazer com ele! É claro que não estou grocando nada, então espero que as almas bondosas aqui possam me mostrar a luz.

Minha intenção é fazer uma função javascript reutilizável que simplesmente faça o seguinte:

  • Cria um gráfico direcionado à força em branco em um elemento DOM especificado
  • Permite que você adicione e exclua nós rotulados e com imagem a esse gráfico, especificando as conexões entre eles

Tomei http://bl.ocks.org/950642 como ponto de partida, já que é essencialmente o tipo de layout que desejo ser capaz de criar:

insira a descrição da imagem aqui

Aqui está a aparência do meu 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 adiciono um novo nó, ele rotula novamente todos os nós existentes; estes se acumulam e as coisas começam a ficar feias. Eu entendo o porquê: porque quando eu chamo a update()função function ao adicionar um novo nó, ela faz um efeito node.append(...)em todo o conjunto de dados. Não consigo descobrir como fazer isso apenas para o nó que estou adicionando ... e aparentemente só posso usar node.enter()para criar um único novo elemento, de modo que não funciona para os elementos adicionais que preciso vincular ao nó . Como posso consertar isso?

Obrigado por qualquer orientação que você possa dar sobre qualquer um desses assuntos!

Editado porque eu rapidamente corri uma fonte de vários outros bugs que foram mencionados anteriormente

Nkoren
fonte

Respostas:

152

Depois de muitas horas sem conseguir fazer isso funcionar, finalmente me deparei com uma demonstração que não acho que tenha nenhum link da documentação: http://bl.ocks.org/1095795 :

insira a descrição da imagem aqui

Esta demonstração continha as chaves que finalmente me ajudaram a resolver o problema.

A adição de vários objetos em um enter()pode ser feita atribuindo o enter()a uma variável e, em seguida, anexando a ela. Isso faz sentido. A segunda parte crítica é que os arrays de nós e links devem ser baseados no force()- caso contrário, o gráfico e o modelo ficarão fora de sincronia conforme os nós forem excluídos e adicionados.

Isso ocorre porque, se uma nova matriz for construída, ela não terá os seguintes atributos :

  • índice - o índice baseado em zero do nó dentro da matriz de nós.
  • x - a coordenada x da posição do nó atual.
  • y - a coordenada y da posição do nó atual.
  • px - a coordenada x da posição do nó anterior.
  • py - a coordenada y da posição do nó anterior.
  • fixed - um booleano que indica se a posição do nó está bloqueada.
  • peso - o peso do nó; o número de links associados.

Esses atributos não são estritamente necessários para a chamada a force.nodes(), mas se não estiverem presentes, eles serão inicializados aleatoriamente por force.start()na primeira chamada.

Se alguém estiver curioso, o código de trabalho se parece com este:

<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
fonte
1
Usar em force.start()vez de force.resume()quando novos dados são adicionados era a chave. Muito obrigado!
Mouagip de
Isso é incrível. Seria legal se ele autoescalonasse o nível de zoom (talvez reduzindo a carga até que tudo se encaixasse?) Para que tudo se encaixasse no tamanho da caixa que estava sendo desenhada.
Rob Grant
1
1 para o exemplo de código limpo. Gosto mais dele do que do exemplo do Sr. Bostock porque mostra como encapsular o comportamento em um objeto. Bem feito. (Considere adicioná-lo à biblioteca de exemplo D3?)
fearless_fool
Isso é bonito! Estou aprendendo a usar forceGraph com d3 por alguns dias agora, e esta é a maneira mais linda de fazer isso que já vi. Muito obrigado!
Lucas Azevedo