user3872094
user3872094

Reputation: 3351

How to Show all the child nodes when there are many

I'm working on a D3 visualization that shows the parent and child relationship.

I'm able to visualize the data perfect when I've less number of children, but when the number of children is more, the child nodes get overlapped. how can I modify my chart so that all the nodes towards the left of the root (right) are visible? like having the nodes in a circular fashion around the root, having some nodes near to the root, and some far in some order. What would bee the best way to show it?

Here is my code.

var data = {
  "name": "Root",
  "img": "https://www.freelogodesign.org/Content/img/logo-samples/flooop.png",
  "children": [{
    "name": "3",
    "img": "https://www.freelogodesign.org/Content/img/logo-samples/flooop.png"
  }, {
    "name": "4",
    "img": "https://www.freelogodesign.org/Content/img/logo-samples/flooop.png"
  }, {
    "name": "1",
    "img": "https://www.freelogodesign.org/Content/img/logo-samples/flooop.png"
  }, {
    "name": "2",
    "img": "https://www.freelogodesign.org/Content/img/logo-samples/flooop.png"
  }, {
    "name": "1",
    "img": "https://www.freelogodesign.org/Content/img/logo-samples/flooop.png"
  }, {
    "name": "2",
    "img": "https://www.freelogodesign.org/Content/img/logo-samples/flooop.png"
  }, {
    "name": "1",
    "img": "https://www.freelogodesign.org/Content/img/logo-samples/flooop.png"
  }, {
    "name": "2",
    "img": "https://www.freelogodesign.org/Content/img/logo-samples/flooop.png"
  }, {
    "name": "1",
    "img": "https://www.freelogodesign.org/Content/img/logo-samples/flooop.png"
  }, {
    "name": "2",
    "img": "https://www.freelogodesign.org/Content/img/logo-samples/flooop.png"
  }, {
    "name": "1",
    "img": "https://www.freelogodesign.org/Content/img/logo-samples/flooop.png"
  }, {
    "name": "2",
    "img": "https://www.freelogodesign.org/Content/img/logo-samples/flooop.png"
  }, {
    "name": "1",
    "img": "https://www.freelogodesign.org/Content/img/logo-samples/flooop.png"
  }, {
    "name": "2",
    "img": "https://www.freelogodesign.org/Content/img/logo-samples/flooop.png"
  }, {
    "name": "1",
    "img": "https://www.freelogodesign.org/Content/img/logo-samples/flooop.png"
  }, {
    "name": "2",
    "img": "https://www.freelogodesign.org/Content/img/logo-samples/flooop.png"
  }],
  "parent": [{
    "name": "1",
    "img": "https://www.freelogodesign.org/Content/img/logo-samples/flooop.png"
  }]
};
var bgColors = ['#fd90b5', '#6ca1e9', '#fa975c', '#eb7092', '#f88962', '#a094ed', '#7f8de1'];
var dr = 0;
// Left data
var data1 = {
  "name": data.name,
  "img": data.img,
  "children": JSON.parse(JSON.stringify(data.children))
};

// Right data
var data2 = {
  "name": data.name,
  "img": data.img,
  "children": JSON.parse(JSON.stringify(data.parent))
};

// Create d3 hierarchies
var right = d3.hierarchy(data1);
var left = d3.hierarchy(data2);

// Render both trees
drawTree(right, "right")
drawTree(left, "left")

// draw single tree
function drawTree(root, pos) {
  var refType;
  if (pos == 'left')
    refType = 'left';
  else
    refType = 'right';

  var SWITCH_CONST = 1;
  if (pos === "left") {
    SWITCH_CONST = -1;
  }

  var svg = d3.select("svg"),
    width = +svg.attr("width"),
    height = +svg.attr("height")

  var g = svg.append("g").attr("transform", "translate(" + width / 2 + ",0)");

  var tree = d3.tree()
    .size([height, SWITCH_CONST * (width - 150) / 2]);

  tree(root)

  var nodes = root.descendants();
  var links = root.links();
  nodes[0].x = height / 2

  // Create links
  var link = g.selectAll(".link")
    .data(links)
    .enter()

  link.append("path")
    .attr("class", "link")
    .attr("d", function(d) {
      //first return returns a curve and the second will return straight lines in
      //return "M" + d.target.y + "," + d.target.x + "C" + (d.target.y + d.source.y) / 2.5 + "," + d.target.x + " " + (d.target.y + d.source.y) / 2 + "," + d.source.x + " " + d.source.y + "," + d.source.x;
      return "M" + d.target.y + "," + d.target.x + "A" + dr + "," + dr + " 1,0 0 " + d.source.y + "," + d.source.x;

    });


  link.append("text")
    .attr("font-family", "Arial, Helvetica, sans-serif")
    .attr("fill", "Black")
    .style("font", "normal 12px Arial")
    .attr("transform", function(d) {
      return "translate(" +
        ((d.source.y + d.target.y) / 2) + "," +
        ((d.source.x + d.target.x) / 2) + ")";
    })
    .attr("dy", ".35em")
    .attr("text-anchor", "middle")
    .data(nodes)
    .text(refType);

  // Create nodes
  var node = g.selectAll(".node")
    .data(nodes)
    .enter()
    .append("g")
    .attr("class", function(d) {
      return "node" + (d.children ? " node--internal" : " node--leaf");
    })
    .attr("transform", function(d) {
      return "translate(" + d.y + "," + d.x + ")";
    })

  node.append('circle')
    .attr('class', 'icon-wrap')
    .attr('x', 0)
    .attr('y', 0)
    .attr('r', 25)
    .style('fill', 'black');


  node.append('image')
    .attr('href', d => d.data.img)
    .attr('x', '-25')
    .attr('y', '-25')
    .attr('height', '50')
    .attr('width', '50');

  node.append("text")
    .attr("dy", 45)
    .style("text-anchor", "middle")
    .text(d => d.data.name);
}
.node circle {
  fill: #999;
}

