Skip to content

Instantly share code, notes, and snippets.

@tophtucker
Forked from mbostock/.block
Last active June 2, 2016 02:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tophtucker/105770b17e1a76f2f18aced7708496d5 to your computer and use it in GitHub Desktop.
Save tophtucker/105770b17e1a76f2f18aced7708496d5 to your computer and use it in GitHub Desktop.
Force words
license: gpl-3.0
height: 500

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:

  • forceLtr (as in "left to right") forces letters toward the right of the previous letter and toward the left of the next letter.

  • forceBaseline forces letters toward the y-coordinate of their neighbors, thereby establishing rough word-by-word baselines.

<!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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment