繁体   English   中英

D3v4 强制有向图 - localStorage 断开链接和节点

[英]D3v4 force directed graph - localStorage disconnects links and nodes

我正在尝试将 localStorage 与我的力导向图一起使用。 我在每个simulation.on("end", function())都保存graph变量,这可以正常工作。 但是,当我重新加载页面时,使用localStorage.getItem并拖动节点,然后链接不再与节点连接。

你能帮我解决这个奇怪的行为吗?

请检查下面的屏幕截图和代码: localStorage-update-screenshot

'use strict';

var test = [];
var datatable;
var index = [];
var graph;

getLocal();
//  load and save data
function getLocal() {
        if (localStorage.getItem("graph") === null) {
        graph = {
            "nodes": [{'id': 1, 'lable': 1, 'group': 'search'}, {'id': 2, 'lable': 2, 'group': '1'}, {'id': 3, 'lable': 3, 'group': '2'}],
            "links": [{'source': 1, 'target': 2, 'value': "1-2"},{'source': 2, 'target': 1, 'value': "2-1"},{'source': 1, 'target': 3, 'value': "1-3"},{'source': 2, 'target': 3, 'value': "2-3"}]
        }} else {
            graph = JSON.parse(localStorage.getItem("graph"));
        };
    };

// Graph variables
var firstLinks = true;

var w = 1680; //window.innerWidth;
var h = 850; //window.innerHeight;

var svg = d3.select("#svgData"),
    scheme = ['#e41a1c','#377eb8','#4daf4a','#984ea3','#ff7f00','#ffff33','#a65628','#f781bf','#999999'],
    width = +svg.attr(w),
    height = +svg.attr(h),
    color = d3.scaleOrdinal(d3.schemeCategory10);
    //color = d3.scaleOrdinal(scheme);

// elements for data join
var link = svg.append("g").attr("class", "link").selectAll(".link"),
    value = svg.append("g").selectAll(".link"),
    node = svg.append("g").attr("class", "node").selectAll(".node"),
    lable = svg.append("g").selectAll(".node"),
    image = svg.append("g").selectAll(".node");

//  simulation initialization
var simulation = d3.forceSimulation()
        .force("charge", d3.forceManyBody().strength(-30).distanceMax(300))
        //.force('collision', d3.forceCollide().radius(30))
        //.force("center", d3.forceCenter(w / 2, h / 2))
        .force("link", d3.forceLink().id(function(d) { return d.id; }).distance(function (d) { if (d.length) {return d.length; } else { return 300;}}))
        .force("x", d3.forceX().x(function(d) { if (d.group == "search") {return w / 2; } else { return " ";}}))
        .force("y", d3.forceY().y(function(d) { if (d.group == "search") {return h / 2; } else { return h / 3;}}));
    //.force("center", d3.forceCenter(w / 2, h / 2));

var marker = d3.select("#svgData").append('defs')
    .append('marker')
    .attr("id", "Triangle")
    .attr('viewBox', '-0 -5 10 10')
    .attr("refX", 25)
    .attr("refY", 0)
    .attr("markerUnits", 'userSpaceOnUse')
    .attr("orient", 'auto')
    .attr("markerWidth", 13)
    .attr("markerHeight", 13)
    .attr('xoverflow', 'visible')
    .append('path')
    .attr("d", 'M 0,-5 L 10 ,0 L 0,5');

// Add search result to graph data
function QuickSearch(value) {
    var new_node = {};
    new_node = {'id': value, 'lable': value, 'group': 'search'};
    graph.nodes.findIndex(x => x.id == new_node.id) == -1 ? graph.nodes.push(new_node) : console.log("object already exists")
    update();
};

update();