.node text {
  font: 12px sans-serif;
}

.node--internal circle {
  fill: #555;
}

.link {
  fill: none;
  stroke: #555;
  stroke-opacity: 0.4;
  stroke-width: 1.5px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<svg width="700" height="400"></svg>

Here is how it looks with less number of children.

enter image description here

Please let me know how can I optimize my code to fit a maximum of 30 child nodes.

Thanks

Upvotes: 0

Views: 59

Answers (1)

Coola
Coola

Reputation: 3142

A possible solution is to create a recursive function which will adjust your root data coordinates in your drawTree function.

Here is an recursive function which will stagger the nodes left and right. Note the comment where it is mentioned which part of the code controls the calculations of the coordinates.

function adjustClashes(data, siblings = 1, index = 1, radius = 20, height = 400) {
  //can the node fit in the current x level?
  // if not adjust it
  let heightneeded = siblings * radius * 2;
  if (heightneeded > height) {
    // the code in this if statement will control the calculations for your new coordinates
    // In the simplest case we adjust the nodes by staggering odd and even nodes
    if (index % 2 != 0){
      data.y = data.y + (radius * 2)
    } else {
      data.y = data.y - (radius * 2)
    }
  }

  // if there are children go deeper and perform same adjustment
  if (data.children) {
    data.children.forEach((f, i) => {
      return adjustClashes( f, data.children.length, i )
    })
  } else {
    return;
  }
  // finally return the data
  return data
}

Full snippet:

var data = {
  name: "Root",
  img: "https://www.freelogodesign.org/Content/img/logo-samples/flooop.png",
  children: [
    {
      name: "3",
      img: "https://www.freelogodesign.org/Content/img/logo-samples/flooop.png"
    },
    {
      name: "4",
      img: "https://www.freelogodesign.org/Content/img/logo-samples/flooop.png"
    },
    {
      name: "1",
      img: "https://www.freelogodesign.org/Content/img/logo-samples/flooop.png"
    },
    {
      name: "2",
      img: "https://www.freelogodesign.org/Content/img/logo-samples/flooop.png"
    },
    {
      name: "1",
      img: "https://www.freelogodesign.org/Content/img/logo-samples/flooop.png"
    },
    {
      name: "2",
      img: "https://www.freelogodesign.org/Content/img/logo-samples/flooop.png"
    },
    {
      name: "1",
      img: "https://www.freelogodesign.org/Content/img/logo-samples/flooop.png"
    },
    {
      name: "2",
      img: "https://www.freelogodesign.org/Content/img/logo-samples/flooop.png"
    },
    {
      name: "1",
      img: "https://www.freelogodesign.org/Content/img/logo-samples/flooop.png"
    },
    {
      name: "2",
      img: "https://www.freelogodesign.org/Content/img/logo-samples/flooop.png"
    },
    {
      name: "1",
      img: "https://www.freelogodesign.org/Content/img/logo-samples/flooop.png"
    },
    {
      name: "2",
      img: "https://www.freelogodesign.org/Content/img/logo-samples/flooop.png"
    },
    {
      name: "1",
      img: "https://www.freelogodesign.org/Content/img/logo-samples/flooop.png"
    },
    {
      name: "2",
      img: "https://www.freelogodesign.org/Content/img/logo-samples/flooop.png"
    },
    {
      name: "1",
      img: "https://www.freelogodesign.org/Content/img/logo-samples/flooop.png"
    },
    {
      name: "2",
      img: "https://www.freelogodesign.org/Content/img/logo-samples/flooop.png"
    }
  ],
  parent: [
    {
      name: "1",
      img: "https://www.freelogodesign.org/Content/img/logo-samples/flooop.png"
    }
  ]
};

var bgColors = [
  "#fd90b5",
  "#6ca1e9",
  "#fa975c",
  "#eb7092",
  "#f88962",
  "#a094ed",
  "#7f8de1"
];
var dr = 0;
// Left data
var data1 = {
  name: data.name,
  img: data.img,
  children: JSON.parse(JSON.stringify(data.children))
};

// Right data
var data2 = {
  name: data.name,
  img: data.img,
  children: JSON.parse(JSON.stringify(data.parent))
};

// Create d3 hierarchies
var right = d3.hierarchy(data1);
var left = d3.hierarchy(data2);

// Render both trees
drawTree(right, "right");
drawTree(left, "left");

// draw single tree
function drawTree(root, pos) {
  var refType;
  if (pos == "left") refType = "left";
  else refType = "right";

  var SWITCH_CONST = 1;
  if (pos === "left") {
    SWITCH_CONST = -1;
  }

  var svg = d3.select("svg"),
    width = +svg.attr("width"),
    height = +svg.attr("height");

  var g = svg.append("g").attr("transform", "translate(" + width / 2 + ",0)");

  var tree = d3.tree().size([height, (SWITCH_CONST * (width - 150)) / 2]);

  tree(root);

  function adjustClashes(
    data,
    siblings = 1,
    index = 1,
    radius = 20,
    height = 400
  ) {
    //can the node fit in the current x level?
    // if not adjust it
    let heightneeded = siblings * radius * 2;
    if (heightneeded > height) {
      // the code in this if statement will control the calculations for your new coordinates
      // In the simplest case we adjust the nodes by staggering odd and even nodes
      if (index % 2 != 0) {
        data.y = data.y + radius * 2;
      } else {
        data.y = data.y - radius * 2;
      }
    }

    // if there are children go deeper and perform same adjustment
    if (data.children) {
      data.children.forEach((f, i) => {
        return adjustClashes(f, data.children.length, i);
      });
    } else {
      return;
    }
    // finally return the data
    return data;
  }

  root = adjustClashes(root);

  var nodes = root.descendants();
  var links = root.links();
  nodes[0].x = height / 2;

  // Create links
  var link = g.selectAll(".link").data(links).enter();

  link
    .append("path")
    .attr("class", "link")
    .attr("d", function (d) {
      //first return returns a curve and the second will return straight lines in
      //return "M" + d.target.y + "," + d.target.x + "C" + (d.target.y + d.source.y) / 2.5 + "," + d.target.x + " " + (d.target.y + d.source.y) / 2 + "," + d.source.x + " " + d.source.y + "," + d.source.x;
      return (
        "M" +
        d.target.y +
        "," +
        d.target.x +
        "A" +
        dr +
        "," +
        dr +
        " 1,0 0 " +
        d.source.y +
        "," +
        d.source.x
      );
    });

  link
    .append("text")
    .attr("font-family", "Arial, Helvetica, sans-serif")
    .attr("fill", "Black")
    .style("font", "normal 12px Arial")
    .attr("transform", function (d) {
      return (
        "translate(" +
        (d.source.y + d.target.y) / 2 +
        "," +
        (d.source.x + d.target.x) / 2 +
        ")"
      );
    })
    .attr("dy", ".35em")
    .attr("text-anchor", "middle")
    .data(nodes)
    .text(refType);

  // Create nodes
  var node = g
    .selectAll(".node")
    .data(nodes)
    .enter()
    .append("g")
    .attr("class", function (d) {
      return "node" + (d.children ? " node--internal" : " node--leaf");
    })
    .attr("transform", function (d) {
      return "translate(" + d.y + "," + d.x + ")";
    });

  node
    .append("circle")
    .attr("class", "icon-wrap")
    .attr("x", 0)
    .attr("y", 0)
    .attr("r", 25)
    .style("fill", "black");

  node
    .append("image")
    .attr("href", (d) => d.data.img)
    .attr("x", "-25")
    .attr("y", "-25")
    .attr("height", "50")
    .attr("width", "50");

  node
    .append("text")
    .attr("dy", 45)
    .style("text-anchor", "middle")
    .text((d) => d.data.name);
}
.node circle {
  fill: #999;
}

.node text {
  font: 12px sans-serif;
}

.node--internal circle {
  fill: #555;
}

.link {
  fill: none;
  stroke: #555;
  stroke-opacity: 0.4;
  stroke-width: 1.5px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<svg width="700" height="400"></svg>

Update

The following CodePen.io pen shows for example a 3 level staggering of child nodes and even adds a slight margin to space it out more evenly.

To do this simply add margin as a default parameter to the adjustClashes function and change the if statement as follows:

    if (heightneeded > height) {
      // the code in this if statement will control the calculations for your new coordinates
      // In the simplest case we adjust the nodes by staggering odd and even nodes
      if (index % 3 == 0) {
        data.y = data.y - radius * 2 - margin ;
      } else if (index % 3 == 1) {
        data.y = data.y;
      } else {
        data.y = data.y + radius * 2 + margin;
      }
    }

Upvotes: 1

Related Questions