Reputation: 159
I'm trying to add arrows to my layout, but it's not working. The problem is that the arrows are not represented in the correct spot, but inside the rectangle that I'm drawing for each node. What's the better way to solve this issue? I've tried changing the coordinates of the link but it hasn't worked, and also changing the diagonal object, but without success.
I'm attaching a MWE so that you can see what's the current state.
var data = {
"name": "Eve",
"children": [
{
"name": "Cain"
},
{
"name": "Seth",
"children": [
{
"name": "Enos"
},
{
"name": "Noam"
}
]
},
{
"name": "Abel"
},
{
"name": "Awan",
"children": [
{
"name": "Enoch"
}
]
},
{
"name": "Azura"
}
]
};
let width = $(document).width();
let height = $(document).height();
const DX = 80;
const DY = 80;
const MIN_ZOOM = 0.15;
const MAX_ZOOM = Infinity;
const RECT_WIDTH = 40;
const RECT_HEIGHT = 15;
const TRANSITION_DURATION = 700;
let tree = d3.tree().nodeSize([DX, DY]);
let diagonal = d3.linkVertical()
.x(d => d.x + RECT_WIDTH / 2)
.y(d => d.y + RECT_HEIGHT / 2);
function createRoot() {
let root = d3.hierarchy(data);
root.x0 = DX / 2;
root.y0 = 0;
root.descendants().forEach((d, i) => {
d.id = i;
// Auxiliar variable to hide and show nodes when user clicks
d._children = d.children;
// Only the root is displayed at first sight
if (d.depth >= 0) d.children = null;
});
return root;
}
function update(source) {
const nodes = root.descendants().reverse();
const links = root.links();
// Compute the new tree layout
tree(root);
const transition = svg.transition()
.duration(TRANSITION_DURATION)
.tween("resize", window.ResizeObserver ? null : () => () => svg.dispatch("toggle"));
/*=============================NODE SECTION============================== */
// Obtain all the nodes
const node = gNode.selectAll("g")
.data(nodes, d => d.id);
// Enter any new nodes at the parent's previous position.
const nodeEnter = node.enter().append("g")
.attr("class", "node")
.attr("transform", d => `translate(${source.x0},${source.y0})`)
.on("click", function (event, d) {
if (d.children) // Node expanded -> Collapse
collapse(d);
else // Node collapsed -> Expand
d.children = d._children
//If we want to restablish the last state is sufficient to do
//d.children = d.children ? null : d._children;
update(d);
centerNode(d);
})
.attr("pointer-events", d => d._children ? "all" : "none");
nodeEnter.append("rect")
// Two different classes, one for the leafs and another for the others
.attr("class", d => d._children ? "expandable" : "leaf")
.attr("height", RECT_HEIGHT)
.attr("width", RECT_WIDTH)
nodeEnter.append("text")
// The position of the text is centered
.attr("x", RECT_WIDTH / 2)
.attr("y", RECT_HEIGHT / 2)
.text(d => d.data.name)
.clone(true).lower();
// Transition nodes to their new position (update)
node.merge(nodeEnter).transition(transition)
.attr("transform", d => `translate(${d.x},${d.y})`)
// Show the nodes
.attr("fill-opacity", 1)
.attr("stroke-opacity", 1);
/* Transition exiting nodes to the parent's new position */
node.exit().transition(transition).remove()
.attr("transform", d => `translate(${source.x},${source.y})`)
// Hide the nodes
.attr("fill-opacity", 0)
.attr("stroke-opacity", 0);
/*=============================LINK SECTION============================== */
const link = gLink.selectAll("path")
.data(links, d => d.target.id);
// Enter any new links at the parent's previous position
const linkEnter = link.enter().append("path")
.attr("class", "link")
.attr("x", RECT_WIDTH / 2)
.attr("y", RECT_HEIGHT / 2)
.attr("marker-end", "url(#end)") // add the arrow to the link end
.attr("d", d => {
const o = {x: source.x0, y: source.y0};
return diagonal({source: o, target: o});
})
// Transition links to their new position
link.merge(linkEnter).transition(transition)
// In this case the link will be the same but moved by the transition
.attr("d", diagonal);
// Transition exiting nodes to the parent's new position
link.exit().transition(transition).remove()
.attr("d", d => {
const o = {x: source.x, y: source.y};
return diagonal({source: o, target: o});
});
// Stash the old positions for transition
root.eachBefore(d => {
d.x0 = d.x;
d.y0 = d.y;
});
}
function centerNode(source) {
let scale = d3.zoomTransform(d3.select("svg").node()).k;
let x = -source.x0 * scale + width / 2 - RECT_WIDTH / 2 * scale;
let y = -source.y0 * scale + height / 2 - RECT_HEIGHT / 2 * scale;
// Define the transition
const transition = svg.transition()
.duration(TRANSITION_DURATION)
.tween("resize", window.ResizeObserver ? null : () => () => svg.dispatch("toggle"));
// Move all the nodes based on the previous parameters
svg.transition(transition)
.call(zoomBehaviours.transform, d3.zoomIdentity.translate(x,y).scale(scale));
}
function collapse(node) {
if (node.children) { // Expanded
node.children = null;
node._children.forEach(collapse)
}
}
// Root creation
const root = createRoot();
// SVG variable that will contain all the configuration for the images.
// We need to append it to the body
const svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
const g = svg.append("g");
// Two groups: One of links and another of nodes
const gLink = g.append("g");
const gNode = g.append("g");
const zoomBehaviours = d3.zoom()
.scaleExtent([MIN_ZOOM, MAX_ZOOM])
.on('zoom', (event) => g.attr('transform', event.transform));
// Add the zoom so that svg knows that it is available
svg.call(zoomBehaviours);
svg.append("svg:defs").selectAll("marker")
.data(["end"]) // Different link/path types can be defined here
.enter().append("marker") // This section adds in the arrows
.attr("id", String)
.attr("viewBox", "0 -5 10 10")
.attr("refX", 0)
.attr("refY", 0)
.attr("markerWidth", 5)
.attr("markerHeight", 5)
.attr("orient", "auto")
.append("path")
.attr("d", "M0,-5L10,0L0,5");
update(root);
centerNode(root);
.node {
cursor: pointer;
}
.node .expandable {
stroke: black;
stroke-width: 1.2;
fill: lightskyblue;
}
.node .leaf {
fill: lightskyblue;
}
.node text {
fill: black;
font: 10px sans-serif;
text-anchor: middle;
text-align: center;
dominant-baseline: central;
}
.link {
fill: none;
stroke: black;
stroke-width: 1.5;
stroke-opacity: 0.5;
}
body {
overflow: hidden;
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
<link rel="stylesheet" href="style.css">
</head>
<body>
</body>
<script src="http://code.jquery.com/jquery-1.10.2.min.js"></script>
<script src="https://d3js.org/d3.v6.min.js"></script>
<script src="./src/main.js"></script>
</html>
Upvotes: 2
Views: 102
Reputation: 102194
You can always change source
and target
the way you want. For instance:
.attr("d", d => {
return diagonal({
source: d.source,
target: {
x: d.target.x,
y: d.target.y - RECT_HEIGHT
}
})
});
That way you can change one independent of the other, which is harder just using the path generator.
Here is your code with that change:
var data = {
"name": "Eve",
"children": [{
"name": "Cain"
},
{
"name": "Seth",
"children": [{
"name": "Enos"
},
{
"name": "Noam"
}
]
},
{
"name": "Abel"
},
{
"name": "Awan",
"children": [{
"name": "Enoch"
}]
},
{
"name": "Azura"
}
]
};
let width = $(document).width();
let height = $(document).height();
const DX = 80;
const DY = 80;
const MIN_ZOOM = 0.15;
const MAX_ZOOM = Infinity;
const RECT_WIDTH = 40;
const RECT_HEIGHT = 15;
const TRANSITION_DURATION = 700;
let tree = d3.tree().nodeSize([DX, DY]);
let diagonal = d3.linkVertical()
.x(d => d.x + RECT_WIDTH / 2)
.y(d => d.y + RECT_HEIGHT / 2);
function createRoot() {
let root = d3.hierarchy(data);
root.x0 = DX / 2;
root.y0 = 0;
root.descendants().forEach((d, i) => {
d.id = i;
// Auxiliar variable to hide and show nodes when user clicks
d._children = d.children;
// Only the root is displayed at first sight
if (d.depth >= 0) d.children = null;
});
return root;
}
function update(source) {
const nodes = root.descendants().reverse();
const links = root.links();
// Compute the new tree layout
tree(root);
const transition = svg.transition()
.duration(TRANSITION_DURATION)
.tween("resize", window.ResizeObserver ? null : () => () => svg.dispatch("toggle"));
/*=============================NODE SECTION============================== */
// Obtain all the nodes
const node = gNode.selectAll("g")
.data(nodes, d => d.id);
// Enter any new nodes at the parent's previous position.
const nodeEnter = node.enter().append("g")
.attr("class", "node")
.attr("transform", d => `translate(${source.x0},${source.y0})`)
.on("click", function(event, d) {
if (d.children) // Node expanded -> Collapse
collapse(d);
else // Node collapsed -> Expand
d.children = d._children
//If we want to restablish the last state is sufficient to do
//d.children = d.children ? null : d._children;
update(d);
centerNode(d);
})
.attr("pointer-events", d => d._children ? "all" : "none");
nodeEnter.append("rect")
// Two different classes, one for the leafs and another for the others
.attr("class", d => d._children ? "expandable" : "leaf")
.attr("height", RECT_HEIGHT)
.attr("width", RECT_WIDTH)
nodeEnter.append("text")
// The position of the text is centered
.attr("x", RECT_WIDTH / 2)
.attr("y", RECT_HEIGHT / 2)
.text(d => d.data.name)
.clone(true).lower();
// Transition nodes to their new position (update)
node.merge(nodeEnter).transition(transition)
.attr("transform", d => `translate(${d.x},${d.y})`)
// Show the nodes
.attr("fill-opacity", 1)
.attr("stroke-opacity", 1);
/* Transition exiting nodes to the parent's new position */
node.exit().transition(transition).remove()
.attr("transform", d => `translate(${source.x},${source.y})`)
// Hide the nodes
.attr("fill-opacity", 0)
.attr("stroke-opacity", 0);
/*=============================LINK SECTION============================== */
const link = gLink.selectAll("path")
.data(links, d => d.target.id);
// Enter any new links at the parent's previous position
const linkEnter = link.enter().append("path")
.attr("class", "link")
.attr("x", RECT_WIDTH / 2)
.attr("y", RECT_HEIGHT / 2)
.attr("marker-end", "url(#end)") // add the arrow to the link end
.attr("d", d => {
const o = {
x: source.x0,
y: source.y0
};
return diagonal({
source: o,
target: o
});
})
// Transition links to their new position
link.merge(linkEnter).transition(transition)
// In this case the link will be the same but moved by the transition
.attr("d", d => {
return diagonal({
source: d.source,
target: {
x: d.target.x,
y: d.target.y - RECT_HEIGHT
}
})
});
// Transition exiting nodes to the parent's new position
link.exit().transition(transition).remove()
.attr("d", d => {
const o = {
x: source.x,
y: source.y
};
return diagonal({
source: o,
target: o
});
});
// Stash the old positions for transition
root.eachBefore(d => {
d.x0 = d.x;
d.y0 = d.y;
});
}
function centerNode(source) {
let scale = d3.zoomTransform(d3.select("svg").node()).k;
let x = -source.x0 * scale + width / 2 - RECT_WIDTH / 2 * scale;
let y = -source.y0 * scale + height / 2 - RECT_HEIGHT / 2 * scale;
// Define the transition
const transition = svg.transition()
.duration(TRANSITION_DURATION)
.tween("resize", window.ResizeObserver ? null : () => () => svg.dispatch("toggle"));
// Move all the nodes based on the previous parameters
svg.transition(transition)
.call(zoomBehaviours.transform, d3.zoomIdentity.translate(x, y).scale(scale));
}
function collapse(node) {
if (node.children) { // Expanded
node.children = null;
node._children.forEach(collapse)
}
}
// Root creation
const root = createRoot();
// SVG variable that will contain all the configuration for the images.
// We need to append it to the body
const svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
const g = svg.append("g");
// Two groups: One of links and another of nodes
const gLink = g.append("g");
const gNode = g.append("g");
const zoomBehaviours = d3.zoom()
.scaleExtent([MIN_ZOOM, MAX_ZOOM])
.on('zoom', (event) => g.attr('transform', event.transform));
// Add the zoom so that svg knows that it is available
svg.call(zoomBehaviours);
svg.append("svg:defs").selectAll("marker")
.data(["end"]) // Different link/path types can be defined here
.enter().append("marker") // This section adds in the arrows
.attr("id", String)
.attr("viewBox", "0 -5 10 10")
.attr("refX", 0)
.attr("refY", 0)
.attr("markerWidth", 5)
.attr("markerHeight", 5)
.attr("orient", "auto")
.append("path")
.attr("d", "M0,-5L10,0L0,5");
update(root);
centerNode(root);
.node {
cursor: pointer;
}
.node .expandable {
stroke: black;
stroke-width: 1.2;
fill: lightskyblue;
}
.node .leaf {
fill: lightskyblue;
}
.node text {
fill: black;
font: 10px sans-serif;
text-anchor: middle;
text-align: center;
alignment-baseline: central;
}
.link {
fill: none;
stroke: black;
stroke-width: 1.5;
stroke-opacity: 0.5;
}
body {
overflow: hidden;
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
<link rel="stylesheet" href="style.css">
</head>
<body>
</body>
<script src="http://code.jquery.com/jquery-1.10.2.min.js"></script>
<script src="https://d3js.org/d3.v6.min.js"></script>
<script src="./src/main.js"></script>
</html>
Upvotes: 1