Menambahkan node baru ke tata letak yang diarahkan secara paksa

89

Pertanyaan pertama tentang Stack Overflow, jadi bersabarlah! Saya baru mengenal d3.js, tetapi secara konsisten kagum dengan apa yang orang lain dapat capai dengannya ... dan hampir sama kagumnya dengan betapa sedikit kemajuan yang dapat saya lakukan dengan itu sendiri! Jelas saya tidak menggerogoti sesuatu, jadi saya harap jiwa-jiwa yang baik hati di sini dapat menunjukkan terang kepada saya.

Tujuan saya adalah membuat fungsi javascript yang dapat digunakan kembali yang hanya melakukan hal berikut:

  • Membuat grafik arah paksa kosong di elemen DOM yang ditentukan
  • Memungkinkan Anda menambah dan menghapus node berlabel bantalan gambar ke grafik itu, menentukan hubungan di antara mereka

Saya telah menggunakan http://bl.ocks.org/950642 sebagai titik awal, karena pada dasarnya itulah jenis tata letak yang ingin saya buat:

masukkan deskripsi gambar di sini

Inilah tampilan kode saya:

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

Setiap kali saya menambahkan node baru, itu memberi label ulang semua node yang ada; tumpukan ini di atas satu sama lain dan hal-hal mulai menjadi buruk. Saya mengerti mengapa ini terjadi: karena ketika saya memanggil update()fungsi fungsi setelah menambahkan node baru, ia melakukan a node.append(...)ke seluruh kumpulan data. Saya tidak tahu bagaimana melakukan ini hanya untuk node yang saya tambahkan ... dan saya hanya dapat menggunakan node.enter()untuk membuat satu elemen baru, jadi itu tidak berfungsi untuk elemen tambahan yang perlu saya ikat ke node . Bagaimana cara memperbaikinya?

Terima kasih atas panduan yang dapat Anda berikan tentang masalah ini!

Diedit karena saya dengan cepat memperbaiki sumber dari beberapa bug lain yang telah disebutkan sebelumnya

nkoren
sumber

Jawaban:

152

Setelah berjam-jam tidak bisa membuat ini berfungsi, saya akhirnya menemukan demo yang menurut saya tidak terkait dengan dokumentasi apa pun: http://bl.ocks.org/1095795 :

masukkan deskripsi gambar di sini

Demo ini berisi kunci yang akhirnya membantu saya memecahkan masalah.

Menambahkan beberapa objek pada satu enter()dapat dilakukan dengan menetapkan enter()ke variabel, dan kemudian menambahkannya. Ini masuk akal. Bagian penting kedua adalah bahwa array node dan link harus didasarkan pada force()- jika tidak, grafik dan model akan tidak selaras saat node dihapus dan ditambahkan.

Ini karena jika array baru dibangun, itu akan kekurangan atribut berikut :

  • index - indeks berbasis nol dari node dalam array node.
  • x - koordinat x dari posisi node saat ini.
  • y - koordinat y dari posisi node saat ini.
  • px - koordinat x dari posisi node sebelumnya.
  • py - koordinat y dari posisi node sebelumnya.
  • fixed - boolean yang menunjukkan apakah posisi node terkunci.
  • berat - berat simpul; jumlah tautan terkait.

Atribut ini tidak benar-benar diperlukan untuk panggilan ke force.nodes(), tetapi jika atribut ini tidak ada, atribut ini akan dijalankan secara acak oleh force.start()panggilan pertama.

Jika ada yang penasaran, kode kerjanya terlihat seperti ini:

<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
sumber
1
Menggunakan force.start()alih-alih force.resume()saat data baru ditambahkan adalah kuncinya. Terima kasih banyak!
Mouagip
Ini luar biasa. Bersikaplah keren jika itu menskalakan otomatis tingkat zoom (mungkin mengurangi muatan sampai semuanya cocok?) Jadi semuanya sesuai dengan ukuran kotak yang digambar.
Rob Grant
1
1 untuk contoh kode bersih. Saya lebih menyukainya daripada contoh Tn. Bostock karena ini menunjukkan bagaimana merangkum perilaku dalam suatu objek. Sudah selesai dilakukan dengan baik. (Pertimbangkan untuk menambahkannya ke pustaka contoh D3?)
fearless_fool
Indah sekali! Saya sedang belajar cara menggunakan forceGraph dengan d3 selama beberapa hari sekarang, dan ini adalah cara paling indah untuk melakukannya yang pernah saya lihat. Terima kasih banyak!
Lucas Azevedo