Capturing/Saving the current state of d3.js visualization - javascript

I'm a newbie to D3 and am looking to build a simple art application that allows users to drop d3 data points on a custom background, creating art in the process.
Is it possible to save each D3 node's position after a user drops it, such that when the page is reloaded, all nodes will migrate back to their positions?
Any help here is greatly appreciated! Thank you!

You have not mentioned which d3 layout you use. anyway, just collecting the data bonded to the nodes and links would do the job. Here is the working code snippet.
1) Update the chart.
2) Clear the chart.
3) Load the chart with the updates.
Hope this helps.
var initialData = {
"nodes":[
{"name":"Myriel","group":1},
{"name":"Napoleon","group":1},
{"name":"Mlle.Baptistine","group":1},
{"name":"Mme.Magloire","group":1},
{"name":"CountessdeLo","group":1},
{"name":"Geborand","group":1},
{"name":"Champtercier","group":1},
{"name":"Cravatte","group":1},
{"name":"Count","group":1}
],
"links":[
{"source":1,"target":0,"value":1},
{"source":2,"target":0,"value":8},
{"source":3,"target":0,"value":10},
{"source":3,"target":2,"value":6},
{"source":4,"target":0,"value":1}
]
};
var width = 960,
height = 500;
var color = d3.scale.category20();
var force = d3.layout.force()
.charge(-120)
.linkDistance(30)
.size([width, height]);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
draw(initialData);
var link, node;
function draw(graph){
force
.nodes(graph.nodes)
.links(graph.links)
.start();
link = svg.selectAll(".link")
.data(graph.links)
.enter().append("line")
.attr("class", "link")
.style("stroke-width", function(d) { return Math.sqrt(d.value); });
var drag = force.drag()
.on("dragstart", dragstart);
node = svg.selectAll(".node")
.data(graph.nodes)
.enter().append("circle")
.attr("class", "node")
.attr("r", 5)
.style("fill", function(d) { return color(d.group); })
.call(drag);
node.append("title")
.text(function(d) { return d.name; });
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("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
});
function dragstart(d) {
d.x = d3.event.x;
d.y = d3.event.y;
d3.select(this).classed("fixed", d.fixed = true);
}
}
var savedGraph = { nodes: [], links: [] };
d3.select("#saveBtn").on('click',function(){
savedGraph.nodes = node.data();
savedGraph.links = link.data();
svg.selectAll("*").remove();
});
d3.select("#loadBtn").on('click',function(){
console.log(savedGraph);
draw(savedGraph);
});
.node {
stroke: #fff;
stroke-width: 1.5px;
}
.link {
stroke: #999;
stroke-opacity: .6;
}
<script src="https://d3js.org/d3.v3.min.js"></script>
<input type="button" value="Clear" id="saveBtn"/>
<input type="button" value="Load" id="loadBtn"/>

I am not aware of a persistence library from D3, so you probably need to persist your with your own way.
If you only care about position, then you just need to create an array of positions, i.e. var positions = [ { x: x1, y: y1 }, { x: x2, y: y2 }, ... ], and you can choose to send this data to server, or persist in browser's local storage if you are fine with only persisting this on the specific browser, e.g.
// persist
window.localStorage.setItem('positions',JSON.stringify(positions));
// When the page is loaded
var positions = JSON.parse(window.localStorage.getItem('positions'));
Then you can use the positions to redraw all the nodes.

Related

JS- how to remove duplicate JSON nodes and add one link for all nodes that get merged

