I am trying to add a threshold to a force directed graph, where I will only include edges between vertices that that are above some threshold I store in a map. My slider is partly working, and the edges successfully get removed.
However, after the edges are removed, the graph stops animating, and there is an error in the console when calling force.start(). Do I really need to add unique ids? In the JSFiddle I linked, he does not do that, and his slider works with no problems.
Thanks!
There are multiple answers on StackOverflow to similar questions, and I have used them to fix my obvious errors (such as d3.js: "Cannot read property 'weight' of undefined" when manually defining both nodes and links for force layout), but I am down to this one. I am using this example: http://jsfiddle.net/simonraper/TdHgx/?utm_source=website&utm_medium=embed&utm_campaign=TdHgx from this website: http://www.coppelia.io/2014/07/an-a-to-z-of-extra-features-for-the-d3-force-layout/
I will send the relevant JS.
function draw(occurence) {
var ratio = window.devicePixelRatio || 1;
var width = Math.min(700,0.8*$(window).width()), height = 700;
var color = d3.scale.category20();
var svg = d3.select("#" + occurence).append("svg")
.attr("width", width)
.attr("height", height)
.attr("id", "#" + occurence + "svg");
d3.json(occurence + ".json", function(error, graph) {
if (error) throw error;
var force = d3.layout.force()
.charge(-120)
.linkDistance(50)
.size([width, height]);
force
.nodes(graph.nodes)
.links(graph.links)
.start();
if (occurence == "occurrences3") {
console.log(graph.nodes);
console.log(graph.links);
}
var graphRec=JSON.parse(JSON.stringify(graph)); //Add this line
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; });
});
node.on("dblclick", function(d) {
$("#uncuratedGraphsModal").modal();
var d = d3.select(this).node().__data__;
var name = d.name;
$("#uncuratedGraphsModalHeader").text("Node " + name);
$("#uncuratedGraphsModalBody").empty();
var edge_by_person = edge_by_person_per_threshold[occurence + "edge_by_person"];
var edges = edge_by_person[name];
edges.sort(function(a,b) {
return a.dest - b.dest;
});
$.each(edges, function(edge) {
var new_edge = $("<li>");
new_edge.text("Neighbor: " + edges[edge].dest + ", Token: " + edges[edge].token);
$("#uncuratedGraphsModalBody").append(new_edge);
});
});
$("#" + occurence + "thresholdSlider").on('input', function(thresh) {
threshold($("#" + occurence + "thresholdSlider").val());
});
//adjust threshold
function threshold(thresh) {
var edge_by_person = edge_by_person_per_threshold_unweighted[occurence + "edge_by_person_unweighted"];
graph.links.splice(0, graph.links.length);
for (var i = 0; i < graphRec.links.length; i++) {
var source = graphRec.links[i].source.name;
var dest = graphRec.links[i].target.name;
var value = -1;
var edges = edge_by_person[source];
var found = false;
for (var edge = 0; !found && edge < edges.length; edge++) {
if (dest == edges[edge].dest) {
value = edges[edge].token;
found = true;
}
}
if (value >= thresh) {
graph.links.push({
source: graphRec.links[i].source.name,
target: graphRec.links[i].target.name,
value: graphRec.links[i].value
});
}
}
restart();
}
//Restart the visualisation after any node and link changes
function restart() {
link = link.data(graph.links);
link.exit().remove();
link.enter().insert("line", ".node").attr("class", "link")
.style("stroke-width", function(d) { return Math.sqrt(d.value); });
node = node.data(graph.nodes);
node.enter().insert("circle", ".cursor").attr("class", "node")
.attr("r", 5)
.style("fill", function(d) { return color(d.group); })
.call(force.drag);
force
.nodes(node)
.links(link)
.start();
}
});
}
This is the JS that he added from the JsFiddle I linked:
This is my understanding: If you have the same nodes, and you just remove a few edges from graph.links, can't you just do a force.start()? When I directly copy his restart() function, all links are broken, and none get redrawn, even though doing a console.log(graph.links) shows the correct links to-redraw.
I believe that it's talking to the wrong nodes at this point, maybe the wrong SVG. I have multiple SVGs on the same page.
Related
I have looked for answer to this but none of the similar questions help me in my situation. I have a D3 tree that creates new nodes at runtime. I would like to add HTML (so I can format) to a node when I mouseover that particular node. Right now I can add HTML but its unformatted. Please help!
JSFiddle: http://jsfiddle.net/Srx7z/
JS Code:
var width = 960,
height = 500;
var tree = d3.layout.tree()
.size([width - 20, height - 60]);
var root = {},
nodes = tree(root);
root.parent = root;
root.px = root.x;
root.py = root.y;
var diagonal = d3.svg.diagonal();
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(-30,40)");
var node = svg.selectAll(".node"),
link = svg.selectAll(".link");
var duration = 750;
$("#submit_button").click(function() {
update();
});
function update() {
if (nodes.length >= 500) return clearInterval(timer);
// Add a new node to a random parent.
var n = {id: nodes.length},
p = nodes[Math.random() * nodes.length | 0];
if (p.children) p.children.push(n); else p.children = [n];
nodes.push(n);
// Recompute the layout and data join.
node = node.data(tree.nodes(root), function (d) {
return d.id;
});
link = link.data(tree.links(nodes), function (d) {
return d.source.id + "-" + d.target.id;
});
// Add entering nodes in the parent’s old position.
var gelement = node.enter().append("g");
gelement.append("circle")
.attr("class", "node")
.attr("r", 20)
.attr("cx", function (d) {
return d.parent.px;
})
.attr("cy", function (d) {
return d.parent.py;
});
// Add entering links in the parent’s old position.
link.enter().insert("path", ".g.node")
.attr("class", "link")
.attr("d", function (d) {
var o = {x: d.source.px, y: d.source.py};
return diagonal({source: o, target: o});
})
.attr('pointer-events', 'none');
node.on("mouseover", function (d) {
var g = d3.select(this);
g.append("text").html('First Line <br> Second Line')
.classed('info', true)
.attr("x", function (d) {
return (d.x+20);
})
.attr("y", function (d) {
return (d.y);
})
.attr('pointer-events', 'none');
});
node.on("mouseout", function (d) {
d3.select(this).select('text.info').remove();
});
// Transition nodes and links to their new positions.
var t = svg.transition()
.duration(duration);
t.selectAll(".link")
.attr("d", diagonal);
t.selectAll(".node")
.attr("cx", function (d) {
return d.px = d.x;
})
.attr("cy", function (d) {
return d.py = d.y;
});
}
Using Lars Kotthoff's excellent direction, I got it working so I decided to post it for others and my own reference:
http://jsfiddle.net/FV4rL/2/
with the following code appended:
node.on("mouseover", function (d) {
var g = d3.select(this); // The node
var div = d3.select("body").append("div")
.attr('pointer-events', 'none')
.attr("class", "tooltip")
.style("opacity", 1)
.html("FIRST LINE <br> SECOND LINE")
.style("left", (d.x + 50 + "px"))
.style("top", (d.y +"px"));
});
I've added a new peice of javascript to an old script I had to add a highlighting functionality to a force network layout. I get the information for the diagram from generated json in a rails app. The original code I've been using is here:
var width = 960,
height = 960;
var color = d3.scale.category20();
var force = d3.layout.force()
.charge(-100)
.linkDistance(530)
.size([width, height]);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var endpoint = window.location.href+".json"
d3.json(endpoint, function(graph) {
force
.nodes(graph.nodes)
.links(graph.links)
.start();
var link = svg.selectAll(".link")
.data(graph.links)
.enter().append("line")
.attr("class", "link")
.style("marker-end", "url(#suit)");
var node = svg.selectAll(".node")
.data(graph.nodes)
.enter().append("circle")
.attr("class", "node")
.attr("r", 10)
.style("fill", function(d) { return color(d.group); })
.call(force.drag)
.on('dblclick', connectedNodes);
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; });
});
});
svg.append("defs").selectAll("marker")
.data(["suit", "licensing", "resolved"])
.enter().append("marker")
.attr("id", function(d) { return d; })
.attr("viewBox", "0 -5 10 10")
.attr("refX", 25)
.attr("refY", 0)
.attr("markerWidth", 6)
.attr("markerHeight", 6)
.attr("orient", "auto")
.append("path")
.attr("d", "M0,-5L10,0L0,5 L10,0 L0, -5")
.style("stroke", "#4679BD")
.style("opacity", "0.6");
//APPENDED CODE ADDED HERE
//Toggle stores whether the highlighting is on
var toggle = 0;
//Create an array logging what is connected to what
var linkedByIndex = {};
for (i = 0; i < graph.nodes.length; i++) {
linkedByIndex[i + "," + i] = 1;
};
graph.links.forEach(function (d) {
linkedByIndex[d.source.index + "," + d.target.index] = 1;
});
//This function looks up whether a pair are neighbours
function neighboring(a, b) {
return linkedByIndex[a.index + "," + b.index];
}
function connectedNodes() {
if (toggle == 0) {
//Reduce the opacity of all but the neighbouring nodes
d = d3.select(this).node().__data__;
node.style("opacity", function (o) {
return neighboring(d, o) | neighboring(o, d) ? 1 : 0.1;
});
link.style("opacity", function (o) {
return d.index==o.source.index | d.index==o.target.index ? 1 : 0.1;
});
//Reduce the op
toggle = 1;
} else {
//Put them back to opacity=1
node.style("opacity", 1);
link.style("opacity", 1);
toggle = 0;
}
}
I then tried to append further code as suggested here and simply added the following to the bottom of the script above where it is marked in capital letters
Could have been so simple.... The script worked but the added functionlity (to add highlights between nodes) didn't. An error message says:
Uncaught ReferenceError: graph is not defined
My susipicion is that it relates to the line
d3.json(endpoint, function(graph) {
and the fact that the subsequent }); is in the wrong place to encompass the new code but I've played with it and I'm not sure how to correct it
UPDATE
I've solved this. The problem was simply that I was declaring graph inside a function and the other functions couldn't access it. The solution is to put the other functions inside the function that delares it which in effect means moving the last
});
from the line
node.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
});
});
to the very last line. Works fine now
The answer is now given in the UPDATE section of the question
I have looked for answer to this but none of the similar questions help me in my situation. I have a D3 tree that creates new nodes at runtime. I would like to add HTML (so I can format) to a node when I mouseover that particular node. Right now I can add HTML but its unformatted. Please help!
JSFiddle: http://jsfiddle.net/Srx7z/
JS Code:
var width = 960,
height = 500;
var tree = d3.layout.tree()
.size([width - 20, height - 60]);
var root = {},
nodes = tree(root);
root.parent = root;
root.px = root.x;
root.py = root.y;
var diagonal = d3.svg.diagonal();
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(-30,40)");
var node = svg.selectAll(".node"),
link = svg.selectAll(".link");
var duration = 750;
$("#submit_button").click(function() {
update();
});
function update() {
if (nodes.length >= 500) return clearInterval(timer);
// Add a new node to a random parent.
var n = {id: nodes.length},
p = nodes[Math.random() * nodes.length | 0];
if (p.children) p.children.push(n); else p.children = [n];
nodes.push(n);
// Recompute the layout and data join.
node = node.data(tree.nodes(root), function (d) {
return d.id;
});
link = link.data(tree.links(nodes), function (d) {
return d.source.id + "-" + d.target.id;
});
// Add entering nodes in the parent’s old position.
var gelement = node.enter().append("g");
gelement.append("circle")
.attr("class", "node")
.attr("r", 20)
.attr("cx", function (d) {
return d.parent.px;
})
.attr("cy", function (d) {
return d.parent.py;
});
// Add entering links in the parent’s old position.
link.enter().insert("path", ".g.node")
.attr("class", "link")
.attr("d", function (d) {
var o = {x: d.source.px, y: d.source.py};
return diagonal({source: o, target: o});
})
.attr('pointer-events', 'none');
node.on("mouseover", function (d) {
var g = d3.select(this);
g.append("text").html('First Line <br> Second Line')
.classed('info', true)
.attr("x", function (d) {
return (d.x+20);
})
.attr("y", function (d) {
return (d.y);
})
.attr('pointer-events', 'none');
});
node.on("mouseout", function (d) {
d3.select(this).select('text.info').remove();
});
// Transition nodes and links to their new positions.
var t = svg.transition()
.duration(duration);
t.selectAll(".link")
.attr("d", diagonal);
t.selectAll(".node")
.attr("cx", function (d) {
return d.px = d.x;
})
.attr("cy", function (d) {
return d.py = d.y;
});
}
Using Lars Kotthoff's excellent direction, I got it working so I decided to post it for others and my own reference:
http://jsfiddle.net/FV4rL/2/
with the following code appended:
node.on("mouseover", function (d) {
var g = d3.select(this); // The node
var div = d3.select("body").append("div")
.attr('pointer-events', 'none')
.attr("class", "tooltip")
.style("opacity", 1)
.html("FIRST LINE <br> SECOND LINE")
.style("left", (d.x + 50 + "px"))
.style("top", (d.y +"px"));
});
I've a problem with creating multiple force layout graphs using d3 and reading data from a json file. I use a for loop to iterate over the graphs, create a separate div containing a svg for each. The problem is, that the force layout is only applied to the last one created, so basically the others just show a dot in the upper left corner. I could solve it partly by putting a for loop at the end of each iteration, but I still lose the interaction capabilities of the separate figures.
Find the code below, thanks in advance.
Cheers, Michael
var color = d3.scale.category20();
var force = new Array();
var div = new Array();
var svg = new Array();
var graph = new Array();
var link;
var node;
var width = 360;
var height = 360;
var brush = new Array();
var shiftKey;
var count = 0;
//loop through the different subsystems in the json-file
for(name_subsystem in graphs) {
//add a div for each subsystem
div[count] = document.createElement("div");
div[count].style.width = "360px";
div[count].style.height = "360px";
div[count].style.cssFloat="left";
div[count].id = name_subsystem;
document.body.appendChild(div[count]);
//force is called. all attributes with default values are noted. see API reference on github.
force[count] = d3.layout.force()
.size([width, height])
.linkDistance(20)
.linkStrength(1)
.friction(0.9)
.charge(-30)
.theta(0.8)
.gravity(0.1);
div[count].appendChild(document.createTextNode(name_subsystem));
//create the svg rectangle in which other elements can be visualised
svg[count] = d3.select("#"+name_subsystem)
.on("keydown.brush", keydown)
.on("keyup.brush", keyup)
.append("svg")
.attr("width", width)
.attr("height", height)
.attr("id",name_subsystem);
brush[count] = svg[count].append("g")
.datum(function() { return {selected: false, previouslySelected: false}; })
.attr("class", "brush");
//force is started
force[count]
.nodes(graphs[name_subsystem].nodes)
.links(graphs[name_subsystem].links)
.start();
//link elements are called, joined with the data, and links are created for each link object in links
link = svg[count].selectAll(".link")
.data(graphs[name_subsystem].links)
.enter().append("line")
.attr("class", "link")
.style("stroke-width", function(d) { return Math.sqrt(d.thickness); })
.style("stroke", function(d){
if (d.linktype === 'reactant'){
return "black";
} else {
return "red";
}
});
//node elements are called, joined with the data, and circles are created for each node object in nodes
node = svg[count].selectAll(".node")
.data(graphs[name_subsystem].nodes)
.enter().append("circle")
.attr("class", "node")
//radius
.attr("r", 5)
//fill
.attr("fill", function(d) {
if (d.type === 'metabolite') {
return "blue";
} else {
return "red";
}
})
.on("mousedown", function(d) {
if (!d.selected) { // Don't deselect on shift-drag.
if (!shiftKey) node.classed("selected", function(p) { return p.selected = d === p; });
else d3.select(this).classed("selected", d.selected = true);
}
})
.on("mouseup", function(d) {
if (d.selected && shiftKey) d3.select(this).classed("selected", d.selected = false);
})
.call(force[count].drag()
.on("dragstart",function dragstart(d){
d.fixed=true;
d3.select(this).classed("fixed",true);
})
);
//gives titles to nodes. i do not know why this is separated from the first node calling.
node.append("title")
.text(function(d) { return d.name; });
//enable brushing of the network
brush[count].call(d3.svg.brush()
.x(d3.scale.identity().domain([0, width]))
.y(d3.scale.identity().domain([0, height]))
.on("brushstart", function(d) {
node.each(function(d) { d.previouslySelected = shiftKey && d.selected; });
})
.on("brush", function() {
var extent = d3.event.target.extent();
node.classed("selected", function(d) {
return d.selected = d.previouslySelected ^
(extent[0][0] <= d.x && d.x < extent[1][0]
&& extent[0][1] <= d.y && d.y < extent[1][1]);
});
})
.on("brushend", function() {
d3.event.target.clear();
d3.select(this).call(d3.event.target);
})
);
//applies force per step or 'tick'.
force[count].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; });
});
//with this it works partly
//for (var i = 0; i < 5000; ++i)force[count].tick();
count++;
};
function keydown() {
if (!d3.event.metaKey) switch (d3.event.keyCode) {
case 38: nudge( 0, -1); break; // UP
case 40: nudge( 0, +1); break; // DOWN
case 37: nudge(-1, 0); break; // LEFT
case 39: nudge(+1, 0); break; // RIGHT
}
shiftKey = d3.event.shiftKey || d3.event.metaKey;
}
function keyup() {
shiftKey = d3.event.shiftKey || d3.event.metaKey;
}
edit: updated the code after the comments, still the same problem.
i am working on force layout only, with many graphs at same time.
1 You don't need to have a count variable for each graph.
2 Don't make these variable(force, svg, graph) as array. There is no need for it. just declare them above as (var svg;) and further on. As you call the function, it automatically makes its different copy and DOM maintain them separately. So every variable you are using in graph, make it declare on top of function.
3 You are drawing all the graphs at same time, so as the new one is called, the previous one stops from being making on svg, that's why only last graph built successfully. So draw them after small time intervals.
<html>
<script>
function draw_graphs(graphs){
var color = d3.scale.category20();
var force;
var div;
var svg;
var graph;
var link;
var node;
var width = 360;
var height = 360;
var brush = new Array();
var shiftKey;
//loop through the different subsystems in the json-file
for(name_subsystem in graphs) {
//add a div for each subsystem
div = document.createElement("div");
div.style.width = "360px";
div.style.height = "360px";
div.style.cssFloat="left";
div.id = name_subsystem;
document.body.appendChild(div);
//force is called. all attributes with default values are noted. see API reference on github.
force = d3.layout.force()
.size([width, height])
.linkDistance(20)
.linkStrength(1)
.friction(0.9)
.charge(-30)
.theta(0.8)
.gravity(0.1);
div.appendChild(document.createTextNode(name_subsystem));
//create the svg rectangle in which other elements can be visualised
svg = d3.select("#"+name_subsystem)
.on("keydown.brush", keydown)
.on("keyup.brush", keyup)
.append("svg")
.attr("width", width)
.attr("height", height)
.attr("id",name_subsystem);
brush = svg.append("g")
.datum(function() { return {selected: false, previouslySelected: false}; })
.attr("class", "brush");
//force is started
force
.nodes(graphs[name_subsystem].nodes)
.links(graphs[name_subsystem].links)
.start();
//link elements are called, joined with the data, and links are created for each link object in links
link = svg.selectAll(".link")
.data(graphs[name_subsystem].links)
.enter().append("line")
.attr("class", "link")
.style("stroke-width", function(d) { return Math.sqrt(d.thickness); })
.style("stroke", function(d){
if (d.linktype === 'reactant'){
return "black";
} else {
return "red";
}
});
//node elements are called, joined with the data, and circles are created for each node object in nodes
node = svg.selectAll(".node")
.data(graphs[name_subsystem].nodes)
.enter().append("circle")
.attr("class", "node")
//radius
.attr("r", 5)
//fill
.attr("fill", function(d) {
if (d.type === 'metabolite') {
return "blue";
} else {
return "red";
}
})
.on("mousedown", function(d) {
if (!d.selected) { // Don't deselect on shift-drag.
if (!shiftKey) node.classed("selected", function(p) { return p.selected = d === p; });
else d3.select(this).classed("selected", d.selected = true);
}
})
.on("mouseup", function(d) {
if (d.selected && shiftKey) d3.select(this).classed("selected", d.selected = false);
})
.call(force.drag()
.on("dragstart",function dragstart(d){
d.fixed=true;
d3.select(this).classed("fixed",true);
})
);
//gives titles to nodes. i do not know why this is separated from the first node calling.
node.append("title")
.text(function(d) { return d.name; });
//enable brushing of the network
brush.call(d3.svg.brush()
.x(d3.scale.identity().domain([0, width]))
.y(d3.scale.identity().domain([0, height]))
.on("brushstart", function(d) {
node.each(function(d) { d.previouslySelected = shiftKey && d.selected; });
})
.on("brush", function() {
var extent = d3.event.target.extent();
node.classed("selected", function(d) {
return d.selected = d.previouslySelected ^
(extent[0][0] <= d.x && d.x < extent[1][0]
&& extent[0][1] <= d.y && d.y < extent[1][1]);
});
})
.on("brushend", function() {
d3.event.target.clear();
d3.select(this).call(d3.event.target);
})
);
//applies force per step or 'tick'.
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; });
});
//with this it works partly
//for (var i = 0; i < 5000; ++i)force[count].tick();
};
function keydown() {
if (!d3.event.metaKey) switch (d3.event.keyCode) {
case 38: nudge( 0, -1); break; // UP
case 40: nudge( 0, +1); break; // DOWN
case 37: nudge(-1, 0); break; // LEFT
case 39: nudge(+1, 0); break; // RIGHT
}
shiftKey = d3.event.shiftKey || d3.event.metaKey;
}
function keyup() {
shiftKey = d3.event.shiftKey || d3.event.metaKey;
}
}
</script>
<script>
$(document).ready(function() {
draw_graphs("pass here the json file");
// this will drawn 2nd graph after 1 second.
var t = setTimeout(function(){
draw_graphs("pass here json file");
}, 1000)
});
Here the code I finally used with the help of the comments above, maybe helpful for others as well:
<script type="text/javascript" src="d3_splitted_var.json"></script>
<script>
function draw_graphs(name_subsystem){
var force;
var div;
var svg;
var link;
var node;
var width = 360;
var height = 360;
var r=5;
var brush = new Array();
var shiftKey;
//add a div for each subsystem
div = document.createElement("div");
div.style.width = "360px";
div.style.height = "360px";
div.style.cssFloat="left";
div.id = name_subsystem;
document.body.appendChild(div);
force = d3.layout.force()
.size([width, height])
.linkDistance(20)
.linkStrength(1)
.friction(0.9)
.charge(-50)
.theta(0.8)
.gravity(0.1);
div.appendChild(document.createTextNode(name_subsystem));
//create the svg rectangle in which other elements can be visualised
svg = d3.select("#"+name_subsystem)
.append("svg")
.attr("width", width)
.attr("height", height)
.attr("id",name_subsystem);
//force is started
force
.nodes(graphs[name_subsystem].nodes)
.links(graphs[name_subsystem].links)
.start();
//link elements are called, joined with the data, and links are created for each link object in links
link = svg.selectAll(".link")
.data(graphs[name_subsystem].links)
.enter().append("line")
.attr("class", "link")
.style("stroke-width", function(d) { return Math.sqrt(d.thickness); })
.style("stroke", function(d){
if (d.linktype === 'reactant'){
return "black";
} else {
return "red";
}
});
//node elements are called, joined with the data, and circles are created for each node object in nodes
node = svg.selectAll(".node")
.data(graphs[name_subsystem].nodes)
.enter().append("circle")
.attr("class", "node")
//radius
.attr("r", r)
//fill
.attr("fill", function(d) {
if (d.type === 'metabolite') {
return "blue";
} else {
return "red";
}
})
.call(force.drag()
.on("dragstart",function dragstart(d){
d.fixed=true;
d3.select(this).classed("fixed",true);
})
);
//gives titles to nodes. i do not know why this is separated from the first node calling.
node.append("title")
.text(function(d) { return d.name; });
//applies force per step or 'tick'.
force.on("tick", function() {
node.attr("cx", function(d) { return d.x = Math.max(r, Math.min(width - r, d.x)); })
.attr("cy", function(d) { return d.y = Math.max(r, Math.min(height - r, d.y)); });
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; });
});
};
for(name_subsystem in graphs) {
draw_graphs(name_subsystem);
}
</script>
Note: graphs is the name of the variable in my json file. You need to include the d3-library.
I've been working with d3 for a while now in an attempt to create an interactive ecosystem explorer tool that maps out relationships between species. Recently I've tried adding a feature that lets users show or hide species (nodes) in the force directed graph. I've tried following other examples and although the code works - it only works inconsistently.
For some reason, when I add back a node, it sometimes isn't visible. The graph moves as if the node is added but then it doesn't show up. I have the feeling that it is adding it but then the node is being hidden again in the force.on("tick") code but have no idea why. I've posted the relevant code below and would really appreciate any ideas! The toggleNode function determines whether a node should be shown or hidden - basically just splicing or adding to the nodes array. I keep the data in an array called dataset that stores a flag to indicate whether a node is visible or not.
var force = d3.layout.force()
.gravity(.05)
.distance(100)
.charge(-100)
.size([w, h]);
var nodes = force.nodes(), links = force.links(); // arrays to hold data
var vis = d3.select("#chart").append("svg:svg")
.attr("width", w)
.attr("height", h);
force.on("tick", function() {
vis.selectAll("circle.node")
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
vis.selectAll("line.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; });
});
function restart() {
// UPDATE nodes
var node = vis.selectAll("circle.node")
.data(nodes, function(d) { return d.id;});
// ENTER new nodes
var nodeEnter = node.enter().append("svg:circle")
.attr("class", "node")
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.style("fill", function(d) { return groupColors[(d.group)-1]; })
.style("stroke","fff")
.style("stroke-width",1)
.on("mousedown", function(d) {
clickNode(d);
})
.call(force.drag);
// REMOVE deleted nodes
var nodeExit = node.exit().remove();
force.start();
}
// Add the nodes and links to the vis
function createVis(json) {
dataset = json; // store data in global
for (var i = 0; i < dataset['nodes'].length; i++) {
// fill node info
nodes.push(dataset['nodes'][i]);
}
restart();
}
// Remove node and associated links.
function toggleNode(nodeKey,eol_id) {
console.log(nodeKey + ': ' + eol_id);
var tLabel; // value for toggle label
if ( dataset['nodes'][nodeKey]['isHidden'] == 0 ) {
// node is visible, so hide it
tLabel = 'Show';
for( var k=0; k<nodes.length; k++ ) {
if ( nodes[k]['eol_id'] == eol_id ) {
nodes.splice(k, 1); // remove this node
break;
}
}
dataset['nodes'][nodeKey]['isHidden'] = 1;
console.log('node removed: ' + nodeKey);
} else {
dataset['nodes'][nodeKey]['isHidden'] = 0;
nodes.push(dataset['nodes'][nodeKey]);
tLabel = 'Hide';
}
$('#primary_title_toggle').html(' ' + tLabel + '<br>');
restart();
}