Update: This prototype has turned into a bigger project: http://is.gd/CableWeaver
Here's a small gallery of Cable2Graph cable reference culsters (the graphs are named after countries, but they don't "represent" the entire body of cables related to them. It's just a handy naming convention). Each node (circle) represents a cable. Each link (line) represents reference between 2 cables.
Reaching a readable layout (where all arrows are visible and distinguishable) can take up to 20 minutes depending on the graph (Syria and Jordan are the most complex, Iran is easy). Here's a screenshot gallery of all graphs after tidying them up a bit.
xxxxxxxxxx
<meta charset="utf-8">
<style>
.menu, .menu li { display: inline; padding:0 4px }
.menu a { text-decoration: none }
.menu a hover { text-decoration: underline }
.menu a.active { color: black; font-weight: bold }
.node {
stroke: #000;
stroke-width: 1.5px;
stroke-opacity: .6;
}
.node.MISSING { stroke:#black; stroke-dasharray:2,2 }
.node.SECRET-NOFORN { stroke: #7f0000; }
.node.SECRET { stroke: #7f0000; }
.node.CONFIDENTIAL-NOFORN { stroke: #00007f; }
.node.CONFIDENTIAL { stroke: #00007f; }
.node.UNCLASSIFIED { stroke: #007f00; }
.node.UNCLASSIFIED-FOR-OFFICIAL-USE-ONLY { stroke: #007f00; }
.node.active {
stroke-opacity: 1;
}
.link {
stroke: #000;
stroke-opacity: .6;
}
</style>
<body>
<div id="header">
Example <a target="_blank" href="https://wlwardiary.github.com/cable2graph/">Cable2Graph</a>
clusters (please see instructions):
</div>
<script src="https://d3js.org/d3.v3.min.js"></script>
<script>
// Helper so that a link doesn't cover the nodes
// returns a line between x1,x2 and y1,y2 that is
// begins r1 "later" and ends r2 "earlier"
// is there a fancy d3 way to do this?!?
function shorten(x1,y1,x2,y2,r1,r2) {
if (x1==y1 && x2==y2)
return {x1:x1,x2:x2,y1:y1,y2:y2};
a = Math.atan(Math.abs(y2-y1)/Math.abs(x2-x1)); // atan can swallow Infinity
return {
x1:x1+(x2>x1?1:-1)*r1*Math.cos(a),
y1:y1+(y2>y1?1:-1)*r1*Math.sin(a),
x2:x2+(x2>x1?-1:1)*r2*Math.cos(a),
y2:y2+(y2>y1?-1:1)*r2*Math.sin(a)
}
}
var menu = [
{name:"Iran",file:"iran.json"},
{name:"Ireland",file:"ireland.json"},
{name:"Jordan",file:"jordan.json"},
{name:"Palestine",file:"palestine.json"},
{name:"Spain",file:"spain.json"},
{name:"Syria",file:"syria.json"}
]
d3.select("#header")
.append("ul").attr("class","menu")
.selectAll("li")
.data(menu)
.enter().append("li")
.append("a").attr("href", "#")
.attr("id", function(d) { return d.name; })
.attr("class", "menu-item")
.text(function(d) { return d.name })
.on("click", function(d) { populate(d.name,d.file); return false; })
var width = 960,
height = 480;
var node_color = d3.scale.category20().domain(d3.range(0,1000));
var force = d3.layout.force()
.charge(-100)
.linkDistance(30)
.size([width, height]);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
svg.append("svg:defs")
.append("svg:marker")
.attr("id","arrow-head").attr("viewBox", "0 0 10 10")
.attr("refX", 1).attr("refY", 5)
.attr("markerWidth", 4).attr("markerHeight", 3)
.attr("markerUnit", "strokeWidth").attr("orient", "auto")
.append("svg:polyline")
.style("fill","#000")
.attr("points", "0,0 5,5 0,10 1,5");
function populate(name,file) {
force.stop();
svg.selectAll("line.link").remove();
svg.selectAll("circle.node").remove();
d3.selectAll("a.menu-item").classed("active",false);
d3.select("#"+name).classed("active",true);
d3.json(file, function(error, graph) {
force
.nodes(graph.nodes)
.links(graph.links)
.start();
var link = svg.selectAll("line.link").data(graph.links)
.enter().append("line")
.attr("marker-end","url(#arrow-head)")
.attr("class", "link");
var node = svg.selectAll("circle.node").data(graph.nodes)
.enter().append("circle")
.attr("id", function(d) { return "c"+d.label })
.attr("class", function(d) {return "node "+d.nodeclass; })
.attr("r", 8)
// hash-generated unique colors - less readable (close colors are possible)
//.style("fill", function(d) { return d.color; })
// more readable color scale, but cycles thru 20 colors (duplicates if >20)
.style("fill", function(d) { return node_color(d.colorindex); })
.on("contextmenu",function(d) {
console.log(d);
return false;
})
.on("click",function(d) {
if (d3.event.shiftKey) {
d3.select(this).classed("active",d.fixed = !d.fixed)
} else {
if (d.uri) {
window.open(d.uri);
} else {
alert('Missing cable');
}
}
})
.call(force.drag);
var link_stroke = d3.scale.linear()
.domain([
d3.min(graph.links,function(d) { return d.betweenness || 0 }),
d3.max(graph.links,function(d) { return d.betweenness || 0 })
])
.range([2,6]);
link.property("stroke_width", function(d) { return link_stroke(d.betweenness || 0); });
link.style("stroke-width", function(d) { return this.stroke_width+"px"; });
var node_stroke = d3.scale.linear()
.domain([
d3.min(graph.nodes,function(d) { return d.authority || 0 }),
d3.max(graph.nodes,function(d) { return d.authority || 0 })
])
.range([2,6]);
node.style("stroke-width", function(d) { return node_stroke(d.authority || 0)+"px"; });
node.append("title")
.text(function(d) {
return d.label + (d.date?" ("+d.date+(d.classification?", "+d.classification:"")+")":"") + (d.subjects?":\n"+d.subjects:"");
});
force.on("tick", function() {
link.property("shorty", function(d) {
return shorten(d.source.x,d.source.y,d.target.x,d.target.y,
parseFloat(d3.select('#c'+d.source.label).attr("r"))+3,
parseFloat(d3.select("#c"+d.target.label).attr("r"))+3+this.stroke_width
);
})
.attr("x1", function(d) { return this.shorty.x1 })
.attr("y1", function(d) { return this.shorty.y1 })
.attr("x2", function(d) { return this.shorty.x2 })
.attr("y2", function(d) { return this.shorty.y2 })
// The https://gist.github.com/1129492 trick to avoid out-of-the-box nodes
node.attr("cx", function(d) { return d.x = Math.max(12, Math.min(width - 12, d.x)); })
.attr("cy", function(d) { return d.y = Math.max(12, Math.min(height - 12, d.y)); });
});
});
}
populate("Palestine","palestine.json")
</script>
Modified http://d3js.org/d3.v3.min.js to a secure url
https://d3js.org/d3.v3.min.js