enter code here I have a JSON File from which I want to create a d3 directed
graph with arrows in the direction of higher influence score
{"nodes":[{"Name":"GJA","influenceScore":81.0,"type":10.0},
{"Name":"JJZ","influenceScore":82.6,"type":30.0},
{"Name":"SAG","influenceScore":89.0,"type":30.0},
{"Name":"JJZ","influenceScore":82.6,"type":30.0}],"links":
[{"source":0,"target":0,"type":"SA","value":1},
{"source":0,"target":1,"type":"SA","value":1},
{"source":0,"target":2,"type":"SA","value":1},
{"source":0,"target":3,"type":"SA","value":1}]}
I am a d3novice, so would like some help from experts here
My d3 code is here:
.link {
stroke: #ccc;
}
.node text {
pointer-events: none;
font: 12px sans-serif;
}
</style>
<body>
<script src="//d3js.org/d3.v3.min.js"></script>
<script>
var width = 1200,
height = 900;
var color = d3.scale.category10();
var fill = d3.scale.category10();
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var force = d3.layout.force()
.gravity(0.052)
.distance(350)
.charge(-20)
.size([width, height]);
d3.json("\\abc.json", function(error, json) {
if (error) throw error;
force
.nodes(json.nodes)
.links(json.links)
.start();
var link = svg.selectAll(".link")
.data(json.links)
.enter().append("line")
.attr("class", "link").style("stroke-width", function(d) { return
Math.sqrt(d.value); }).style("stroke", function(d) {return
fill(d.value);});
var node = svg.selectAll(".node")
.data(json.nodes)
.enter().append("g")
.attr("class", "node")
.call(force.drag);
node.append("circle")
.attr("class", "node")
.attr("r", function(d) { return (d.influenceScore/10) + 10;
}).style("fill", function(d) { return color(d.type); });
node.append("text")
.attr("dx", -35)
.attr("dy", "4.5em").text(function(d) { return d.Name });
node.append("title").text(function(d) { return d.Name ;});
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 +
")"; });
});
});
I am getting the following image
I would like the target node e.g JJZ here just to occur once ( currently it's occurring as many number of times as it is repeated in the JSON i.e 2 times in the given example) however the line joining the two nodes should increase in thickness depending on the number of times the nodes repeat. so the blue line linking JJZ with GJA should be thicker than GJA and SAG and if another node occurs 5 times that should be thicker than JJZ and GJA. Also how do I insert directed arrows in the direction of a higher influence score
Your question here has little to do with D3: you can manipulate your array with plain JavaScript.
This function looks for the objects on json.nodes based on the property Name. If it doesn't exist, it pushes the object into an array that I named filtered. If it already exists, it increases the value of count in that object:
var filtered = []
json.nodes.forEach(function(d) {
if (!this[d.Name]) {
d.count = 0;
this[d.Name] = d;
filtered.push(this[d.Name])
}
this[d.Name].count += 1
}, Object.create(null))
Here is the demo:
var json = {"nodes":[{"Name":"GJA","influenceScore":81.0,"type":10.0},
{"Name":"JJZ","influenceScore":82.6,"type":30.0},
{"Name":"SAG","influenceScore":89.0,"type":30.0},
{"Name":"JJZ","influenceScore":82.6,"type":30.0}],"links":
[{"source":0,"target":0,"type":"SA","value":1},
{"source":0,"target":1,"type":"SA","value":1},
{"source":0,"target":2,"type":"SA","value":1},
{"source":0,"target":3,"type":"SA","value":1}]};
var filtered = []
json.nodes.forEach(function(d){
if(!this[d.Name]){
d.count = 0;
this[d.Name] = d;
filtered.push(this[d.Name])
}
this[d.Name].count += 1
}, Object.create(null))
console.log(filtered)
Then, you just need to use the property count to set the stroke-width of your links.

Unpredictable Lengths Of Links between Nodes

In my D3 force layout when I add circles and links dynamically, the length of the link increases to infinity sometimes. After adding some more nodes it automatically gets corrected.
The code is
<!DOCTYPE html>
<meta charset="utf-8">
<style>
.node {
stroke: #fff;
stroke-width: 1.5px;
}
.link {
stroke: #999;
stroke-opacity: .6;
}
</style>
<body>
<script src="d3.min.js"></script>
<script>
// var in1=prompt("Name");
// var in2=prompt("Name");
var n= new Array();
var l=new Array();
var n=[];
var l=[];
function show()
{
var width = 960,
height = 500;
var color = d3.scale.category20();
var force = d3.layout.force()
.charge(-120)
.linkDistance(30)
.size([width, height]);
d3.select("svg").remove();
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
force
.nodes(n)
.links(l)
.start();
var link = svg.selectAll(".link")
.data(l)
.enter().append("line")
.attr("class", "link")
.style("stroke-width", function(d) { return Math.sqrt(d.value); });
var node = svg.selectAll(".node")
.data(n)
.enter().append("circle")
.attr("class", "node")
.attr("r", 5)
.style("fill", function(d) { return color(d.group); })
.call(force.drag);
node.append("title")
.text(function(d) { return d.name; });
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("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
});
}
function add()
{
try{
var add_name=prompt("name","Name");
var add_group=parseInt(prompt("group","0"));
n.push({"name":add_name,"group":add_group});
if(n.length>1)
{
var add_source=parseInt(prompt("source","0"));
var add_target=parseInt(prompt("target","0"));
var add_value=parseInt(prompt("value","0"));
l.push({"source":add_source,"target":add_target,"value":add_value});
console.log(n);
console.log(l);
}
show();
}
catch(e)
{
console.log(e);
}
}
</script>
<body onload="show();">
<input type="button" onclick="add();" value="dd">
I think the problem is that you are adding a node to the layout while the simulation is still ongoing. I was able to reproduce the problem if I added one node and quickly added another.
One possibility is to stop the layout during your add() function, by calling the force layout's stop() method. You would need to declare your force variable outside of your show() function, in the same way you have declared your lists of nodes n and links l.
An alternative, and probably better, approach is only to create the force layout once. At the moment you create one force layout on page load and one additional force layout for each additional node added. Problems arise when you update the lists of nodes and links when they are still being used by an 'old' force layout running in the background. If there's only one force layout created, you shouldn't need to stop it: you just need to call its start() method to allow it to reinitialise itself once you've modified the data it is running off.

