blob: be0b581c70b95b558d3f53b1296d42a2a939fc40 [file] [log] [blame]
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>TITLE_PLACEHOLDER</title>
<style>
.node {
font: 300 13px "Helvetica Neue", Helvetica, Arial, sans-serif;
fill: #bbb;
}
.link {
stroke: steelblue;
stroke-opacity: .4;
fill: none;
pointer-events: none;
}
.node--focus {
font-weight: 700;
fill: #000;
}
.node:hover {
fill: steelblue;
}
.node:hover,
.node--source,
.node--target {
font-weight: 700;
}
.node--source {
fill: #2ca02c;
}
.node--target {
fill: #d59800;
}
.link--source,
.link--target {
stroke-opacity: 1;
stroke-width: 3px;
}
.link--source {
stroke: #d59800;
}
.link--target {
stroke: #2ca02c;
}
.link--cycle {
stroke: #ff0000;
}
.summary {
font: 300 13px "Helvetica Neue", Helvetica, Arial, sans-serif;
position: fixed;
top: 32px;
right: 32px;
width: 192px;
background-color: #ffffff;
box-shadow: 2px 2px 4px 2px #777777;
padding: 5px;
}
.details {
display: none;
font: 300 13px "Helvetica Neue", Helvetica, Arial, sans-serif;
position: fixed;
top: 220px;
right: 32px;
width: 192px;
background-color: #ffffff;
box-shadow: 2px 2px 4px 2px #777777;
padding: 5px;
}
.shown {
display:block;
}
.stat {
text-align: right;
width: 64px;
}
.title {
font-size: 16px;
font-weight: bold;
}
#package {
font-size: 14px;
font-weight: bold;
}
</style>
</head>
<body>
<div class="summary">
<div class="title">Project TITLE_PLACEHOLDER</div>
<table>
<tr>
<td>Sources:</td>
<td id="sourceCount" class="stat"></td>
</tr>
<tr>
<td>Packages:</td>
<td id="packageCount" class="stat"></td>
</tr>
<tr>
<td>Cyclic Segments:</td>
<td id="segmentCount" class="stat"></td>
</tr>
<tr>
<td>Cycles:</td>
<td id="cycleCount" class="stat"></td>
</tr>
</table>
<div><hr size="1"></div>
<div><input type="checkbox"> Highlight cycles</input></div>
<div><input style="width: 95%" type="range" min="0" max="100" value="75"></div>
</div>
<div class="details">
<div id="package">Package Details</div>
<table>
<tr>
<td>Sources:</td>
<td id="psourceCount" class="stat"></td>
</tr>
<tr>
<td>Dependents:</td>
<td id="pdependentCount" class="stat"></td>
</tr>
<tr>
<td>Cyclic Segments:</td>
<td id="psegmentCount" class="stat"></td>
</tr>
<tr>
<td>Cycles:</td>
<td id="pcycleCount" class="stat"></td>
</tr>
</table>
</div>
<script>
D3JS_PLACEHOLDER
var catalog =
DATA_PLACEHOLDER
;
var diameter = 1000,
radius = diameter / 2,
innerRadius = radius - 300;
var cluster = d3.layout.cluster()
.size([360, innerRadius])
.sort(null)
.value(function(d) { return d.size; });
var bundle = d3.layout.bundle();
var line = d3.svg.line.radial()
.interpolate("bundle")
.tension(.75)
.radius(function(d) { return d.y; })
.angle(function(d) { return d.x / 180 * Math.PI; });
var svg = d3.select("body").append("svg")
.attr("width", diameter)
.attr("height", diameter)
.append("g")
.attr("transform", "translate(" + radius + "," + radius + ")");
var link = svg.append("g").selectAll(".link"),
node = svg.append("g").selectAll(".node"),
cycles = {}, highlightCycles, selectedNode;
function isCyclicLink(l) {
return highlightCycles &&
(cycles[l.source.key + "-" + l.target.key] || cycles[l.target.key + "-" + l.source.key]);
}
function isCyclicPackageLink(l, p) {
var key = l.source.key + "-" + l.target.key,
rKey = l.target.key + "-" + l.source.key;
return isCyclicLink(l) && (p.cycleSegments[key] || p.cycleSegments[rKey]);
}
function refreshPaths() {
svg.selectAll("path.link").classed("link--cycle", isCyclicLink);
}
function processCatalog() {
var nodes = cluster.nodes(packageHierarchy(catalog.packages)),
links = packageImports(nodes),
splines = bundle(links);
cycles = catalog.cycleSegments;
d3.select("input[type=checkbox]").on("change", function() {
highlightCycles = this.checked;
refreshPaths();
});
link = link
.data(splines)
.enter().append("path")
.each(function(d) { d.source = d[0], d.target = d[d.length - 1]; })
.attr("class", "link")
.classed("link--cycle", isCyclicLink)
.attr("d", function(d, i) { return line(splines[i]); });
node = node
.data(nodes.filter(function(n) { return !n.children; }))
.enter().append("text")
.attr("class", "node")
.attr("dy", ".31em")
.attr("transform", function(d) { return "rotate(" + (d.x - 90) + ")translate(" + (d.y + 8) + ",0)" + (d.x < 180 ? "" : "rotate(180)"); })
.style("text-anchor", function(d) { return d.x < 180 ? "start" : "end"; })
.text(function(d) { return d.key; })
.on("focus", processSelect)
.on("blur", processSelect);
d3.select("input[type=range]").on("change", function() {
line.tension(this.value / 100);
svg.selectAll("path.link")
.data(splines)
.attr("d", function(d, i) { return line(splines[i]); });
});
d3.select("#packageCount").text(catalog.summary.packages);
d3.select("#sourceCount").text(catalog.summary.sources);
d3.select("#segmentCount").text(catalog.summary.cycleSegments);
d3.select("#cycleCount").text(catalog.summary.cycles);
}
function processSelect(d) {
if (selectedNode === d) {
deselected(d);
selectedNode = null;
} else if (selectedNode) {
deselected(selectedNode);
selectedNode = null;
selected(d);
} else {
selected(d);
selectedNode = d;
}
}
function selected(d) {
node
.each(function(n) { n.target = n.source = false; })
.classed("node--focus", function(n) { return n === d; });
link
.classed("link--cycle", function(l) { return isCyclicPackageLink(l, d); })
.classed("link--target", function(l) { if (l.target === d) return l.source.source = true; })
.classed("link--source", function(l) { if (l.source === d) return l.target.target = true; })
.filter(function(l) { return l.target === d || l.source === d; })
.each(function() { this.parentNode.appendChild(this); });
node
.classed("node--target", function(n) { return n.target; })
.classed("node--source", function(n) { return n.source; });
d3.select("#psourceCount").text(d.size);
d3.select("#pdependentCount").text(d.imports.length);
d3.select("#psegmentCount").text(d.cycleSegmentCount);
d3.select("#pcycleCount").text(d.cycleCount);
d3.select(".details").classed("shown", function() { return true; });
}
function deselected(d) {
link
.classed("link--cycle", isCyclicLink)
.classed("link--target", false)
.classed("link--source", false);
node
.classed("node--target", false)
.classed("node--source", false)
.classed("node--focus", false);
d3.select(".details").classed("shown", function() { return false; });
}
d3.select(self.frameElement).style("height", diameter + "px");
// Lazily construct the package hierarchy.
function packageHierarchy(packages) {
var map = {}, cnt = 0;
// Builds the structure top-down to the specified leaf or until
// another leaf in which case hook this leaf to the same parent
function buildHierarchy(leaf, i) {
var leafName = leaf.name,
node, name, parent = map[""], start = 0;
while (start < leafName.length) {
name = parentName(leafName, start);
node = map[name];
if (!node) {
node = map[name] = parentNode(name, parent);
parent.children.push(node);
} else if (node.imports) {
leaf.parent = parent;
parent.children.push(leaf);
break;
}
parent = node;
start = name.length + 1;
}
}
function parentNode(name, parent) {
return {name: name, parent: parent, key: name, children: []};
}
function parentName(leafName, start) {
var i = leafName.indexOf(".", start);
return i > 0 ? leafName.substring(0, i) : leafName;
}
// First populate all packages as leafs
packages.forEach(function(d) {
map[d.name] = d;
d.key = d.name;
});
// Next synthesize the intermediate structure, by-passing any leafs
map[""] = parentNode("", null);
var i = 0;
packages.forEach(function(d) {
buildHierarchy(d, i++);
});
return map[""];
}
// Return a list of imports for the given array of nodes.
function packageImports(nodes) {
var map = {},
imports = [];
// Compute a map from name to node.
nodes.forEach(function(d) {
map[d.name] = d;
});
// For each import, construct a link from the source to target node.
nodes.forEach(function(d) {
if (d.imports) d.imports.forEach(function(i) {
imports.push({source: map[d.name], target: map[i]});
});
});
return imports;
}
processCatalog();
</script>
</body>
</html>