|
<!doctype html> |
|
<meta charset='utf-8'> |
|
<style> |
|
#container { position: relative; } |
|
#container,textarea {font-family: Menlo,"Courier New", monospace;font-size:15px;} |
|
h2 { font-size: 12px;text-transform:uppercase;color:#aaa; } |
|
h3 { font-size: 17px; } |
|
#options {margin-top:1em;} |
|
li { margin: 0.5em 0; color: #555; cursor:pointer; } |
|
li:hover { color: #000; text-decoration: underline;} |
|
|
|
line { stroke: #000; } |
|
circle { fill: #000; cursor: pointer; } |
|
circle.selected { fill: yellow; } |
|
.arrow { fill: white; stroke: #000; } |
|
|
|
#left { width: 460px; position: absolute; } |
|
#right { width: 440px; left: 480px; position: absolute; } |
|
#form { width: 960px; position: relative; z-index: 10;} |
|
#form a { position: absolute; right: 0; font-size: 12px; color: #555; top: 10px;} |
|
#form form { width: 420px; margin: 1em auto; display: none; } |
|
#form.active form { display: block; } |
|
#form.active a { display: none; } |
|
textarea { width: 420px; height: 400px; font-size: 11px;} |
|
|
|
</style> |
|
|
|
<div id="container"> |
|
<div id="left"> |
|
</div> |
|
|
|
<div id="right"> |
|
</div> |
|
|
|
<div id="form"> |
|
<a href="#">Reset</a> |
|
<form onSubmit="return getTextValue()"> |
|
<h2>Paste text and click submit:</h2> |
|
<textarea id="userText"></textarea> |
|
<input type="submit"> |
|
</form> |
|
</div> |
|
|
|
</div> |
|
|
|
<script src="http://d3js.org/d3.v3.min.js"></script> |
|
|
|
<script> |
|
|
|
var width = 480, |
|
height = 500, |
|
title = "Untitled"; |
|
|
|
function parse(text) { |
|
function fail(ln) { |
|
throw(new Error("Problem parsing on line number "+ln+1)); |
|
} |
|
var currentNode, |
|
nodes = [], |
|
lines = text.split("\n"), |
|
line, l, m, i; |
|
for (i=0, l=lines.length; i<l; i++) { |
|
line = lines[i]; |
|
if(m = line.match(/^\*(\w+\d):\s*(.*?)\*?\s*$/)) { |
|
// new node. |
|
if(currentNode) nodes.push(currentNode); |
|
currentNode = { id: m[1], subject: m[2] } |
|
} else if(m = line.match(/^>(\w+\d):\s*(.*?)\s*$/)) { |
|
if(!currentNode) fail(i); |
|
if(!currentNode.options) currentNode.options = []; |
|
currentNode.options.push({ target: m[1], subject: m[2] }) |
|
} |
|
else { |
|
if(!currentNode) { |
|
if(!nodes.length) { |
|
if(line.match(/\w+.*/)) title = line; |
|
} else { |
|
fail(i); |
|
} |
|
}else { |
|
if(!currentNode.msg) currentNode.msg = ""; |
|
if(nodes.length > 0 && nodes[nodes.length - 1].msg.match(/^\s*$/)) { |
|
currentNode.msg += line + "<br><br>"; |
|
}else { |
|
currentNode.msg += line + "\n"; |
|
} |
|
} |
|
} |
|
} |
|
if(currentNode) nodes.push(currentNode); |
|
return nodes; |
|
} |
|
|
|
function generateLinks(nodes) { |
|
var i, j, l_o, l=nodes.length, node, |
|
source, target, |
|
links = [], |
|
objectMap = {}; |
|
for(i=0;i<l;i++) { objectMap[nodes[i].id]=i; } |
|
for(i=0;i<l;i++) { |
|
node = nodes[i]; |
|
if(node.options) { |
|
for(j=0,l_o=node.options.length;j<l_o;j++) { |
|
target = objectMap[node.options[j].target]; |
|
if(!target)console.log("Target undefined", node.options[j]); |
|
links.push({ |
|
target: target, |
|
source: objectMap[node.id] |
|
}); |
|
} |
|
} |
|
} |
|
return links; |
|
} |
|
|
|
function init(nodes, links) { |
|
var force = d3.layout.force() |
|
.nodes(nodes) |
|
.links(links) |
|
.charge(-420) |
|
.linkDistance(80) |
|
.size([width, height]) |
|
.start(); |
|
|
|
d3.select('#form').attr("class", ""); |
|
d3.select("#right").append("h2").text(title); |
|
|
|
var subject = d3.select('#right').append("h3") |
|
.attr("id", "subject"); |
|
|
|
var narration = d3.select('#right').append("div") |
|
.attr("id", "narration"); |
|
|
|
var options = d3.select('#right').append("div") |
|
.attr("id", "options"); |
|
|
|
var svg = d3.select("#left").append("svg:svg") |
|
.attr("width", width) |
|
.attr("height", height); |
|
|
|
var link = svg.selectAll(".link") |
|
.data(links) |
|
.enter().append("line") |
|
.attr("class", "link"); |
|
|
|
var arrow = svg.selectAll(".arrow") |
|
.data(links) |
|
.enter().append("path") |
|
.attr("d", d3.svg.symbol().type("triangle-down")) |
|
.attr("class", "arrow"); |
|
|
|
var node = svg.selectAll(".node") |
|
.data(nodes) |
|
.enter().append("circle") |
|
.attr("class", "node") |
|
.attr("id", function(d) { return d.id; }) |
|
.attr("r", 5) |
|
.call(force.drag) |
|
.on("click", function(d) { choose(d.id); }); |
|
|
|
var name = svg.selectAll(".name") |
|
.data(nodes) |
|
.enter().append("text") |
|
.attr("dx", "0.6em") |
|
.attr("dy", "0.4em") |
|
.text(function(d) { return d.id; }); |
|
|
|
force.on("tick", function() { |
|
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; }); |
|
|
|
arrow.attr("transform", function(d) { |
|
var p1 = [d.source.x,d.source.y], |
|
p2 = [d.target.x,d.target.y], |
|
ang = Math.atan2(p2[1] - p1[1], p2[0] - p1[0]), |
|
deg = ang * 180 / Math.PI, |
|
dis = -8, |
|
dx = dis * Math.cos(ang), |
|
dy = dis * Math.sin(ang), |
|
x = d.target.x + dx, |
|
y = d.target.y + dy, |
|
rotation = deg + 270; |
|
return " translate(" + x + "," + y + ") rotate(" + rotation + ")"; |
|
}); |
|
|
|
node.attr("cx", function(d) { return d.x; }) |
|
.attr("cy", function(d) { return d.y; }) |
|
name.attr("x", function(d) { return d.x; }) |
|
.attr("y", function(d) { return d.y; }) |
|
}); |
|
|
|
svg.selectAll(".node") |
|
.attr("class", function(d) { |
|
var k = "node"; |
|
if(d.selecteD) k+= " selected"; |
|
return k |
|
}); |
|
|
|
function choose(nodeid) { |
|
svg.selectAll(".node").attr("class", "node"); |
|
var node = svg.select("#"+nodeid) |
|
.attr("class", "node selected"); |
|
var d = node.data()[0]; |
|
subject.html(d.subject); |
|
narration.html(d.msg); |
|
options.selectAll('li').remove(); |
|
if(d.options) { |
|
for(i=0,l=d.options.length;i<l;i++) { |
|
(function(i) { |
|
options.append("li").html(d.options[i].subject) |
|
.on("click", function() { choose(d.options[i].target); }); |
|
})(i); |
|
} |
|
} |
|
} |
|
|
|
choose(nodes[0].id); |
|
} |
|
|
|
function destroy () { |
|
d3.selectAll("#left *").remove(); |
|
d3.selectAll("#right *").remove(); |
|
} |
|
|
|
d3.select("#form a").on("click", function() { |
|
destroy(); |
|
d3.select('#form').attr("class", "active"); |
|
}); |
|
|
|
function setText(text) { |
|
var nodes = parse(text); |
|
var links = generateLinks(nodes); |
|
d3.select('#form').attr("class", "active"); |
|
d3.select("#userText").node().value = text; |
|
init(nodes, links); |
|
} |
|
|
|
function getTextValue() { |
|
try { |
|
var text = d3.select("#userText").node().value; |
|
setText(text); |
|
} catch(e) { |
|
alert("Whoops! Something went wrong with the parsing.", e.message); |
|
return false; |
|
} |
|
return false; |
|
} |
|
|
|
d3.text("README.md", function(error, text) { |
|
var extract = (/<pre>\n?((.|\n)*)\n<\/pre>/).exec(text)[1] |
|
setText(extract); |
|
}); |
|
</script> |