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