block by curran 2aafab81bb2029e1f4f24d258b790ce4

Fractal Pie Chart

Full Screen

Finally after having sketched out this visualization idea in notebooks for years, it has come into reality. This example highlights the utility of d3-component for recursive visualization components.

The idea here is to extend the notion of a pie chart to represent hierarchical data. Each slice gets expanded into a “sub-pie” whose area is equal to the area of the slice. The sub-pie slices get broken out recursively.

Have some fun and play with it on Blockbuilder!

web counter

index.html

<!DOCTYPE html>
<head>
  <meta charset="utf-8">
  <script src="https://unpkg.com/d3@4"></script>
  <script src="https://unpkg.com/d3-component@3"></script>
</head>
<body style="margin: 0">
  <svg width="960" height="500"></svg>
  <script>
    const svg = d3.select("svg");
    const width = +svg.attr("width");
    const height = +svg.attr("height");
    const pie = d3.pie().value(d => d.value);
    const arc = d3.arc().innerRadius(0);

    const slice = d3.component("path")
      .render((selection, d) => {
        selection
          .attr("d", arc(d))
          .attr("fill", "#f4f4f4")
          .attr("stroke", "#707070")
          .attr("stroke-width", 1)
          .attr("stroke-linejoin", "round");
      });

    const connector = d3.component("line")
      .render((selection, {parentRadius, stemDistance, radius}) => {
        if(parentRadius !== 0){
          selection
              .attr("x1", -radius)
              .attr("y1", 0)
              .attr("x2", -(stemDistance - parentRadius))
              .attr("y2", 0)
              .attr("stroke", "#a0a0a0");
        }
      });

    const fractal = d3.component("g")
      .render((selection, d) => {
        const {
          radius,
          parentRadius,
          angle,
          children,
          stemSize,
          stemWeight,
          rotateChildren
        } = d;

        const slices = pie(children);
        arc.outerRadius(radius);
        
        
        //const translate = (parentRadius + radius)/2 * stem;
        //const translate = radius * stem;
        const base = parentRadius + radius;
        const stemDistance = base + (
          (parentRadius * stemWeight + radius * (1 - stemWeight)) / 2
        ) * stemSize;
        const degrees = angle / Math.PI * 180;
        
        const fractals = slices
          .filter(d => d.data.children)
          .map(d => {

            const parentRadius = radius;
            const parentSliceFraction = (d.endAngle - d.startAngle) / (2*Math.PI);
            const parentTotalArea =  Math.PI * radius * radius;
            const parentSliceArea = parentTotalArea * parentSliceFraction;
            const childRadius = Math.sqrt(parentSliceArea / Math.PI);

            return Object.assign({}, d.data, {
              radius: childRadius,
              parentRadius: radius,
              angle: (d.startAngle + d.endAngle) / 2 - Math.PI/2,
              stemSize,
              stemWeight,
              rotateChildren
            });
          });
        
        selection
            .attr("transform", `rotate(${degrees}) translate(${stemDistance})`)
            .call(connector, d, {stemDistance, rotateChildren, degrees})
            .call(slice, slices)
            .call(fractal, fractals);
      });

    const generateData = (value) => {
      const epsilon = 0.02;
      const childRatios = [1/2, 1/4, 1/8, 1/8 * 2/3, 1/8 * 1/3];
      const d = { value };
      if(value > epsilon){
        d.children = childRatios
          .map(ratio => ratio * value)
          .map(generateData);
      }
      return d;
    }
    
    const data = generateData(1);

    d3.select("svg").append("g")
        .attr("transform", "translate(313, 224)")
        .call(fractal, data, {
          radius: 53,
          parentRadius: 0,
          angle: 0,
          stemSize: 3.2,
      
       		// 1 = weighted by parent radius,
      		// 0 = weighted by child radius
      		stemWeight: 1.007155150848
        });

  </script>
</body>