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.
Related
I am trying to incorporate this example for adding/removing nodes from a Force Directed Graph. I am able to add nodes just fine, and for some reason I am also able to remove the nodes I have added without issues. However when I try to remove any of the original nodes, the node is not removed from the display, and it also breaks many other nodes' links. However, it and it's links are properly removed from the JSON based on the logs.
This example uses seemingly the same method as I do for removing nodes with splice.
Here also seems to use splice method though I don't fully follow the rest of what it is doing with filtering.
There is also this question that asks a somewhat similar thing though the answers only address adding.
I have tried exit/removing after the append which did not work. I have also tried using .insert instead of .append. The console.log during the update() function prints the JSON being used and it shows that nodes and links are all properly removed, but the display doesn't reflect that.
Relevant code is below.
Initializing/updating graph
//Get the SVG element
var svg = d3.select("svg");
var width = 960, height = 600;
var color = d3.scaleOrdinal(d3.schemeCategory20);
var link = svg.append("g").selectAll(".link");
var node = svg.append("g").selectAll(".node");
var label = svg.append("g").selectAll(".label");
//Begin the force simulation
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function (d) { return d.id; }).distance(50).strength(0.3))
.force("charge", d3.forceManyBody().strength(-15))
.force("center", d3.forceCenter(width / 2, height / 2));
//Highlight variables
var highlight_color = "blue";
var tHighlight = 0.05;
var config;
var linkedByIndex = {};
//Get the data
d3.json("/../../data.json", function (data) {
//if (!localStorage.graph)
//{
localStorage.graph = JSON.stringify(data);
//}
update();
forms();
});
function update() {
config = JSON.parse(localStorage.graph);
console.log(JSON.stringify(config));
linkedByIndex = {};
//Create an array of source,target containing all links
config.links.forEach(function (d) {
linkedByIndex[d.source + "," + d.target] = true;
linkedByIndex[d.target + "," + d.source] = true;
});
//Draw links
link = link.data(config.links);
link.exit().remove();
link = link.enter().append("line")
.attr("class", "link")
.attr("stroke-width", 2)
.attr("stroke", "#888")
//.attr("opacity", function (d) { if (d.target.radius > 7) { return 1 }; return 0; })
.merge(link);
node = node.data(config.nodes);
node.exit().remove();
node = node.enter().append("circle")
.attr("class", "node")
.attr("r", function(d) { return d.radius; })
.attr("fill", function (d) { return color(d.id); })
.attr("stroke", "black")
// .attr("pointer-events", function (d) { if (d.radius <= 7) { return "none"; } return "visibleAll"; })
// .attr("opacity", function (d) { if (d.radius <= 7) { return 0; } return 1; })
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended))
.on("mouseover", mouseOver)
.on("mouseout", mouseOut)
.merge(node);
label = label.data(config.nodes);
label.exit().remove();
label = label.enter().append("text")
.attr("class", "label")
.attr("dx", function (d) { return d.radius * 1.25; })
.attr("dy", ".35em")
.attr("opacity", function (d) { if (d.radius <= 7) { return 0; } return 1; })
.attr("font-weight", "normal")
.style("font-size", 10)
.text(function (d) { return d.id; })
.merge(label);
//Add nodes to simulation
simulation
.nodes(config.nodes)
.on("tick", ticked);
//Add links to simulation
simulation.force("link")
.links(config.links);
simulation.alphaTarget(0.3).restart();
}
//Animating by ticks function
function ticked() {
node
.attr("cx", function (d) { return d.x = Math.max(d.radius, Math.min(width - d.radius, d.x)); })
.attr("cy", function (d) { return d.y = Math.max(d.radius, Math.min(height - d.radius, 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; });
label
.attr("x", function (d) { return d.x = Math.max(d.radius, Math.min(width - d.radius, d.x)); })
.attr("y", function (d) { return d.y = Math.max(d.radius, Math.min(height - d.radius, d.y)); });
}
//Using above array, check if two nodes are linked
function isConnected(node1, node2) {
return linkedByIndex[node1.id + "," + node2.id] || node1.index == node2.index;
}
Adding/removing nodes and links.
ADDING works perfectly fine for both nodes and links this way.
function newNode(name, rad)
{
if (name == "")
{
alert("Must specify name");
return;
}
console.log("Adding node with name: " + name + " and radius: " + rad);
var temp = JSON.parse(localStorage.graph);
temp.nodes.push({ "id": name, "radius": Number(rad) });
localStorage.graph = JSON.stringify(temp);
update();
}
function newLink(source, target)
{
var foundSource = false;
var foundTarget = false;
if (source == "" && target == "")
{
alert("Must specify source and target");
return;
}
else if(source == "")
{
alert("Must specify source");
return;
}
else if (target == "")
{
alert("Must specify target")
return;
}
var temp = JSON.parse(localStorage.graph);
for (var i=0; i < temp.nodes.length; i++)
{
if(temp.nodes[i]['id'] === source)
{
foundSource = true;
}
if(temp.nodes[i]['id'] === target)
{
foundTarget = true;
}
}
if (foundSource && foundTarget) {
temp.links.push({ "source": source, "target": target });
localStorage.graph = JSON.stringify(temp);
update();
}
else {
alert("Invalid source or target");
return;
}
return;
}
function removeLink(linkSource, linkTarget)
{
}
function removeNode(nodeName)
{
var temp = JSON.parse(localStorage.graph);
var found = false;
if (nodeName == "")
{
alert("Must specify node name");
return;
}
for(var i=0; i<temp.nodes.length; i++)
{
if(temp.nodes[i]['id'] === nodeName)
{
console.log("Removing node: " + nodeName);
found = true;
temp.nodes.splice(i, 1);
temp.links = temp.links.filter(function (d) { return d.source != nodeName && d.target != nodeName; });
}
}
if(!found)
{
alert("Node does not exist");
return;
}
localStorage.graph = JSON.stringify(temp);
update();
}
JSON Data is in the format
{
"nodes":[
{
"id": "id1",
"radius": 5},
{
"id: "id2",
"radius": 6}
],
"links":[{
"source": "id1",
"target": "id2"
]
}
By default d3 joins data by index, so after you remove node, it assigns data incorrectly. The solution is to pass second argument to .data function. You need to replace
node = node.data(config.nodes);
with
node = node.data(config.nodes, d => d.id);
You can also do this for links, but if their styles are equal (i.e. they only differ by x1, x2, y1 and y2) it will make no difference.
Update: you should do this for labels as well
label = label.data(config.nodes, d => d.id);
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.
I am attempting to make a tree viewing web application. I am using the d3 tree layout to generate my trees. What I would like to be able to do is scale the size of the tree in either axis. In otherwords being able to scale the x and y axis of the viewer. The would effectively just increase either the vertical distance between nodes or increase the length of the branches.
I have been unable to accomplish this. I have attempted to change the nodeSize() of the tree layout, and then update which had no effect. I have also attempted to modify the x or y coordinate of each node which again had no effect. I have been completely unable to modify the physical layout of the tree once it has been rendered.
I have read and attempted the fixes in the following threads:
Dynamically resize the d3 tree layout based on number of childnodes
D3 Tree Layout Separation Between Nodes using NodeSize
But they did not solve my problem.
I am implementing a TreeGenerator object, which handles all of the rendering of the SVG, using prototyped functions. I am going for a full screen feeling, so the TreeGenerator appends an SVG to a div with width: 100%; height: 100%. I should node that I have implemented both zooming and panning functionality.
Here is the constructor:
TreeGenerator = function(target, treeString) {
// Set up everything here
// colors
this.nodeColor = "#3B6073";
this.highlightColor = "#F22248";
this.searchColor = "#0DFFC2";
this.nodeHeight = 5;
this.nodeLength = 20;
this.zoomX = 1;
this.zoomY = 5;
this._winHeight = window.innerHeight;
this._winWidth = window.innerWidth;
//this.xScale = d3.scale.linear().domain([0,100]).range([0, this._winWidth])
//this.yScale = d3.scale.linear().domain([0,100]).range([0, this._winHeight])
// keep the matching results from last search
this._searchResults = [];
// path lengths
this.maxPathLength = 0;
this._loadSVG(target);
this._tree(treeString);
}
And here are the two functions _loadSVG and _tree which are called from the constructor:
TreeGenerator.prototype._loadSVG = function(target) {
var zoom = d3.behavior.zoom()
//.size([this._winWidth, this._winHeight])
.scaleExtent([this.zoomX,this.zoomY])
//.x(this.xScale)
//.y(this.yScale)
//.center([height/2, width/2])
.on("zoom", zoomed);
var drag = d3.behavior.drag()
.origin(function(d) { return d; })
.on("dragstart", dragstarted)
.on("drag", dragged)
.on("dragend", dragended);
this.svg = d3.select(target, ":first-child").append("svg")
.append("g")
//.attr("transform", "translate("+width / 2+","+height / 2+")")
.call(zoom)
.on("dblclick.zoom", null)
var rect = this.svg.append("rect")
.attr("width", "100%")
.attr("height", "100%")
.style("fill", "none")
.style("pointer-events", "all");
this.container = this.svg.append("g")
// scope it for d3 funcs
var container = this.container
var self = this;
function dottype(d) {
d.x = +d.x;
d.y = +d.y;
return d;
}
function zoomed() {
container.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
}
function dragstarted(d) {
d3.event.sourceEvent.stopPropagation();
d3.select(this).classed("dragging", true);
}
function dragged(d) {
d3.select(this).attr("cx", d.x = d3.event.x).attr("cy", d.y = d3.event.y);
}
function dragended(d) {
d3.select(this).classed("dragging", false);
}
}
And _tree:
TreeGenerator.prototype._tree = function (treeString) {
// needs an array for d3
this.treeData = this.parseTree(treeString);
this.root = this.treeData[0];
// set up the layout
this.tree = d3.layout.tree()
.nodeSize([this.nodeHeight,this.nodeLength]);
// set path dists
this._setPathDist();
this.yScale = d3.scale.linear().domain([0, this.maxPathLength]).range([0, 20]);
this.xScale = d3.scale.linear().domain([1,100]).range([10, 30]);
var self = this;
update(this.root);
function update(source) {
var i = 0;
// generator for paths from
// node ---> node
var horizontal = d3.svg.line().interpolate('step-before')
.x(function (d) { return d.x; })
.y(function (d) { return d.y; });
// Compute the new tree layout.
var nodes = self.tree.nodes(source).reverse()
nodes.forEach(function (d) {
if(!d.pathLength == 0) {
d.y = d.y * self.yScale(d.pathLength);
}
else if(d.children != undefined) {
d.y += 5;
}
});
links = self.tree.links(nodes);
// Declare the nodes
var node = self.container.selectAll("g.node")
.data(nodes, function(d) { return d.id || (d.id = ++i); })
// Enter the nodes.
doubleclickTimer = false // dont ask
var nodeEnter = node.enter().append("g")
.attr("class", "node")
.attr("id", function(d) { return "node-"+d.id.toString(); })
.attr("transform", function(d) {
return "translate(" + d.y + "," + d.x + ")"; })
.on("click", clicked);
nodeEnter.append("circle")
.attr("r", 1)
.style("fill", self.nodeColor)
nodeEnter.append("text")
.attr("x", function(d) {
return d.children || d._children ? -1 : 1; })
.attr("y", function(d) {
return d.children == undefined ? 1 : -1;
})
.attr("text-anchor", function(d) {
return d.children || d._children ? "end" : "start"; })
.text(function(d) { return d.name; })
.style("fill-opacity", 1);
// try and update
//var nodeUpdate = node.update()
// .attr("x", function(d) { return d.x });
// Declare the links
var link = self.container.selectAll("path.link")
.data(links, function(d) { return d.target.id; });
// Enter the links.
link.enter().insert("path", "g")
.attr("class", "link")
.attr("d", function(d) {
return horizontal([
{y: d.source.x, x: d.source.y},
{y: d.target.x, x: d.target.y}
]);
});
function clicked(d) {
// need the g group for highlight
node = this;
// if double click timer is active, this click is the double click
if ( doubleclickTimer )
{
clearTimeout(doubleclickTimer);
doubleclickTimer = false;
collapse(d);
}
// otherwise, what to do after single click (double click has timed out)
else {
doubleclickTimer = setTimeout( function() {
doubleclickTimer = false;
highlight(node, d);
}, 175);
}
}
function highlight(node,d) {
// we want to bold the text
// and color the node
self._unhighlightNode();
// toggle if clicking again
if(self._highlightedNode == node) {
self._highlightedNode = undefined;
self.selectedNode = undefined;
return;
}
// set the new one
var circle = d3.select(node).select("circle");
var text = d3.select(node).select("text");
circle.style("fill", self.highlightColor);
text.style("font-size","4px");
// update the pointer
self._highlightedNode = node;
self.selectedNode = d;
}
function collapse(d) {
}
}
};
I know the code is a bit messy, but it is just full of commented out things I did in an attempt to fix this issue. I appreciate any help that can be offered. Hopefully I included enough info.
I am having some issues with d3js and I can't figure out what is going on. The idea is to draw initial graph from some endpoint data (first img), that's fine works well. Each node is clickable, on click ajax call is made for that node and data is returned, based on some criteria at that point nodes.push(xx), links.push(xx) happens to add new nodes and restart() is called to draw new nodes and links. The issue is that the main graph is doing the correct thing (Not showed on screenshots as I had to put fake data on the first graph i.e. calling an endpoint /record/id/first doesn't return a data) but there are bunch of random nodes showing up in the right bottom corner.
You can also see on the example below, even if the data doesn't change after clicking on first/second/third something wrong goes with node.enter() after restart() with the same data passed in...
JS FIDDLE: http://jsfiddle.net/5754j86e/
var w = 1200,
h = 1200;
var nodes = [];
var links = [];
var node;
var link;
var texts;
var ids = [];
var circleWidth = 10;
var initialIdentifier = "marcin";
nodes = initialBuildNodes(initialIdentifier, sparql);
links = initialBuildLinks(sparql);
//Add SVG
var svg = d3.select('#chart').append('svg')
.attr('width', w)
.attr('height', h);
var linkGroup = svg.append("svg:g").attr("id", "link-group");
var nodeGroup = svg.append("svg:g").attr("id", "node-group");
var textGroup = svg.append("svg:g").attr("id", "text-group");
//Add Force Layout
var force = d3.layout.force()
.size([w, h])
.gravity(.05)
.charge(-1040);
force.linkDistance(120);
restart();
function restart() {
force.links(links)
console.log("LINKS ARE: ", links)
link = linkGroup.selectAll(".link").data (links);
link.enter().append('line')
.attr("class", "link");
link.exit().remove();
force.nodes(nodes)
console.log("NODES ARE: ", nodes)
node = nodeGroup.selectAll(".node").data (nodes);
node.enter().append("svg:g")
.attr("class", "node")
.call(force.drag);
node.append('circle')
.attr('cx', function(d) { return d.x; })
.attr('cy', function(d) { return d.y; })
.attr('r', circleWidth )
.attr('fill', function(d, i) {
if (i>0) { return palette.pink }
else { return palette.blue }
})
.on("click", function(d) {
nodeClicked (d);
})
.on('mouseenter', function(d){
nodeMouseEnter(d)
})
.on('mouseout', function(d){
nodeMouseOut(d)
});
node.exit().remove();
var annotation = textGroup.selectAll(".annotation").data (nodes);
annotation.enter().append("svg:g")
.attr("class", "annotation")
.append("text")
.attr("x", function(d) { return d.radius + 4 })
.attr("y", ".31em")
.attr("class", "label")
.text(function(d) { return d.name; });
annotation.exit().remove();
force.start();
}
function nodeClicked (d) {
// AJAX CALL happens here and bunch of nodes.push({name: "new name"}) happen
}
force.on('tick', function(e) {
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, i) {
return 'translate('+ d.x +', '+ d.y +')';
})
svg.selectAll(".annotation").attr("transform", function(d) {
var labelx = d.x + 13;
return "translate(" + labelx + "," + d.y + ")";
})
});
Okay I got it, based on the docs (https://github.com/mbostock/d3/wiki/Selections#enter):
var update_sel = svg.selectAll("circle").data(data)
update_sel.attr(/* operate on old elements only */)
update_sel.enter().append("circle").attr(/* operate on new elements only */)
update_sel.attr(/* operate on old and new elements */)
update_sel.exit().remove() /* complete the enter-update-exit pattern */
From my code you can see I do enter() and then once again I add circle on node in a separate statement.
node = nodeGroup.selectAll(".node").data (nodes);
node.enter().append("svg:g")
.attr("class", "node")
.call(force.drag);
node.append('circle')
.attr('cx', function(d) { return d.x; })
.attr('cy', function(d) { return d.y; })
.attr('r', circleWidth )
.attr('fill', function(d, i) {
if (i>0) { return palette.pink }
else { return palette.blue }
});
Adding circle should be within the scope of enter() otherwise it happens to all nodes not only the new nodes therefore it should be :
node.enter().append("svg:g")
.attr("class", "node")
.call(force.drag)
.append('circle')
.attr('cx', function(d) { return d.x; })
.attr('cy', function(d) { return d.y; })
.attr('r', circleWidth )
.attr('fill', function(d, i) {
if (i>0) { return palette.pink }
else { return palette.blue }
});
I'm adding nodes to a D3 tree layout at runtime but when the new child nodes are inserted, the original child is pushed over to the very left. I would like the original child to be in the middle (or close to) of the group of children so that if the graph is something like :
Parent
Child C
adding additional nodes A,B,D, and E results in a graph like this:
Parent
ChildA ChildB ChildC ChildD ChildE
rather than this:
Parent
ChildC ChildA ChildB ChildD ChildE
If relevant, code for this update function is below:
function update(record_to_add, parent) {
if (nodes.length >= 500) return clearInterval(timer);
// Add a new node to a random parent.
var n = {id: nodes.length, Username: record_to_add.Username},
p = nodes[parent];
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; });
nodes.forEach(function (d) {
});
// Add entering links in the parent’s old position.
link.enter().insert("path", ".node")
.attr("class", "link")
.attr("d", function(d) {
var o = {x: d.source.px, y: d.source.py};
return diagonal({source: o, target: o});
});
node.enter().insert("text")
.attr("x", function(d) { return (d.parent.px);})
.attr("y", function(d) { return (d.parent.py);})
.text(function(d) { return d.Username; });
// Add entering nodes in the parent’s old position.
node.enter().append("circle", "g")
.attr("class", "node")
.attr("r", 10)
.attr("cx", function(d) { return d.parent.px; })
.attr("cy", function(d) { return d.parent.py; });
node.on("mousedown", function (d) {
var g = d3.select(this); // The node
// The class is used to remove the additional text later
console.log(d.Username);
if (d.id == null)
{
console.log("ASDgasd");
}
else
{
try {
downstream_length =
DownstreamRecords[d.Username].length;
}
catch(err) {
downstream_length = 0;
}
for (var i = 0; i < downstream_length; ++i)
{
update(DownstreamRecords[d.Username][i], d.id);
}
}
});
node.on("mouseover", function (d) {
var g = d3.select(this); // The node
// The class is used to remove the additional text later
var info = g.append('text')
.classed('info', true)
.attr('x', 20)
.attr('y', 10)
.text('More info');
});
// 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; });
t.selectAll("text")
.style("fill-opacity", 1)
.attr("x", function(d) { return d.px = d.x + 20; })
.attr("y", function(d) { return d.py = d.y; });
}
rather than using push for adding child use
arry.splice(index, 0, newObject);
so you can add new child on your selected position but you have put some validation like lenth or array and index point etc.
like this
if (p.children) p.children.push(n); else p.children = [n];
replace it with
if (p.children){
p.children.splice(0, 0, n);
// or you can do some calculation according to number of child
available and made index
//p.children.push(n);
}
else{ p.children = [n];}