|
<!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> |