Horizontal D3 FlexTree with Composite Node Sizes

by Rajesh Danabal

HTML

<script src="https://d3js.org/d3.v7.min.js"></script>
  <script src="https://unpkg.com/[email protected]/build/d3-flextree.min.js"></script>

<svg width="1400" height="600"></svg>

CSS

svg {
    font-family: sans-serif;
    border: 1px solid #ccc;
  }
  rect {
    fill: #4a90e2;
    stroke: #333;
    rx: 5;
    ry: 5;
  }
  rect.leaf {
    fill: #7ed6df;
  }
  text {
    fill: white;
    font-size: 12px;
    pointer-events: none;
  }

JavaScript

const data = {
    name: "Root",
    children: [
      {
        name: "Composite A",
        children: [
          { name: "Leaf A1" },
          { name: "Leaf A2" },
          { name: "Leaf A3" }
        ]
      },
      {
        name: "Composite B",
        children: [
          {
            name: "Composite B1",
            children: [
              { name: "Leaf B1.1" },
              { name: "Leaf B1.2" }
            ]
          },
          { name: "Leaf B2" }
        ]
      },
      { name: "Leaf C" }
    ]
  };

  // Constants for fixed leaf size and padding
  const LEAF_WIDTH = 140;
  const LEAF_HEIGHT = 50;
  const H_PADDING = 30;
  const V_PADDING = 20;

  // Precompute size for nodes recursively
  function computeNodeSize(node) {
    if (!node.children || node.children.length === 0) {
      // Leaf node: fixed size
      node.width = LEAF_WIDTH;
      node.height = LEAF_HEIGHT;
    } else {
      // Composite node: size based on children bounding box plus padding
      node.children.forEach(computeNodeSize);
      // width: max child width + horizontal padding * 2
      node.width = Math.max(...node.children.map(c => c.width)) + H_PADDING * 2;
      // height: sum of child heights + vertical padding * (child count - 1) + vertical padding * 2
      node.height = node.children.reduce((sum, c) => sum + c.height, 0) +
        V_PADDING * (node.children.length - 1) + V_PADDING * 2;
    }
  }

  computeNodeSize(data);

  // Use d3-flextree
  const layout = d3.flextree()
    .nodeSize(d => [d.data.height, d.data.width])  // Note: [height, width], but we'll swap later for horizontal
    .spacing(() => 30);

  const root = layout.hierarchy(data);
  layout(root);

  // Swap x and y to make it horizontal layout
  root.each(d => {
    const tmp = d.x;
    d.x = d.y;
    d.y = tmp;
  });

  const svg = d3.select("svg");
  const g = svg.append("g").attr("transform", "translate(80, 50)");

 ...