Simon
Simon

Reputation: 10158

d3 force layout - how to create a more sensible node structure

I have a d3 force layout which loads nodes from a JSON file and links from a separate CSV file, but there are a couple of (possibly related) issues:

  1. The resulting structure is very evenly spaced out, and there is a lot of overlap in links which makes it difficult to see which nodes are connected. I'm not sure why I'm getting such a rounded structure, and why some nodes with only 1 link are placed on the opposite side from their connecting node
  2. When I drag a node, none of the connecting nodes move with it. Everything seems to stay stationary apart from the one node I'm moving, and the graph just seems to zoom in slowly

You can see both issues in the snippet below:

svg {
  display: block;
  margin: auto;
}

.links line {
  stroke: #999;
  stroke-opacity: 0.6;
  pointer-events: none;
}

.nodes circle {
  stroke: #fff;
  stroke-width: 1.5px;
}

text {
  font-family: sans-serif;
  font-size: 14px;
  font-weight: bold;
}

.primary {
  color: #af0000;
}

.secondary {
  color: #00a700;
}

div.tooltip {
  position: absolute;
  top: 10px;
  right: 10px;
  background-color: white;
  max-width: 200px;
  height: auto;
  padding: 10px;
  border-style: solid;
  border-radius: 2px;
  border-width: 1px;
  box-shadow: 3px 3px 10px rgba(0, 0, 0, 0.5);
}

div.tooltip .Legendary {
  color: #ff9b00;
}

div.tooltip .Epic {
  color: #ac41c2;
}

div.tooltip .Elite {
  color: #058cc3;
}

div.tooltip .Advanced {
  color: #2d9830;
}

div.tooltip table {
  border: 1px solid black;
  border-collapse: collapse;
}

div.tooltip table td {
  padding: 5px;
}
<svg width="960" height="700"></svg>

<script src="https://d3js.org/d3.v4.min.js"></script>

<script>
  const svg = d3.select('svg'),
    width = +svg.attr('width'),
    height = +svg.attr('height');

  const color = d3
    .scaleOrdinal()
    .domain(['Legendary', 'Epic', 'Elite', 'Advanced'])
    .range(['#ff9b00', '#ac41c2', '#058cc3', '#2d9830']);

  const simulation = d3
    .forceSimulation()
    .force('link', d3.forceLink().id(d => d.id))
    .force(
      'charge',
      d3
      .forceManyBody()
      .strength(-20)
      .distanceMax([500])
    )
    .force('center', d3.forceCenter(width / 2, height / 2));

  d3.queue()
    .defer(d3.json, 'https://gist.githubusercontent.com/sho-87/bfe4d69be60454dccfd859b004262381/raw/ac259b468ae6f1140d5c10596f1663c743535b5a/commanders.json')
    .defer(d3.csv, 'https://gist.githubusercontent.com/sho-87/89aa3c48baffc9c388bb98e613b9a5f4/raw/896e24a0187d555d395840d7def3b36b8fb28074/links.csv')
    .await(function(error, commanders, links) {
      const nodeByID = d3.map();
      commanders.nodes.forEach(commander => {
        nodeByID.set(commander.id, commander);
      });

      links.forEach(link => {
        link.source = nodeByID.get(link.primary);
        link.target = nodeByID.get(link.secondary);
      });

      const link = svg
        .append('g')
        .attr('class', 'links')
        .selectAll('line')
        .data(links)
        .enter()
        .append('line')
        .attr('stroke-width', d => Math.sqrt(d.value * 1));

      const node = svg
        .selectAll('.node')
        .data(commanders.nodes)
        .enter()
        .append('g')
        .attr('class', 'nodes')
        .call(
          d3
          .drag()
          .on('start', dragstarted)
          .on('drag', dragged)
          .on('end', dragended)
        )
        .append('circle')
        .attr('r', 8)
        .attr('fill', d => color(d.group))

      simulation.nodes(commanders.nodes).on('tick', ticked);
      simulation.force('link').links(link);

      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('transform', d => `translate(${d.x},${d.y})`);
      }
    });

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

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

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

What I want is something more asymmetrical where related/connected nodes are placed closer together. Ideally I want to reduce the number of overlapping links as much as possible, so something more like this:

