How can a composite node be added into a d3 force directed graph?

43 Views Asked by At

I create a force directed layout graph. I then create a composite node. The composite node looks like:

composite node

I would like to create an edge between one of the circle nodes to the composite node. I would like the composite node to participate in the force directed layout.

How can I do this?

I figure I need to merge nodes and labelNodes together and pass them into .forceSimulation(nodes). However, I am not sure how to do that.

I am sure this is straightforward, but I am getting lost in the documentation and am missing some key concept that would point me to the solution.

    //
    // Create the force directed graph
    //

    const width = 1000
    const height = 400

    const node_data = Array.from({ length: 5 }, () => ({
      group: Math.floor(Math.random() * 3),
    }))

    const edge_data = Array.from({ length: 10 }, () => ({
      source: Math.floor(Math.random() * 5),
      target: Math.floor(Math.random() * 5),
      value: Math.floor(Math.random() * 10) + 1,
    }))

    const links = edge_data.map((d) => ({ ...d }))
    const nodes = node_data.map((d, index) => ({ id: index, ...d }))
    const color = d3.scaleOrdinal(d3.schemeCategory10)

    const svg = d3.select('#chart')
    
    const simulation = d3
      .forceSimulation(nodes)
      .force(
        'link',
        d3
          .forceLink(links)
          .id((d) => d.id)
          .distance((d) => 100)
      )
      .force('charge', d3.forceManyBody())
      .force('center', d3.forceCenter(width / 2, height / 2))
      .on('tick', ticked)

    const link = svg
      .append('g')
      .attr('stroke', '#999')
      .attr('stroke-opacity', 0.6)
      .selectAll()
      .data(links)
      .join('line')
      .attr('stroke-width', (d) => Math.sqrt(d.value))

    const node = svg
      .append('g')
      .attr('stroke', '#fff')
      .attr('stroke-width', 1.5)
      .selectAll()
      .data(nodes)
      .join('circle')
      .attr('r', 16)
      .attr('fill', (d) => color(d.group))

    node.append('title').text((d) => `hello ${d.id}`)

    function ticked() {
      link
        .attr('x1', (d) => d.source.x)
        .attr('y1', (d) => d.source.y)
        .attr('x2', (d) => d.target.x)
        .attr('y2', (d) => d.target.y)

      node.attr('cx', (d) => d.x).attr('cy', (d) => d.y)
    }

    node.call(d3.drag().on('start', dragstarted).on('drag', dragged).on('end', dragended))

    function dragstarted(event) {
      if (!event.active) simulation.alphaTarget(0.3).restart()
      event.subject.fx = event.subject.x
      event.subject.fy = event.subject.y
    }

    function dragged(event) {
      event.subject.fx = event.x
      event.subject.fy = event.y
    }

    function dragended(event) {
      if (!event.active) simulation.alphaTarget(0)
      event.subject.fx = null
      event.subject.fy = null
    }

    //
    // Create the composite node
    //

    const labelNodes = [
      {
        id: 0,
        name: 'A title',
      },
    ]

    let composite = svg
      .append('g')
      .attr('id', 'composite')
      .selectAll('g')
      .data(labelNodes, (d) => d.id)

    const g = composite.enter()

    const rectangularNode = g
      .append('rect')
      .attr('class', 'node')
      .attr('rx', '15')
      .attr('x', (d) => 0)
      .attr('y', (d) => 0)
      .attr('width', () => 200)
      .attr('height', () => 25)
      .attr('fill', '#dceed3')

    var outerNodebbox = rectangularNode.node().getBBox()

    const label = g
      .append('text')
      .attr('class', 'name')
      .attr('ref', 'name')
      .attr('id', 'name')
      .attr('dominant-baseline', 'middle')
      .attr('font-size', '10px')
      .attr('x', () => 13)
      .attr('y', () => 13)
      .text((d) => {
        return d['name']
      })

    composite = g.merge(composite)

    // ******************************************************
    // What goes here to add the composite node to the graph?
    // ******************************************************
    //
    // ??
    .graph {
      width: 1000px;
      height: 400px;      
    }
