I am trying to use localStorage with my force-directed graph.
I am saving the graph variable every each simulation.on("end", function()) and this works correctly.
However, when I reload the page, use localStorage.getItem and drag node, then links are not connected with nodes anymore.
Could you help me with fixing this strange behavior?
Please check the screenshot and code below: 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);
}
})
};