<div id="root"></div>
body {
  margin: 0;
}

.d3-component svg {
  background: #ddd;
  max-height: 300px;
}
import * as cola from "https://cdn.skypack.dev/webcola@3.4.0";
const size = 600;
const defaultRadius = 20;

const level2Colors = (level) => {
  const colors = {
    1: "#A01928",
    2: "#9F62A4",
    3: "#0B7A7A",
    4: "#3F3F38",
  };
  return colors[level];
};
const level2Rad = (level) => {
  return (defaultRadius * 2.5) / level;
};

const loadData = () => {
  const nodes = [
    { id: 1, level: 1, r: 8 },
    { id: 2, level: 2, r: 8 },
    { id: 3, level: 2, r: 8 },
    { id: 4, level: 2, r: 8 },
    { id: 5, level: 3, r: 8 },
    { id: 6, level: 3, r: 8 },
    { id: 7, level: 3, r: 8 },
    { id: 8, level: 3, r: 8 },
    { id: 9, level: 3, r: 8 },
    { id: 10, level: 3, r: 8 },
    { id: 11, level: 4, r: 8 },
    { id: 12, level: 4, r: 8 },
    { id: 13, level: 4, r: 8 },
    { id: 14, level: 4, r: 8 },
    { id: 15, level: 4, r: 8 },
    { id: 16, level: 4, r: 8 },
  ];

  const links = [
    { source: 0, target: 1 },
    { source: 0, target: 2 },
    { source: 0, target: 3 },
    { source: 1, target: 4 },
    { source: 1, target: 5 },
    { source: 2, target: 6 },
    { source: 2, target: 7 },
    { source: 2, target: 8 },
    { source: 3, target: 4 },
    { source: 3, target: 8 },
    { source: 3, target: 9 },
    { source: 4, target: 10 },
    { source: 4, target: 11 },
    { source: 6, target: 12 },
    { source: 7, target: 13 },
    { source: 9, target: 14 },
    { source: 9, target: 15 },
    { source: 8, target: 15 },
  ];

  return { nodes, links };
};

const { d3adaptor } = cola

class App extends React.Component {
  svg = null;

  componentDidMount() {
    const node = document.querySelector("#d3Node");

    this.initialize(node);
  }

  initialize(node) {
    const d3Cola = d3adaptor(d3).avoidOverlaps(true).size([size, size]);
    const svg = (this.svg = d3.select(node).append("svg"));
    svg
      .attr("viewBox", `0 0 ${size} ${size}`)
      .style("width", "100%")
      .style("height", "auto");

    const constraints = [];
    const { nodes, links } = loadData();
    const groups = _.groupBy(nodes, "level");

    for (const level of Object.keys(groups)) {
      const nodeGroup = groups[level];
      const constraint = {
        type: "alignment",
        axis: "y",
        offsets: [],
      };
      let prevNodeId = -1;
      for (const node of nodeGroup) {
        constraint.offsets.push({
          node: _.findIndex(nodes, (d) => d.id === node.id),
          offset: 0,
        });

        if (prevNodeId !== -1) {
          constraints.push({
            axis: "x",
            left: _.findIndex(nodes, (d) => d.id === prevNodeId),
            right: _.findIndex(nodes, (d) => d.id === node.id),
            gap: 70,
          });
        }

        prevNodeId = node.id;
      }

      constraints.push(constraint);
    }

    d3Cola
      .nodes(nodes)
      .links(links)
      .constraints(constraints)
      .flowLayout("y", 150)
      .linkDistance(50)
      .symmetricDiffLinkLengths(40)
      .avoidOverlaps(true)
      .start(50, 50, 150);

    var link = svg
      .append("g")
      .attr("class", "links")
      .selectAll("line")
      .data(links)
      .enter()
      .append("line")
      .attr("x1", (d) => d.source.x)
      .attr("y1", (d) => d.source.y + 80)
      .attr("x2", (d) => d.target.x)
      .attr("y2", (d) => d.target.y + 80)
      .attr("stroke", "grey")
      .attr("stroke-width", 1);

    var node = svg
      .append("g")
      .attr("class", "nodes")
      .selectAll("circle")
      .data(nodes)
      .enter()
      .append("circle")
      .attr("fill", (d) => level2Colors(d.level))
      .attr("cx", (d) => d.x)
      .attr("cy", (d) => d.y + 80)
      .attr("r", function (d) {
        return level2Rad(d.level);
      })
      .call(d3Cola.drag);

    d3Cola.on("tick", () => {
      link
        .attr("x1", function (d) {
          return d.source.x;
        })
        .attr("y1", function (d) {
          return d.source.y + 80;
        })
        .attr("x2", function (d) {
          return d.target.x;
        })
        .attr("y2", function (d) {
          return d.target.y + 80;
        });

      node
        .attr("cx", function (d) {
          return d.x;
        })
        .attr("cy", function (d) {
          return d.y + 80;
        });
    });
  }

  render() {
    return <div id="d3Node" className="d3-component"></div>;
  }
}

ReactDOM.render(<App />, document.querySelector('#root'))
View Compiled

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/d3/5.16.0/d3.min.js
  2. https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js
  3. https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js
  4. https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js