Jorge Monroy
Jorge Monroy

Reputation: 428

How to create a data custom hierachy sum in D3?

I am using D3v5 and React.

I have this data

{
  "name": "main",
  "children": [
    {
      "name": "testedin",
      "children": [
        {
          "name":"positive",
          "value": 53,
          "floor":2
        },
        {
          "name":"negative",
          "value": 24,
          "floor":2
        },
        {
          "name":"unknown",
          "value": 23,
          "floor":2
        }

      ],
      "floor":1,
      "value":55
    },
    {
      "name": "Not tested",
      "value": 45,
      "floor":1
    }
  ],
  "floor":0
}

And I want to draw this chart, where the data for the second column of rects is the percentage value of the rect parent from the first column.

target chart

I have follow this example: icecle Chart and everything works fine with it.

The Problem is that with that chart al values are just sum up due to this segment of code:

const partition = data => {
      const root = d3.hierarchy(data)
          .sum(d => d.value)
          .sort((a, b) => b.height - a.height || b.value - a.value);  
      return d3.partition()
          .size([height, (root.height + 1) * width / 4])
        (root);
    }

where the .sum function just adds the values of each children. Having this result: enter image description here

I tried to alter the sum function so I can have different usage of values, but cant find a way to edit it so I can have the percentages.

Upvotes: 1

Views: 880

Answers (1)

Andrew Reid
Andrew Reid

Reputation: 38201

Sum won't work here. Instead we can create the hierarchy and then go through the tree and calculate a standardized value representing height for each node/bar:

  root.value = 1;
  root.eachBefore(d=>{
    if(d.parent) d.value = d.data.value * d.parent.value / 100;
  })

We set the total value to 1 for the root. In using root.eachBefore we visit nodes "such that a given node is only visited after all of its ancestors have already been visited (docs)". Whenever we visit a node we scale its value relative to its parents: if a node represents 55%, it's sizing value will be 55% of its parents. Now we have a set of nodes with a value property that represents a given node's relative height in comparison to the root, a standardized value we can pass to the partition.

In scaling children nodes, the parent's scaled value (d.value) is used along with the child's unscaled value (d.data.value). I also use d.data.value below to access the original percentage for labelling (as the original data is preserved in d.data).

const width = 500;
const height = 150;
const color = d3.scaleOrdinal()
  .range(d3.schemeCategory10)

const data = {
  "name": "main",
  "children": [
    {
      "name": "testedin",
      "children": [
        {
          "name":"positive",
          "value": 53,
          "floor":2
        },
        {
          "name":"negative",
          "value": 24,
          "floor":2
        },
        {
          "name":"unknown",
          "value": 23,
          "floor":2
        }

      ],
      "floor":1,
      "value":55
    },
    {
      "name": "Not tested",
      "value": 45,
      "floor":1
    }
  ],
  "floor":0
}

const partition = data => {
      const root = d3.hierarchy(data)
          .sort((a, b) => b.height - a.height || b.value - a.value); 
          
      
      root.value = 1;
      root.eachBefore(d=>{
        if(d.parent) d.value = d.data.value * d.parent.value / 100;
      })
      
      // or, slightly more compactly:
      // root.eachBefore(d=>{d.value = d.parent ? d.data.value * d.parent.value / 100 : 1})
          
          
      return d3.partition()
          .size([height, (root.height + 1) * width / 4])
        (root);
    }
    
const root = partition(data);



const svg = d3.select("body").append("svg")
      .attr("viewBox", [0, 0, width, height])
      .style("font", "10px sans-serif");

  const cell = svg
    .selectAll("g")
    .data(root.descendants())
    .join("g")
      .attr("transform", d => `translate(${d.y0},${d.x0})`);

  cell.append("rect")
      .attr("width", d => d.y1 - d.y0)
      .attr("height", d => d.x1 - d.x0)
      .attr("fill-opacity", 0.6)
      .attr("fill", d => {
        if (!d.depth) return "#ccc";
        while (d.depth > 1) d = d.parent;
        return color(d.data.name);
      });

  const text = cell.filter(d => (d.x1 - d.x0) > 16).append("text")
      .attr("x", 4)
      .attr("y", 13);

  text.append("tspan")
      .text(d => d.data.name);

  text.append("tspan")
      .attr("fill-opacity", 0.7)
      .text(d =>Math.round(d.data.value) || "")
      .attr("dx", 10);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.0.0/d3.min.js"></script>

Upvotes: 1

Related Questions