D3 v4 Update-Pattern for Groups - javascript

Given the following layout:
<g>
... // many nodes
<g>
<circle></circle>
<text></text>
</g>
...
</g>
How would a correct update pattern look like in d3 v4?
What do I have to use as parameter in merge(?), how often do I have to call merge (only on node? node + circle + text ?)
I created an working example on fiddle: https://jsfiddle.net/cvvfsg97/6/
Code:
function update(items) {
node = nodeLayer.selectAll(".node")
.data(items, function(d) { return d.id; })
node = node.enter() // insert
.append("g")
.attr("class", "node");
node.append("circle") // insert
.attr("r", 2.5)
.attr('class', 'circle')
.merge(nodeLayer.selectAll('.node > circle')) // is this correct?! // merge
.attr('fill', 'red') // just for testing purposes
.exit().remove(); // exit
node.append("text") // insert
.attr("dy", 3)
.text(function(d) { return d.name; })
.merge(nodeLayer.selectAll('.node > text')) // is this correct?! // merge
.attr('fill', 'green') // just for testing purposes
.exit().remove();
node.merge(nodeLayer.selectAll('.node')) // is this correct?! // merge
.attr('class', 'anotherClass')
.exit().remove(); // does not work // exit
}
Could someone bring some clarity in terms of how to use enter(), merge(), exit() in groups?
I potentially like to do changes in every stage for every element.
Update: I simplified the example, I don't need links or a force-layout. My question is only about the update-pattern, not about forces. The updated jsfiddle does not have the force-layout.

You are over complicating the pattern. Here's your update function written properly:
function update(items) {
var node = nodeLayer.selectAll(".node") // bind the data, this is update
.data(items, function(d) {
return d.id;
});
node.exit().remove(); // exit, remove the g
nodeEnter = node.enter() // enter, append the g
.append("g")
.attr("class", "node");
nodeEnter.append("circle") // enter, append the circle on the g
.attr("r", 2.5)
.attr('class', 'circle')
.attr('fill', 'red');
nodeEnter.append("text") // enter, append the text on the g
.attr("dy", 3)
.text(function(d) {
return d.name;
})
.attr('fill', 'green');
node = nodeEnter.merge(node); // enter + update on the g
node.attr('transform', function(d){ // enter + update, position the g
return 'translate(' + d.x + ',' + d.y + ')';
});
node.select("text") // enter + update on subselection
.text(function(d) {
return d.name;
});
}
Here it is running with multiple calls:
<!DOCTYPE html>
<html>
<head>
<script data-require="d3#4.0.0" data-semver="4.0.0" src="https://d3js.org/d3.v4.min.js"></script>
</head>
<body>
<script>
var nodeLayer = d3.select('body')
.append('svg')
.attr('width',500)
.attr('height',500);
update([
{
id: 1,
name: 'A',
x: Math.random() * 500,
y: Math.random() * 500
},{
id: 2,
name: 'B',
x: Math.random() * 500,
y: Math.random() * 500
},{
id: 3,
name: 'C',
x: Math.random() * 500,
y: Math.random() * 500
}
]);
setTimeout(function(){
update([
{
id: 1,
name: 'A',
x: Math.random() * 500,
y: Math.random() * 500
},{
id: 4,
name: 'This is a new name...',
x: Math.random() * 500,
y: Math.random() * 500
},{
id: 3,
name: 'C',
x: Math.random() * 500,
y: Math.random() * 500
}
]);
}, 3000);
function update(items) {
var node = nodeLayer.selectAll(".node")
.data(items, function(d) {
return d.id;
});
node.exit().remove(); // exit, remove the g
nodeEnter = node.enter() // enter the g
.append("g")
.attr("class", "node");
nodeEnter.append("circle") // enter the circle on the g
.attr("r", 2.5)
.attr('class', 'circle')
.attr('fill', 'red');
nodeEnter.append("text") // enter the text on the g
.attr("dy", 3)
.attr('fill', 'green');
node = nodeEnter.merge(node); // enter + update
node.attr('transform', function(d){
return 'translate(' + d.x + ',' + d.y + ')';
});
node.select("text")
.text(function(d) {
return d.name;
});
}
</script>
</body>
</html>

I've done this recently in my code - I use select(subSelector) on the current selection that contains the elements. In your example I would change it as follows:
function update(items) {
var node = nodeLayer.selectAll(".node")
.data(items, function(d) { return d.id; })
var nodeEnter = node.enter()
.append("g")
.attr("class", "node");
nodeEnter.append("circle")
.attr("r", 2.5)
.attr('class', 'circle')
.merge(node.select('circle'))
.attr('fill', 'red');
nodeEnter.append("text") // insert
.attr("dy", 3)
.text(function(d) { return d.name; })
.merge(node.select('text'))
.attr('fill', 'green');
// You only need to call remove on the group, all the other exit().remove() calls are superfluous
node.exit().remove();
simulation
.nodes(items);
}

