Daniel Herranz
Daniel Herranz

Reputation: 159

Arrows in links hidden inside rectangles

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

Answers (1)

Gerardo Furtado
Gerardo Furtado

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

Related Questions