Liad Idan
Liad Idan

Reputation: 606

D3.js placing nodes inside node

I'm trying to create a force layout inside force layout nodes

I've created three arrays from the data, 2 for the outer nodes & links and the last one for the inner nodes. And drew the outer chart but I'm not sure how to create the inner chart for each node. Should I create a separate simulation for each inner nodes?

I'm adding my current code any help would be grateful!

data.json

{
  "id": "group1",
  "members": [
    {"id": "member1"},
    {"id": "member2"}
  ],
  "children": [
    { 
      "id": "group2",
      "members": [
        {"id": "member1"},
        {"id": "member2"}
      ],
    },
    { 
      "id": "group3",
      "members": [
        {"id": "member1"},
        {"id": "member2"},
        {
          "id": "member3",
          "children": [
            { "id": "group4" },
            { "id": "group5" }
          ]
        }
      ]
    }
  ]
}

main.js

const width = window.innerWidth;
const height = window.innerHeight;
const svg = d3.select("body")
  .append('svg')
  .attr('width', width)
  .attr('height', height)
  .call(d3.zoom()
    .scaleExtent([1 / 2, 8])
    .on('zoom', zoomed)
  );

const outerNodes = [];
const outerLinks = [];
const innerNodes = [];
const canvas = svg.append("g");
const simulation = d3.forceSimulation()
  .force('center', d3.forceCenter(width / 2, height / 2))
  .force('charge', d3.forceManyBody().strength(-500))
  .force('link', d3.forceLink().id(d => d.id).distance(80).strength(1));

let outerGroup, node, link;

d3.json('data.json').then(data => {
  flatten(data);

  link = canvas.append('g')
    .attr('class', 'links')
    .selectAll('line')
    .data(outerLinks)
    .enter().append('line')
      .attr('stroke', '#777');

  outerGroup = canvas.append('g')
    .attr('class', 'nodes')
    .selectAll('path')
    .data(outerNodes)
    .enter().append('g')
    .attr('id', d => d.id)
    .call(d3.drag()
      .on('start', onDragStart)
      .on('drag', onDrag)
      .on('end', onDragEnd)
    );

  node = outerGroup.append('path')
    .attr('d', generateShapePath())
    .attr('fill', d => d.children ? 'blue' : 'green')

  simulation
    .nodes(outerNodes)
    .on('tick', ticked);

  simulation.force('link')
    .links(outerLinks);
});

function flatten(node) {
  outerNodes.push(node);

  if (node.members) {
    innerNodes.push({parent: node.id, nodes: node.members})
  }

  if (node.children) {   
    node.children.forEach(child => {
      outerLinks.push({ source: node.id, target: child.id });
      flatten(child);
    }); 
  }
}

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('d', generateShapePath);
}

Upvotes: 0

Views: 799

Answers (1)

rioV8
rioV8

Reputation: 28838

You need to decouple all the individual simulations. Model the inner simulations relative to the parent position.

  • add a g for each node. This g will be translated to the node position in the tick()
  • add the node pentagon to this g modeled relative to 0,0
  • for each member do the same: g with a relative pentagon path
  • setup independent drag functions for the simulations => mouse event coords will be relative
  • for the demo I have limited the width and height
  • if you have more nodes with members, like in the question, use a simulation for each group of them.
  • what is the use of the d3.tree?

  • do not use select if you want each

    parent.select(d => {
      d.updateMembers = () => {
        innerSimulation.force('center', d3.forceCenter(d.x, d.y));
      }
    });
    

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<script src="https://d3js.org/d3.v5.min.js"></script>
</head>
<body>
<script>
run();