enter image description here

Upvotes: 0

Views: 54

Answers (1)

rioV8
rioV8

Reputation: 28838

Shorten the following code

// const nodeByID = d3.map();
// commanders.nodes.forEach(commander => {
//   nodeByID.set(commander.id, commander);
// });
// links.forEach(link => {
//   link.source = nodeByID.get(link.primary);
//   link.target = nodeByID.get(link.secondary);
// });

links = links.map(d => ({source:d.primary, target:d.secondary, value:d.value}));

And set the link objects to the objects not the d3 selection

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

Then you need to play with the link distance and strength accessors.

svg {
  display: block;
  margin: auto;
}

.links line {
  stroke: #999;
  stroke-opacity: 0.6;
  pointer-events: none;
}

.nodes circle {
  stroke: #fff;
  stroke-width: 1.5px;
}

text {
  font-family: sans-serif;
  font-size: 14px;
  font-weight: bold;
}

.primary {
  color: #af0000;
}

.secondary {
  color: #00a700;
}

div.tooltip {
  position: absolute;
  top: 10px;
  right: 10px;
  background-color: white;
  max-width: 200px;
  height: auto;
  padding: 10px;
  border-style: solid;
  border-radius: 2px;
  border-width: 1px;
  box-shadow: 3px 3px 10px rgba(0, 0, 0, 0.5);
}

div.tooltip .Legendary {
  color: #ff9b00;
}

div.tooltip .Epic {
  color: #ac41c2;
}

div.tooltip .Elite {
  color: #058cc3;
}

div.tooltip .Advanced {
  color: #2d9830;
}

div.tooltip table {
  border: 1px solid black;
  border-collapse: collapse;
}

div.tooltip table td {
  padding: 5px;
}
<svg width="960" height="700"></svg>

<script src="https://d3js.org/d3.v4.min.js"></script>

<script>
  const svg = d3.select('svg'),
    width = +svg.attr('width'),
    height = +svg.attr('height');

  const color = d3
    .scaleOrdinal()
    .domain(['Legendary', 'Epic', 'Elite', 'Advanced'])
    .range(['#ff9b00', '#ac41c2', '#058cc3', '#2d9830']);

  const simulation = d3
    .forceSimulation()
    .force('link', d3.forceLink().id(d => d.id))
    .force(
      'charge',
      d3
      .forceManyBody()
      .strength(-20)
      .distanceMax([500])
    )
    .force('center', d3.forceCenter(width / 2, height / 2));

  d3.queue()
    .defer(d3.json, 'https://gist.githubusercontent.com/sho-87/bfe4d69be60454dccfd859b004262381/raw/ac259b468ae6f1140d5c10596f1663c743535b5a/commanders.json')
    .defer(d3.csv, 'https://gist.githubusercontent.com/sho-87/89aa3c48baffc9c388bb98e613b9a5f4/raw/896e24a0187d555d395840d7def3b36b8fb28074/links.csv')
    .await(function(error, commanders, links) {

// const nodeByID = d3.map();
    // commanders.nodes.forEach(commander => {
    //   nodeByID.set(commander.id, commander);
    // });
    // links.forEach(link => {
    //   link.source = nodeByID.get(link.primary);
    //   link.target = nodeByID.get(link.secondary);
    // });

    links = links.map(d => ({source:d.primary, target:d.secondary, value:d.value}));

      const link = svg
        .append('g')
        .attr('class', 'links')
        .selectAll('line')
        .data(links)
        .enter()
        .append('line')
        .attr('stroke-width', d => Math.sqrt(d.value * 1));

      const node = svg
        .selectAll('.node')
        .data(commanders.nodes)
        .enter()
        .append('g')
        .attr('class', 'nodes')
        .call(
          d3
          .drag()
          .on('start', dragstarted)
          .on('drag', dragged)
          .on('end', dragended)
        )
        .append('circle')
        .attr('r', 8)
        .attr('fill', d => color(d.group))

      simulation.nodes(commanders.nodes).on('tick', ticked);
      simulation.force('link').links(links);

      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('transform', d => `translate(${d.x},${d.y})`);
      }
    });

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

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

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

Upvotes: 1

Related Questions