block by tophtucker 105770b17e1a76f2f18aced7708496d5

Force words

Full Screen

Using d3-drag + d3-force to render text. Words are totally unconnected here. Gray links shown just for demonstration purposes. Inspired by BW’s 2013 How To Issue.

Uses two little custom forces with d3-force:

index.html

<!DOCTYPE html>
<meta charset="utf-8">
<style>

h1 {
  position: absolute;
  visibility: hidden;
}

svg {
  overflow: visible;
}

.links line {
  stroke: #aaa;
  /*stroke-width: 0;*/
}

.nodes text {
  pointer-events: all;
  font-family: helvetica;
  font-size: 50px;
  text-anchor: middle;
}

</style>

  <h1>The How To Issue</h1>

<svg width="960" height="500"></svg>
<script src="https://d3js.org/d3.v4.0.0-alpha.40.min.js"></script>
<script>

var svg = d3.select("svg"),
    width = +svg.attr("width"),
    height = +svg.attr("height");

var graph = {};

var words = d3.select('h1').text().split(' ').map(function(d) { return d + ' '; });
var lettersByWord = words.map(function(word) {
  return word.split('').map(function(letter, i) {
    return {
      letter: letter,
      word: word,
      letterIndex: i
    }
  })
});

var wordStyles = words.map(function(d) {
  if (d==='How ') {
    return 80;
  } else if (d==='To ') {
    return 80;
  } else {
    return 50;
  }
})

graph.nodes = [].concat.apply([],lettersByWord);

graph.nodes.forEach(function(d,i) {
  d.id = i;

  var em = 4;
  var lineHeight = 4;

  d.x = -(d.word.length / 2 * em) + d.letterIndex * em + Math.random() * 50;
  d.y = -(words.length / 2 * lineHeight) + words.indexOf(d.word) * lineHeight + Math.random() * 50;
  
  d.prev = graph.nodes[i-1] ? graph.nodes[i-1].word === d.word ? graph.nodes[i-1] : undefined : undefined;
  d.next = graph.nodes[i+1] ? graph.nodes[i+1].word === d.word ? graph.nodes[i+1] : undefined : undefined;
});

graph.links = d3.pairs(graph.nodes).map(function(d,i) {
  return {
    source: d[0].id,
    target: d[1].id,
    value: d[0].word === d[1].word ? 1 : 0
  }
});

graph.links = graph.links.filter(function(d) {
  return d.value > 0;
})

var simulation = d3.forceSimulation()
    .force("link", d3.forceLink().id(function(d) { return d.id; }))
    .force("charge", d3.forceManyBody().strength(1.1))
    .force("collide", d3.forceCollide().radius(function(d) { 
      return wordStyles[words.indexOf(d.word)] / 2.7;
    }))
    .force("center", d3.forceCenter(width / 2, height / 2))
    .force("ltr", forceLtr(1, 22))
    .force("baseline", forceBaseline(.2));

var link = svg.append("g")
    .attr("class", "links")
  .selectAll("line")
  .data(graph.links)
  .enter().append("line");

var node = svg.append("g")
    .attr("class", "nodes")
  .selectAll("text")
  .data(graph.nodes)
  .enter().append("text")
    .style('font-size', function(d) { return wordStyles[words.indexOf(d.word)] + 'px'; })
    .text(function(d) { return d.letter; })
    .call(d3.drag()
        .on("start", dragstarted)
        .on("drag", dragged)
        .on("end", dragended));

node.append("title")
    .text(function(d) { return d.id; });

simulation
    .nodes(graph.nodes)
    .on("tick", ticked);

simulation.force("link")
    .links(graph.links);

function ticked() {
  link
      .attr("x1", function(d) { return d.source.x; })
      .attr("y1", function(d) { return d.source.y; })
      .attr("x2", function(d) { return d.target.x; })
      .attr("y2", function(d) { return d.target.y; });

  node
      .attr("x", function(d) { return d.x; })
      .attr("y", function(d) { return d.y; });
}

function dragstarted(d) {
  if (!d3.event.active) simulation.alphaTarget(0.3).restart()
  simulation.fix(d);
}

function dragged(d) {
  simulation.fix(d, d3.event.x, d3.event.y);
}

function dragended(d) {
  if (!d3.event.active) simulation.alphaTarget(0);
  simulation.unfix(d);
}

function forceLtr(_, __) {

  // offset means "each letter wants to be `offset` pixels ahead of the previous"
  // like letter spacing or ems

  var nodes,
      strength = _ || 1,
      offset = __ || 0;

  function force(alpha) {
    for (var i = 0, n = nodes.length, node, k = alpha; i < n; ++i) {
      node = nodes[i];
      if(node.prev !== undefined) {
        node.vx += k * strength * Math.max(0, node.prev.x - node.x + offset);
      }
      if(node.next !== undefined) {
        node.vx -= k * strength * Math.max(0, node.x - node.next.x + offset);
      }
    }
  }

  force.initialize = function(_) {
    nodes = _;
  }

  return force;

}

function forceBaseline(_) {

  var nodes,
      strength = _ || 1;

  function force(alpha) {
    for (var i = 0, n = nodes.length, node, k = alpha; i < n; ++i) {
      node = nodes[i];
      if(node.prev !== undefined) {
        node.vy += k * strength * (node.prev.y - node.y);
      }
      if(node.next !== undefined) {
        node.vy -= k * strength * (node.y - node.next.y);
      }
    }
  }

  force.initialize = function(_) {
    nodes = _;
  }

  return force;

}

</script>