function run() {
const data = {
  "id": "group1",
  "members": [
    {"id": "member1"},
    {"id": "member2"}
  ],
  "children": [
    {"id": "group2"},
    {"id": "group3"},
    {"id": "group4"},
    {
      "id": "group5",
      "children": [
        {"id": "group6"},
        {"id": "group7"}
      ]
    }
  ]
};

const width = 500; // window.innerWidth;
const height = 400; // window.innerHeight;
const svg = d3.select("body")
  .append('svg')
  .attr('width', width)
  .attr('height', height)
  .call(d3.zoom()
  .scaleExtent([1 / 2, 8])
  .on("zoom", zoomed));

const outerNodes = [];
const outerLinks = [];
const innerNodes = [];
const canvas = svg.append("g");
const color = d3.scaleOrdinal(d3.schemeCategory10);
const tree = d3.tree().size(height, width);
const simulation = d3.forceSimulation()
  .force("center", d3.forceCenter(width / 2, height / 2))
  .force("charge", d3.forceManyBody().strength(-500))
  .force("link", d3.forceLink().id(d => d.id).distance(80).strength(1));

const innerSimulation = d3.forceSimulation()
  .force("charge", d3.forceManyBody().strength(5))
  .force('collision', d3.forceCollide().radius(10))
  .force('center', d3.forceCenter());

let link, node, nodeGroup;

onLoad(data);

function onLoad(data) {
  flatten(data);

  link = canvas.append('g')
    .attr('class', 'links')
    .selectAll('line')
    .data(outerLinks)
    .enter().append('line')
      .attr('stroke', '#eee');

  nodeGroup = canvas.append('g')
    .attr('class', 'nodes')
    .selectAll('path')
    .data(outerNodes)
    .enter().append('g')
    .attr('id', d => d.id)

    .call(d3.drag()
      .on('start', onDragStart)
      .on('drag', onDrag)
      .on('end', onDragEnd)
    );

  nodeGroup.append('path')
    .attr('d', generatePentagonPath(25))
    .attr('fill', d => d.children ? 'blue' : 'green');

  innerNodes.map(member => {
    const parent = canvas.select(`#${member.parent}`);

    const members = parent
      .selectAll('.member')
      .data(member.nodes)
      .enter()
      .append('g')
      .attr('class', 'member')
      .call(d3.drag()
        .on('start', (d) => {
          if (!d3.event.active) {
            innerSimulation.alphaTarget(.3).restart();
          }
          members.each(d => {
            d.fx = d.x;
            d.fy = d.y
          })
        })
        .on('drag', function (d) {
          d.fx = d3.event.x;
          d.fy = d3.event.y;
        })
        .on('end', (d) => {
          if (!d3.event.active) {
            innerSimulation.alphaTarget(0).restart();
          }
          d.fx = d.fy = null;
        })
      );
    members
      .append('path')
      .attr('d', generatePentagonPath(5))
      .attr('fill', 'orange');

    innerSimulation
      .nodes(member.nodes)
      .on('tick', () => {
        members.attr("transform", d => `translate(${d.x},${d.y})`);
      });
  });

  simulation
    .nodes(outerNodes)
    .on('tick', ticked);

  simulation.force('link')
    .links(outerLinks);
}

function onDragStart(d) {
  if (!d3.event.active) {
    simulation.alphaTarget(0.5).restart();
  }

  nodeGroup.each(d => {
    d.fx = d.x;
    d.fy = d.y;
  });
}

function onDrag(d) {
  d.fx = d3.event.x;
  d.fy = d3.event.y;
}

function onDragEnd(d) {
  if (!d3.event.active) {
    simulation.alphaTarget(0).restart();
  }
  d.fx = null;
  d.fy = null;
}

function zoomed() {
  canvas.attr("transform", d3.event.transform);
}

function flatten(node) {
  outerNodes.push(node);

  if (node.members) {
    innerNodes.push({parent: node.id, nodes:  node.members});
  }

  if (node.children) {
    node.children.forEach(child => {
      outerLinks.push({ source: node.id, target: child.id });
      flatten(child);
    });
  }
}

function generatePentagonPath(radius = 25, x = 0, y = 0) {
  const numPoints = 5;
  const points = d3.range(numPoints)
    .map(i => {
      const angle = i / numPoints * Math.PI * 2 + Math.PI;
      return [Math.sin(angle) * radius + x, Math.cos(angle) * radius + y];
    });

  return d3.line()(points);
}

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);

  nodeGroup.attr("transform", d => `translate(${d.x},${d.y})`);
}

}
</script>
</body>
</html>

Upvotes: 2

Related Questions