Can i plot a graph with only links in a json file in case of D3?

I want to get a list of nodes from links data in JSON file.
Later I would like to plot a graph with links and edges.
My sample json file:
{
"links": [
{ "source":0, "target":1, "value":20, "orientation":"down" },
{ "source":1, "target":2, "value":1, "orientation":"left" },
{ "source":2, "target":3, "value":1, "orientation":"right" }
]
}
my JS function:
$(function() {
var width = 500,
height = 300;
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var force = d3.layout.force()
.gravity(.05)
.distance(100)
.charge(-100)
.size([width, height]);
d3.json("test_1.json", function(error, json) {
if (error) throw error;
var count = 0;
var nodes = [];
json.links.forEach(function(e) {
count++;
nodes.push({name: "test_"+e.source, orientation: e.orientation});
});
console.log("Count is : "+count);
force
.nodes(json.nodes)
.links(json.links)
.start();
var link = svg.selectAll(".link")
.data(json.links)
.enter().append("line")
.attr("class", "link");
var node = svg.selectAll(".node")
.data(json.nodes)
.attr("class", "node")
.call(force.drag);
node.append("circle")
.attr("class", "node")
.attr("r", 5);
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 + ")"; });
});
});
});
I cannot get a list from the function though as there seems to be an error with the forEach method of JSON.
Could anyone please help?
UPDATE: added the complete code.
Error: Cannot read property 'weight' of undefined.
So i added some nodes in my JSON because i think d3 always expects a node.
As far as you assign newly created nodes array to nodes variable you should pass it to your force layout, not links.nodes.
force
.nodes(nodes)
.links(json.links)
.start();
Or just add it to links object
...
json.links.forEach(function(e) {
count++;
nodes.push({name: "test_"+e.source, orientation: e.orientation});
});
links.nodes = nodes;
...
So I could find a way to do it:
my JSON file:
[
{"source":"entry","target":"chocolate","value":20,"orientation":"down","x": 200, "y": 150},
{"source":"chocolate","target":"bread","value":1,"orientation":"left","x": 140, "y": 300},
{"source":"bread","target":"soap","value":1,"orientation":"right","x": 400, "y": 190}
]
my JS code:
var width = 960,
height = 500;
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var force = d3.layout.force()
.size([width, height])
.linkDistance(20)
.charge(-500);
d3.json("name.json", function(error, links) {
if (error) throw error;
var nodesByName = {};
// Create nodes for each unique source and target.
links.forEach(function(link) {
link.source = nodeByName(link.source);
link.target = nodeByName(link.target);
});
// Extract the array of nodes from the map by name.
var nodes = d3.values(nodesByName);
// Create the link lines.
var link = svg.selectAll(".link")
.data(links)
.enter().append("line")
.attr("class", "link");
// Create the node circles.
var node = svg.selectAll(".node")
.data(nodes)
.enter().append("circle")
.attr("class", "node")
.attr("r", 4.5)
.call(force.drag);
// Start the force layout.
force
.nodes(nodes)
.links(links)
.on("tick", tick)
.start();
function tick() {
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("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
}
function nodeByName(name) {
return nodesByName[name] || (nodesByName[name] = {name: name});
}
});
});
It can draw the required layout.
Reference: https://gist.github.com/mbostock/2949937
Thanks!

