Thomas Vachuska | 6b33126 | 2015-04-27 11:09:07 -0700 | [diff] [blame] | 1 | <!DOCTYPE html> |
| 2 | <html> |
| 3 | <head> |
| 4 | <meta charset="utf-8"> |
| 5 | <title>TITLE_PLACEHOLDER</title> |
| 6 | <style> |
| 7 | |
| 8 | .node { |
| 9 | font: 300 13px "Helvetica Neue", Helvetica, Arial, sans-serif; |
| 10 | fill: #bbb; |
| 11 | } |
| 12 | |
| 13 | .link { |
| 14 | stroke: steelblue; |
| 15 | stroke-opacity: .4; |
| 16 | fill: none; |
| 17 | pointer-events: none; |
| 18 | } |
| 19 | |
| 20 | .node--focus { |
| 21 | font-weight: 700; |
| 22 | fill: #000; |
| 23 | } |
| 24 | |
| 25 | .node:hover { |
| 26 | fill: steelblue; |
| 27 | } |
| 28 | |
| 29 | .node:hover, |
| 30 | .node--source, |
| 31 | .node--target { |
| 32 | font-weight: 700; |
| 33 | } |
| 34 | |
| 35 | .node--source { |
| 36 | fill: #2ca02c; |
| 37 | } |
| 38 | |
| 39 | .node--target { |
| 40 | fill: #d59800; |
| 41 | } |
| 42 | |
| 43 | .link--source, |
| 44 | .link--target { |
| 45 | stroke-opacity: 1; |
| 46 | stroke-width: 3px; |
| 47 | } |
| 48 | |
| 49 | .link--source { |
| 50 | stroke: #d59800; |
| 51 | } |
| 52 | |
| 53 | .link--target { |
| 54 | stroke: #2ca02c; |
| 55 | } |
| 56 | |
| 57 | .link--cycle { |
| 58 | stroke: #ff0000; |
| 59 | } |
| 60 | |
| 61 | .summary { |
| 62 | font: 300 13px "Helvetica Neue", Helvetica, Arial, sans-serif; |
| 63 | position: fixed; |
| 64 | top: 32px; |
| 65 | right: 32px; |
| 66 | width: 192px; |
| 67 | background-color: #ffffff; |
| 68 | box-shadow: 2px 2px 4px 2px #777777; |
| 69 | padding: 5px; |
| 70 | } |
| 71 | |
| 72 | .details { |
| 73 | display: none; |
| 74 | font: 300 13px "Helvetica Neue", Helvetica, Arial, sans-serif; |
| 75 | position: fixed; |
| 76 | top: 220px; |
| 77 | right: 32px; |
| 78 | width: 192px; |
| 79 | background-color: #ffffff; |
| 80 | box-shadow: 2px 2px 4px 2px #777777; |
| 81 | padding: 5px; |
| 82 | } |
| 83 | |
| 84 | .shown { |
| 85 | display:block; |
| 86 | } |
| 87 | |
| 88 | .stat { |
| 89 | text-align: right; |
| 90 | width: 64px; |
| 91 | } |
| 92 | |
| 93 | .title { |
| 94 | font-size: 16px; |
| 95 | font-weight: bold; |
| 96 | } |
| 97 | |
| 98 | #package { |
| 99 | font-size: 14px; |
| 100 | font-weight: bold; |
| 101 | } |
| 102 | </style> |
| 103 | </head> |
| 104 | <body> |
| 105 | <div class="summary"> |
| 106 | <div class="title">Project TITLE_PLACEHOLDER</div> |
| 107 | <table> |
| 108 | <tr> |
| 109 | <td>Sources:</td> |
| 110 | <td id="sourceCount" class="stat"></td> |
| 111 | </tr> |
| 112 | <tr> |
| 113 | <td>Packages:</td> |
| 114 | <td id="packageCount" class="stat"></td> |
| 115 | </tr> |
| 116 | <tr> |
| 117 | <td>Cyclic Segments:</td> |
| 118 | <td id="segmentCount" class="stat"></td> |
| 119 | </tr> |
| 120 | <tr> |
| 121 | <td>Cycles:</td> |
| 122 | <td id="cycleCount" class="stat"></td> |
| 123 | </tr> |
| 124 | </table> |
| 125 | <div><hr size="1"></div> |
| 126 | <div><input type="checkbox"> Highlight cycles</input></div> |
| 127 | <div><input style="width: 95%" type="range" min="0" max="100" value="75"></div> |
| 128 | </div> |
| 129 | <div class="details"> |
| 130 | <div id="package">Package Details</div> |
| 131 | <table> |
| 132 | <tr> |
| 133 | <td>Sources:</td> |
| 134 | <td id="psourceCount" class="stat"></td> |
| 135 | </tr> |
| 136 | <tr> |
| 137 | <td>Dependents:</td> |
| 138 | <td id="pdependentCount" class="stat"></td> |
| 139 | </tr> |
| 140 | <tr> |
| 141 | <td>Cyclic Segments:</td> |
| 142 | <td id="psegmentCount" class="stat"></td> |
| 143 | </tr> |
| 144 | <tr> |
| 145 | <td>Cycles:</td> |
| 146 | <td id="pcycleCount" class="stat"></td> |
| 147 | </tr> |
| 148 | </table> |
| 149 | </div> |
| 150 | <script> |
| 151 | D3JS_PLACEHOLDER |
| 152 | |
| 153 | var catalog = |
| 154 | DATA_PLACEHOLDER |
| 155 | ; |
| 156 | |
| 157 | var diameter = 1000, |
| 158 | radius = diameter / 2, |
| 159 | innerRadius = radius - 300; |
| 160 | |
| 161 | var cluster = d3.layout.cluster() |
| 162 | .size([360, innerRadius]) |
| 163 | .sort(null) |
| 164 | .value(function(d) { return d.size; }); |
| 165 | |
| 166 | var bundle = d3.layout.bundle(); |
| 167 | |
| 168 | var line = d3.svg.line.radial() |
| 169 | .interpolate("bundle") |
| 170 | .tension(.75) |
| 171 | .radius(function(d) { return d.y; }) |
| 172 | .angle(function(d) { return d.x / 180 * Math.PI; }); |
| 173 | |
| 174 | var svg = d3.select("body").append("svg") |
| 175 | .attr("width", diameter) |
| 176 | .attr("height", diameter) |
| 177 | .append("g") |
| 178 | .attr("transform", "translate(" + radius + "," + radius + ")"); |
| 179 | |
| 180 | var link = svg.append("g").selectAll(".link"), |
| 181 | node = svg.append("g").selectAll(".node"), |
| 182 | cycles = {}, highlightCycles, selectedNode; |
| 183 | |
| 184 | function isCyclicLink(l) { |
| 185 | return highlightCycles && |
| 186 | (cycles[l.source.key + "-" + l.target.key] || cycles[l.target.key + "-" + l.source.key]); |
| 187 | } |
| 188 | |
| 189 | function isCyclicPackageLink(l, p) { |
| 190 | var key = l.source.key + "-" + l.target.key, |
| 191 | rKey = l.target.key + "-" + l.source.key; |
| 192 | return isCyclicLink(l) && (p.cycleSegments[key] || p.cycleSegments[rKey]); |
| 193 | } |
| 194 | |
| 195 | function refreshPaths() { |
| 196 | svg.selectAll("path.link").classed("link--cycle", isCyclicLink); |
| 197 | } |
| 198 | |
| 199 | function processCatalog() { |
| 200 | var nodes = cluster.nodes(packageHierarchy(catalog.packages)), |
| 201 | links = packageImports(nodes), |
| 202 | splines = bundle(links); |
| 203 | cycles = catalog.cycleSegments; |
| 204 | |
| 205 | d3.select("input[type=checkbox]").on("change", function() { |
| 206 | highlightCycles = this.checked; |
| 207 | refreshPaths(); |
| 208 | }); |
| 209 | |
| 210 | link = link |
| 211 | .data(splines) |
| 212 | .enter().append("path") |
| 213 | .each(function(d) { d.source = d[0], d.target = d[d.length - 1]; }) |
| 214 | .attr("class", "link") |
| 215 | .classed("link--cycle", isCyclicLink) |
| 216 | .attr("d", function(d, i) { return line(splines[i]); }); |
| 217 | |
| 218 | |
| 219 | node = node |
| 220 | .data(nodes.filter(function(n) { return !n.children; })) |
| 221 | .enter().append("text") |
| 222 | .attr("class", "node") |
| 223 | .attr("dy", ".31em") |
| 224 | .attr("transform", function(d) { return "rotate(" + (d.x - 90) + ")translate(" + (d.y + 8) + ",0)" + (d.x < 180 ? "" : "rotate(180)"); }) |
| 225 | .style("text-anchor", function(d) { return d.x < 180 ? "start" : "end"; }) |
| 226 | .text(function(d) { return d.key; }) |
| 227 | .on("focus", processSelect) |
| 228 | .on("blur", processSelect); |
| 229 | |
| 230 | d3.select("input[type=range]").on("change", function() { |
| 231 | line.tension(this.value / 100); |
| 232 | svg.selectAll("path.link") |
| 233 | .data(splines) |
| 234 | .attr("d", function(d, i) { return line(splines[i]); }); |
| 235 | }); |
| 236 | |
| 237 | d3.select("#packageCount").text(catalog.summary.packages); |
| 238 | d3.select("#sourceCount").text(catalog.summary.sources); |
| 239 | d3.select("#segmentCount").text(catalog.summary.cycleSegments); |
| 240 | d3.select("#cycleCount").text(catalog.summary.cycles); |
| 241 | } |
| 242 | |
| 243 | function processSelect(d) { |
| 244 | if (selectedNode === d) { |
| 245 | deselected(d); |
| 246 | selectedNode = null; |
| 247 | |
| 248 | } else if (selectedNode) { |
| 249 | deselected(selectedNode); |
| 250 | selectedNode = null; |
| 251 | selected(d); |
| 252 | |
| 253 | } else { |
| 254 | selected(d); |
| 255 | selectedNode = d; |
| 256 | } |
| 257 | } |
| 258 | |
| 259 | function selected(d) { |
| 260 | node |
| 261 | .each(function(n) { n.target = n.source = false; }) |
| 262 | .classed("node--focus", function(n) { return n === d; }); |
| 263 | |
| 264 | link |
| 265 | .classed("link--cycle", function(l) { return isCyclicPackageLink(l, d); }) |
| 266 | .classed("link--target", function(l) { if (l.target === d) return l.source.source = true; }) |
| 267 | .classed("link--source", function(l) { if (l.source === d) return l.target.target = true; }) |
| 268 | .filter(function(l) { return l.target === d || l.source === d; }) |
| 269 | .each(function() { this.parentNode.appendChild(this); }); |
| 270 | |
| 271 | node |
| 272 | .classed("node--target", function(n) { return n.target; }) |
| 273 | .classed("node--source", function(n) { return n.source; }); |
| 274 | |
| 275 | d3.select("#psourceCount").text(d.size); |
| 276 | d3.select("#pdependentCount").text(d.imports.length); |
| 277 | d3.select("#psegmentCount").text(d.cycleSegmentCount); |
| 278 | d3.select("#pcycleCount").text(d.cycleCount); |
| 279 | d3.select(".details").classed("shown", function() { return true; }); |
| 280 | } |
| 281 | |
| 282 | function deselected(d) { |
| 283 | link |
| 284 | .classed("link--cycle", isCyclicLink) |
| 285 | .classed("link--target", false) |
| 286 | .classed("link--source", false); |
| 287 | |
| 288 | node |
| 289 | .classed("node--target", false) |
| 290 | .classed("node--source", false) |
| 291 | .classed("node--focus", false); |
| 292 | d3.select(".details").classed("shown", function() { return false; }); |
| 293 | } |
| 294 | |
| 295 | d3.select(self.frameElement).style("height", diameter + "px"); |
| 296 | |
| 297 | // Lazily construct the package hierarchy. |
| 298 | function packageHierarchy(packages) { |
| 299 | var map = {}, cnt = 0; |
| 300 | |
| 301 | // Builds the structure top-down to the specified leaf or until |
| 302 | // another leaf in which case hook this leaf to the same parent |
| 303 | function buildHierarchy(leaf, i) { |
| 304 | var leafName = leaf.name, |
| 305 | node, name, parent = map[""], start = 0; |
| 306 | while (start < leafName.length) { |
| 307 | name = parentName(leafName, start); |
| 308 | node = map[name]; |
| 309 | if (!node) { |
| 310 | node = map[name] = parentNode(name, parent); |
| 311 | parent.children.push(node); |
| 312 | |
| 313 | } else if (node.imports) { |
| 314 | leaf.parent = parent; |
| 315 | parent.children.push(leaf); |
| 316 | break; |
| 317 | } |
| 318 | parent = node; |
| 319 | start = name.length + 1; |
| 320 | } |
| 321 | } |
| 322 | |
| 323 | function parentNode(name, parent) { |
| 324 | return {name: name, parent: parent, key: name, children: []}; |
| 325 | } |
| 326 | |
| 327 | function parentName(leafName, start) { |
| 328 | var i = leafName.indexOf(".", start); |
| 329 | return i > 0 ? leafName.substring(0, i) : leafName; |
| 330 | } |
| 331 | |
| 332 | // First populate all packages as leafs |
| 333 | packages.forEach(function(d) { |
| 334 | map[d.name] = d; |
| 335 | d.key = d.name; |
| 336 | }); |
| 337 | |
| 338 | // Next synthesize the intermediate structure, by-passing any leafs |
| 339 | map[""] = parentNode("", null); |
| 340 | var i = 0; |
| 341 | packages.forEach(function(d) { |
| 342 | buildHierarchy(d, i++); |
| 343 | }); |
| 344 | |
| 345 | return map[""]; |
| 346 | } |
| 347 | |
| 348 | // Return a list of imports for the given array of nodes. |
| 349 | function packageImports(nodes) { |
| 350 | var map = {}, |
| 351 | imports = []; |
| 352 | |
| 353 | // Compute a map from name to node. |
| 354 | nodes.forEach(function(d) { |
| 355 | map[d.name] = d; |
| 356 | }); |
| 357 | |
| 358 | // For each import, construct a link from the source to target node. |
| 359 | nodes.forEach(function(d) { |
| 360 | if (d.imports) d.imports.forEach(function(i) { |
| 361 | imports.push({source: map[d.name], target: map[i]}); |
| 362 | }); |
| 363 | }); |
| 364 | |
| 365 | return imports; |
| 366 | } |
| 367 | |
| 368 | processCatalog(); |
| 369 | </script> |
| 370 | </body> |
| 371 | </html> |