I have a set of nodes data
var dataNodes = [
{ id: 1, x: 10, y:30, text: "node 1", muteText: false },
{ id: 2, x: 30, y:50, text: "node 2", muteText: false },
{ id: 3, x: 50, y:70, text: "node 3", muteText: false }
];
I add the elements in the DOM using this kind of function (not my real code because of a lot of business complexity) :
function redraw(newData) {
var node = d3
.select("body")
.selectAll("g.node")
.data(dataNodes)
.transition().duration(500)
.attr("transform", d => "translate(" + d.x + "," + d.y + ")");
node.enter()
.append("g")
.attr("class", "node")
.attr("transform", d => "translate(" + d.x + "," + d.y + ")")
.append("text")
.text(d => d.text)
.style("opacity", "0")
.transition().duration(500)
.style("opacity", "1");
node.exit()
.style("opacity", "0");
}
I want to be able to do all the following when the data get updated:
add entering nodes
make already existing nodes move with a transition
hide exitings nodes (opacity 0) because they may reappear
when nodes get their "muteText" property changed to true, make the inner text disapear
I'm quite confortable with the 3 first requiremeents but I really don't know how to do the last one : how can I remove (or even change) sub elements based on a filtered set of data ? Can I use the filter in the d3.data function to do it ?
Let me know if my question is unclear.
If you want to filter, do it on your update selection:
var node = svg
.selectAll("g.node")
.data(someData);
var nodeE = node.enter()
.append("g")
.attr("class", "node");
nodeE.append("text")
.text(d => d.text);
// node is now UPDATE + ENTER
node = nodeE.merge(node);
// filter the text and set how you care
node.filter(function(d) {
return d.muteText
})
.select("text")
.style("opacity", 1)
.transition()
.style("opacity", 0);
node.filter(function(d) {
return !d.muteText
})
.select("text")
.style("opacity", 0)
.transition()
.style("opacity", 1);
Here's a running example:
<!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>
<style>
text {
fill: black;
font-family: arial;
}
</style>
</head>
<body>
<script>
var dataNodes = [{
id: 1,
x: 10,
y: 30,
text: "node 1",
muteText: false
}, {
id: 2,
x: 30,
y: 50,
text: "node 2",
muteText: false
}, {
id: 3,
x: 50,
y: 70,
text: "node 3",
muteText: false
}];
var svg = d3.select('body')
.append('svg')
.attr('width', 500)
.attr('height', 500);
redraw([{
id: 1,
x: 10,
y: 30,
text: "node 1",
muteText: false
}, {
id: 2,
x: 30,
y: 50,
text: "node 2",
muteText: false
}, {
id: 3,
x: 50,
y: 70,
text: "node 3",
muteText: false
}]);
setTimeout(function() {
redraw([{
id: 1,
x: 10,
y: 30,
text: "node 1",
muteText: true
}, {
id: 2,
x: 100,
y: 50,
text: "node 2",
muteText: false
}, {
id: 3,
x: 50,
y: 70,
text: "node 3",
muteText: true
}])
}, 2000)
setTimeout(function() {
redraw([{
id: 1,
x: 10,
y: 30,
text: "node 1",
muteText: true
}, {
id: 2,
x: 100,
y: 50,
text: "node 2",
muteText: false
}, {
id: 3,
x: 50,
y: 70,
text: "node 3",
muteText: false
},{
id: 4,
x: 60,
y: 90,
text: "node 4",
muteText: false
}])
}, 4000)
function redraw(someData) {
var node = svg
.selectAll("g.node")
.data(someData);
var nodeE = node.enter()
.append("g")
.attr("class", "node")
.attr("transform", d => "translate(" + d.x + "," + d.y + ")");
nodeE.append("text")
.text(d => d.text)
.style("opacity", 0)
.transition()
.style("opacity", 1);
node = nodeE.merge(node);
node.exit()
.style("opacity", "0");
node.transition().duration(500)
.attr("transform", d => "translate(" + d.x + "," + d.y + ")");
node.filter(function(d) {
return d.muteText
})
.select("text")
.transition()
.style("opacity", 0);
}
</script>
</body>
</html>
Related
I'm a D3 newbie and trying to build word cloud page using
JasonDavies d3-cloud.
But sometimes all words are showing in circle layout, but sometimes in rectangular.
How can I make them always locate in circle layout?
d3-cloud's layout algorithm places words within an ellipse starting from the center of the rectangle you've provided (d3.layout.cloud().size([width, height])).
If you have a container (size([width, height])) big enough compared to your number of words and the size you've given to these words, then you'll get a nice circle (or an ellipse if your container isn't a square):
Math.seedrandom('hello words');
var data = [{ text: "Hello", count: 38 }, { text: "World", count: 27 }, { text: "Whatever", count: 21 }, { text: "Massive", count: 21 }, { text: "Thing", count: 16 }, { text: "Something", count: 14 }, { text: "What", count: 12 }, { text: "Else", count: 9 }, { text: "Blabla", count: 6 }, { text: "Small", count: 6 }, { text: "VeryLong", count: 6 }, { text: "Word", count: 3 }, { text: "abcdef", count: 4 }, { text: "Elsewhere", count: 9 }, { text: "Anything", count: 3 }, { text: "Placeholder", count: 14 }, { text: "WhateverSmall", count: 3 }, { text: "Work", count: 12 }, { text: "Wording", count: 7 }, { text: "Verb", count: 4 }];
var svg = d3.select("svg").append("g");
let fill = d3.scaleOrdinal(d3.schemeCategory20);
let size = d3.scaleLinear().range([0, 20]).domain([0, d3.max(data, d => d.count)]);
let word_cloud_data = data
.map( function(d) {
return { text: d.text, size: 9 + size(d.count) * 3.5 };
});
let layout = d3.layout.cloud()
.size([600, 600])
.words(word_cloud_data)
.padding(2.5)
.rotate(d => ~~(Math.random() * 2) * -90)
.fontSize(d => d.size)
.on("end", draw);
layout.start();
function draw(words) {
svg.append("g")
.attr("transform", "translate(250,250)")
.selectAll("text")
.data(words)
.enter().append("text")
.style("fill", (d, i) => { d.color = fill(i); return d.color; })
.style("text-anchor", "middle")
.attr("transform", d => "translate(" + [d.x, d.y] + ")rotate(" + d.rotate + ")")
.text(d => d.text)
.style("font-size", d => d.size + "px");
}
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="https://cdn.rawgit.com/jasondavies/d3-cloud/master/build/d3.layout.cloud.js"></script>
<script src="https://d3js.org/d3-scale.v1.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/seedrandom/2.4.4/seedrandom.min.js">
</script>
<svg width="700" height="700"></svg>
If your container can't hold your words as an ellipse/circle, d3-cloud's layout algorithm will start finding space in the corners of the container you've chosen to contain your cloud. And it will start looking like a rectangle/square. (Up to the point there's not even enough space to add the remaining words):
Math.seedrandom('hello words');
var data = [{ text: "Hello", count: 38 }, { text: "World", count: 27 }, { text: "Whatever", count: 21 }, { text: "Massive", count: 21 }, { text: "Thing", count: 16 }, { text: "Something", count: 14 }, { text: "What", count: 12 }, { text: "Else", count: 9 }, { text: "Blabla", count: 6 }, { text: "Small", count: 6 }, { text: "VeryLong", count: 6 }, { text: "Word", count: 3 }];
var svg = d3.select("svg").append("g");
let fill = d3.scaleOrdinal(d3.schemeCategory20);
let size = d3.scaleLinear().range([0, 20]).domain([0, d3.max(data, d => d.count)]);
let word_cloud_data = data
.map( function(d) {
return { text: d.text, size: 9 + size(d.count) * 3.5 };
});
let layout = d3.layout.cloud()
.size([275, 275])
.words(word_cloud_data)
.padding(2.5)
.rotate(d => ~~(Math.random() * 2) * -90)
.fontSize(d => d.size)
.on("end", draw);
layout.start();
function draw(words) {
svg.append("g")
.attr("transform", "translate(150,150)")
.selectAll("text")
.data(words)
.enter().append("text")
.style("fill", (d, i) => { d.color = fill(i); return d.color; })
.style("text-anchor", "middle")
.attr("transform", d => "translate(" + [d.x, d.y] + ")rotate(" + d.rotate + ")")
.text(d => d.text)
.style("font-size", d => d.size + "px");
}
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="https://cdn.rawgit.com/jasondavies/d3-cloud/master/build/d3.layout.cloud.js"></script>
<script src="https://d3js.org/d3-scale.v1.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/seedrandom/2.4.4/seedrandom.min.js">
</script>
<svg width="700" height="700"></svg>
The solutions consist in either scaling down the size of your words in order to give d3-layout enough space or increase the width and height of the cloud container.
For instance by scaling down the size of words:
// domain reduced from [0, 20] to [0, 13]:
var sizeScale = d3.scaleLinear().range([0, 13]).domain([0, d3.max(data, d => d.count)]);
Math.seedrandom('hello word');
var data = [{ text: "Hello", count: 38 }, { text: "World", count: 27 }, { text: "Whatever", count: 21 }, { text: "Massive", count: 21 }, { text: "Thing", count: 16 }, { text: "Something", count: 14 }, { text: "What", count: 12 }, { text: "Else", count: 9 }, { text: "Blabla", count: 6 }, { text: "Small", count: 6 }, { text: "VeryLong", count: 6 }, { text: "Word", count: 3 }];
var svg = d3.select("svg").append("g");
let fill = d3.scaleOrdinal(d3.schemeCategory20);
let size = d3.scaleLinear().range([0, 13]).domain([0, d3.max(data, d => d.count)]);
let word_cloud_data = data
.map( function(d) {
return { text: d.text, size: 9 + size(d.count) * 3.5 };
});
let layout = d3.layout.cloud()
.size([275, 275])
.words(word_cloud_data)
.padding(2.5)
.rotate(d => ~~(Math.random() * 2) * -90)
.fontSize(d => d.size)
.on("end", draw);
layout.start();
function draw(words) {
svg.append("g")
.attr("transform", "translate(150,150)")
.selectAll("text")
.data(words)
.enter().append("text")
.style("fill", (d, i) => { d.color = fill(i); return d.color; })
.style("text-anchor", "middle")
.attr("transform", d => "translate(" + [d.x, d.y] + ")rotate(" + d.rotate + ")")
.text(d => d.text)
.style("font-size", d => d.size + "px");
}
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="https://cdn.rawgit.com/jasondavies/d3-cloud/master/build/d3.layout.cloud.js"></script>
<script src="https://d3js.org/d3-scale.v1.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/seedrandom/2.4.4/seedrandom.min.js">
</script>
<svg width="700" height="700"></svg>
I am a newbie in d3.js. I have tried to create a static architecture with 5 nodes and link them with each other according to preferences, the nodes should be organized like so:
At the beginning I set the position of the nodes and then create the links. Though, when the nodes get linked, the architecture changes and the result is the one displayed below:
Here is my code:
var width = 640,
height = 400;
var nodes = [
{ x: 60, y: 0, id: 0},
{ x: 150, y: height/4, id: 1},
{ x: 220, y: height/4, id: 2},
{ x: 340, y: height/4, id: 3},
{ x: 420, y: height/2, id: 4},
{ x: 480, y: height/2, id: 5}
];
var links = [
{ source: 1, target: 5 },
{ source: 0, target: 5 },
{ source: 2, target: 1 },
{ source: 3, target: 2 },
{ source: 4, target: 5 }
];
var graph = d3.select('#graph');
var svg = graph.append('svg')
.attr('width', width)
.attr('height', height);
var force = d3.layout.force()
.size([width, height])
.nodes(nodes)
.links(links);
force.linkDistance(width/2);
var link = svg.selectAll('.link')
.data(links)
.enter().append('line')
.attr('class', 'link');
var div = d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 1e-6);
var node = svg.selectAll('.node')
.data(nodes)
.enter().append("circle")
.attr("cx", d=> d.x)
.attr("cy", d=> d.y)
.attr('class', 'node')
.on("mouseover", function(d){
d3.select(this)
.transition()
.duration(500)
.style("cursor", "pointer")
div
.transition()
.duration(300)
.style("opacity", "1")
.style("display", "block")
console.log("label", d.label);
div
.html("IP: " + d.label + " x: " + d.x + " y: " + d.y)
.style("left", (d3.event.pageX ) + "px")
.style("top", (d3.event.pageY) + "px");
})
.on("mouseout", mouseout);
function mouseout() {
div.transition()
.duration(300)
.style("opacity", "0")
}
console.log("wait...");
force.on('end', function() {
node.attr('r', width/25)
.attr('cx', function(d) { return d.x; })
.attr('cy', function(d) { return d.y; });
link.attr('x1', function(d) { console.log("LINE x1-> ", d.source.x); return d.source.x; })
.attr('y1', function(d) { console.log("LINE y1-> ", d.source.y); return d.source.y; })
.attr('x2', function(d) { console.log("LINE x2-> ", d.source.x); return d.target.x; })
.attr('y2', function(d) { console.log("LINE y2-> ", d.source.y); return d.target.y; })
.attr("stroke-width", 2)
.attr("stroke","black");
});
force.start();
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<div id="graph"></div>
Could you please help me?
Thank you in advance.
A force layout offers some advantages that derive from its nature as a self organizing layout:
It places nodes and links automatically avoiding manual positioning of potentially thousands of elements
It organizes nodes and links based on assigned forces to an ideal spacing and layout
You have nodes to which you have already assigned positions, the two advantages listed above do not apply. You've already manually done the first item, and the second item will disturb and overwrite the positions you manually set.
We could fix the node positions, but if we do this with all nodes, it defeats the purpose of the force layout: to position nodes by simulating forces.
Instead, if you have the position of all nodes, we can skip the force and just append everything based on the data. The snippet below places the links first (so they are behind the nodes) using the index contained in d.source/d.target to access the specific node in the nodes array and get the appropriate x or y coordinate. The nodes are positioned normally.
It appears you have adjusted the code to use circles in your question though the screenshot uses images (as you also used in a previous question), I'll just use circles here. Based on the coordinates you've given some lines overlap. I modified the first node so that the y value wasn't 0 (which would have pushed half the circle off the svg)
var width = 640,
height = 400;
var nodes = [
{ x: 60, y: height/8, id: 0},
{ x: 150, y: height/4, id: 1},
{ x: 220, y: height/4, id: 2},
{ x: 340, y: height/4, id: 3},
{ x: 420, y: height/2, id: 4},
{ x: 480, y: height/2, id: 5}
];
var links = [
{ source: 1, target: 5 },
{ source: 0, target: 5 },
{ source: 2, target: 1 },
{ source: 3, target: 2 },
{ source: 4, target: 5 }
];
var graph = d3.select('#graph');
var svg = graph.append('svg')
.attr('width', width)
.attr('height', height);
// append links:
svg.selectAll()
.data(links)
.enter()
.append("line")
.attr("x1", function(d) { return nodes[d.source].x; })
.attr("y1", function(d) { return nodes[d.source].y; })
.attr("x2", function(d) { return nodes[d.target].x; })
.attr("y2", function(d) { return nodes[d.target].y; })
.attr("stroke-width", 2)
.attr("stroke","black");
// append nodes:
svg.selectAll()
.data(nodes)
.enter()
.append("circle")
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("r", 8);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<div id="graph"></div>
I want the nodes to have labels, and the lines to be arrows pointing to the edge of the nodes. And I also want the weight to be on the edges. I am new to D3 and having troubles finding examples to do so. Most of the example graphs are force directed, or creating a directing graph. I wanted to make a kind of a path diagram that is NOT interactive at all.
Basically, I want the source node to point to the target nodes. I just want to draw this graph in d3. I feel like this is really simple, but I just can't seem to figure it out. Any suggestions?
<div id="graph">
<script>
var vis = d3.select("#graph")
.append("svg")
.attr("width", 1000)
.attr("height", 1000);
var nodes = [
{label: "Social Dominance", x: 300, y:400},
{label: "Gender Identification", x: 500, y: 200},
{label: "Hostile Sexism", x:500, y:600},
{label: "Collactive Action", x:700, y:400}
],
edges =[
{source: nodes[0], target: nodes[1], weight: 0},
{source: nodes[0], target: nodes[2], weight: 0},
{source: nodes[0], target: nodes[3], weight: 0},
{source: nodes[1], target: nodes[3], weidht: 0},
{source: nodes[2], target: nodes[3], weight: 0}
];
vis.selectAll("circle.nodes")
.data(nodes)
.enter()
.append("svg:circle")
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("r", "60px")
.attr("fill", "pink");
vis.selectAll("line")
.data(edges)
.enter()
.append("line")
.attr("id", function(d,i){return 'edge'})
.attr('marker-end', 'url(#arrowhead)')
.style("stroke", "#ccc");
vis.selectAll(".nodelabel")
.data(nodes)
.enter()
.append("text")
.attr({"cx":function(d){return d.x;},
"cy":function(d){return d.y;},
"class":"nodelabel",
"stroke":"black"})
.text(function(d){return d.name;});
vis.selectAll(".edgepath")
.data(edges)
.enter()
.append('path')
.attr({'d': function(d) {return 'M '+d.source.x+' '+d.source.y+' L '+ d.target.x +' '+d.target.y},
'class':'edgepath',
'fill-opacity':100,
'stroke-opacity':100,
'fill':'blue',
'stroke':'black',
'id':function(d,i) {return 'edgepath'+i}});
vis.append('defs').append('marker')
.attr({'id':'arrowhead',
'viewBox':'-0 -5 10 10',
'refX':25,
'refY':0,
//'markerUnits':'strokeWidth',
'orient':'auto',
'markerWidth':100,
'markerHeight':100,
'xoverflow':'visible'})
.append('svg:path')
.attr('d', 'M 0,-5 L 10 ,0 L 0,5')
.attr('fill', '#ccc')
.attr('stroke','#ccc');
</script>
</div>
You need to make some changes in your code. Here is a summary:
Texts don't have cx or cy attributes. They should be x and y;
The text property is label, not name;
You have to set the markers to the <path> elements, not to the <line> ones;
Change the marker attributes to better fit the edge of the circles.
Here is your code with those changes:
<script src="https://d3js.org/d3.v3.min.js"></script>
<div id="graph">
<script>
var vis = d3.select("#graph")
.append("svg")
.attr("width", 1000)
.attr("height", 1000);
var nodes = [{
label: "Social Dominance",
x: 300,
y: 400
}, {
label: "Gender Identification",
x: 500,
y: 200
}, {
label: "Hostile Sexism",
x: 500,
y: 600
}, {
label: "Collactive Action",
x: 700,
y: 400
}],
edges = [{
source: nodes[0],
target: nodes[1],
weight: 0
}, {
source: nodes[0],
target: nodes[2],
weight: 0
}, {
source: nodes[0],
target: nodes[3],
weight: 0
}, {
source: nodes[1],
target: nodes[3],
weidht: 0
}, {
source: nodes[2],
target: nodes[3],
weight: 0
}];
vis.selectAll(".edgepath")
.data(edges)
.enter()
.append('path')
.attr({
'd': function(d) {
return 'M ' + d.source.x + ' ' + d.source.y + ' L ' + d.target.x + ' ' + d.target.y
},
'class': 'edgepath',
'fill-opacity': 100,
'stroke-opacity': 100,
'fill': 'blue',
'stroke': 'black',
'marker-end': 'url(#arrowhead)',
'id': function(d, i) {
return 'edgepath' + i
}
});
vis.selectAll("circle.nodes")
.data(nodes)
.enter()
.append("svg:circle")
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
})
.attr("r", "60px")
.attr("fill", "pink");
vis.selectAll(".nodelabel")
.data(nodes)
.enter()
.append("text")
.attr({
"x": function(d) {
return d.x;
},
"y": function(d) {
return d.y;
},
"class": "nodelabel",
"text-anchor": "middle",
"fill": "black"
})
.text(function(d) {
return d.label;
});
vis.append('defs').append('marker')
.attr({
'id': 'arrowhead',
'viewBox': '-0 -5 10 10',
'refX': 70,
'refY': 0,
//'markerUnits':'strokeWidth',
'orient': 'auto',
'markerWidth': 10,
'markerHeight': 10,
'xoverflow': 'visible'
})
.append('svg:path')
.attr('d', 'M 0,-5 L 10 ,0 L 0,5')
.attr('fill', '#aaa')
.attr('stroke', '#aaa');
</script>
</div>
Given the following code which calls the update function which creates 4 nodes with a circle and text element nested in a g element, waits 500ms, then calls the function again with updated data:
var data1 = [
{ x: 10, y: 10, text: "A" },
{ x: 30, y: 30, text: "B" },
{ x: 50, y: 50, text: "C" },
{ x: 70, y: 70, text: "D" }
];
var data2 = [
{ x: 30, y: 10, text: "X" },
{ x: 50, y: 30, text: "Y" },
{ x: 70, y: 50, text: "Z" },
{ x: 90, y: 70, text: "W" }
];
var svg = d3.select("body").append("svg");
update(data1);
setTimeout(function() { update(data2); }, 500);
function update(data) {
var nodes = svg.selectAll(".node")
.data(data);
var nodesUpdate = nodes
.attr("class", "node update")
var nodesEnter = nodes.enter();
var node = nodesEnter.append("g")
.attr("class", "node enter")
node
.attr("transform", function(d) { return "translate("+d.x+","+d.y+")"; });
node.append("circle")
.attr("r", 10)
.style("opacity", 0.2);
node.append("text")
.text(function(d) { return d.text; });
}
With the code as it is the second call has no effect, because everything is set in the enter selection. I'm trying to make it so I can call update with new data, and change properties on both the enter and update selections, without duplicating code. I can achieve this for top-level elements (ie the g elements) using merge, by making this change:
node
.merge(nodesUpdate)
.attr("transform", function(d) { return "translate("+d.x+","+d.y+")"; });
Now the nodes update their position after 500ms. However, I haven't been able to figure out how to update the text element. If I do nodes.selectAll("text") I end up with nested data, which doesn't work.
I've scoured the following docs to try and figure this out:
https://bl.ocks.org/mbostock/3808218
https://github.com/d3/d3-selection
https://bost.ocks.org/mike/nest/
It should just be nodes.select when dealing with a subselection.
Here's a quick refactor with comments and clearer variable names:
<!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 data1 = [{
x: 10,
y: 10,
text: "A"
}, {
x: 30,
y: 30,
text: "B"
}, {
x: 50,
y: 50,
text: "C"
}, {
x: 70,
y: 70,
text: "D"
}];
var data2 = [{
x: 30,
y: 10,
text: "X"
}, {
x: 50,
y: 30,
text: "Y"
}, {
x: 70,
y: 50,
text: "Z"
}, {
x: 90,
y: 70,
text: "W"
}];
var svg = d3.select("body").append("svg");
update(data1);
setTimeout(function() {
update(data2);
}, 500);
function update(data) {
var nodesUpdate = svg.selectAll(".node")
.data(data); // UPDATE SELECTION
var nodesEnter = nodesUpdate.enter()
.append("g")
.attr("class", "node"); // ENTER THE Gs
nodesEnter.append("text"); // APPEND THE TEXT
nodesEnter.append("circle") // APPEND THE CIRCLE
.attr("r", 10)
.style("opacity", 0.2);
var nodesEnterUpdate = nodesEnter.merge(nodesUpdate); // UPDATE + ENTER
nodesEnterUpdate // MOVE POSITION
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
nodesEnterUpdate.select("text") // SUB-SELECT THE TEXT
.text(function(d) {
return d.text;
});
}
</script>
</body>
</html>
Without refactoring a lot of your code, the simplest solution is using a key in the data function, followed by an "exit" selection:
var nodes = svg.selectAll(".node")
.data(data, d=> d.text);
nodes.exit().remove();
Here is the demo:
var data1 = [{
x: 10,
y: 10,
text: "A"
}, {
x: 30,
y: 30,
text: "B"
}, {
x: 50,
y: 50,
text: "C"
}, {
x: 70,
y: 70,
text: "D"
}];
var data2 = [{
x: 30,
y: 10,
text: "X"
}, {
x: 50,
y: 30,
text: "Y"
}, {
x: 70,
y: 50,
text: "Z"
}, {
x: 90,
y: 70,
text: "W"
}];
var svg = d3.select("body").append("svg");
update(data1);
setTimeout(function() {
update(data2);
}, 500);
function update(data) {
var nodes = svg.selectAll(".node")
.data(data, d => d.text);
nodes.exit().remove();
var nodesUpdate = nodes
.attr("class", "node update")
var nodesEnter = nodes.enter();
var node = nodesEnter.append("g")
.attr("class", "node enter")
node
.merge(nodesUpdate)
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
node.append("circle")
.attr("r", 10)
.style("opacity", 0.2);
node.append("text")
.text(function(d) {
return d.text;
});
}
<script src="https://d3js.org/d3.v4.min.js"></script>
This will create a different "enter" selection. If, on the other hand, you want to get the data bound to the "update" selection, you'll have to refactor your code.
I've seen this answer and this too, but they don't work.
My code is on Fiddle.
Two questions:
1. On clicking a node and pressing the delete button on the keyboard the node and corresponding links get deleted, but why am I not able to drag the remaining nodes afterward?
2. I tried attaching an image (using the path in the nodes array), but the image doesn't appear. The circles just disappear and no image appears (the path to the image is correct. In the same program I tried displaying the image at a corner of the screen and it worked).
The code:
<!DOCTYPE html>
<meta charset="utf-8">
<style>
.background { /*stroke: gray; stroke-width: 1px; fill: rgb(252,231,216);*/ cursor: move; }
.link { stroke: #000; stroke-width: 1.5px; }
.node { fill: #ccc; /*stroke: #000;*/ stroke-width: 1.5px; cursor: pointer;}
.node.fixed { fill: #f00; cursor: pointer;}
text { font: 10px sans-serif; pointer-events: none; text-shadow: 0 1px 0 #fff, 1px 0 0 #fff, 0 -1px 0 #fff, -1px 0 0 #fff; }
</style>
<body>
<script src="d3/d3.v3.js"></script>
<div id="topologyArea"></div>
<script>
var nodes = [//it's not necessary to give x and y values to nodes. One gets created for every empty object you insert here like this {}
{id: 1, x: 470, y: 410, icon: "images/abc.jpg"},
{id: 2, x: 493, y: 364, icon: "images/abc.jpg"},
{id: 3, x: 442, y: 365, icon: "images/abc.jpg"},
{id: 4, x: 467, y: 314, icon: "images/abc.jpg"},
{id: 5, x: 477, y: 248, icon: "images/fsd.jpg"},
{id: 6, x: 425, y: 207, icon: "images/sdfs.jpg"},
{id: 7, x: 402, y: 155, icon: "images/dfs.jpg"},
{id: 8, x: 369, y: 196, icon: "images/abc.jpg"},
{id: 9, x: 350, y: 148, icon: "images/abc.jpg"},
{id: 10, x: 539, y: 222, icon: "images/abc.jpg"},
{id: 11, x: 594, y: 235, icon: "images/abc.jpg"},
{id: 12, x: 582, y: 185, icon: "images/abc.jpg"},
{id: 13, x: 633, y: 200, icon: "images/abc.jpg"}
];
var links = [
{id: 1, source: 0, target: 1},
{id: 2, source: 1, target: 2},
{id: 3, source: 0, target: 2},
{id: 4, source: 1, target: 3},
{id: 5, source: 3, target: 2},
{id: 6, source: 3, target: 4},
{id: 7, source: 4, target: 5},
{id: 8, source: 5, target: 6},
{id: 9, source: 5, target: 7},
{id: 10, source: 6, target: 7},
{id: 11, source: 6, target: 8},
{id: 12, source: 7, target: 8},
{id: 13, source: 9, target: 4},
{id: 14, source: 9, target: 11},
{id: 15, source: 9, target: 10},
{id: 16, source: 10, target: 11},
{id: 17, source: 11, target: 12},
{id: 18, source: 12, target: 10}
];
var margin = {top: -5, right: -5, bottom: -5, left: -5}, width = 960 - margin.left - margin.right, height = 500 - margin.top - margin.bottom;
var iconOffset = -10, iconSize = 20;
var mousedown_node = null, mouseup_node = null, mousedown_link = null;
var nodeDeletionActivated = false;
var zoom = d3.behavior.zoom().scaleExtent([0.2, 2]).on("zoom", zoomed);
var svg = d3.select("#topologyArea").append("svg").attr("width", width + margin.left + margin.right).attr("height", height + margin.top + margin.bottom).attr('class', 'background').attr("transform", "translate(" + margin.left + "," + margin.right + ")");
var rect = svg.append("rect").attr("fill","transparent").attr("width", width + margin.left + margin.right).attr("height", height + margin.top + margin.bottom)
.on("mousedown", mousedownOnBackground);
rect.call(zoom);
var elementHolderLayer = svg.append("g");;
var linkLayer, nodeLayer;
d3.select(window).on("keydown", keydown);// add keyboard callback
redraw(elementHolderLayer);
function redraw(theLayer)//after updating the nodes and links arrays, use this function to re-draw the force graph
{
var force = d3.layout.force().size([width, height]).charge(-400).linkDistance(40).on("tick", tick);
var dragElement = force.drag().on("dragstart", dragstarted);
linkLayer = null; nodeLayer = null;
linkLayer = theLayer.selectAll(".link");
nodeLayer = theLayer.selectAll(".node");
linkLayer = linkLayer.data(links, function(d) {return d.id; }).exit().remove();
linkLayer = theLayer.selectAll(".link").data(links, function(d) {return d.id; }).enter().append("line").attr("class", "link");
nodeLayer = nodeLayer.data(nodes, function(d) {return d.id; }).exit().remove();
nodeLayer = theLayer.selectAll(".node").data(nodes, function(d) {return d.id; }).enter().append("circle").attr("class", "node").attr("r", 12)
.on("dblclick", dblclick).style("fill", function(d,i) { return d3.rgb(i*15, i*15, i*15); })
.on("mouseup", function(d,i) { mouseup(d,i);})
.on("mousemove", function(d,i) {mousemove(d,i);})
.on("mousedown", function(d,i) {mousedown(d,i);})
.call(dragElement)
//.classed("dragging", true)
.classed("fixed", function(d) {d.fixed = true;});
force.nodes(nodes).links(links).start();
}//redraw
function tick()
{
linkLayer.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; });
nodeLayer.attr("cx", function(d) {return d.x; }).attr("cy", function(d) { return d.y; });
}
function dblclick(d) { d3.select(this).classed("fixed", d.fixed = false); }
function dragstarted(d)
{
console.log("dragstarted for "+this);
//d3.event.sourceEvent.stopPropagation();
//d3.select(this).classed("dragging", true);
//d3.select(this).classed("fixed", d.fixed = true);
}
function zoomed() { elementHolderLayer.attr("transform", "translate("+d3.event.translate+")scale(" + d3.event.scale + ")"); }
function spliceLinksForNode(node) //remove the links attached to a node that got deleted
{
toSplice = links.filter(function(l) { return (l.source === node) || (l.target === node); });
toSplice.map(function(l) {links.splice(links.indexOf(l), 1); });
}
function keydown()
{
//if (!selected_node && !selected_link) return;
switch (d3.event.keyCode)
{
case 8:
{// backspace
}
case 46:
{ // delete
if (mousedown_node)
{
selected_node = mousedown_node;
if (selected_node)
{
nodes.splice(nodes.indexOf(selected_node), 1);
spliceLinksForNode(selected_node);
}
else if (selected_link) { links.splice(links.indexOf(selected_link), 1); }
selected_link = null;
selected_node = null;
redraw(elementHolderLayer);
}
break;
}
}
}//keydown
function mousedown(d,i) { mousedown_node = d; console.log("mousedown"); }
function mousedownOnBackground() {resetMouseVars();}
function mousemove(d, i) {console.log("mousemove");}
function mouseup(d, i) {console.log("mouseup");}
function resetMouseVars()
{
mousedown_node = null;
mouseup_node = null;
mousedown_link = null;
}
</script>
There is one problem in the redraw function in your code.
linkLayer = linkLayer.data(links, function(d) {return d.id; })
.exit()
.remove();
Above line has no use in your code since you are assigning the same variable with links having the old data again.
linkLayer = theLayer.selectAll(".link").data(links, function(d) { return d.id; })
.enter()
.append("line")
.attr("class", "link");
Same happens for nodes. Change your code as shown below.
//Creating links
linkLayer = theLayer.selectAll(".link").data(links, function(d) {
return d.id;
});
linkLayer.enter().append("line").attr("class", "link");
linkLayer.exit().remove();
//Creating Nodes with image icons
var gNodes = nodeLayer.enter().append("g")
.attr("class", "node")
.on("dblclick", dblclick).style("fill", function(d, i) {
return d3.rgb(i * 15, i * 15, i * 15);
})
.on("mouseup", mouseup)
.on("mousemove", mousemove)
.on("mousedown", mousedown)
.call(dragElement)
.classed("fixed", function(d) {
d.fixed = true;
});
gNodes.append("circle")
.attr("r", 12);
gNodes.append("svg:image")
.attr("class", "circle")
.attr("xlink:href",function(d){ return d.icon })
.attr("x", "-8px")
.attr("y", "-8px")
.attr("width", "16px")
.attr("height", "16px");
nodeLayer.exit().remove();
For updating position of circles and images easily, I have grouped them using g elements. So you will need to update the position of g element using transform attribute instead of updating cx and cy attributes of circle. Now, tick function will look like this. Updated fiddle
function tick() {
linkLayer.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; });
nodeLayer.attr("transform", function(d) {return "translate("+d.x+","+d.y+")"; });
}