var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height"),
nodeLayer = svg.append('g'),
node;
var list = [];
var links = [];
var simulation = d3.forceSimulation(list)
.force("charge", d3.forceManyBody().strength(-1000))
.force("link", d3.forceLink(links).distance(200))
.force("center", d3.forceCenter(width / 2, height / 2))
.on("tick", ticked);
function createNodes(index) {
links.push({
'source': 0,
'target': index
})
list.push({
"id": index,
"name": "server " + index
});
return list;
}
var iteration = 0;
update(createNodes(iteration)); // just simulating updates
d3.interval(function(timer) {
iteration++;
update(createNodes(iteration));
}, 1000); //<-- this was commented out incorrectly just now
function update(items) {
var dataJoin = nodeLayer.selectAll(".node")
.data(items, function(d) {
return d.id;
});
node = dataJoin.enter() // insert
.append("g")
.attr("class", "node");
node.append("circle") // insert
.attr("r", 2.5)
.attr('class', 'circle')
.merge(dataJoin) // not the class, the actual selected group you called enter() on
.select('.circle')
.style('fill', 'red') // just for testing purposes
.exit().remove(); // exit
node.append("text") // insert
.attr("dy", 3)
.attr('class', 'text')
.text(function(d) {
return d.name;
})
.merge(dataJoin)
.select('.text')
.style('fill', 'green') // fill is a style
dataJoin.exit().remove();
simulation.nodes(list);
simulation.force("link").links(links);
simulation.alpha(1).restart();
}
function ticked() {
node.attr("transform", function(d) {
var a = d.x || 0;
var b = d.y || 0;
return "translate(" + a + ", " + b + ")";
});
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.4.1/d3.min.js"></script>
<svg height="300" width="300"></svg>
there are bunch of bugs,
first of all 'update'(misnomer) should be called with the list of all the data nodes not just the change.
you might want to explore https://bl.ocks.org/ and copy how people are doing force directed graphs. you need links to have forces.
the idea behind merge is to compare the new list with the old list, which means you need to use dataJoin or the group that has the data/id attached.
I am not an expert, do take a look at all the examples of force-directed graphs and see how they update/merge. (there's more than 1 way to update/restart the graph)

Related

D3.js update graph when pushing new nodes

I am trying to add new nodes to a D3 graph dynamically by nodes.push({"id": item, "group": 1, "size": 30}); but when I do this there is a visual bug where there are duplicates. Anytime I update() I get a double of whatever was already there. Anyone have any advice? Would be appreciated.
var node;
var link;
var circles
var lables;
function update(){
node = svg.append("g")
.attr("class", "nodes")
.selectAll("g")
.data(nodes)
.enter().append("g")
link = svg.append("g")
.attr("class", "links")
.selectAll("line")
.data(links)
.enter().append("line")
.attr("stroke-width", function(d) { return Math.sqrt(d.value); });
circles = node.append("circle")
.attr("r", function(d) { return (d.size / 10) + 1})
.attr("fill", function(d) { return color(3); })
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended))
.on("click", clicked);
lables = node.append("text")
.text(function(d) {
return d.id;
})
.attr('x', 6)
.attr('y', 3)
.style("font-size", "20px");
node.append("title")
.text(function(d) { return d.id; });
simulation
.nodes(nodes)
.on("tick", ticked);
simulation.force("link")
.links(links);
}
function ticked() {
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 + ")";
})
}
Looking just at the nodes (the links are essentially the same issue), every time you update your data you:
Create a new parent g (svg.append("g"))
Select all the child g elements of that new parent g (.selectAll("g")). Since this new parent g has no children - you just made it, nothing is selected.
Bind data to the selection (.data(nodes))
Using the enter selection, append a g for each item in the data array (as there are no elements in the selection, everything is entered (the enter selection creates an element in the DOM for every item in the data array for which no corresponding element exists in the selection.)
Append a circle to each newly appended g. (.enter().append("g"))
Nowhere do you select the already existing nodes - these are just cast aside. They are ignored by the tick function because link and node refer to selections of newly created nodes and links. Neither do you remove the old links and nodes - so they just sit there for all eternity or until you close the browser.
The canonical solution is to:
Append structural elements once. I say structural in reference to the parent g elements: they aren't data dependent, they're organizational. They should be appended once outside of the update function.
Use the update function to manage (create, update, remove) elements that are dependent on the data: the nodes and links themselves. Anything that depends on the data needs to be modified in the update function, nothing else.
So we could append the parent g elements outside of the update function:
var nodeG = svg.append("g").attr("class", "nodes");
var linkG = svg.append("g").attr("class", "links");
Then in the update function we can use those selections to conduct the enter/update/exit cycle. This is slightly complicated in your case, and many others, because we have nodes represented by a g with child elements. Something like the following:
function update() {
var node = nodeG.selectAll("g")
.data(nodes)
// remove excess nodes.
node.exit().remove();
// enter new nodes as required:
var nodeEnter = node.enter().append("g")
.attr(...
// append circles to new nodes:
nodeEnter.append("circle")
.attr(...
// merge update and enter.
node = nodeEnter.merge(node);
// do enter/update/exit with lines.
var link = linkG.selectAll("line")
.attr(...
link.exit().remove();
var linkEnter = link.enter().append("line")
.attr(...
link = linkEnter.merge(link);
...
Which in your case may look like:
// Random data:
let graph = { nodes: [], links : [] }
function randomizeData(graph) {
// generate nodes:
let n = Math.floor(Math.random() * 10) + 6;
let newNodes = [];
for(let i = 0; i < n; i++) {
if (graph.nodes[i]) newNodes.push(graph.nodes[i]);
else newNodes.push({ id: i,
color: Math.floor(Math.random()*10),
size: Math.floor(Math.random() * 10 + 2),
x: (Math.random() * width),
y: (Math.random() * height)
})
}
// generate links
let newLinks = [];
let m = Math.floor(Math.random() * n) + 1;
for(let i = 0; i < m; i++) {
a = 0; b = 0;
while (a == b) {
a = Math.floor(Math.random() * n);
b = Math.floor(Math.random() * n);
}
newLinks.push({source: a, target: b})
if(i < newNodes.length - 2) newLinks.push({source: i, target: i+1})
}
return { nodes: newNodes, links: newLinks }
}
// On with main code:
// Set up the structure:
const svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height");
const color = d3.scaleOrdinal(d3.schemeCategory10);
const simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function(d) { return d.id; }).strength(0.004))
.force("charge", d3.forceManyBody())
// to attract nodes to center, use forceX and forceY:
.force("x", d3.forceX().x(width/2).strength(0.01))
.force("y", d3.forceY().y(height/2).strength(0.01));
const nodeG = svg.append("g").attr("class","nodes")
const linkG = svg.append("g").attr("class", "links")
graph = randomizeData(graph);
update();
// Two variables to hold our links and nodes - declared outside the update function so that the tick function can access them.
var links;
var nodes;
// Update based on data:
function update() {
// Select all nodes and bind data:
nodes = nodeG.selectAll("g")
.data(graph.nodes);
// Remove excess nodes:
nodes.exit()
.transition()
.attr("opacity",0)
.remove();
// Enter new nodes:
var newnodes = nodes.enter().append("g")
.attr("opacity", 0)
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended))
// for effect:
newnodes.transition()
.attr("opacity",1)
.attr("class", "nodes")
newnodes.append("circle")
.attr("r", function(d) { return (d.size * 2) + 1})
.attr("fill", function(d) { return color(d.color); })
newnodes.append("text")
.text(function(d) { return d.id; })
.attr('x', 6)
.attr('y', 3)
.style("font-size", "20px");
newnodes.append("title")
.text(function(d) { return d.id; });
// Combine new nodes with old nodes:
nodes = newnodes.merge(nodes);
// Repeat but with links:
links = linkG.selectAll("line")
.data(graph.links)
// Remove excess links:
links.exit()
.transition()
.attr("opacity",0)
.remove();
// Add new links:
var newlinks = links.enter()
.append("line")
.attr("stroke-width", function(d) { return Math.sqrt(d.value); });
// for effect:
newlinks
.attr("opacity", 0)
.transition()
.attr("opacity",1)
// Combine new links with old:
links = newlinks.merge(links);
// Update the simualtion:
simulation
.nodes(graph.nodes) // the data array, not the selection of nodes.
.on("tick", ticked)
.force("link").links(graph.links)
simulation.alpha(1).restart();
}
function ticked() {
links // the selection of all links:
.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; });
nodes
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
})
}
function dragstarted(d) {
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragended(d) {
if (!d3.event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
d3.select("button")
.on("click", function() {
graph = randomizeData(graph);
update();
})
.links line {
stroke: #999;
stroke-opacity: 0.6;
}
.nodes circle {
stroke: #fff;
stroke-width: 1.5px;
}
<button> Update</button>
<svg width="500" height="300"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
Note
I've updated the force paramaters a bit to use forceX and forceY: forces which attract the nodes to the center. The centering force only ensures the center of gravity is a specific value, not how close the nodes must be.
Alternative approaches:
Of course, you could just remove everything and append it each time: but this limits ability to transition from one dataset to the next and is generally not canonical.
If you only need to enter elements once (no elements need to be added or removed during updates) then you could avoid using the full enter/update/exit cycle and append once outside the update function, updating node/link attributes with new data on update rather than using the more involved enter/update/exit cycle in the snippet above.

Force simulation is jittery when using svg transforms to update position

JSFiddle example
I've noticed that when updating positions of svg elements in a d3-force diagram, updating the positions of elements using (in the case of circles) the cx and cy attributes is much smoother than using the transform attribute.
In the example JSFiddle, there are two separate force simulations side-by-side. The one on the left updates positions using the transform attribute:
sim_transform.on('tick', function () {
circles_transform.attr('transform', function (d) {
return 'translate(' + d.x + ',' + d.y + ')';
});
});
The one on the right updates positions using the cx and cy attributes of a circle:
sim_position.on('tick', function () {
circles_position
.attr('cx', function (d) {
return d.x;
})
.attr('cy', function (d) {
return d.y;
})
});
The simulations appear identical until they're just about to become static, at which point the one using transforms starts to jitter quite a bit. Any ideas what is causing this? Can it be fixed so that the animation remains smooth using transforms?
It seems to me that the issue you're observing (only reproducible in FireFox, as #altocumulus noted) has something to do with the way FF uses floating numbers for the translate of the transform attribute.
We can see this if we set both simulations to use integers, doing ~~(d.x) and ~~(d.y). Have a look, both will jitter:
var svg = d3.select('svg');
var graph_transform = gen_data();
var graph_position = gen_data();
var force_left = d3.forceCenter(
parseInt(svg.style('width')) / 3,
parseInt(svg.style('height')) / 2
)
var force_right = d3.forceCenter(
2 * parseInt(svg.style('width')) / 3,
parseInt(svg.style('height')) / 2
)
var sim_transform = d3.forceSimulation()
.force('left', force_left)
.force('collide', d3.forceCollide(65))
.force('link', d3.forceLink().id(id));
var sim_position = d3.forceSimulation()
.force('right', force_right)
.force('collide', d3.forceCollide(65))
.force('link', d3.forceLink().id(id));
var g_transform = svg.append('g');
var g_position = svg.append('g');
var circles_transform = g_transform.selectAll('circle')
.data(graph_transform.nodes)
.enter()
.append('circle')
.attr('r', 40);
var circles_position = g_position.selectAll('circle')
.data(graph_position.nodes)
.enter()
.append('circle')
.attr('r', 40);
sim_transform
.nodes(graph_transform.nodes)
.force('link')
.links(graph_transform.links);
sim_position
.nodes(graph_position.nodes)
.force('link')
.links(graph_position.links);
sim_transform.on('tick', function() {
circles_transform.attr('transform', function(d) {
return 'translate(' + (~~(d.x)) + ',' + (~~(d.y)) + ')';
});
});
sim_position.on('tick', function() {
circles_position
.attr('cx', function(d) {
return ~~d.x;
})
.attr('cy', function(d) {
return ~~d.y;
})
});
function id(d) {
return d.id;
}
function gen_data() {
var nodes = [{
id: 'a'
},
{
id: 'b'
},
{
id: 'c'
},
{
id: 'd'
}
]
var links = [{
source: 'a',
target: 'b'
},
{
source: 'b',
target: 'c'
},
{
source: 'c',
target: 'd'
},
{
source: 'd',
target: 'a'
}
];
return {
nodes: nodes,
links: links
}
}
svg {
width: 100%;
height: 500px;
}
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg></svg>
So, in your original code, it seems like the circles move correctly when using cx and cy, but they jump from integer to integer when using translate (or maybe half pixel, see the last demo). If the hypothesis here is correct, the reason that you just see the effect when the simulation is cooling down is because, at that moment, the movements are smaller.
Demos
Now, if we get rid of the simulation, we can see that this strange behaviour also happens with a very basic transform. To check this, I created a transition for a big black circle, using a linear ease and a very long time (to facilitate seeing the issue). The circle will move 30px to the right. I also put a gridline to make the jumps more noticeable.
(Warning: the demos below are only reproducible in FireFox, you won't see any difference in Chrome/Safari)
If we use cx, the transition is smooth:
var svg = d3.select("svg");
var gridlines = svg.selectAll(null)
.data(d3.range(10))
.enter()
.append("line")
.attr("y1", 0)
.attr("y2", 200)
.attr("x1", function(d) {
return 300 + d * 3
})
.attr("x2", function(d) {
return 300 + d * 3
})
.style("stroke", "lightgray")
.style("stroke-width", "1px");
var circle = svg.append("circle")
.attr("cx", 200)
.attr("cy", 100)
.attr("r", 98)
.transition()
.duration(10000)
.ease(d3.easeLinear)
.attr("cx", "230")
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg width="500" height="200"></svg>
However, if we use translate, you can see the circle jumping 1px at every move:
var svg = d3.select("svg");
var gridlines = svg.selectAll(null)
.data(d3.range(10))
.enter()
.append("line")
.attr("y1", 0)
.attr("y2", 200)
.attr("x1", function(d) {
return 300 + d * 3
})
.attr("x2", function(d) {
return 300 + d * 3
})
.style("stroke", "lightgray")
.style("stroke-width", "1px");
var circle = svg.append("circle")
.attr("cx", 200)
.attr("cy", 100)
.attr("r", 98)
.transition()
.duration(10000)
.ease(d3.easeLinear)
.attr("transform", "translate(30,0)")
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg width="500" height="200"></svg>
For you people running this in Chrome/Safari, this is how the last snippet looks like in Firefox. It's like the circle is being moved half a pixel at every change... definitely not as smooth as changing cx:
var svg = d3.select("svg");
var gridlines = svg.selectAll(null)
.data(d3.range(10))
.enter()
.append("line")
.attr("y1", 0)
.attr("y2", 200)
.attr("x1", function(d) {
return 300 + d * 3
})
.attr("x2", function(d) {
return 300 + d * 3
})
.style("stroke", "lightgray")
.style("stroke-width", "1px");
var circle = svg.append("circle")
.attr("cx", 200)
.attr("cy", 100)
.attr("r", 98);
var timer = d3.timer(function(t){
if(t>10000) timer.stop();
circle.attr("cx", 200 + (~~(60/(10000/t))/2));
})
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg width="500" height="200"></svg>
As this is an implementation issue only visible in FF, it may be worth reporting a bug.

D3 updating graph with new elements create edges with the wrong nodes

My code creates a graph and creates a pivot point on each node, if you double click them it'll fetch more data associated with that node and hopefully creates new links. Now here's the problem I'm running into:
I clicked on one of the outermost nodes but for some reason the new links were being attached to the first node (the blue one). Any idea why this is happening?
function draw_graph(graph) {
var color = d3.scaleOrdinal(d3.schemeCategory20);
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height"),
node,
link;
svg.append('defs').append('marker')
.attrs({
'id': 'arrowhead',
'viewBox': '-0 -5 10 10',
'refX': 13,
'refY': 0,
'orient': 'auto',
'markerWidth': 13,
'markerHeight': 13,
'xoverflow': 'visible'
})
.append('svg:path')
.attr('d', 'M 0,-5 L 10 ,0 L 0,5')
.attr('fill', '#999')
.style('stroke', 'none');
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function (d) {
return d.id;
}).distance(200).strength(1))
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(width / 2, height / 2));
update(graph.links, graph.nodes);
svg.selectAll('circle').on('dblclick', function () {
var pivot_id = ($(this).siblings('title').text())
console.log('pivoting on', pivot_id)
pivot_search(pivot_id)
});
function update(links, nodes) {
link = svg.selectAll(".link")
.data(links)
.enter()
.append("line")
.attr("class", "link")
.attr('marker-end', 'url(#arrowhead)')
edgepaths = svg.selectAll(".edgepath")
.data(links)
.enter()
.append('path')
.attrs({
'class': 'edgepath',
'fill-opacity': 0,
'stroke-opacity': 0,
'id': function (d, i) {
return 'edgepath' + i
}
})
.style("pointer-events", "none");
edgelabels = svg.selectAll(".edgelabel")
.data(links)
.enter()
.append('text')
.style("pointer-events", "none")
.attrs({
'class': 'edgelabel',
'id': function (d, i) {
return 'edgelabel' + i
},
'font-size': 10,
'fill': '#aaa'
});
node = svg.selectAll(".node")
.data(nodes)
.enter()
.append("g")
.attr("class", "node")
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
);
node.append("circle")
.attr("r", 5)
.attr("fill", function (d) {
return color(d.group);
})
node.append("title")
.text(function (d) {
return d.id;
});
node.append("text")
.attr("dy", -3)
.text(function (d) {
return d.label;
});
simulation
.nodes(nodes)
.on("tick", ticked);
simulation.force("link")
.links(links);
}
function ticked() {
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 + ")";
});
edgepaths.attr('d', function (d) {
return 'M ' + d.source.x + ' ' + d.source.y + ' L ' + d.target.x + ' ' + d.target.y;
});
edgelabels.attr('transform', function (d) {
if (d.target.x < d.source.x) {
var bbox = this.getBBox();
rx = bbox.x + bbox.width / 2;
ry = bbox.y + bbox.height / 2;
return 'rotate(180 ' + rx + ' ' + ry + ')';
}
else {
return 'rotate(0)';
}
});
}
function dragstarted(d) {
if (!d3.event.active) simulation.alphaTarget(0.3).restart()
d.fx = d.x;
d.fy = d.y;
}
function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
}
function pivot_search(entity_id) {
var json = {
'nodes': [],
'links': [],
}
get_entities({'id': entity_id})
.done(function (data) {
json.nodes.push({
'label': data['results'][0]['label'],
'id': data['results'][0]['id'],
'group': data['results'][0]['entity_type'],
})
get_entities({
'related_entities': entity_id,
'related_entities__entity_instance__entity_type__strong_entity': true,
'page_size': 500
})
.done(function (data) {
for (var i = 0; i < data['results'].length; i++) {
json.nodes.push({
'label': data['results'][i]['label'],
'id': data['results'][i]['id'],
'group': data['results'][i]['entity_type'],
})
json.links.push({
'source': entity_id,
'target': data['results'][i]['id'],
})
}
draw_graph(json)
})
})
}
EDIT: Upon further investigations seems like it's replacing the existing node links with the new data and creating new potentially duplicate nodes.
link = svg.selectAll('.link')
.data(links, function (d) {
return d.id;
})
.enter()
.append('line')
.attr('class', 'link')
.attr('marker-end', 'url(#arrowhead)')
edgepaths = svg.selectAll('.edgepath')
.data(links)
.enter()
.append('path')
.attrs({
'class': 'edgepath',
'fill-opacity': 0,
'stroke-opacity': 0,
'id': function (d, i) {
return 'edgepath' + i
}
})
.style('pointer-events', 'none');
node = svg.selectAll('.node')
.data(nodes, function (d) {
return d.id;
})
.enter()
.append('g')
.attr('class', 'node')
.call(d3.drag()
.on('start', dragstarted)
.on('drag', dragged)
);
I added an ID to help deal with node duplication, but now I have a problem with index root displacement.
It appears that the problem that you are having is due to the fact that your data might be duplicating within D3's data-join functionality. It is likely that the best way to solve the issue is to create a "UUID / GUID" for each node in your data before D3 binds it (see here for an example). Once you have done that, then you can bind the data and use the data-join's key-specify function (see here for an explanation) to tell D3 to use the UUID / GUID values you created for each object to guarantee consistency. From there, you should be able to handle the parent-child relationships easier.
Edit #1
Since that worked for the duplicate values, the next problem that you are likely having is because the reference to the "source" object is not set up the way that D3 would expect. In D3, the link's "source" property is a reference to the actual source object, where you are just providing the ID value (see here for the D3v4 docs reference). Try providing the reference to the actual source object within the array and that should fix it.
Edit #2
You are correct in that you are handling NEW data coming into the visualization, but I don't think that you're handling EXISTING or OLD (meaning, data points that are no longer relevant and the nodes / links need to be removed). In this case, try updating your code with the following example from Mike Bostock (the original creator of the D3.js library) here and then report back once that is done. It is possible that the new nodes that you are seeing are simply the older nodes that need to be removed since they no longer have the children tied to them, so D3js sees them as "new" or "existing" nodes that actually need to be removed.