function update() {
    // DATA JOIN
    link = link.data(graph.links, d => d.id);

    // EXIT
    // Remove old links
    link.exit().remove();

    // ENTER
    // Create new links as needed.  
    link = link.enter().append("path")
        .attr("id", function(_, i) {
            return "path" + i
        })
        .attr("marker-end", "url(#Triangle)")
        .merge(link);

    // DATA JOIN
    value = value.data(graph.links, d => d.id);

    // EXIT
    value.exit().remove();

    // ENTER
    value = value.enter().append("text")
        .attr("dy", -4)
        .append("textPath")
        .attr("xlink:href", function(_, i) {
            return "#path" + i
        })
        .attr("startOffset", "50%")
        .text(function(d) {
            return d.value;
        })
        .merge(value);
    
    // DATA JOIN
    node = node.data(graph.nodes, d => d.id);

    // EXIT
    node.exit().remove();

    // ENTER

    node = node.enter().append("circle")
        .attr('stroke-width', 3)
        .attr('stroke', function(d) { if (d.group == "addr") {return '#1f77b4'; } else {return color(d.group)}})
        .call(d3.drag()
                .on("start", dragstarted)
                .on("drag", dragged)
                .on("end", dragended))
        .merge(node);

    // DATA JOIN
    lable = lable.data(graph.nodes, d => d.id);

    // EXIT
    lable.exit().remove();

    // ENTER

    lable = lable.enter().append("text")
        .text(function (d) {
            if(d.id.length > 10)
                return d.id.substring(0,10)+'...';
            else
                return d.id;                       
        })
        .merge(lable);
    

    //  Set nodes, links, and alpha target for simulation
    simulation.nodes(graph.nodes).on("tick", ticked);
    simulation.force("link").links(graph.links).distance(function (d) { if (d.length) {return d.length; } else { return 200;}});
    simulation.alphaTarget(1).restart();
    simulation.on("end", function() {
        node.each(function(d) {
            d.fx = d.x;
            d.fy = d.y;
        });
        localStorage.setItem("graph", JSON.stringify(graph));
        console.log("saving");
    });
    

    function ticked() {
        node
            .attr("cx", function(d) { return d.x; })
            .attr("cy", function(d) { return d.y; });

        lable
            .attr("x", function(d) { return d.x; })
            .attr("y", function(d) { return d.y - 30; });

        link
            .attr("d", function(d) {
                return "M" + d.source.x + "," + d.source.y
                    + "L" + d.target.x + "," + d.target.y;
            })
            .attr("stroke-dasharray", function() { 
                return this.getTotalLength() - 25;
            });

        value
            .attr("x", function(d) { return (d.source.x + d.target.x)/2; })
            .attr("y", function(d) { return ((d.source.y + d.target.y)/2) - 10; });

    }

    // Zoom
    var zoom = d3.zoom()
    .scaleExtent([0, 10])
    .on("zoom", zoomed);

    d3.select("#svgData").call(zoom);

    function zoomed() {
    const currentTransform = d3.event.transform;
    svg.selectAll("g").attr("transform", currentTransform);
    }

    function slided(d) {
        zoom.scaleTo(svg, d3.select(this).property("value"));
    }

    // drag nodes
    function dragstarted(d){
        if (!d3.event.active) simulation.alphaTarget(0.3).restart();
        d.fx = d.x;
        d.fy = d.y;
    }
    
    function dragged(d){
        d.fx = d3.event.x;
        d.fy = d3.event.y;
        //fix_nodes(d);
    }
    
    function dragended(d){
        if (!d3.event.active) simulation.alphaTarget(0);
        d.fx = d.x;
        d.fy = d.y;
    }
  
  // Preventing other nodes from moving while dragging one node
    function fix_nodes(this_node) {
        node.each(function(d) {
            if (this_node != d) {
            d.fx = d.x;
            d.fy = d.y;
            }
        });
    }
};

var deleteLeafs = function() {
    var source = [];
    var target = [];
    link.each(function (d) {
        source.push(d.source.index);
        target.push(d.target.index);
    });
    node.each(function (d, i) {
        if (i in source && i in target ) {
            index.push(d);
        }
    })
    
};

D3-force 将链接中的源和目标属性从给定节点的某个标识符替换为节点的 object 引用本身。 因此,所有链接都包含源和目标 object 引用,节点 object 引用自身。

Using JSON.parse and JSON.stringify to store/re-create the data doesn't result in an object where links and nodes share object references: node and link contain object references to different objects (even if they look identical):

下面看看链接源是否与节点相同,它是在力初始化之后,但不是在解析和字符串化之后。

 var graph = { nodes: [ {id: 1}, {id: 2} ], links: [ {source: 1, target: 2} ] } var force = d3.forceSimulation().nodes(graph.nodes).force("link", d3.forceLink().id(d=>d.id).links(graph.links)); // force only: console.log(graph.nodes[0] == graph.links[0].source); // parsed: graph = JSON.parse(JSON.stringify(graph)) console.log(graph.nodes[0] == graph.links[0].source); // For comparison: console.log("graph.nodes[0]:") console.log(graph.nodes[0]) console.log("graph.links[0].source:") console.log(graph.links[0].source)
 <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

如果您只是创建/删除节点,您可以每次从起始链接数组重新加载链接,只存储节点。 然后,通过重新初始化节点和链接,您应该获得一个新的力布局,其中节点位置(以及因此链接)在它们的最后位置。

如果要添加/删除链接,则可以使用另一种解析 object 的方法,例如Flatted ,它似乎适用于强制布局:

 var graph = { nodes: [ {id: 1}, {id: 2} ], links: [ {source: 1, target: 2} ] } var force = d3.forceSimulation().nodes(graph.nodes).force("link", d3.forceLink().id(d=>d.id).links(graph.links)); // force only: console.log(graph.nodes[0] == graph.links[0].source); // parsed: graph = Flatted.parse(Flatted.stringify(graph)) console.log(graph.nodes[0] == graph.links[0].source);
 <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/flatted@3.1.1/min.js"></script>

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM