Emilie Picard-Cantin
Emilie Picard-Cantin

Reputation: 290

How to display nested nodes from nested data in d3js?

I'm trying to display a network graph with clustered nodes but I'm having trouble with the nested nodes in D3. The first "layer" contains clusters and each node of the first layer can contain multiple nodes. Links in the network would probably only occur between clusters (meaning between nodes of the first layer).

Here is the code I have so far. I'm able to display the first level of nodes. I cannot figure out how to display the nested nodes (see code in const data in each node children).

const node_radius = 100;
const width = 800;
const height = 400;

const links = [
    { "source": 1, "target": 6}
] ;

const data = [
    {
        "id":1,
        "level": "cluster",
        "name": "analytics1",
        "children": [
            {
                "id":2,
                "name": "animate1",
                "level": "leaf",
                "size": 15,
                "parent": 1
            },
            {
                "id":3,
                "name": "animate2",
                "level": "leaf",
                "size": 15,
                "parent": 1
            },
            {
                "id":4,
                "name": "animate3",
                "level": "leaf",
                "size": 15,
                "parent": 1
            }
        ]
    },
    {
        "id":6,
        "name": "analytics2",
        "level": "cluster",
        "children": [
            {
                "id":7,
                "name": "animate4",
                "level": "leaf",
                "size": 10,
                "parent": 6
            }
        ]
    }
]

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

var simulation = d3.forceSimulation()
    // pull nodes together based on the links between them
    .force("link", d3.forceLink().id(function(d) { return d.id; }).strength(0.0001))
    // push nodes apart to space them out
    .force("charge", d3.forceManyBody().strength(-10))
    // add some collision detection so they don't overlap
    .force("collide", d3.forceCollide().radius(node_radius))
    // and draw them around the centre of the space
    .force("center", d3.forceCenter(width / 2, height / 2));

var link = svg.append("g")
    .attr("class", "links")
    .selectAll("line")
    .data(links).enter().append("line")
    .attr("stroke-width", 5)
    .attr("stroke","#000");

var node = svg.append("g")
    .attr("class", "nodes")
    .selectAll("circle")
    .data(data)
    .enter().append("circle")
    .attr("id", function(d) {return "circle"+d.id;})
    .attr("class", "node")
    .attr("r", node_radius)
    .style("opacity", 0.2)
    .attr("dx", 12)
    .attr("dy", ".35em");

var text = svg.append("g")
    .attr("class", "label")
    .selectAll("text")
    .data(data)
    .enter().append("text")
    .text(function(d) { return d.name });

// Update and restart the simulation.
simulation.nodes(data).on("tick", ticked);
simulation.force("link").links(links);
simulation.alpha(1).restart();

function ticked() {
    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; });

    node.attr("transform", positionNode);

    text
        .attr("dx", function(d) { return d.x - 30; })
        .attr("dy", function(d) { return d.y + 15; });
}

// move the node based on forces calculations
function positionNode(d) {
    // keep the node within the boundaries of the svg
    if (d.x < node_radius) {
        d.x = 2*node_radius
    };
    if (d.y < node_radius) {
        d.y = 2*node_radius
    };
    if (d.x > width-node_radius) {
        d.x = width-(2*node_radius)
    };
    if (d.y > height-node_radius) {
        d.y = height-(2*node_radius)
    };
    return "translate(" + d.x + "," + d.y + ")";
}
<script src="https://d3js.org/d3.v4.min.js"></script>

I would like to have something like the following image. The two clusters are displayed and in each group, all children (leaf nodes) are represented by a smaller node. Node size should be customizable for both "layers" from data input. enter image description here

Example I'm trying to follow on Fiddle.

I have also tried to use d3.pack() to pack circles inside of other circles. Here is an example. The problem I have currently with this approach is that I did not succeed in adding space and links between nodes of the first "layer" (between clusters). The high level clusters are also packed together and it would be impossible to add comprehensible links betwen them.

Upvotes: 1

Views: 861

Answers (1)

Emilie Picard-Cantin
Emilie Picard-Cantin

Reputation: 290

I finally succeeded in merging the d3.pack() example with the clustering example. Here is my solution.

var data =  [
  {
    "id":1,
    "level": "cluster",
    "name": "analytics1",
    "children": [
      {
        "id":2,
        "name": "animate1",
        "level": "leaf",
        "size": 8,
        "parent": 1,
        "icon":"https://image.freepik.com/free-icon/apple-logo_318-40184.jpg"
      },
      {
        "id":3,
        "name": "animate2",
        "level": "leaf",
        "size": 10,
        "parent": 1,
        "icon": "https://www.freelogodesign.org/img/logo-ex-7.png"
      },
      {
        "id":4,
        "name": "animate3",
        "level": "leaf",
        "size": 5,
        "parent": 1,
        "icon": "http://brandmark.io/logo-rank/random/pepsi.png"
      }
    ]
  },
  {
    "id":6,
    "name": "analytics2",
    "level": "cluster",
    "children": [
      {
        "id":7,
        "name": "animate4",
        "level": "leaf",
        "size": 10,
        "parent": 6,
        "icon":"https://www.seoclerk.com/pics/558390-11FO8A1505384509.png"
      }
    ]
  }
]

