I've been trying to get my force directed graph to go faster/smoother, and it seems commenting this part out of the "tick" function does the trick. Of course, it also makes all the edges disappear although the nodes still move together as if attached by invisible threads.
I have about 2-3 hundred nodes in a network-ish graph and when setting the opacity of each element I also check it's weight and if it's 1 I remove it.I repeat this for all nodes and text labels. (and edges using d.target.weight)
Is it just a number of nodes that weighing down everything? After removing the elements down to 20 or so why is it still so slow? Do I really have to piggyback my removal onto .style("opacity", function(d){//do stuff, return 1;})?
force.on("tick", function() {
// edges.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; })
// .style("stroke", function(d){
// return d.source.color;
// });
nodes.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
text.attr("x", function(d) { return d.x; })
.attr("y", function(d) { return d.y; });
});
functions for drawing the svg elements if it helps:
//Create edges as lines
var edges = group.selectAll("line")
.data(dataset.edges)
.enter()
.append("line")
.style("stroke", function(d,i){
if (d.target.weight === 1)
d3.select(this).remove();
return "#FFFFFF";
})
.style("opacity",0.5)
.style("stroke-width", 2);
//Create nodes as circles
var nodes = group.selectAll("circle")
.data(dataset.nodes)
.enter()
.append("circle")
.style("opacity",0.8)
.attr("r", function(d,i){
if (d.weight === 1)
d3.select(this).remove();
return nodeScale(d.weight * 2);
})
.style("fill", function(d, i) {
return d.color;
})
.call(force.drag);
var text = group.selectAll("text")
.data(dataset.nodes)
.enter()
.append("text")
.attr("fill","black")
.style("font-size",function(d){
return d.size;
})
.style("text-anchor", "middle")
.text(function(d){
return d.name;
})
.style("opacity",function(d){
if (d.weight === 1)
d3.select(this).remove();
else
return 0.8;
})
.on("mouseover",function(){
d3.select(this)
.style("opacity",1)
.style("font-size", 25);
})
.on("mouseout",function(){
d3.select(this)
.style("font-size", function(d) { return d.size; });
})
.call(force.drag);
Also the initiation function, became quite random after I fiddle around with it a lot:
(I also have a slider for each one that I play with when rendered)
var force = d3.layout.force()
.nodes(dataset.nodes)
.links(dataset.edges)
.size([w, h])
.linkDistance([50])
.charge([-2000])
.friction(0.5)
.gravity(0.5)
.theta(0.5)
.start();
I believe the problem was that removing an element still doesn't affect the actual dataset being used which is composed of hundred of nodes in this case.
Changing the code to removing the element from the dataset and using exit().remove() makes it go faster.
Related
I was trying to add labels to circles that are constantly moving in D3. One of the ways I found was to insert the label and the circle to a g tag. However, this causes errors whenever I want to EXIT the elements and merge them (which is not happening right now).
Is there any other way to do it? My graph right now seems to be with lower frames per second because it is not using the merge to update from the current x/y position to the new one.
The code is as follow:
var t = d3.transition()
.duration(0);
// JOIN new data with old elements.
var circles = g.append("g").selectAll('g')
.data(data.values)
.enter()
.append('g')
// EXIT elements
d3.selectAll("circle").transition()
.attr("class", "exit")
.remove();
// EXIT elements
d3.selectAll(".label")
.attr("class", "exit")
.remove();
// ENTER new elements present in new data.
// Circles
circles.append("circle")
.attr("class", "enter")
.attr("fill", function (d) { return color(d.country); })
.attr("cy", function (d) { return y(d.active_cases); })
.attr("cx", function (d) { return x(d.total_deaths) })
.merge(circles)
.transition(t)
.attr("r", 15)
// Labels
circles.append("text")
.attr("class", "label")
.attr("font-size", 10)
.style("text-anchor", "middle")
.style("fill", "white")
.attr("x", function (d) { return x(d.total_deaths) })
.attr("y", function (d) { return 3 + y(d.active_cases) })
.merge(circles)
.text(function (d) { return d.country })
Using this I was able to add names to the map: Add names of the states to a map in d3.js
Now, how do I rotate those text as it is overlapping with each other.
For those who are looking for the solution.
Adding this solved it. It produces composition matrix.
.attr('transform', function(d) {
return 'translate('+path.centroid(d)[0]+','+path.centroid(d)[1]+') rotate(-45);
})
Here is the updated code:
function draw(){
d3.json("readme.json", function(json) {
g.selectAll("path")
.data(json.features)
.enter()
.append("path")
.attr("d", path)
.on("click", click);
g.selectAll("text")
.data(json.features)
.enter()
.append("svg:text")
.text(function(d){
return d.properties.name;
})
// .attr("x", function(d){
// return path.centroid(d)[0];
// })
// .attr("y", function(d){
// return path.centroid(d)[1];
// })
.attr("text-anchor","middle")
.attr('font-size','6pt')
// added
.attr('transform', function(d) {
return 'translate('+path.centroid(d)[0]+','+path.centroid(d)[1]+') rotate(-45);
})
});
}
I draw a graph from a set of nodes and links using the following update function with nodes and links arrays defined earlier in the code.
function update() {
var link = svg.selectAll(".link")
.data(links);
link.enter().insert("line")
.attr("class", "link");
var node = svg.selectAll(".node")
.data(nodes);
node.enter().append("svg:g")
.attr("class", "node")
.attr("id", function(d) { return d.name })
.on("dblclick", dblclick)
.on("mouseover", function(d) {drawTags(tags, d.name);})
.on("mouseout", function(d) {
d3.select("#"+d.name).selectAll(".tool")
.transition()
.delay(800)
.remove();
})
.call(force.drag);
node.append("circle")
.attr("class", "circle")
.attr("r", 40);
// display name in nodes if node structure
node.append("text")
.attr("text-anchor", "middle")
.attr("dy", ".35em")
.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 + ")";
});
});
force.start();
}
On dynamic update of the graph, after double click event on a node, I fetch additional nodes and links and display them. So far, I was able to append nodes and links to their corresponding arrays, hence update drawing. However, the update function would loop through all elements for the display.
Therefore I would like to pass newNodes and newEdges to the update function and only draw new elements. The issue is that new elements replace existing ones in the svg code rather than to be appended.
Is there a way to append new elements to previously existing ones in the drawing?
Full code is available on fiddle (https://jsfiddle.net/pducrot/4eyb81kx/) mouse over a node and click on any appearing tag to see what's happening.
I am using a pack layout in d3.js (https://github.com/mbostock/d3/wiki/Pack-Layout) and load in a json file with a parent-child structure.
My question is probably a very trivial one: I would like to append a circle or an image(or maybe a rectangle) depending on whether d.children returns parent or child (where child in basically a leaf node).
This is a bit of the code that appends circles to all nodes:
vis.selectAll("circle")
.data(nodes)
.enter().append("svg:circle")
.attr("class", function(d) { return d.children ? "parent" : "child"; })
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("r", function(d) { return d.r; });
Does anyone have any suggestions?
Thank you,
Use a filter function:
// if you have children, append a circle
node.filter(function(d){
return d.children;
})
.append("circle")
.attr("r", function(d) { return d.r; });
// if you don't have children, append a rect
node.filter(function(d){
return !d.children;
})
.append("rect")
.attr("width", function(d) { return d.r; })
.attr("height", function(d) { return d.r; })
.attr("x", function(d) { return -d.r/2; })
.attr("y", function(d) { return -d.r/2; });
Example here.
I am new to D3 and I am working with code from here. I changed the code so I can add new nodes (neighbors) and edges to them with data from MySql upon click on a node, which is why part of the node code is in start(). I want to append text labels to the nodes, and from some googling I understand that both the circle element and the text element needs to be within a g. However, when I do this, I get an error from d3.js on line 742:
Uncaught NotFoundError: Failed to execute 'insertBefore' on 'Node':
The node before which the new node is to be inserted is not a child of
this node
Why is this and how do I fix it to get what I want, while preserving the addNode functionality?
Here is my code:
var width = 960,
height = 500;
var color = d3.scale.category10();
var nodes = [],
links = [];
var force = d3.layout.force()
.nodes(nodes)
.links(links)
.charge(-400)
.linkDistance(120)
.size([width, height])
.on("tick", tick);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var node = svg.selectAll(".node")/*.append("g")*/,
link = svg.selectAll(".link");
function start() {
link = link.data(force.links(), function(d) { return d.source.id + "-" + d.target.id; });
link.enter().insert("line", ".node").attr("class", "link");
link.exit().remove();
node = node.data(force.nodes(), function(d) { return d.id;});
node.enter()/*.append("g")*/
.append("circle")
.attr("class", function(d) { return "node " + d.id; })
.attr("id", function(d) { return d.id; })
.attr("r", 8)
.on("click", nodeClick)
.append("text")
.text(function(d) {return d.id; });
node.exit().remove();
force.start();
}
function nodeClick() {
var node_id = event.target.id;
handleClick(node_id, "node");
}
function tick() {
node.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return 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; });
}
The commented-out append("g") indicates where I tried to place it (separate attempts).
You want to cache the d3 selector before appending the text.
node.enter().append("g")
.append("circle")
.attr("class", function(d) { return "node " + d.id; })
.attr("id", function(d) { return d.id; })
.attr("r", 8)
.on("click", nodeClick)
.append("text")
.text(function(d) {return d.id; });
That'll work but create a xml structure like this:
<g>
<circle>
<text>...</text>
</circle>
</g>
What you want is:
<g>
<circle>...</circle>
<text>...</text>
</g>
To achieve that, you must insert one step:
var g = node.enter().append("g");
g.append("circle")
.attr("class", function(d) { return "node " + d.id; })
.attr("id", function(d) { return d.id; })
.attr("r", 8)
.on("click", nodeClick);
g.append("text")
.text(function(d) {return d.id; });