I got a small D3 forced graph with 3 main nodes. Those nodes contains a attribute shoes, which hold a integer. On top of the graph are two buttons, to either add or remove shoes. As soon as one of those buttons are clicked, I want to update the D3 forced graph data. Basically the integer value in the blue node should either increase or decrease.
I searched and found several stackoverflow articles which explain the steps to achieve my need. Unforutnately I was not able yet to successfully map those articles on my prototype.
The problem is: It adds an element to the last data node but does not visually change the amount in the blue circle. The console output instead shows the correct value and increases or decreases the amount of shoes correctly.
What do I miss?
var width = window.innerWidth,
height = window.innerHeight;
var buttons = d3.select("body").selectAll("button")
.data(["add Shoes", "remove Shoes"])
.enter()
.append("button")
.text(function(d) {
return d;
})
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.call(d3.zoom().on("zoom", function(event) {
svg.attr("transform", event.transform)
}))
.append("g")
////////////////////////
// outer force layout
var data = {
"nodes":[
{ "id": "A", "shoes": 1},
{ "id": "B", "shoes": 1},
{ "id": "C", "shoes": 0},
],
"links": [
{ "source": "A", "target": "B"},
{ "source": "B", "target": "C"},
{ "source": "C", "target": "A"}
]
};
var simulation = d3.forceSimulation()
.force("size", d3.forceCenter(width / 2, height / 2))
.force("charge", d3.forceManyBody().strength(-1000))
.force("link", d3.forceLink().id(function (d) { return d.id }).distance(250))
linksContainer = svg.append("g").attr("class", "linkscontainer")
nodesContainer = svg.append("g").attr("class", "nodesContainer")
var links = linksContainer.selectAll("g")
.data(data.links)
.join("g")
.attr("fill", "transparent")
var linkLine = linksContainer.selectAll(".linkPath")
.data(data.links)
.join("path")
.attr("stroke", "red")
.attr("fill", "transparent")
.attr("stroke-width", 3)
nodes = nodesContainer.selectAll(".nodes")
.data(data.nodes, function (d) { return d.id; })
.join("g")
.attr("class", "nodes")
.attr("id", function (d) { return d.id; })
.call(d3.drag()
.on("start", dragStarted)
.on("drag", dragged)
.on("end", dragEnded)
)
nodes.selectAll("circle")
.data(d => [d])
.join("circle")
.style("fill", "lightgrey")
.style("stroke", "blue")
.attr("r", 40)
var smallCircle = nodes.selectAll("g")
//.data(d => d.shoes)
.data(d => [d])
.enter()
.filter(function(d) {
return d.shoes !== 0;
})
.append("g")
.attr("cursor", "pointer")
.attr("transform", function(d, i) {
const factor = (i / 40) * (15 / 2) * 5;
return `translate(${40 * Math.cos(factor - Math.PI * 0.5)},${40 * Math.sin(factor - Math.PI * 0.5)})`;
});
smallCircle.append('circle')
.attr("class", "circle-small")
.attr('r', 15)
.attr("fill", "blue")
smallCircle.append("text")
.attr("font-size", 15)
.attr("fill", "white")
.attr("dominant-baseline", "central")
.style("text-anchor", "middle")
.attr("pointer-events", "cursor")
.text(function(d) {
return d.shoes;
})
simulation
.nodes(data.nodes)
.on("tick", tick)
simulation
.force("link")
.links(data.links)
function tick() {
linkLine.attr("d", function(d) {
var dx = (d.target.x - d.source.x),
dy = (d.target.y - d.source.y),
dr = Math.sqrt(dx * dx + dy * dy)
return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y;
})
nodes
.attr("transform", d => `translate(${d.x}, ${d.y})`);
}
function dragStarted(event, d) {
if (!event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(event, d) {
d.fx = event.x;
d.fy = event.y;
}
function dragEnded(event, d) {
if (!event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
buttons.on("click", function(d) {
if (d.srcElement.__data__ == "add Shoes") {
data.nodes.forEach(function(item) {
item.shoes = item.shoes + 1
})
} else if (d.srcElement.__data__ == "remove Shoes") {
data.nodes.forEach(function(item) {
if (!item.shoes == 0) {
item.shoes = item.shoes - 1
}
})
}
restart()
})
function restart() {
// Apply the general update pattern to the nodes.
smallCircle = nodes.selectAll("g")
.data(d => [d])
.enter()
.filter(function(d) {
return d.shoes !== 0;
})
.append("g")
.attr("cursor", "pointer")
.attr("transform", function(d, i) {
const factor = (i / 40) * (15 / 2) * 5;
return `translate(${40 * Math.cos(factor - Math.PI * 0.5)},${40 * Math.sin(factor - Math.PI * 0.5)})`;
});
smallCircle.append("circle")
.attr("class", "circle-small")
.attr('r', 15)
.attr("fill", "blue")
smallCircle.append("text")
.attr("font-size", 15)
.attr("fill", "white")
.attr("dominant-baseline", "central")
.style("text-anchor", "middle")
.text(function(d) {
return d.shoes;
})
smallCircle.exit().remove();
// Update and restart the simulation.
simulation.nodes(data.nodes);
simulation.restart()
}
body {
background: whitesmoke,´;
overflow: hidden;
margin: 0px;
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>D3v7</title>
<!-- d3.js framework -->
<script src="https://d3js.org/d3.v7.js"></script>
</head>
<body>
</body>
</html>
For further d3 selection behavings.
I accepted the answer, which is completely correct. In addition I tried to add another g element on the same level and seems the d3 selector can´t handle selections by group. At least selectAll("gblue") behaves different than selectAll("g")
var width = window.innerWidth,
height = window.innerHeight;
var buttons = d3.select("body").selectAll("button")
.data(["add blue", "remove blue", "add red", "remove red"])
.enter()
.append("button")
.text(function (d) {
return d;
})
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.call(d3.zoom().on("zoom", function (event) {
svg.attr("transform", event.transform)
}))
.append("g")
////////////////////////
// outer force layout
var data = {
"nodes": [{
"id": "A",
"blue": 1,
"red": 0,
},
{
"id": "B",
"blue": 1,
"red": 1
},
{
"id": "C",
"blue": 0,
"red": 1
},
],
"links": [{
"source": "A",
"target": "B"
},
{
"source": "B",
"target": "C"
},
{
"source": "C",
"target": "A"
}
]
};
var simulation = d3.forceSimulation()
.force("size", d3.forceCenter(width / 2, height / 2))
.force("charge", d3.forceManyBody().strength(-1000))
.force("link", d3.forceLink().id(function (d) {
return d.id
}).distance(250))
linksContainer = svg.append("g").attr("class", "linkscontainer")
nodesContainer = svg.append("g").attr("class", "nodesContainer")
var links = linksContainer.selectAll("g")
.data(data.links)
.join("g")
.attr("fill", "transparent")
var linkLine = linksContainer.selectAll(".linkPath")
.data(data.links)
.join("path")
.attr("stroke", "red")
.attr("fill", "transparent")
.attr("stroke-width", 3)
nodes = nodesContainer.selectAll(".nodes")
.data(data.nodes, function (d) {
return d.id;
})
.join("g")
.attr("class", "nodes")
.call(d3.drag()
.on("start", dragStarted)
.on("drag", dragged)
.on("end", dragEnded)
)
nodes.selectAll("circle")
.data(d => [d])
.join("circle")
.style("fill", "lightgrey")
.style("stroke", "blue")
.attr("r", 40)
var blueNode = nodes.selectAll("gblue")
.data(d => d.blue ? [d] : [])
.enter()
.append("g")
.attr("class", "gblue")
.attr("cursor", "pointer")
.attr("transform", function (d, i) {
const factor = (i / 40) * (15 / 2) * 5;
return `translate(${40 * Math.cos(factor - Math.PI * 0.5)},${40 * Math.sin(factor - Math.PI * 0.5)})`;
});
blueNode.append('circle')
.attr('r', 15)
.attr("fill", "blue")
blueNode.append("text")
.attr("font-size", 15)
.attr("fill", "white")
.attr("dominant-baseline", "central")
.style("text-anchor", "middle")
.attr("pointer-events", "cursor")
.text(function (d) {
return d.blue;
})
var redNode = nodes.selectAll("gred")
.data(d => d.red ? [d] : [])
.enter()
.append("g")
.attr("class", "gred")
.attr("cursor", "pointer")
.attr("transform", function (d, i) {
const factor = (i / 40) * (15 / 2) * 5;
return `translate(${40 * Math.cos(factor - Math.PI * 0.3)},${40 * Math.sin(factor - Math.PI * 0.3)})`;
});
redNode.append('circle')
.attr('r', 15)
.attr("fill", "red")
redNode.append("text")
.attr("font-size", 15)
.attr("fill", "white")
.attr("dominant-baseline", "central")
.style("text-anchor", "middle")
.attr("pointer-events", "cursor")
.text(function (d) {
return d.red;
})
simulation
.nodes(data.nodes)
.on("tick", tick)
simulation
.force("link")
.links(data.links)
function tick() {
linkLine.attr("d", function (d) {
var dx = (d.target.x - d.source.x),
dy = (d.target.y - d.source.y),
dr = Math.sqrt(dx * dx + dy * dy)
return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y;
})
nodes
.attr("transform", d => `translate(${d.x}, ${d.y})`);
}
function dragStarted(event, d) {
if (!event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(event, d) {
d.fx = event.x;
d.fy = event.y;
}
function dragEnded(event, d) {
if (!event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
buttons.on("click", function (_, d) {
if (d === "add blue") {
data.nodes.forEach(function (item) {
item.blue = item.blue + 1
})
} else if (d === "remove blue") {
data.nodes.forEach(function (item) {
if (!item.blue == 0) {
item.blue = item.blue - 1
}
})
} else if (d === "add red") {
data.nodes.forEach(function (item) {
item.red = item.red + 1
})
} else if (d === "remove red") {
data.nodes.forEach(function (item) {
if (!item.red == 0) {
item.red = item.red - 1
}
})
}
restart()
})
function restart() {
// Apply the general update pattern to the nodes.
let blueNode = nodes.selectAll("g")
.data(d => d.blue ? [d] : []);
blueNode.exit().remove();
const blueNodeEnter = blueNode.enter()
.append("g")
.attr("class", "blue")
.attr("cursor", "pointer")
.attr("transform", function (d, i) {
const factor = (i / 40) * (15 / 2) * 5;
return `translate(${40 * Math.cos(factor - Math.PI * 0.5)},${40 * Math.sin(factor - Math.PI * 0.5)})`;
});
blueNodeEnter.append("circle")
.attr('r', 15)
.attr("fill", "blue")
blueNodeEnter.append("text")
.attr("font-size", 15)
.attr("fill", "white")
.attr("dominant-baseline", "central")
.style("text-anchor", "middle")
.text(function (d) {
return d.blue;
});
blueNode = blueNodeEnter.merge(blueNode);
blueNode.select("text")
.text(function (d) {
return d.blue;
});
let redNode = nodes.selectAll("g")
.data(d => d.red ? [d] : []);
redNode.exit().remove();
const redNodeEnter = redNode.enter()
.append("g")
.attr("class", "red")
.attr("cursor", "pointer")
.attr("transform", function (d, i) {
const factor = (i / 40) * (15 / 2) * 5;
return `translate(${40 * Math.cos(factor - Math.PI * 0.5)},${40 * Math.sin(factor - Math.PI * 0.5)})`;
});
redNodeEnter.append("circle")
.attr('r', 15)
.attr("fill", "blue")
redNodeEnter.append("text")
.attr("font-size", 15)
.attr("fill", "white")
.attr("dominant-baseline", "central")
.style("text-anchor", "middle")
.text(function (d) {
return d.red;
});
redNode = redNodeEnter.merge(redNode);
redNode.select("text")
.text(function (d) {
return d.red;
});
// Update and restart the simulation.
simulation.nodes(data.nodes);
simulation.restart()
}
.link {
stroke: #000;
stroke-width: 1.5px;
}
.nodes {
fill: whitesmoke;
}
<!DOCTYPE html>
<html lang="de">
<head>
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0">
<meta charset="utf-8">
<!-- jQuery -->
<script src="https://code.jquery.com/jquery-3.6.3.js"></script>
<!-- D3 -->
<script src="https://d3js.org/d3.v7.min.js"></script>
<!-- fontawesome stylesheet https://fontawesome.com/ -->
<script src="https://kit.fontawesome.com/98a5e27706.js" crossorigin="anonymous"></script>
</head>
<body>
</body>
</html>
First of all, do not mess with private variables, conventionally assigned as __foo__. So, instead of...
buttons.on("click", function(d) {
if (d.srcElement.__data__ == "add Shoes") { etc...
...just do:
buttons.on("click", function(_, d) {
if (d == "add Shoes") {
Back to the problem: the issue here is an incorrect enter-update-exit pattern. This should be it:
//the update selection:
let smallCircle = nodes.selectAll("g")
.data(d => d.shoes ? [d] : []);
//the exit selection:
smallCircle.exit().remove();
//the enter selection:
const smallCircleEnter = smallCircle.enter()
.append("g")
//etc...
//appending elements in the enter selection only:
smallCircleEnter.append("circle")
//etc...
smallCircleEnter.append("text")
//etc...
//merging the enter and the update selections:
smallCircle = smallCircleEnter.merge(smallCircle);
//modifying the update selection
smallCircle.select("text")
.text(function(d) {
return d.shoes;
});
Also, I'm removing that filter and passing an empty array if the number of shoes is zero.
Here's your code with those changes:
var width = window.innerWidth,
height = window.innerHeight;
var buttons = d3.select("body").selectAll("button")
.data(["add Shoes", "remove Shoes"])
.enter()
.append("button")
.text(function(d) {
return d;
})
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.call(d3.zoom().on("zoom", function(event) {
svg.attr("transform", event.transform)
}))
.append("g")
////////////////////////
// outer force layout
var data = {
"nodes": [{
"id": "A",
"shoes": 1
},
{
"id": "B",
"shoes": 1
},
{
"id": "C",
"shoes": 0
},
],
"links": [{
"source": "A",
"target": "B"
},
{
"source": "B",
"target": "C"
},
{
"source": "C",
"target": "A"
}
]
};
var simulation = d3.forceSimulation()
.force("size", d3.forceCenter(width / 2, height / 2))
.force("charge", d3.forceManyBody().strength(-1000))
.force("link", d3.forceLink().id(function(d) {
return d.id
}).distance(250))
linksContainer = svg.append("g").attr("class", "linkscontainer")
nodesContainer = svg.append("g").attr("class", "nodesContainer")
var links = linksContainer.selectAll("g")
.data(data.links)
.join("g")
.attr("fill", "transparent")
var linkLine = linksContainer.selectAll(".linkPath")
.data(data.links)
.join("path")
.attr("stroke", "red")
.attr("fill", "transparent")
.attr("stroke-width", 3)
nodes = nodesContainer.selectAll(".nodes")
.data(data.nodes, function(d) {
return d.id;
})
.join("g")
.attr("class", "nodes")
.attr("id", function(d) {
return d.id;
})
.call(d3.drag()
.on("start", dragStarted)
.on("drag", dragged)
.on("end", dragEnded)
)
nodes.selectAll("circle")
.data(d => [d])
.join("circle")
.style("fill", "lightgrey")
.style("stroke", "blue")
.attr("r", 40)
var smallCircle = nodes.selectAll("g")
//.data(d => d.shoes)
.data(d => [d])
.enter()
.filter(function(d) {
return d.shoes !== 0;
})
.append("g")
.attr("cursor", "pointer")
.attr("transform", function(d, i) {
const factor = (i / 40) * (15 / 2) * 5;
return `translate(${40 * Math.cos(factor - Math.PI * 0.5)},${40 * Math.sin(factor - Math.PI * 0.5)})`;
});
smallCircle.append('circle')
.attr("class", "circle-small")
.attr('r', 15)
.attr("fill", "blue")
smallCircle.append("text")
.attr("font-size", 15)
.attr("fill", "white")
.attr("dominant-baseline", "central")
.style("text-anchor", "middle")
.attr("pointer-events", "cursor")
.text(function(d) {
return d.shoes;
})
simulation
.nodes(data.nodes)
.on("tick", tick)
simulation
.force("link")
.links(data.links)
function tick() {
linkLine.attr("d", function(d) {
var dx = (d.target.x - d.source.x),
dy = (d.target.y - d.source.y),
dr = Math.sqrt(dx * dx + dy * dy)
return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y;
})
nodes
.attr("transform", d => `translate(${d.x}, ${d.y})`);
}
function dragStarted(event, d) {
if (!event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(event, d) {
d.fx = event.x;
d.fy = event.y;
}
function dragEnded(event, d) {
if (!event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
buttons.on("click", function(_, d) {
if (d === "add Shoes") {
data.nodes.forEach(function(item) {
item.shoes = item.shoes + 1
})
} else if (d === "remove Shoes") {
data.nodes.forEach(function(item) {
if (!item.shoes == 0) {
item.shoes = item.shoes - 1
}
})
}
restart()
})
function restart() {
// Apply the general update pattern to the nodes.
let smallCircle = nodes.selectAll("g")
.data(d => d.shoes ? [d] : []);
smallCircle.exit().remove();
const smallCircleEnter = smallCircle.enter()
.append("g")
.attr("cursor", "pointer")
.attr("transform", function(d, i) {
const factor = (i / 40) * (15 / 2) * 5;
return `translate(${40 * Math.cos(factor - Math.PI * 0.5)},${40 * Math.sin(factor - Math.PI * 0.5)})`;
});
smallCircleEnter.append("circle")
.attr("class", "circle-small")
.attr('r', 15)
.attr("fill", "blue")
smallCircleEnter.append("text")
.attr("font-size", 15)
.attr("fill", "white")
.attr("dominant-baseline", "central")
.style("text-anchor", "middle")
.text(function(d) {
return d.shoes;
});
smallCircle = smallCircleEnter.merge(smallCircle);
smallCircle.select("text")
.text(function(d) {
return d.shoes;
});
// Update and restart the simulation.
simulation.nodes(data.nodes);
simulation.restart()
}
body {
background: whitesmoke, ´;
overflow: hidden;
margin: 0px;
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>D3v7</title>
<!-- d3.js framework -->
<script src="https://d3js.org/d3.v7.js"></script>
</head>
<body>
</body>
</html>
Related
When I click on a node, I want a new node to be added to it. They should both have labels (I'm trying to build a thesaurus visualization).
I'm very new to D3, so I apologize if you have to explain things in a bit more detail.
This is my code so far:
var width = 960;
var height = 500;
var force = d3.layout.force()
.gravity(0.05)
.distance(100)
.charge(-100)
.size([width, height])
.nodes([{ "name": "One", "group": 1 }])
.start();
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var nodes = force.nodes();
var links = force.links();
var link = svg.selectAll(".link")
.data(links)
.enter().append("line")
.attr("class", "link");
var node = svg.selectAll(".node")
.data(force.nodes())
.enter().append("g")
.attr("class", "node")
.call(force.drag);
node.append("circle")
.attr("r", 10)
.on("mousedown", onClick);
node.append("text")
.attr("dx", 12)
.attr("dy", ".35em")
.text(d => d.name);
force.on("tick", function() {
link.attr("x1", d => d.source.x)
.attr("y1", d => d.source.y)
.attr("x2", d => d.target.x)
.attr("y2", d => d.target.y);
node.attr("transform", d => {
// d.x & d.y are NaN for new nodes
return "translate(" + d.x + "," + d.y + ")";
});
});
restart();
function restart() {
node = node.data(nodes);
node.enter().append("g")
.attr("class", "node")
.call(force.drag);
node.append("circle")
.attr("r", 10)
.on("mousedown", onClick);
node.append("text")
.attr("dx", 12)
.attr("dy", ".35em")
.text(d => d.name);
node.exit().remove();
link = link.data(links);
}
function onClick(clicked_node) {
console.log("click!");
console.log(clicked_node);
var new_node = { name: "Test", group: 2 };
nodes.push(new_node);
// Has x & y set to NaN after adding
links.push({ source: clicked_node, target: new_node });
restart();
}
As soon as I click on the first node, causing the node
{ name: "Test", group: 2 };
to be added, D3 throws errors within
node.attr("transform", d => {
// d.x & d.y are NaN for new nodes
return "translate(" + d.x + "," + d.y + ")";
});
because the d.x and d.y for this new node are NaN.
I tried setting them explicitly:
{ name: "Test", group: 2, x: clicked_node.y, y: clicked_node.y };
But I get the same error. In the inspector, when this node is added to the screen, the x and y values becomes the px and py values instead!
I don't understand why this happens.
You are missing one line of code. After you add your new node you need to restart the simulation for d3 to calculate it's position:
function restart() {
node = node.data(nodes);
node.enter().append("g")
.attr("class", "node")
.call(force.drag);
node.append("circle")
.attr("r", 10)
.on("mousedown", onClick);
node.append("text")
.attr("dx", 12)
.attr("dy", ".35em")
.text(d => d.name);
node.exit().remove();
link = link.data(links);
force.start(); //<-- start simulation
}
Running code:
<!DOCTYPE html>
<html>
<head>
<script data-require="d3#3.5.17" data-semver="3.5.17" src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.js"></script>
</head>
<body>
<script>
var width = 960;
var height = 500;
var force = d3.layout.force()
.gravity(0.05)
.distance(100)
.charge(-100)
.size([width, height])
.nodes([{ "name": "One", "group": 1 }])
.start();
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var nodes = force.nodes();
var links = force.links();
var link = svg.selectAll(".link")
.data(links)
.enter().append("line")
.attr("class", "link");
var node = svg.selectAll(".node")
.data(force.nodes())
.enter().append("g")
.attr("class", "node")
.call(force.drag);
node.append("circle")
.attr("r", 10)
.on("mousedown", onClick);
node.append("text")
.attr("dx", 12)
.attr("dy", ".35em")
.text(d => d.name);
force.on("tick", function() {
link.attr("x1", d => d.source.x)
.attr("y1", d => d.source.y)
.attr("x2", d => d.target.x)
.attr("y2", d => d.target.y);
node.attr("transform", d => {
// d.x & d.y are NaN for new nodes
return "translate(" + d.x + "," + d.y + ")";
});
});
restart();
function restart() {
node = node.data(nodes);
node.enter().append("g")
.attr("class", "node")
.call(force.drag);
node.append("circle")
.attr("r", 10)
.on("mousedown", onClick);
node.append("text")
.attr("dx", 12)
.attr("dy", ".35em")
.text(d => d.name);
node.exit().remove();
link = link.data(links);
force.start();
}
function onClick(clicked_node) {
console.log("click!");
console.log(clicked_node);
var new_node = { name: "Test", group: 2 };
nodes.push(new_node);
// Has x & y set to NaN after adding
links.push({ source: clicked_node, target: new_node });
restart();
}
</script>
</body>
</html>
I am trying to color the connections in my hierarchical edge bundling visualization based on the groups they are connecting to. An example of this can be seen here.
Here is my current mouseover function:
function mouseover(d) {
svg.selectAll("path.link.target-" + d.key)
.classed("target", true)
.each(updateNodes("source", true));
svg.selectAll("path.link.source-" + d.key)
.classed("source", true)
.each(updateNodes("target", true));
}
And here is the mouseover function from the example I've posted:
function mouseovered(d)
{
// Handle tooltip
// Tooltips should avoid crossing into the center circle
d3.selectAll("#tooltip").remove();
d3.selectAll("#vis")
.append("xhtml:div")
.attr("id", "tooltip")
.style("opacity", 0)
.html(d.title);
var mouseloc = d3.mouse(d3.select("#vis")[0][0]),
my = ((rotateit(d.x) > 90) && (rotateit(d.x) < 270)) ? mouseloc[1] + 10 : mouseloc[1] - 35,
mx = (rotateit(d.x) < 180) ? (mouseloc[0] + 10) : Math.max(130, (mouseloc[0] - 10 - document.getElementById("tooltip").offsetWidth));
d3.selectAll("#tooltip").style({"top" : my + "px", "left": mx + "px"});
d3.selectAll("#tooltip")
.transition()
.duration(500)
.style("opacity", 1);
node.each(function(n) { n.target = n.source = false; });
currnode = d3.select(this)[0][0].__data__;
link.classed("link--target", function(l) {
if (l.target === d)
{
return l.source.source = true;
}
if (l.source === d)
{
return l.target.target = true;
}
})
.filter(function(l) { return l.target === d || l.source === d; })
.attr("stroke", function(d){
if (d[0].name == currnode.name)
{
return color(d[2].cat);
}
return color(d[0].cat);
})
.each(function() { this.parentNode.appendChild(this); });
d3.selectAll(".link--clicked").each(function() { this.parentNode.appendChild(this); });
node.classed("node--target", function(n) {
return (n.target || n.source);
});
}
I am somewhat new to D3, but I am assuming what I'll need to do is check the group based on the key and then match it to the same color as that group.
My full code is here:
<script type="text/javascript">
color = d3.scale.category10();
var w = 840,
h = 800,
rx = w / 2,
ry = h / 2,
m0,
rotate = 0
pi = Math.PI;
var splines = [];
var cluster = d3.layout.cluster()
.size([360, ry - 180])
.sort(function(a, b) {
return d3.ascending(a.key, b.key);
});
var bundle = d3.layout.bundle();
var line = d3.svg.line.radial()
.interpolate("bundle")
.tension(.5)
.radius(function(d) {
return d.y;
})
.angle(function(d) {
return d.x / 180 * Math.PI;
});
// Chrome 15 bug: <http://code.google.com/p/chromium/issues/detail?id=98951>
var div = d3.select("#bundle")
.style("width", w + "px")
.style("height", w + "px")
.style("position", "absolute");
var svg = div.append("svg:svg")
.attr("width", w)
.attr("height", w)
.append("svg:g")
.attr("transform", "translate(" + rx + "," + ry + ")");
svg.append("svg:path")
.attr("class", "arc")
.attr("d", d3.svg.arc().outerRadius(ry - 180).innerRadius(0).startAngle(0).endAngle(2 * Math.PI))
.on("mousedown", mousedown);
d3.json("TASKS AND PHASES.json", function(classes) {
var nodes = cluster.nodes(packages.root(classes)),
links = packages.imports(nodes),
splines = bundle(links);
var path = svg.selectAll("path.link")
.data(links)
.enter().append("svg:path")
.attr("class", function(d) {
return "link source-" + d.source.key + " target-" + d.target.key;
})
.attr("d", function(d, i) {
return line(splines[i]);
});
var groupData = svg.selectAll("g.group")
.data(nodes.filter(function(d) {
return (d.key == 'Department' || d.key == 'Software' || d.key == 'Tasks' || d.key == 'Phases') && d.children;
}))
.enter().append("group")
.attr("class", "group");
var groupArc = d3.svg.arc()
.innerRadius(ry - 177)
.outerRadius(ry - 157)
.startAngle(function(d) {
return (findStartAngle(d.__data__.children) - 2) * pi / 180;
})
.endAngle(function(d) {
return (findEndAngle(d.__data__.children) + 2) * pi / 180
});
svg.selectAll("g.arc")
.data(groupData[0])
.enter().append("svg:path")
.attr("d", groupArc)
.attr("class", "groupArc")
.attr("id", function(d, i) {console.log(d.__data__.key); return d.__data__.key;})
.style("fill", function(d, i) {return color(i);})
.style("fill-opacity", 0.5)
.each(function(d,i) {
var firstArcSection = /(^.+?)L/;
var newArc = firstArcSection.exec( d3.select(this).attr("d") )[1];
newArc = newArc.replace(/,/g , " ");
svg.append("path")
.attr("class", "hiddenArcs")
.attr("id", "hidden"+d.__data__.key)
.attr("d", newArc)
.style("fill", "none");
});
svg.selectAll(".arcText")
.data(groupData[0])
.enter().append("text")
.attr("class", "arcText")
.attr("dy", 15)
.append("textPath")
.attr("startOffset","50%")
.style("text-anchor","middle")
.attr("xlink:href",function(d,i){return "#hidden" + d.__data__.key;})
.text(function(d){return d.__data__.key;});
svg.selectAll("g.node")
.data(nodes.filter(function(n) {
return !n.children;
}))
.enter().append("svg:g")
.attr("class", "node")
.attr("id", function(d) {
return "node-" + d.key;
})
.attr("transform", function(d) {
return "rotate(" + (d.x - 90) + ")translate(" + d.y + ")";
})
.append("svg:text")
.attr("dx", function(d) {
return d.x < 180 ? 25 : -25;
})
.attr("dy", ".31em")
.attr("text-anchor", function(d) {
return d.x < 180 ? "start" : "end";
})
.attr("transform", function(d) {
return d.x < 180 ? null : "rotate(180)";
})
.text(function(d) {
return d.key.replace(/_/g, ' ');
})
.on("mouseover", mouseover)
.on("mouseout", mouseout);
d3.select("input[type=range]").on("change", function() {
line.tension(this.value / 100);
path.attr("d", function(d, i) {
return line(splines[i]);
});
});
});
d3.select(window)
.on("mousemove", mousemove)
.on("mouseup", mouseup);
function mouse(e) {
return [e.pageX - rx, e.pageY - ry];
}
function mousedown() {
m0 = mouse(d3.event);
d3.event.preventDefault();
}
function mousemove() {
if (m0) {
var m1 = mouse(d3.event),
dm = Math.atan2(cross(m0, m1), dot(m0, m1)) * 180 / Math.PI;
div.style("-webkit-transform", "translate3d(0," + (ry - rx) + "px,0)rotate3d(0,0,0," + dm + "deg)translate3d(0," + (rx - ry) + "px,0)");
}
}
function mouseup() {
if (m0) {
var m1 = mouse(d3.event),
dm = Math.atan2(cross(m0, m1), dot(m0, m1)) * 180 / Math.PI;
rotate += dm;
if (rotate > 360) rotate -= 360;
else if (rotate < 0) rotate += 360;
m0 = null;
div.style("-webkit-transform", "rotate3d(0,0,0,0deg)");
svg.attr("transform", "translate(" + rx + "," + ry + ")rotate(" + rotate + ")")
.selectAll("g.node text")
.attr("dx", function(d) {
return (d.x + rotate) % 360 < 180 ? 25 : -25;
})
.attr("text-anchor", function(d) {
return (d.x + rotate) % 360 < 180 ? "start" : "end";
})
.attr("transform", function(d) {
return (d.x + rotate) % 360 < 180 ? null : "rotate(180)";
});
}
}
function mouseover(d) {
svg.selectAll("path.link.target-" + d.key)
.classed("target", true)
.each(updateNodes("source", true));
svg.selectAll("path.link.source-" + d.key)
.classed("source", true)
.each(updateNodes("target", true));
}
function mouseout(d) {
svg.selectAll("path.link.source-" + d.key)
.classed("source", false)
.each(updateNodes("target", false));
svg.selectAll("path.link.target-" + d.key)
.classed("target", false)
.each(updateNodes("source", false));
}
function updateNodes(name, value) {
return function(d) {
if (value) this.parentNode.appendChild(this);
svg.select("#node-" + d[name].key).classed(name, value);
};
}
function cross(a, b) {
return a[0] * b[1] - a[1] * b[0];
}
function dot(a, b) {
return a[0] * b[0] + a[1] * b[1];
}
function findStartAngle(children) {
var min = children[0].x;
children.forEach(function(d) {
if (d.x < min)
min = d.x;
});
return min;
}
function findEndAngle(children) {
var max = children[0].x;
children.forEach(function(d) {
if (d.x > max)
max = d.x;
});
return max;
}
</script>
Here's an example solution in D3 v6 adapting the Observable example plus my answer to this other question. Basic points:
You will to add the 'group' into the input data - for the data you mention in the comments I've defined group as the 2nd element (per dot separation) of the name. The hierarchy function in the Observable appears to strip this.
It's probably fortunate that all the name values are e.g. root.parent.child - this makes the leafGroups work quite well for your data (but might not for asymmetric hierarchies).
Define a colour range e.g. const colors = d3.scaleOrdinal().domain(leafGroups.map(d => d[0])).range(d3.schemeTableau10); which you can use for arcs, label text (nodes), paths (links)
I've avoided using the mix-blend-mode styling with the example as it doesn't look good to me.
I'm applying the styles in overed and outed - see below for the logic.
See the comments in overed for styling logic on mouseover:
function overed(event, d) {
//link.style("mix-blend-mode", null);
d3.select(this)
// set dark/ bold on hovered node
.style("fill", colordark)
.attr("font-weight", "bold");
d3.selectAll(d.incoming.map(d => d.path))
// each link has data with source and target so you can get group
// and therefore group color; 0 for incoming and 1 for outgoing
.attr("stroke", d => colors(d[0].data.group))
// increase stroke width for emphasis
.attr("stroke-width", 4)
.raise();
d3.selectAll(d.outgoing.map(d => d.path))
// each link has data with source and target so you can get group
// and therefore group color; 0 for incoming and 1 for outgoing
.attr("stroke", d => colors(d[1].data.group))
// increase stroke width for emphasis
.attr("stroke-width", 4)
.raise()
d3.selectAll(d.incoming.map(([d]) => d.text))
// source and target nodes to go dark and bold
.style("fill", colordark)
.attr("font-weight", "bold");
d3.selectAll(d.outgoing.map(([, d]) => d.text))
// source and target nodes to go dark and bold
.style("fill", colordark)
.attr("font-weight", "bold");
}
See the comments in outed for styling logic on mouseout:
function outed(event, d) {
//link.style("mix-blend-mode", "multiply");
d3.select(this)
// hovered node to revert to group colour on mouseout
.style("fill", d => colors(d.data.group))
.attr("font-weight", null);
d3.selectAll(d.incoming.map(d => d.path))
// incoming links to revert to 'colornone' and width 1 on mouseout
.attr("stroke", colornone)
.attr("stroke-width", 1);
d3.selectAll(d.outgoing.map(d => d.path))
// incoming links to revert to 'colornone' and width 1 on mouseout
.attr("stroke", colornone)
.attr("stroke-width", 1);
d3.selectAll(d.incoming.map(([d]) => d.text))
// incoming nodes to revert to group colour on mouseout
.style("fill", d => colors(d.data.group))
.attr("font-weight", null);
d3.selectAll(d.outgoing.map(([, d]) => d.text))
// incoming nodes to revert to group colour on mouseout
.style("fill", d => colors(d.data.group))
.attr("font-weight", null);
}
Working example with the data you mentioned in the comments:
const url = "https://gist.githubusercontent.com/robinmackenzie/5c5d2af4e3db47d9150a2c4ba55b7bcd/raw/9f9c6b92d24bd9f9077b7fc6c4bfc5aebd2787d5/harvard_vis.json";
const colornone = "#ccc";
const colordark = "#222";
const width = 600;
const radius = width / 2;
d3.json(url).then(json => {
// hack in the group name to each object
json.forEach(o => o.group = o.name.split(".")[1]);
// then render
render(json);
});
function render(data) {
const line = d3.lineRadial()
.curve(d3.curveBundle.beta(0.85))
.radius(d => d.y)
.angle(d => d.x);
const tree = d3.cluster()
.size([2 * Math.PI, radius - 100]);
const root = tree(bilink(d3.hierarchy(hierarchy(data))
.sort((a, b) => d3.ascending(a.height, b.height) || d3.ascending(a.data.name, b.data.name))));
const svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", width)
.append("g")
.attr("transform", `translate(${radius},${radius})`);
const arcInnerRadius = radius - 100;
const arcWidth = 20;
const arcOuterRadius = arcInnerRadius + arcWidth;
const arc = d3
.arc()
.innerRadius(arcInnerRadius)
.outerRadius(arcOuterRadius)
.startAngle((d) => d.start)
.endAngle((d) => d.end);
const leafGroups = d3.groups(root.leaves(), d => d.parent.data.name);
const arcAngles = leafGroups.map(g => ({
name: g[0],
start: d3.min(g[1], d => d.x),
end: d3.max(g[1], d => d.x)
}));
const colors = d3.scaleOrdinal().domain(leafGroups.map(d => d[0])).range(d3.schemeTableau10);
svg
.selectAll(".arc")
.data(arcAngles)
.enter()
.append("path")
.attr("id", (d, i) => `arc_${i}`)
.attr("d", (d) => arc({start: d.start, end: d.end}))
.attr("fill", d => colors(d.name))
svg
.selectAll(".arcLabel")
.data(arcAngles)
.enter()
.append("text")
.attr("x", 5)
.attr("dy", (d) => ((arcOuterRadius - arcInnerRadius) * 0.8))
.append("textPath")
.attr("class", "arcLabel")
.attr("xlink:href", (d, i) => `#arc_${i}`)
.text((d, i) => ((d.end - d.start) < (6 * Math.PI / 180)) ? "" : d.name);
// add nodes
const node = svg.append("g")
.attr("font-family", "sans-serif")
.attr("font-size", 10)
.selectAll("g")
.data(root.leaves())
.join("g")
.attr("transform", d => `rotate(${d.x * 180 / Math.PI - 90}) translate(${d.y}, 0)`)
.append("text")
.attr("dy", "0.31em")
.attr("x", d => d.x < Math.PI ? (arcWidth + 5) : (arcWidth + 5) * -1)
.attr("text-anchor", d => d.x < Math.PI ? "start" : "end")
.attr("transform", d => d.x >= Math.PI ? "rotate(180)" : null)
.text(d => d.data.name)
.style("fill", d => colors(d.data.group))
.each(function(d) { d.text = this; })
.on("mouseover", overed)
.on("mouseout", outed)
.call(text => text.append("title").text(d => `${id(d)} ${d.outgoing.length} outgoing ${d.incoming.length} incoming`));
// add edges
const link = svg.append("g")
.attr("stroke", colornone)
.attr("fill", "none")
.selectAll("path")
.data(root.leaves().flatMap(leaf => leaf.outgoing))
.join("path")
//.style("mix-blend-mode", "multiply")
.attr("d", ([i, o]) => line(i.path(o)))
.each(function(d) { d.path = this; });
function overed(event, d) {
//link.style("mix-blend-mode", null);
d3.select(this)
.style("fill", colordark)
.attr("font-weight", "bold");
d3.selectAll(d.incoming.map(d => d.path))
.attr("stroke", d => colors(d[0].data.group))
.attr("stroke-width", 4)
.raise();
d3.selectAll(d.outgoing.map(d => d.path))
.attr("stroke", d => colors(d[1].data.group))
.attr("stroke-width", 4)
.raise()
d3.selectAll(d.incoming.map(([d]) => d.text))
.style("fill", colordark)
.attr("font-weight", "bold");
d3.selectAll(d.outgoing.map(([, d]) => d.text))
.style("fill", colordark)
.attr("font-weight", "bold");
}
function outed(event, d) {
//link.style("mix-blend-mode", "multiply");
d3.select(this)
.style("fill", d => colors(d.data.group))
.attr("font-weight", null);
d3.selectAll(d.incoming.map(d => d.path))
.attr("stroke", colornone)
.attr("stroke-width", 1);
d3.selectAll(d.outgoing.map(d => d.path))
.attr("stroke", colornone)
.attr("stroke-width", 1);
d3.selectAll(d.incoming.map(([d]) => d.text))
.style("fill", d => colors(d.data.group))
.attr("font-weight", null);
d3.selectAll(d.outgoing.map(([, d]) => d.text))
.style("fill", d => colors(d.data.group))
.attr("font-weight", null);
}
function id(node) {
return `${node.parent ? id(node.parent) + "." : ""}${node.data.name}`;
}
function bilink(root) {
const map = new Map(root.leaves().map(d => [id(d), d]));
for (const d of root.leaves()) d.incoming = [], d.outgoing = d.data.imports.map(i => [d, map.get(i)]);
for (const d of root.leaves()) for (const o of d.outgoing) o[1].incoming.push(o);
return root;
}
function hierarchy(data, delimiter = ".") {
let root;
const map = new Map;
data.forEach(function find(data) {
const {name} = data;
if (map.has(name)) return map.get(name);
const i = name.lastIndexOf(delimiter);
map.set(name, data);
if (i >= 0) {
find({name: name.substring(0, i), children: []}).children.push(data);
data.name = name.substring(i + 1);
} else {
root = data;
}
return data;
});
return root;
}
}
.node {
font: 300 11px "Helvetica Neue", Helvetica, Arial, sans-serif;
fill: #fff;
}
.arcLabel {
font: 300 14px "Helvetica Neue", Helvetica, Arial, sans-serif;
fill: #fff;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.0.0/d3.min.js"></script>
This is a follow on from my previous question
d3 rect in one group interfering with rect in another group
Two issues:
Incorrect drag behaviour. When clicking on the second rect to drag it, it jumps to where the third one is.
I added a anonymous function which runs when the svg in clicked on anywhere. This should add a new rect. However that is the working.
I know I should have only one issue per question but these are related and I suspect they will be solved together.
<!DOCTYPE html>
<meta charset="utf-8">
<style>
/*.active {
stroke: #000;
stroke-width: 2px;
}*/
</style>
<svg width="960" height="500"></svg>
<script src="//d3js.org/d3.v4.min.js"></script>
<script>
var margin = {
top: 20,
right: 20,
bottom: 20,
left: 20
},
width = 600 - margin.left - margin.right,
height = 600 - margin.top - margin.bottom;
var svg = d3.select("svg");
var data = [{
x: 200
}, {
x: 300
}, {
x: 400
}];
var groove = svg.append("g")
.attr("class", "groove_group");
groove.append("rect")
.attr("x", 100)
.attr("y", 150)
.attr("rx", 2)
.attr("ry", 2)
.attr("height", 6)
.attr("width", 800)
.style("fill", "grey");
groove.append("rect")
.attr("x", 102)
.attr("y", 152)
.attr("rx", 2)
.attr("ry", 2)
.attr("height", 2)
.attr("width", 796)
.style("fill", "black");
// create group
var group = svg.selectAll(null)
.data(data)
.enter().append("g")
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended))
.on("click", removeElement);
group.append("rect")
.attr("x", function(d) {
return d.x;
})
.attr("y", 100)
.attr("height", 100)
.attr("width", 15)
.style("fill", "lightblue")
.attr('id', function(d, i) {
return 'handle_' + i;
})
.attr("rx", 6)
.attr("ry", 6)
.attr("stroke-width", 2)
.attr("stroke", "black");
group.append("text")
.attr("x", function(d) {
return d.x
})
.attr("y", 100)
.attr("text-anchor", "start")
.style("fill", "black")
.text(function(d) {
return "x:" + d.x
});
// create group
var group = svg.selectAll("g")
.data(data)
.enter().append("g")
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended))
.on("click", removeElement);
group.append("rect")
.attr("x", function(d) {
return d.x;
})
.attr("y", 200)
.attr("height", 100)
.attr("width", 15)
.style("fill", "lightblue")
.attr('id', function(d, i) {
return 'handle_' + i;
})
.attr("rx", 6)
.attr("ry", 6)
.attr("stroke-width", 2)
.attr("stroke", "black");
group.append("text")
.attr("x", function(d) {
return d.x
})
.attr("y", 200)
.attr("text-anchor", "start")
.style("fill", "black")
.text(function(d) {
return "x:" + d.x
});
svg.on("click", function() {
var coords = d3.mouse(this);
var newData = {
x: d3.event.x,
}
data.push(newData);
group.selectAll("rect")
.data(data)
.enter()
.append("rect")
.attr("x", function(d) {
return d.x;
})
.attr("y", 200)
.attr("height", 100)
.attr("width", 15)
.style("fill", "steelblue")
.attr('id', function(d, i) {
return 'rect_' + i;
})
.attr("rx", 6)
.attr("ry", 6)
.attr("stroke-width", 2)
.attr("stroke", "black");
});
function dragstarted(d) {
d3.select(this).raise().classed("active", true);
}
function dragged(d) {
d3.select(this).select("text")
.attr("x", d.x = d3.event.x);
d3.select(this).select("rect")
.attr("x", d.x = d3.event.x);
}
function dragended(d) {
d3.select(this)
.classed("active", false);
}
function removeElement(d) {
d3.event.stopPropagation();
data = data.filter(function(e) {
return e != d;
});
d3.select(this)
.remove();
}
</script>
Here are the explanations to your issues:
You are reassigning var groups, that is, you have two var groups in your code, the last one overwriting the first one. Just remove the last variable.
In your function to append new rectangles, you are using an update selection that selects rectangles. However, your enter selection appends groups (<g>) elements, not rectangles.
Have a look at the refactored function, it binds the data to a newly created group and appends the rectangle to that group:
var newGroup = svg.selectAll(".group")
.data(data, function(d) {
return d.x
})
.enter()
.append("g")
//etc...
newGroup.append("rect")
//etc...
Also, use a key selection in the data binding, so you know exactly what rectangle is being dragged:
.data(data, function(d){return d.x})
Here is your code with those changes:
<!DOCTYPE html>
<meta charset="utf-8">
<style>
/*.active {
stroke: #000;
stroke-width: 2px;
}*/
</style>
<svg width="960" height="500"></svg>
<script src="//d3js.org/d3.v4.min.js"></script>
<script>
var margin = {
top: 20,
right: 20,
bottom: 20,
left: 20
},
width = 600 - margin.left - margin.right,
height = 600 - margin.top - margin.bottom;
var svg = d3.select("svg");
var data = [{
x: 200
}, {
x: 300
}, {
x: 400
}];
var groove = svg.append("g")
.attr("class", "groove_group");
groove.append("rect")
.attr("x", 100)
.attr("y", 150)
.attr("rx", 2)
.attr("ry", 2)
.attr("height", 6)
.attr("width", 800)
.style("fill", "grey");
groove.append("rect")
.attr("x", 102)
.attr("y", 152)
.attr("rx", 2)
.attr("ry", 2)
.attr("height", 2)
.attr("width", 796)
.style("fill", "black");
// create group
var group = svg.selectAll(null)
.data(data, function(d){return d.x})
.enter().append("g")
.attr("class", "group")
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended))
.on("click", removeElement);
group.append("rect")
.attr("x", function(d) {
return d.x;
})
.attr("y", 100)
.attr("height", 100)
.attr("width", 15)
.style("fill", "lightblue")
.attr('id', function(d, i) {
return 'handle_' + i;
})
.attr("rx", 6)
.attr("ry", 6)
.attr("stroke-width", 2)
.attr("stroke", "black");
group.append("text")
.attr("x", function(d) {
return d.x
})
.attr("y", 100)
.attr("text-anchor", "start")
.style("fill", "black")
.text(function(d) {
return "x:" + d.x
});
svg.on("click", function() {
var coords = d3.mouse(this);
var newData = {
x: coords[0],
}
data.push(newData);
var newGroup = svg.selectAll(".group")
.data(data, function(d){return d.x})
.enter()
.append("g")
.attr("class", "group")
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended))
.on("click", removeElement);
newGroup.append("rect")
.attr("x", function(d) {
return d.x;
})
.attr("y", 200)
.attr("height", 100)
.attr("width", 15)
.style("fill", "steelblue")
.attr('id', function(d, i) {
return 'rect_' + i;
})
.attr("rx", 6)
.attr("ry", 6)
.attr("stroke-width", 2)
.attr("stroke", "black");
});
function dragstarted(d) {
d3.select(this).raise().classed("active", true);
}
function dragged(d) {
d3.select(this).select("text")
.attr("x", d.x = d3.event.x);
d3.select(this).select("rect")
.attr("x", d.x = d3.event.x);
}
function dragended(d) {
d3.select(this)
.classed("active", false);
}
function removeElement(d) {
d3.event.stopPropagation();
data = data.filter(function(e) {
return e != d;
});
d3.select(this)
.remove();
}
</script>
For correctly drag-and-drop behavior, rewrite your code like this:
var margin = {
top: 20,
right: 20,
bottom: 20,
left: 20
},
width = 600 - margin.left - margin.right,
height = 600 - margin.top - margin.bottom;
var svg = d3.select("svg");
var data = [{
x: 200
}, {
x: 300
}, {
x: 400
}];
var groove = svg.append("g")
.attr("class", "groove_group");
groove.append("rect")
.attr("x", 100)
.attr("y", 150)
.attr("rx", 2)
.attr("ry", 2)
.attr("height", 6)
.attr("width", 800)
.style("fill", "grey");
groove.append("rect")
.attr("x", 102)
.attr("y", 152)
.attr("rx", 2)
.attr("ry", 2)
.attr("height", 2)
.attr("width", 796)
.style("fill", "black");
// create group
var group = svg.selectAll(null)
.data(data)
.enter().append("g")
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended))
.on("click", removeElement);
group.append("rect")
.attr("x", function(d) {
return d.x;
})
.attr("y", 100)
.attr("height", 100)
.attr("width", 15)
.style("fill", "lightblue")
.attr('id', function(d, i) {
return 'handle_' + i;
})
.attr("rx", 6)
.attr("ry", 6)
.attr("stroke-width", 2)
.attr("stroke", "black");
group.append("text")
.attr("x", function(d) {
return d.x
})
.attr("y", 100)
.attr("text-anchor", "start")
.style("fill", "black")
.text(function(d) {
return "x:" + d.x
});
svg.on("click", function() {
var coords = d3.mouse(this);
var newData = {
x: d3.event.x,
}
data.push(newData);
group.selectAll("rect")
.data(data)
.exit()
.enter()
.append("rect")
.attr("x", function(d) {
return d.x;
})
.attr("y", 200)
.attr("height", 100)
.attr("width", 15)
.style("fill", "steelblue")
.attr('id', function(d, i) {
return 'rect_' + i;
})
.attr("rx", 6)
.attr("ry", 6)
.attr("stroke-width", 2)
.attr("stroke", "black");
});
function dragstarted(d) {
d3.select(this).raise().classed("active", true);
}
function dragged(d) {
d3.select(this).select("text")
.attr("x", d.x = d3.event.x);
d3.select(this).select("rect")
.attr("x", d.x = d3.event.x);
}
function dragended(d) {
d3.select(this)
.classed("active", false);
}
function removeElement(d) {
d3.event.stopPropagation();
data = data.filter(function(e) {
return e != d;
});
d3.select(this)
.remove();
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.2/d3.min.js"></script>
<svg width="960" height="500"></svg>
But, what the problem with adding the new element, I have no idea.
My last question was answered so quickly and smoothly I thought I'd return with another issue I'm failing to figure out on my own.
I used one of the examples to create this graph:
data = [{ "label": "1", "value": 20 },
{ "label": "2", "value": 50 },
{ "label": "3", "value": 30 },
{ "label": "4", "value": 45 }];
var width = 400,
height = 450;
var outerRadius = 200,
innerRadius = outerRadius / 3,
color = d3.scale.category20c();
var pie = d3.layout.pie()
.value(function (d) { return d.value; });
var pieData = pie(data);
var arc = d3.svg.arc()
.innerRadius(innerRadius);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + outerRadius + "," + (outerRadius + 50) + ")");
svg.append("text")
.attr("x", 0)
.attr("y", -(outerRadius + 10))
.style("text-anchor", "middle")
.text("Title[enter image description here][1]");
svg.selectAll("path")
.data(pieData)
.enter().append("path")
.each(function (d) { d.outerRadius = outerRadius - 20; })
.attr("d", arc)
.attr("fill", function (d, i) { return color(i); })
.on("mouseover", arcTween(outerRadius, 0))
.on("mouseout", arcTween(outerRadius - 20, 150));
svg.selectAll("path")
.append("text")
.attr("transform", function (d) {
d.innerRadius = 0;
d.outerRadius = outerRadius;
return "translate(" + arc.centroid(d) + ")";
})
.attr("fill", "white")
.attr("text-anchor", "middle")
.text(function (d, i) { return data[i].label; });
function arcTween(outerRadius, delay) {
return function () {
d3.select(this).transition().delay(delay).attrTween("d", function (d) {
var i = d3.interpolate(d.outerRadius, outerRadius);
return function (t) { d.outerRadius = i(t); return arc(d); };
});
};
}
The idea being that when you hover over a section on the pie chart (donut chart?) it expands. However, this made my labels dissapear and I can't manage to make them come back. I either get an error, or they just don't show up on the screen (even though I see the tag in the inspector). Any obvious thing I'm missing?
Thanks!
You cannot append a <text> element to a <path> element. It simply doesn't work in an SVG. Even not working, the <text> element will be appended.
That being said, a solution is creating a new "enter" selection for the texts:
svg.selectAll(null)
.data(pieData)
.enter()
.append("text")
.attr("transform", function(d) {
d.innerRadius = 0;
d.outerRadius = outerRadius;
return "translate(" + arc.centroid(d) + ")";
})
.attr("fill", "white")
.attr("text-anchor", "middle")
.text(function(d, i) {
return data[i].label;
});
Here is your updated code:
data = [{
"label": "1",
"value": 20
}, {
"label": "2",
"value": 50
}, {
"label": "3",
"value": 30
}, {
"label": "4",
"value": 45
}];
var width = 400,
height = 450;
var outerRadius = 200,
innerRadius = outerRadius / 3,
color = d3.scale.category20c();
var pie = d3.layout.pie()
.value(function(d) {
return d.value;
});
var pieData = pie(data);
var arc = d3.svg.arc()
.innerRadius(innerRadius);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + outerRadius + "," + (outerRadius + 50) + ")");
svg.append("text")
.attr("x", 0)
.attr("y", -(outerRadius + 10))
.style("text-anchor", "middle")
.text("Title[enter image description here][1]");
svg.selectAll("path")
.data(pieData)
.enter().append("path")
.each(function(d) {
d.outerRadius = outerRadius - 20;
})
.attr("d", arc)
.attr("fill", function(d, i) {
return color(i);
})
.on("mouseover", arcTween(outerRadius, 0))
.on("mouseout", arcTween(outerRadius - 20, 150));
svg.selectAll(null)
.data(pieData)
.enter()
.append("text")
.attr("transform", function(d) {
d.innerRadius = 0;
d.outerRadius = outerRadius;
return "translate(" + arc.centroid(d) + ")";
})
.attr("fill", "white")
.attr("text-anchor", "middle")
.text(function(d, i) {
return data[i].label;
});
function arcTween(outerRadius, delay) {
return function() {
d3.select(this).transition().delay(delay).attrTween("d", function(d) {
var i = d3.interpolate(d.outerRadius, outerRadius);
return function(t) {
d.outerRadius = i(t);
return arc(d);
};
});
};
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
I have developed a force layout to represent relationships between social groups. Now I would like to get the nodes to be distributed in a circle with links joining them. What is the best way to do this?
The complete version of the code (without data) is here http://jsfiddle.net/PatriciaW/zZSJT/
(Why do I have to include code here too? Here is the main portion)
d3.json("/relationships?nocache=" + (new Date()).getTime(),function(error,members){
var links=members.organizations.map(function(members) {
return members.member;
});
var nodes = {};
links.forEach(function(link) {
link.source = nodes[link.xsource] || (nodes[link.xsource] = {source: link.xsource, name: link.xsource, category: link.categorysource, path: link.pathsource, desc: link.descsource, title: link.titlesource});
link.target = nodes[link.xtarget] || (nodes[link.xtarget] = {target: link.xtarget, name: link.xtarget, category: link.categorytarget, path: link.pathtarget, desc: link.desctarget, title: link.titletarget});
});
force = d3.layout.force()
.nodes(d3.values(nodes))
.links(links)
.size([width, height])
.charge(-120)
.linkDistance(function() {return (Math.random() * 200) + 100;})
.linkStrength(0.5)
.on("tick", tick)
.start();
var link = svg.selectAll(".link")
.data(force.links())
.enter().append("line")
.attr("class", "link");
var node_drag = d3.behavior.drag()
.on("dragstart", dragstart)
.on("drag", dragmove)
.on("dragend", dragend);
var loading = svg.append("text")
.attr("x", width / 2)
.attr("y", height / 2)
.attr("dy", ".35em")
.style("text-anchor", "middle")
.text("Simulating. One moment please…");
function dragstart(d, i) {
force.stop() // stops the force auto positioning before you start dragging
}
function dragmove(d, i) {
d.px += d3.event.dx;
d.py += d3.event.dy;
d.x += d3.event.dx;
d.y += d3.event.dy;
tick(); // this is the key to make it work together with updating both px,py,x,y on d !
}
function dragend(d, i) {
d.fixed = true; // of course set the node to fixed so the force doesn't include the node in its auto positioning stuff
tick();
force.resume();
};
var node = svg.selectAll(".node")
.data(force.nodes())
.enter().append("g")
.attr("class", "node")
.on("mouseover", mouseover)
.on("mouseout", mouseout)
.on("click", clickAlert)
.call(node_drag);
node.append("circle")
.attr("r", 8)
.style("fill", function(d) {
return categoryColour [d.category];
})
// add an image marker
node.append("image")
.attr("x",-8)
.attr("y",-8)
.attr("width", 16)
.attr("height", 16)
.attr("xlink:href", function(d) {
return categoryImage [d.category]
})
.on("click", clickAlert)
.style("cursor", "pointer")
node.append("text")
.attr("x", 12)
.attr("dy", ".35em")
.text(function(d) {
return d.name;
});
// Use a timeout to allow the rest of the page to load first.
setTimeout(function() {
// Run the layout a fixed number of times.
// The ideal number of times scales with graph complexity.
force.start();
for (var i = n * n; i > 0; --i) force.tick();
force.stop();
svg.selectAll("line")
.data(links)
.enter().append("line")
.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; });
svg.selectAll("circle")
.data(nodes)
.enter().append("circle")
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("r", 4.5);
loading.remove();
}, 10);
function tick() {
link
.attr("x1", function(d) {
return d.source.x + xadj; })
.attr("y1", function(d) {
return d.source.y + yadj; })
.attr("x2", function(d) {
return d.target.x +xadj; })
.attr("y2", function(d) {
return d.target.y +yadj; });
node
.attr("transform", function(d) {
return "translate(" + (d.x + xadj) + "," + (d.y + yadj) + ")";
});
};
function mouseover() {
d3.select(this).select("circle").transition()
.duration(750)
.attr("r", 16);
d3.select(this).select("text")
.attr("font-size","34px")
.style("font-weight", "bold");
};
function mouseout() {
d3.select(this).select("circle").transition()
.duration(750)
.attr("r", 8);
d3.select(this).select("text")
.attr("font-size","12px")
.style("font-weight", "normal");
};
}) // end json
Here's someone else's solution:
This network graph uses the D3 force layout to draw nodes and links, but instead of using d3.force() to find the best node positions, we draw an invisible arc and evenly places nodes along the circumference.
<!DOCTYPE html>
<html>
<head>
<script src="http://d3js.org/d3.v3.min.js"></script>
<meta charset="utf-8">
<title>JS Bin</title>
<style>
line.node-link, path.node-link {
fill: none;
stroke: black
}
circle.node {
fill: white;
stroke: black
}
circle.node+text {
text-anchor: middle;
}
text {
font-family: sans-serif;
pointer-events: none;
}
</style>
</head>
<body>
<script type="text/javascript">
// number of random nodes (gets crowded at >25 unless you change node diameter)
var num = 20;
// returns random int between 0 and num
function getRandomInt() {return Math.floor(Math.random() * (num));}
// nodes returns a [list] of {id: 1, fixed:true}
var nodes = d3.range(num).map(function(d) { return {id: d}; });
// links returns a [list] of {source: 0, target: 1} (values refer to indicies of nodes)
var links = d3.range(num).map(function(d) { return {source: getRandomInt(), target: getRandomInt()}; });
var width = 500,
height = 500;
var force = d3.layout.force()
.nodes(nodes)
.links(links)
.size([width, height]);
// evenly spaces nodes along arc
var circleCoord = function(node, index, num_nodes){
var circumference = circle.node().getTotalLength();
var pointAtLength = function(l){return circle.node().getPointAtLength(l)};
var sectionLength = (circumference)/num_nodes;
var position = sectionLength*index+sectionLength/2;
return pointAtLength(circumference-position)
}
// fades out lines that aren't connected to node d
var is_connected = function(d, opacity) {
lines.transition().style("stroke-opacity", function(o) {
return o.source === d || o.target === d ? 1 : opacity;
});
}
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
// invisible circle for placing nodes
// it's actually two arcs so we can use the getPointAtLength() and getTotalLength() methods
var dim = width-80
var circle = svg.append("path")
.attr("d", "M 40, "+(dim/2+40)+" a "+dim/2+","+dim/2+" 0 1,0 "+dim+",0 a "+dim/2+","+dim/2+" 0 1,0 "+dim*-1+",0")
.style("fill", "#f5f5f5");
force.start();
// set coordinates for container nodes
nodes.forEach(function(n, i) {
var coord = circleCoord(n, i, nodes.length)
n.x = coord.x
n.y = coord.y
});
// use this one for straight line links...
// var lines = svg.selectAll("line.node-link")
// .data(links).enter().append("line")
// .attr("class", "node-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; });
// ...or use this one for curved line links
var lines = svg.selectAll("path.node-link")
.data(links).enter().append("path")
.attr("class", "node-link")
.attr("d", function(d) {
var dx = d.target.x - d.source.x,
dy = d.target.y - d.source.y,
dr = Math.sqrt(dx * dx + dy * dy);
return "M" +
d.source.x + "," +
d.source.y + "A" +
dr + "," + dr + " 0 0,1 " +
d.target.x + "," +
d.target.y;
});
var gnodes = svg.selectAll('g.gnode')
.data(nodes).enter().append('g')
.attr("transform", function(d) {
return "translate("+d.x+","+d.y+")"
})
.classed('gnode', true);
var node = gnodes.append("circle")
.attr("r", 25)
.attr("class", "node")
.on("mouseenter", function(d) {
is_connected(d, 0.1)
node.transition().duration(100).attr("r", 25)
d3.select(this).transition().duration(100).attr("r", 30)
})
.on("mouseleave", function(d) {
node.transition().duration(100).attr("r", 25);
is_connected(d, 1);
});
var labels = gnodes.append("text")
.attr("dy", 4)
.text(function(d){return d.id})
</script>
</body>
</html>