var links = [
    { "source": 1, "target": 6}
] ;

var w = 1200, h = 500;
var cluster_padding = 5;
var node_padding = 2;
var size_ratio =100;

var color = d3.scaleOrdinal(d3.schemeCategory20c);

let sumSizes = 0;
data.forEach(function(cluster){
    cluster.children.forEach(function(node){
        sumSizes += node.size;
    });
});

// Compute sum of sizes for cluster size.
data.forEach(function(cluster){
    cluster.size = (
      cluster.children.map(function(d){return d.size;})
        .reduce(function(acc, val){ return acc+val+node_padding; })/sumSizes
    )*size_ratio + cluster_padding;
    
    cluster.children = cluster.children.sort(function(a,b){
        return (a.size < b.size) ? 1 : ((b.size < a.size) ? -1 : 0);
    })
    
    cluster.children.forEach(function(node){
        node.parentSize = cluster.size;
        node.size = node.size*size_ratio/sumSizes;
    });
});

var svg = d3.select("body").append("svg")
  .attr("width", w)
  .attr("height", h);

////////////////////////
// outer force layout
var outerSimulation = d3.forceSimulation()
  // pull nodes together based on the links between them
  .force("link", d3.forceLink().id(function(d) { return d.id; }).strength(0.001))
  // push nodes apart to space them out
  .force("charge", d3.forceManyBody().strength(-5))
  // add some collision detection so they don't overlap
  .force("collide", d3.forceCollide().radius(function(d){return d.size+cluster_padding;}))
  // and draw them around the centre of the space
  .force("center", d3.forceCenter(w / 2, h / 2));

var outerLinks = svg.selectAll("line")
    .data(links)
    .enter().append("line")
    .attr("class", "links")
    .attr("stroke-width", 5);

var outerNodes = svg.selectAll("g.outer")
  .data(data, function (d) {return d.id;})
  .enter()
  .append("g")
  .attr("class", "outer")
  .attr("id", function (d) {return "cluster"+d.id;})
  .attr("x", w/2)
  .attr("y", w/2)
  .call(d3.drag());

outerNodes.append("circle")
  .style("fill", function(d,i){return color(i);})
  .style("stroke", "blue")
  .attr("r", function(d){return d.size});

// Update and restart the simulation.
outerSimulation.nodes(data).on("tick", outerTick);
outerSimulation.force("link").links(links);
outerSimulation.alpha(1).restart();

////////////////////////
// inner force layouts

var innerNodes = [];
var innerTexts = [];
var packs = [];

var margin = 20;

data.forEach(function(n){
  // Pack hierarchy definition
  var pack = d3.pack()
      .size([2*n.size, 2*n.size])
      .padding(cluster_padding);

  var root = d3.hierarchy(n)
      .sum(function(d) { return d.size; })
      .sort(function(a, b) { return b.value - a.value; });

  var nodes = pack(root).descendants();

  // Round images
  var defs = svg.append("defs").attr("id", "imgdefs")

  var pattern = defs
    .selectAll("pattern")
    .data(nodes.filter(function(d) { return d.parent }))
    .enter().append("pattern")
    .attr("id", function(d){return "photo"+d.data.name})
    .attr("height", 1)
    .attr("width", 1)
    .attr("x", "0")
    .attr("y", "0");

  var image = pattern.append('image')
    .attr("class","roundImg")
    .attr("id", function(d){return "photo"+d.data.name;})
    .attr("xlink:href", function(d){return d.data.icon ? d.data.icon : "";})
    .attr("height", function(d){return 3.2*d.r ;})
    ;

  // Nodes
  var circle = svg.select("g.outer#cluster"+n.id).selectAll("g.inner")
    .data(nodes.filter(function(d) { return d.parent }))
    .enter().append("circle")
    .attr("class", "node node--leaf ")
    .attr("id", function(d) {return d.data.name})
    .style("fill", function(d) { return "url(#photo"+d.data.name+")";})
    .attr("r", function(d) { return d.r; })
    .attr("transform", function(d) { return "translate("+(d.x-n.size) +","+ (d.y-n.size)+")"; })
    ;
});

////////////////////////
// functions
function outerTick (e) {
  outerNodes.attr("transform", positionNode);

  outerLinks
    .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; });
}

function positionNode(d) {
  // keep the node within the boundaries of the svg
  if (d.x - d.size < 0) {
      d.x = d.size + 2
  };
  if (d.y - d.size < 0) {
      d.y = d.size + 2
  };
  if (d.x + d.size > w) {
      d.x = w - d.size - 2
  };
  if (d.y + d.size > h) {
      d.y = h - d.size - 2
  };
  return "translate(" + d.x + "," + d.y + ")";
}
<!DOCTYPE html>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v4.min.js"></script>

<link rel="stylesheet" type="text/css" href="css/pack.css">

<body>
    <div class="packed" id="packed"></div>
</body>

Upvotes: 1

Related Questions