Henry
Henry

Reputation: 421

How to hierarchically append "composed" elements based on elements' attributes using D3 d3js?

I am using D3.js and I have a list of elements with an attribute describing categories like this one:

var nodes = [
{id:"a", cat:[0,1]},
{id:"b", cat:[0]},
{id:"c", cat:[0,1,2]}];

Each element is a multi-circle-node positioned by a force layout. The number of categories of an element defines the number of circles that will represent it.

Nodes

The "solution" I have for now is to create layers for each category. The force layout handles the list of data. The layers handle the svg elements. Each layer is an element.

The problem is that each data element is being represented by separated circles in separated layers.

How can we represent each element by a group of circles like this:

    <g class="node" id="a">
       <circle>... <circle>...
    </g>
   <g class="node" id="b">
       <circle>...
    </g>
   <g class="node" id="c">
       <circle>... <circle>... <circle>...
    </g>

Is it possible to use something hierarchically in D3 style of code like:

svg.selectAll(".node")
    .data(nodes)
    .enter()
    .append("g")
    .attr("id", function(d){return d.id;}
    //for each category on this node              
    .append("circle")
    .attr("r", function(){ return (cat+1)*3;});

A solution like this would be the best option.

Or do I have to create the elements <g class="node" id="..."> separately, selectAll(".node") and then link them to the data (node list)?

Upvotes: 2

Views: 112

Answers (2)

Gerardo Furtado
Gerardo Furtado

Reputation: 102218

It seems to me that you just need nested selections:

var groups = svg.selectAll(null)
  .data(nodes)
  .enter()
  .append("g")
  .attr("id", function(d) {
    return d.id
  });

var circles = groups.selectAll(null)
  .data(function(d) {
    return d.cat
  })
  .enter()
  .append("circle")
  //etc...

This is a demo showing it, with your nodes data:

var nodes = [{
  id: "a",
  cat: [0, 1]
}, {
  id: "b",
  cat: [0]
}, {
  id: "c",
  cat: [0, 1, 2]
}];

var colors = d3.scaleOrdinal(d3.schemeCategory10);

var svg = d3.select("svg");

var groups = svg.selectAll(null)
  .data(nodes)
  .enter()
  .append("g")
  .attr("id", function(d) {
    return d.id
  })
  .attr("transform", function(d, i) {
    return "translate(" + (50 + 100 * i) + ",75)";
  });

var circles = groups.selectAll(null)
  .data(function(d) {
    return d.cat
  })
  .enter()
  .append("circle")
  .attr("r", function(d) {
    return 10 + (d * 10)
  })
  .style("fill", function(d, i) {
    return colors(i)
  });

circles.sort(function(a, b) {
  return d3.descending(a, b)
})
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg></svg>

If you inspect your SVG, you're gonna see this:

<svg>
    <g id="a" transform="translate(50,75)">
        <circle r="30" style="fill: rgb(31, 119, 180);"></circle>
        <circle r="20" style="fill: rgb(255, 127, 14);"></circle>
    </g>
    <g id="b" transform="translate(150,75)">
        <circle r="30" style="fill: rgb(31, 119, 180);"></circle>
    </g>
    <g id="c" transform="translate(250,75)">
        <circle r="30" style="fill: rgb(31, 119, 180);"></circle>
        <circle r="20" style="fill: rgb(255, 127, 14);"></circle>
        <circle r="10" style="fill: rgb(44, 160, 44);"></circle>
    </g>
</svg>

PS: I'm sorting the circles (have a look at the bottom of the code) to bring the smaller circles to the top, making them visible.

Upvotes: 3

Mikhail Shabrikov
Mikhail Shabrikov

Reputation: 8509

You can do it this way:

svg.selectAll(".node")
  .data(nodes)
  .enter()
  .append("g")
  .attr("id", function(d){return d.id;})  
  .selectAll("circle")
  .data(function(d) {return d.cat}) // <-- bind cat data
  .enter()
  .append("circle")
  .attr("r", function(d) {
        //set radius 
  });

Upvotes: 0

Related Questions