<script src="https://d3js.org/d3.v7.min.js" charset="utf-8"></script>

<svg ref="chart" id="chart" class="graph"></svg>

1

There are 1 best solutions below

0
Mark On BEST ANSWER

Making the display of composite nodes data-driven. Example:

<!DOCTYPE html>

<html>
  <head>
    <script src="https://d3js.org/d3.v7.min.js" charset="utf-8"></script>
    <style>
      .graph {
        width: 1000px;
        height: 400px;
      }
    </style>
  </head>

  <body>
    <svg ref="chart" id="chart" class="graph"></svg>
    <script>
      //
      // Create the force directed graph
      //

      const width = 1000;
      const height = 400;

      const node_data = Array.from({ length: 5 }, () => ({
        group: Math.floor(Math.random() * 3),
        isComposite: Math.random() > 0.5,
      }));

      const edge_data = Array.from({ length: 10 }, () => ({
        source: Math.floor(Math.random() * 5),
        target: Math.floor(Math.random() * 5),
        value: Math.floor(Math.random() * 10) + 1,
      }));

      const links = edge_data.map((d) => ({ ...d }));
      const nodes = node_data.map((d, index) => ({ id: index, ...d }));
      const color = d3.scaleOrdinal(d3.schemeCategory10);

      const svg = d3.select('#chart');

      const simulation = d3
        .forceSimulation(nodes)
        .force(
          'link',
          d3
            .forceLink(links)
            .id((d) => d.id)
            .distance((d) => 100)
        )
        .force('charge', d3.forceManyBody())
        .force('center', d3.forceCenter(width / 2, height / 2))
        .on('tick', ticked);

      const link = svg
        .append('g')
        .attr('stroke', '#999')
        .attr('stroke-opacity', 0.6)
        .selectAll()
        .data(links)
        .join('line')
        .attr('stroke-width', (d) => Math.sqrt(d.value));

      const node = svg
        .append('g')
        .attr('stroke', '#fff')
        .attr('stroke-width', 1.5)
        .selectAll()
        .data(nodes)
        .join('g');

      node
        .filter((d) => !d.isComposite)
        .append('circle')
        .attr('r', 16)
        .attr('fill', (d) => color(d.group));

      const composite = node
        .filter((d) => d.isComposite)
        .append('g')
        .attr('class', 'composite');

      composite
        .append('rect')
        .attr('class', 'node')
        .attr('rx', '15')
        .attr('width', () => 200)
        .attr('height', () => 25)
        .attr('fill', '#dceed3');

      composite
        .append('text')
        .attr('class', 'name')
        .attr('ref', 'name')
        .attr('id', 'name')
        .attr('dominant-baseline', 'middle')
        .attr('font-size', '10px')
        .attr('x', () => 13)
        .attr('y', () => 13)
        .text((d, i) => {
          return 'name ' + i;
        });

      node.append('title').text((d) => `hello ${d.id}`);

      function ticked() {
        link
          .attr('x1', (d) => d.source.x)
          .attr('y1', (d) => d.source.y)
          .attr('x2', (d) => d.target.x)
          .attr('y2', (d) => d.target.y);

        node
          .selectAll('circle')
          .attr('cx', (d) => d.x)
          .attr('cy', (d) => d.y);
        node
          .selectAll('.composite')
          .attr(
            'transform',
            (d) => 'translate(' + (d.x - 100) + ',' + d.y + ')'
          );
      }

      node.call(
        d3
          .drag()
          .on('start', dragstarted)
          .on('drag', dragged)
          .on('end', dragended)
      );

      function dragstarted(event) {
        if (!event.active) simulation.alphaTarget(0.3).restart();
        event.subject.fx = event.subject.x;
        event.subject.fy = event.subject.y;
      }

      function dragged(event) {
        event.subject.fx = event.x;
        event.subject.fy = event.y;
      }

      function dragended(event) {
        if (!event.active) simulation.alphaTarget(0);
        event.subject.fx = null;
        event.subject.fy = null;
      }
    </script>
  </body>
</html>