Добавление новых узлов в компоновку с принудительным управлением

89

Первый вопрос о переполнении стека, так что несите меня! Я новичок в d3.js, но постоянно удивляюсь тому, что другие могут сделать с ним ... и почти так же поражен тем, насколько мало я смог продвинуться с этим сам! Ясно, что я что-то не гроклю, поэтому надеюсь, что добрые души здесь могут показать мне свет.

Я намерен создать многоразовую функцию javascript, которая просто выполняет следующие действия:

  • Создает пустой принудительно направленный граф в указанном элементе DOM
  • Позволяет добавлять и удалять помеченные узлы, несущие изображения, в этот граф, определяя связи между ними.

Я взял http://bl.ocks.org/950642 в качестве отправной точки, поскольку я хочу создать именно такой макет:

введите описание изображения здесь

Вот как выглядит мой код:

<!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>

Каждый раз, когда я добавляю новый узел, он меняет метки на все существующие узлы; эти кучи накладываются друг на друга, и все становится некрасиво. Я понимаю, почему это так: потому что, когда я вызываю update()функцию-функцию при добавлении нового узла, она выполняет операцию node.append(...)со всем набором данных. Я не могу понять, как это сделать только для добавляемого узла ... и я, по-видимому, могу использовать только node.enter()для создания одного нового элемента, так что это не работает для дополнительных элементов, которые мне нужно привязать к узлу. . Как я могу это исправить?

Спасибо за любые рекомендации, которые вы можете дать по любой из этих проблем!

Отредактировано, потому что я быстро исправил источник нескольких других ошибок, которые были упомянуты ранее.

нкорен
источник

Ответы:

152

После многих долгих часов, когда я не мог заставить это работать, я наконец наткнулся на демонстрацию, которая, как мне кажется, не связана с какой-либо документацией: http://bl.ocks.org/1095795 :

введите описание изображения здесь

Эта демонстрация содержала ключи, которые, наконец, помогли мне решить проблему.

Добавление нескольких объектов в enter()можно выполнить, присвоив enter()переменной, а затем добавив к ней. Это имеет смысл. Вторая важная часть заключается в том, что массивы узлов и ссылок должны быть основаны на force()- в противном случае граф и модель выйдут из синхронизации при удалении и добавлении узлов.

Это связано с тем, что если вместо этого создается новый массив, в нем будут отсутствовать следующие атрибуты :

  • index - отсчитываемый от нуля индекс узла в массиве узлов.
  • x - координата x текущего положения узла.
  • y - y-координата текущего положения узла.
  • px - координата x предыдущей позиции узла.
  • py - y-координата предыдущей позиции узла.
  • fixed - логическое значение, указывающее, заблокировано ли положение узла.
  • weight - вес узла; количество связанных ссылок.

Эти атрибуты не являются строго необходимыми для вызова force.nodes(), но если они не присутствуют, то они будут случайным образом инициализированы при force.start()первом вызове.

Если кому-то интересно, рабочий код выглядит так:

<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>
нкорен
источник
1
Ключевым моментом было использование force.start()вместо force.resume()добавления новых данных. Большое спасибо!
Mouagip
Это круто. Было бы здорово, если бы он автоматически масштабировал уровень масштабирования (может быть, уменьшил заряд, пока все не поместилось?), Чтобы все умещалось в размере коробки, в которую оно втягивалось.
Роб Грант
1
+1 за чистый пример кода. Мне он нравится больше, чем пример мистера Бостока, потому что он показывает, как инкапсулировать поведение в объекте. Отлично сработано. (Рассмотрите возможность добавления его в библиотеку примеров D3?)
fearless_fool
Это красиво! Я уже пару дней учусь использовать forceGraph с d3, и это самый красивый способ сделать это из всех, что я видел. Большое спасибо!
Лукас Азеведо