[英]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.