535 lines
17 KiB
JavaScript
535 lines
17 KiB
JavaScript
$(document).ready(function () {
|
|
|
|
var width = 680,
|
|
height = 480;
|
|
|
|
var color = {
|
|
'a': '#e77',
|
|
'b': '#7e7',
|
|
'c': '#77e',
|
|
'a1': '#faa',
|
|
'b1': '#afa',
|
|
'c1': '#aaf',
|
|
}
|
|
|
|
var plinthColor = {
|
|
'a': '#fdd',
|
|
'b': '#dfd',
|
|
'c': '#ddf',
|
|
}
|
|
|
|
var messageColor = {};
|
|
|
|
var csDistance = 75;
|
|
var ssDistance = 150;
|
|
var cRadius = 10;
|
|
var sRadius = 50;
|
|
|
|
var network = {
|
|
nodes: [
|
|
{
|
|
id: "a", name: "matrix.alice.com", type: "hs", // 0
|
|
fixed: true,
|
|
x: width / 2 - .433 * ssDistance,
|
|
y: height / 2,
|
|
},
|
|
{
|
|
id: "b", name: "matrix.bob.com", type: "hs", // 1
|
|
fixed: true,
|
|
x: width / 2 + .433 * ssDistance,
|
|
y: height / 2 - .5 * ssDistance,
|
|
},
|
|
{
|
|
id: "c", name: "matrix.charlie.com", type: "hs", // 2
|
|
fixed: false,
|
|
x: width / 2 + .433 * ssDistance,
|
|
y: height / 2 + .5 * ssDistance
|
|
},
|
|
{ id: "a1", name: "@alice:alice.com", type: "client" }, // 3
|
|
{ id: "b1", name: "@bob:bob.com", type: "client" }, // 4
|
|
{ id: "c1", name: "@charlie:charlie.com", type: "client" }, // 5
|
|
],
|
|
links: [
|
|
// source's ID always needs to be lexicographically less than target's
|
|
{ source: 0, target: 1 },
|
|
{ source: 1, target: 2 },
|
|
{ source: 0, target: 2 },
|
|
{ source: 0, target: 3, leaf: true },
|
|
{ source: 1, target: 4, leaf: true },
|
|
{ source: 2, target: 5, leaf: true },
|
|
],
|
|
};
|
|
|
|
var graph;
|
|
var forceSvg;
|
|
|
|
var stepIndex;
|
|
var pendingStages;
|
|
var stageIndex;
|
|
var animations;
|
|
var unveiled = false;
|
|
|
|
|
|
var steps = [
|
|
[], // step 0
|
|
[
|
|
{ sid: 1, id: "m1", type: "msg", source: "a1", target: "a", msg: "1", pause: true },
|
|
],
|
|
[
|
|
{ sid: 2, id: "m1", type: "msg", source: "a", target: "b", target2: "b1", msg: "1", pause: true },
|
|
{ sid: 3, id: "m1", type: "msg", source: "a", target: "c", target2: "c1", msg: "1", pause: true },
|
|
],
|
|
[
|
|
{ sid: 4, id: "m2", type: "msg", source: "b1", target: "b", msg: "2", parents: ["m1"] },
|
|
],
|
|
[
|
|
{ sid: 5, id: "m3", type: "msg", source: "c1", target: "c", msg: "3", parents: ["m1"] },
|
|
],
|
|
[
|
|
{ sid: 6, id: "m2", type: "msg", source: "b", target: "a", target2: "a1", msg: "2", parents: ["m1"] },
|
|
{ sid: 7, id: "m2", type: "msg", source: "b", target: "c", target2: "c1", msg: "2", parents: ["m1"] },
|
|
],
|
|
[
|
|
{ sid: 8, id: "m3", type: "msg", source: "c", target: "a", target2: "a1", msg: "3", parents: ["m1"] },
|
|
{ sid: 9, id: "m3", type: "msg", source: "c", target: "b", target2: "b1", msg: "3", parents: ["m1"] },
|
|
],
|
|
[
|
|
{ sid: 10, id: "m4", type: "msg", source: "a1", target: "a", msg: "4", parents: ["m2", "m3"] },
|
|
],
|
|
[
|
|
{ sid: 11, id: "m4", type: "msg", source: "a", target: "b", target2: "b1", msg: "4", parents: ["m2", "m3"] },
|
|
{ sid: 12, id: "m4", type: "msg", source: "a", target: "c", target2: "c1", msg: "4", parents: ["m2", "m3"] },
|
|
],
|
|
];
|
|
|
|
function unveil() {
|
|
if (unveiled) return;
|
|
var $e = $("#diagram");
|
|
if ($e.length == 0) return;
|
|
var th = 100;
|
|
|
|
var wt = $(window).scrollTop(),
|
|
wb = wt + $(window).height(),
|
|
et = $e.offset().top,
|
|
eb = et + $e.height();
|
|
|
|
if (eb >= wt - th && et <= wb + th) {
|
|
initNetwork();
|
|
unveiled = true;
|
|
}
|
|
}
|
|
|
|
$(window).on("scroll.unveil resize.unveil lookup.unveil", unveil);
|
|
unveil();
|
|
|
|
function initNetwork() {
|
|
|
|
// reset state
|
|
graph = {};
|
|
|
|
// the animation is broken down into steps, then substeps, and then stages (which happen if you pause)
|
|
stepIndex = 0;
|
|
pendingStages = [];
|
|
stageIndex = 0;
|
|
animations = 0;
|
|
|
|
var force = d3.layout.force()
|
|
.charge(-2000)
|
|
.friction(0.75)
|
|
.size([width, height]);
|
|
|
|
var svg = d3.select("#diagram");
|
|
if (!svg) return;
|
|
|
|
$('.legendNav').click(nextStage);
|
|
|
|
force
|
|
.nodes(network.nodes)
|
|
.links(network.links)
|
|
.gravity(-0.0)
|
|
.linkDistance(function (d) { return (d.leaf ? csDistance : ssDistance) })
|
|
.start();
|
|
|
|
forceSvg = svg.append("g");
|
|
|
|
function resize() {
|
|
newWidth = $("#diagram").width();
|
|
newHeight = (height * newWidth / width) | 0;
|
|
svg.attr("width", newWidth).attr("height", newHeight);
|
|
forceSvg.attr("transform", "scale(" + newWidth / width + ")");
|
|
}
|
|
|
|
$(window).resize(resize);
|
|
resize();
|
|
|
|
var link = forceSvg.selectAll(".networkLink")
|
|
.data(network.links)
|
|
.enter().append("path")
|
|
.attr("class", "networkLink")
|
|
.attr("id", function (d) { return "link_" + d.source.id + "_" + d.target.id });
|
|
|
|
var node = forceSvg.selectAll(".networkNode")
|
|
.data(network.nodes)
|
|
.enter().append("g")
|
|
.attr("class", "networkNode")
|
|
.each(createGraph)
|
|
.call(force.drag);
|
|
|
|
node.append("circle")
|
|
.attr("r", function (d) { return (d.type == "hs" ? 0 : 20); })
|
|
.style("fill", function (d) { return color[d.id] })
|
|
|
|
node.append("title")
|
|
.text(function (d) { return d.name; });
|
|
|
|
node.append("text")
|
|
.attr("dx", function (d) { return ((d.id == "a1" ? -1 : 1) * (cRadius + 14)); })
|
|
.attr("dy", 0)
|
|
.style("fill", function (d) { return d3.rgb(color[d.id]).darker(1) })
|
|
.attr("text-anchor", function (d) { return (d.id == "a1" ? "end" : "") })
|
|
.text(function (d) { return d.type == "client" ? d.name : "" })
|
|
|
|
force.on("tick", function (e) {
|
|
//console.log("main: " + e.alpha);
|
|
|
|
// 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; });
|
|
|
|
link.attr("d", function (d) {
|
|
return "M" + d.source.x + "," + d.source.y
|
|
+ " " + d.target.x + "," + d.target.y;
|
|
});
|
|
|
|
// node.attr("cx", function(d) { return d.x; })
|
|
// .attr("cy", function(d) { return d.y; });
|
|
|
|
node.attr("transform", function (d) {
|
|
return "translate(" + d.x + "," + d.y + ")";
|
|
});
|
|
});
|
|
|
|
nextStage();
|
|
}
|
|
|
|
function translateAlong(path, backwards) {
|
|
var node = path.node();
|
|
return function (d, i, a) {
|
|
return function (t) {
|
|
t = backwards ? (1.0 - t) : t;
|
|
var l = node.getTotalLength();
|
|
var p = node.getPointAtLength(t * l);
|
|
return "translate(" + p.x + "," + p.y + ")";
|
|
};
|
|
};
|
|
}
|
|
|
|
function transition(message, path, backwards) {
|
|
return message.transition()
|
|
.duration(1000)
|
|
.attrTween("transform", translateAlong(path, backwards));
|
|
}
|
|
|
|
function sendMessage(source, target, msg, id, sid) {
|
|
console.log("sendMessage " + source + " " + target + " " + msg);
|
|
var backwards = false;
|
|
if (source > target) { // we're going backwards
|
|
backwards = true;
|
|
var tmp = source;
|
|
source = target;
|
|
target = tmp;
|
|
}
|
|
|
|
// reuse previous message if this is a two-step
|
|
var message = forceSvg.select("#m_" + sid);
|
|
|
|
if (message.size() == 0) {
|
|
message = forceSvg.append("g")
|
|
.attr("id", "m_" + sid)
|
|
.attr("class", "message");
|
|
|
|
message.append("circle")
|
|
.attr("r", 5)
|
|
.style("stroke", messageColor[id]);
|
|
|
|
message.append("text")
|
|
.attr("dx", "8px")
|
|
.attr("dy", "-8px")
|
|
.text(msg);
|
|
}
|
|
|
|
var path = forceSvg.select("#link_" + source + "_" + target);
|
|
animations++;
|
|
return transition(message, path, backwards);
|
|
// .remove();
|
|
// .each("end", function() { message.remove() });
|
|
}
|
|
|
|
function createGraph(d) {
|
|
if (d.id.length != 1) return; // only put graphs in servers(!)
|
|
|
|
var id = d.id;
|
|
|
|
graph[id] = {
|
|
layout: {},
|
|
svg: {},
|
|
nodes: [],
|
|
links: [],
|
|
nodeMap: {},
|
|
nodeSel: {},
|
|
linkSel: {},
|
|
};
|
|
|
|
graph[id].layout = d3.layout.force()
|
|
.size([sRadius * 2, sRadius * 2])
|
|
.on("tick", function (e) {
|
|
//console.log(id + ": " + e.alpha);
|
|
|
|
//updateGraph(id);
|
|
|
|
graph[id].linkSel.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; });
|
|
|
|
graph[id].nodeSel.attr("transform",
|
|
function (d) { return "translate(" + d.x + "," + d.y + ")"; });
|
|
});
|
|
|
|
graph[id].svg = d3.select(this).append("g")
|
|
.attr("transform", "translate(-" + sRadius + ", -" + sRadius + ")");
|
|
|
|
var server = graph[id].svg.append("circle")
|
|
.attr("cx", sRadius)
|
|
.attr("cy", sRadius)
|
|
.attr("r", sRadius)
|
|
.style("fill", plinthColor[id]);
|
|
|
|
graph[id].svg.append("text")
|
|
.attr("dx", function (d) {
|
|
return d.id == "c" ? sRadius * 2.5 :
|
|
d.id == "b" ? sRadius / 2 : sRadius;
|
|
})
|
|
.attr("dy", -10)
|
|
.style("fill", function (d) { return d3.rgb(color[d.id]).darker(1) })
|
|
.attr("text-anchor", "middle")
|
|
.text(d.name);
|
|
|
|
graph[id].linkSel = graph[id].svg.selectAll(".graphLink");
|
|
graph[id].nodeSel = graph[id].svg.selectAll(".graphNode");
|
|
}
|
|
|
|
function updateGraph(id) {
|
|
// Restart the force layout.
|
|
graph[id].layout
|
|
.nodes(graph[id].nodes)
|
|
.links(graph[id].links)
|
|
.linkDistance(35)
|
|
.charge(-100)
|
|
.start();
|
|
|
|
// cache for convenience
|
|
var linkSel = graph[id].linkSel;
|
|
var nodeSel = graph[id].nodeSel;
|
|
|
|
// Update the links…
|
|
linkSel = linkSel.data(graph[id].links);
|
|
|
|
// Exit any old links.
|
|
linkSel.exit().remove();
|
|
|
|
// Enter any new links.
|
|
var enter = linkSel.enter().insert("line", ".graphNode")
|
|
.attr("class", "graphLink")
|
|
.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; })
|
|
.style("opacity", 1e-6)
|
|
.transition()
|
|
.duration(250)
|
|
.style("opacity", 1);
|
|
|
|
// Update the nodes…
|
|
nodeSel = nodeSel.data(graph[id].nodes);
|
|
|
|
// Exit any old nodes.
|
|
nodeSel.exit().remove();
|
|
|
|
// Enter any new nodes.
|
|
var g = nodeSel.enter().append("svg:g")
|
|
.attr("class", "graphNode")
|
|
.attr("transform",
|
|
function (d) { return "translate(" + d.x + "," + d.y + ")"; });
|
|
|
|
g.style("opacity", 1e-6)
|
|
.transition()
|
|
.duration(250)
|
|
.style("opacity", 1);
|
|
|
|
// special case to add a magical leading edge to the first node
|
|
if (nodeSel.size() == 1) {
|
|
g.append("line")
|
|
.attr("class", "danglingGraphLink")
|
|
.attr("x1", function (d) { return 0; })
|
|
.attr("y1", function (d) { return 0; })
|
|
.attr("x2", function (d) { return 0; })
|
|
.attr("y2", function (d) { return -19; })
|
|
.style("opacity", 1e-6)
|
|
.transition()
|
|
.duration(250)
|
|
.style("opacity", 1);
|
|
}
|
|
|
|
g.append("circle")
|
|
.attr("r", 5)
|
|
.style("stroke", function (d) { return messageColor[d.id]; })
|
|
.call(graph[id].layout.drag);
|
|
|
|
g.append("text")
|
|
.attr("dx", "10px")
|
|
.attr("dy", ".35em")
|
|
.text(function (d) { return d.value; })
|
|
.style("fill-opacity", 1);
|
|
|
|
graph[id].linkSel = linkSel;
|
|
graph[id].nodeSel = nodeSel;
|
|
}
|
|
|
|
function updateState(graphId, nodeId, msg, parents) {
|
|
|
|
// whenever we update state we don't want any other message bubbles hanging around
|
|
forceSvg.selectAll(".message").remove();
|
|
|
|
var node = {
|
|
id: nodeId,
|
|
value: msg,
|
|
};
|
|
|
|
if (nodeId == "m1") {
|
|
node.fixed = true;
|
|
node.x = sRadius + 0;
|
|
node.y = sRadius - 30;
|
|
}
|
|
else if (nodeId == "m4") {
|
|
node.fixed = true;
|
|
node.x = sRadius + 0;
|
|
node.y = sRadius + 30;
|
|
}
|
|
else {
|
|
node.x = 0;
|
|
node.y = 0;
|
|
}
|
|
|
|
graph[graphId].nodes.push(node);
|
|
graph[graphId].nodeMap[nodeId] = node;
|
|
|
|
if (parents) {
|
|
for (var i = 0; i < parents.length; i++) {
|
|
graph[graphId].links.push({
|
|
source: graph[graphId].nodeMap[parents[i]],
|
|
target: node,
|
|
});
|
|
}
|
|
}
|
|
|
|
updateGraph(graphId);
|
|
}
|
|
|
|
function performSubStep(subStep) {
|
|
// assign these all to local variables to capture them in the scope of this fn
|
|
var source = subStep.source;
|
|
var target = subStep.target;
|
|
var target2 = subStep.target2;
|
|
var msg = subStep.msg;
|
|
var parents = subStep.parents;
|
|
var id = subStep.id;
|
|
var pause = subStep.pause;
|
|
var sid = subStep.sid;
|
|
|
|
if (!messageColor[id]) {
|
|
messageColor[id] = color[target];
|
|
}
|
|
|
|
function performUpdateState() {
|
|
updateState(target, id, msg, parents);
|
|
if (target2) { // 2nd hop
|
|
if (pause) {
|
|
pendingStages.push(performSecondHop);
|
|
}
|
|
else {
|
|
performSecondHop();
|
|
}
|
|
}
|
|
}
|
|
|
|
function performSecondHop() {
|
|
console.log("2nd hop: " + target + " " + target2 + " " + msg);
|
|
sendMessage(target, target2, msg, id, sid).each("end", function () { animations--; });
|
|
}
|
|
|
|
sendMessage(source, target, msg, id, sid)
|
|
.each("end", function () {
|
|
animations--;
|
|
if (pause) {
|
|
pendingStages.push(performUpdateState);
|
|
}
|
|
else {
|
|
performUpdateState();
|
|
}
|
|
});
|
|
}
|
|
|
|
function nextStage() {
|
|
if (animations) return;
|
|
|
|
var dissolveTime = 500;
|
|
|
|
d3.select("#legend" + stageIndex).style({ "display": "block" })
|
|
.transition()
|
|
.duration(dissolveTime)
|
|
.style("display", "none");
|
|
d3.select("#legend" + (stageIndex + 1)).style({ "display": "none" })
|
|
.transition()
|
|
.duration(dissolveTime)
|
|
.style({ "display": "block" });
|
|
|
|
if (pendingStages.length > 0) {
|
|
var c = pendingStages.length;
|
|
console.log("starting c=" + c);
|
|
for (i = 0; i < c; i++) {
|
|
console.log("i=" + i + ", len=" + pendingStages.length);
|
|
pendingStages[0]();
|
|
pendingStages.shift();
|
|
}
|
|
}
|
|
else {
|
|
var step = steps[stepIndex];
|
|
|
|
if (steps[stepIndex]) {
|
|
// clean up messages from previous step
|
|
console.log("cleaning up messages");
|
|
forceSvg.selectAll(".message").remove();
|
|
|
|
for (var i = 0; i < step.length; i++) {
|
|
performSubStep(step[i]);
|
|
}
|
|
|
|
if (stepIndex == steps.length - 1) {
|
|
d3.select(".legendNav").html("Start over");
|
|
}
|
|
}
|
|
else {
|
|
d3.select(".legendNav").html("Next");
|
|
forceSvg.selectAll("*").remove();
|
|
initNetwork();
|
|
return;
|
|
}
|
|
stepIndex++;
|
|
}
|
|
stageIndex++;
|
|
}
|
|
|
|
});
|