Adding element to array in d3

I'm creating a webpage that will make a d3 force-layout graph using data from a JSON file, and when a button is pushed the user will be prompted for a name, and a new node will be created with the provided name. The page loads successfully and when the button is pushed an alert asks for a name, but when a name is entered there is an error that says 'Uncaught TypeError: undefined is not a function' and a new node isn't added.
What can I do to fix this error? Using Chrome I know the error is happening in the line where I try to append newNode. Here's my code:
<button onclick="addNode()">Click to add Node</button>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script>
function addNode() {
var nodeName = prompt("Name of new node:");
var newGroup = Math.floor(Math.random() * 6);
if (nodeName != null) {
var newNode = {"node":{"name":nodeName,"group":newGroup}};
force.nodes.append(newNode);
force.start();
}
}
var width = 960,
height = 500;
var color = d3.scale.category20();
var force = d3.layout.force()
.charge(-120)
.linkDistance(30)
.size([width, height]);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
d3.json("user_interactions(1).json", function(error, graph) {
force
.nodes(graph.nodes)
.links(graph.links)
.start();
var link = svg.selectAll(".link")
.data(graph.links)
.enter().append("line")
.attr("class", "link")
.style("stroke-width", function(d) { return Math.sqrt(d.value); });
var node = svg.selectAll(".node")
.data(graph.nodes)
.enter().append("circle")
.attr("class", "node")
.attr("r", 5)
.style("fill", function(d) { return color(d.group); })
.call(force.drag);
node.append("title")
.text(function(d) { return d.name; });
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("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
});
});
</script>
force.nodes is a function that returns the array of nodes. It's not a property. So force.nodes.append doesn't make any sense. You probably want to add the new node to the data,
force.nodes(force.nodes().push(newNode));
I'm not certain, however, that D3 will gracefully handle changing the nodes array dynamically.
In any case you'll have to add SVG elements for the new node and specify their attributes in a manner similar to how you're setting the initial properties (r, fill, etc.)

D3 Force Directed JSON Adjacency List

I have a Graph Adjacency List, like so:
{
nodes: [
{
"id": 1,
"label": "Mark
},...],
edges: [
{
"source": 1,
"target": 2
},....]
}
The edges use the id from the node!
I've been using PHP to echo this list onto client side.
D3 cannot load my JSON from file.
So I try to parse it manually:
var width = window.innerWidth,
height = window.innerHeight;
var color = d3.scale.category20();
var force = d3.layout.force()
.linkDistance(40)
.linkStrength(2)
.charge(-120)
.size([width, height]);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var nodes = [], links = [];
d3.json( null, function( error )
{
JSONData.nodes.forEach( function(node)
{
nodes.push( node );
});
JSONData.edges.forEach( function(edge)
{
links.push({source: edge.source, target: edge.target});
});
force.nodes(nodes)
.links(links)
.start();
var link = svg.selectAll(".link")
.data(links)
.enter().append("path")
.attr("fill", "none")
.attr("class", "link");
var node = svg.selectAll(".node")
.data(nodes)
.enter().append("circle")
.attr("class", "node")
.attr("r", 5)
.style("fill", "#4682B4" )
.call(force.drag);
node.append("title").text(function(d) { return d.label; });
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("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
});
});
However, all I can see is the nodes, but not the edges.
I have added a CSS file with:
.node {
stroke: #4682B4;
stroke-width: 1.5px;
}
.link {
stroke: #ddd;
stroke-width: 1.5px;
}
But to no avail.
Is there some way around this?
You need to use:
.enter().append("line")
instead of
.enter().append("path")
(maybe also you need to change some other attributes so that they correspond to a line, not a path).
Alternatively, you can draw some more complex paths instead of straight lines, but I think it would be an overkill in your case.
EDIT:
I just found two related and interesting questions:
enter link description here - describes another kind of problems while displaying links in a force layout - make sure you don't hit a similar obstacle
enter link description here - a guy trying to draw curvbes instead of lines, if you decide to do something similar
But hope that this answer and hints in the comments are sufficient for you to get what you want.

Categories