Dynamically update D3 Sunburst if the source json is updated (item added or deleted)

I am new to D3 and trying to dynamically update the chart if the source json is modified. But I am not able to achieve this.
Please check this plunkr
Js:
var width = 500,
height = 500,
radius = Math.min(width, height) / 2;
var x = d3.scale.linear()
.range([0, 2 * Math.PI]);
var y = d3.scale.sqrt()
.range([0, radius]);
var color = d3.scale.category10();
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + (height / 2 + 10) + ") rotate(-90 0 0)");
var partition = d3.layout.partition()
.value(function(d) {
return d.size;
});
var arc = d3.svg.arc()
.startAngle(function(d) {
return Math.max(0, Math.min(2 * Math.PI, x(d.x)));
})
.endAngle(function(d) {
return Math.max(0, Math.min(2 * Math.PI, x(d.x + d.dx)));
})
.innerRadius(function(d) {
return Math.max(0, y(d.y));
})
.outerRadius(function(d) {
return Math.max(0, y(d.y + d.dy));
});
//d3.json("/d/4063550/flare.json", function(error, root) {
var root = initItems;
var g = svg.selectAll("g")
.data(partition.nodes(root))
.enter().append("g");
var path = g.append("path")
.attr("d", arc)
.style("fill", function(d) {
return color((d.children ? d : d.parent).name);
})
.on("click", click)
.each(function(d) {
this.x0 = d.x;
this.dx0 = d.dx;
});
//.append("text")
var text = g.append("text")
.attr("x", function(d) {
return y(d.y);
})
.attr("dx", "6") // margin
.attr("dy", ".35em") // vertical-align
.attr("transform", function(d) {
return "rotate(" + computeTextRotation(d) + ")";
})
.text(function(d) {
return d.name;
})
.style("fill", "white");
function computeTextRotation(d) {
var angle = x(d.x + d.dx / 2) - Math.PI / 2;
return angle / Math.PI * 180;
}
function click(d) {
console.log(d)
// fade out all text elements
if (d.size !== undefined) {
d.size += 100;
};
text.transition().attr("opacity", 0);
path.transition()
.duration(750)
.attrTween("d", arcTween(d))
.each("end", function(e, i) {
// check if the animated element's data e lies within the visible angle span given in d
if (e.x >= d.x && e.x < (d.x + d.dx)) {
// get a selection of the associated text element
var arcText = d3.select(this.parentNode).select("text");
// fade in the text element and recalculate positions
arcText.transition().duration(750)
.attr("opacity", 1)
.attr("transform", function() {
return "rotate(" + computeTextRotation(e) + ")"
})
.attr("x", function(d) {
return y(d.y);
});
}
});
} //});
// Word wrap!
var insertLinebreaks = function(t, d, width) {
alert(0)
var el = d3.select(t);
var p = d3.select(t.parentNode);
p.append("g")
.attr("x", function(d) {
return y(d.y);
})
// .attr("dx", "6") // margin
//.attr("dy", ".35em") // vertical-align
.attr("transform", function(d) {
return "rotate(" + computeTextRotation(d) + ")";
})
//p
.append("foreignObject")
.attr('x', -width / 2)
.attr("width", width)
.attr("height", 200)
.append("xhtml:p")
.attr('style', 'word-wrap: break-word; text-align:center;')
.html(d.name);
alert(1)
el.remove();
alert(2)
};
//g.selectAll("text")
// .each(function(d,i){ insertLinebreaks(this, d, 50 ); });
d3.select(self.frameElement).style("height", height + "px");
// Interpolate the scales!
function arcTween(d) {
var xd = d3.interpolate(x.domain(), [d.x, d.x + d.dx]),
yd = d3.interpolate(y.domain(), [d.y, 1]),
yr = d3.interpolate(y.range(), [d.y ? 20 : 0, radius]);
return function(d, i) {
return i ? function(t) {
return arc(d);
} : function(t) {
x.domain(xd(t));
y.domain(yd(t)).range(yr(t));
return arc(d);
};
};
}
function arcTweenUpdate(a) {
console.log(path);
var _self = this;
var i = d3.interpolate({ x: this.x0, dx: this.dx0 }, a);
return function(t) {
var b = i(t);
console.log(window);
_self.x0 = b.x;
_self.dx0 = b.dx;
return arc(b);
};
}
setTimeout(function() {
path.data(partition.nodes(newItems))
.transition()
.duration(750)
.attrTween("d", arcTweenUpdate)
}, 2000);
In addition to what #Cyril has suggested about removing the following line:
d3.select(self.frameElement).style("height", height + "px");
I made further modifications in your fiddle: working fiddle
The idea used here is to add a function updateChart which takes the items and then generate the chart:
var updateChart = function (items) {
// code to update the chart with new items
}
updateChart(initItems);
setTimeout(function () { updateChart(newItems); }, 2000);
This doesn't use the arcTweenUpdate function you have created but I will try to explain the underlying concept:
First, you will need to JOIN the new data with your existing data:
// DATA JOIN - Join new data with old elements, if any.
var gs = svg.selectAll("g").data(partition.nodes(root));
then, ENTER to create new elements if required:
// ENTER
var g = gs.enter().append("g").on("click", click);
But, we also need to UPDATE the existing/new path and text nodes with new data:
// UPDATE
var path = g.append("path");
gs.select('path')
.style("fill", function(d) {
return color((d.children ? d : d.parent).name);
})
//.on("click", click)
.each(function(d) {
this.x0 = d.x;
this.dx0 = d.dx;
})
.transition().duration(500)
.attr("d", arc);
var text = g.append("text");
gs.select('text')
.attr("x", function(d) {
return y(d.y);
})
.attr("dx", "6") // margin
.attr("dy", ".35em") // vertical-align
.attr("transform", function(d) {
return "rotate(" + computeTextRotation(d) + ")";
})
.text(function(d) {
return d.name;
})
.style("fill", "white");
and, after everything is created/updated remove the g nodes which are not being used i.e. EXIT:
// EXIT - Remove old elements as needed.
gs.exit().transition().duration(500).style("fill-opacity", 1e-6).remove();
This whole pattern of JOIN + ENTER + UPDATE + EXIT is demonstrated in following articles by Mike Bostock:
General Update Pattern - I
General Update Pattern - II
General Update Pattern - III
In side the fiddle the setTimeout is not running because:
d3.select(self.frameElement).style("height", height + "px");
You will get Uncaught SecurityError: Failed to read the 'frame' property from 'Window': Blocked a frame with origin "https://fiddle.jshell.net" from accessing a frame with origin and the setTimeout never gets called.
So you can remove this line d3.select(self.frameElement).style("height", height + "px"); just for the fiddle.
Apart from that:
Your timeout function should look like this:
setTimeout(function() {
//remove the old graph
svg.selectAll("*").remove();
root = newItems;
g = svg.selectAll("g")
.data(partition.nodes(newItems))
.enter().append("g");
/make path
path = g.append("path")
.attr("d", arc)
.style("fill", function(d) {
return color((d.children ? d : d.parent).name);
})
.on("click", click)
.each(function(d) {
this.x0 = d.x;
this.dx0 = d.dx;
});
//make text
text = g.append("text")
.attr("x", function(d) {
return y(d.y);
})
.attr("dx", "6") // margin
.attr("dy", ".35em") // vertical-align
.attr("transform", function(d) {
return "rotate(" + computeTextRotation(d) + ")";
})
.text(function(d) {
return d.name;
})
.style("fill", "white");
}
working fiddle here
for the enter() and transitions to work you need to give d3 a way to identify each item in your data. the .data() function has a second parameter that lets you return something to use as an id. enter() will use the id to decide whether the object is new.
try changing
path.data(partition.nodes(newItems))
.data(partition.nodes(root));
to
path.data(partition.nodes(newItems), function(d){return d.name});
.data(partition.nodes(root), function(d){return d.name});

How can I dynamically update text labels in d3?

I want to add labels to my vertical bar chart that display the current percentage value that corresponds to the current hight of the bar.
So I need to continuously update the percentage value and I also need a transition to make the text element move insync with the bar chart.
I tried this:
var percentageLabels = svg.selectAll(".percentage-label")
.data(dataset);
percentageLabels.remove();
percentageLabels
.enter()
.append("text")
.attr("class", "percentage-label")
.style("fill", "white")
.text(function(d) {
return d;
})
.attr("y", function(d) {
return y(d);
})
.attr("x", function(d, i) {
return i * (w / dataset.length) + 2.5 / 100 * w + w * 10/100;
})
.transition().duration(1750).ease("linear")
.attr("y", function(d) {
return y(d);
});
Check out the fiddle
I'd make a couple changes here. First, wrap the rect and the text in a g, so you only need to data-bind once. Then you are free to transition them together:
var uSel = svg.selectAll(".input")
.data(dataset); //<-- selection of gs
uSel.exit().remove(); //<-- anybody leaving? remove g (both rect and text)
var gs = uSel
.enter()
.append("g")
.attr("class", "input"); //<-- enter selection, append g
gs.append("rect")
.attr("fill", "rgb(250, 128, 114)"); //<-- enter selection, rect to g
gs.append("text")
.attr("class", "percentage-label")
.style("fill", "white")
.attr("x", function(d, i) {
return i * (w / dataset.length) + 2.5 / 100 * w + w * 10/100;
}); //<-- enter selection, text to g
uSel.select("rect")
.attr("x", function(d, i) {
return i * (w / dataset.length) + 2.5 / 100 * w;
})
.attr("width", w / dataset.length - barPadding)
.attr("height", y(0))
.transition().duration(1750).ease("linear")
.attr("y", function(d) {
return y(d);
})
.attr("height", function(d) {
return h - y(d);
}); //<-- update rects with transition
uSel.select("text")
.transition().duration(1750).ease("linear")
.attr("y", function(d) {
return y(d);
})
.text(function(d) {
return d + "%";
}); //<-- update text with transition
Updated fiddle.
EDITS
To transition the text, you are probably going to have to use a custom tween function:
uSel.select("text")
.transition().duration(1750).ease("linear")
.attr("y", function(d) {
return y(d); //<-- move the text
})
.tween("", function(d) {
var self = d3.select(this),
oldValue = y.invert(self.attr("y")), //<-- get the current value
i = d3.interpolateRound(oldValue, d); //<-- interpolate to new value
return function(t) {
self.text(i(t) + '%') <-- update the text on each iteration
};
});
Updated, updated fiddle.
From the docs:
The transition.each method can be used to chain transitions and apply shared timing across a set of transitions. For example:
d3.transition()
.duration(750)
.ease("linear")
.each(function() {
d3.selectAll(".foo").transition()
.style("opacity", 0)
.remove();
})
.transition()
.each(function() {
d3.selectAll(".bar").transition()
.style("opacity", 0)
.remove();
});
You might want to check out this: https://github.com/mbostock/d3/wiki/Transitions#tween

Categories