blob: be0b581c70b95b558d3f53b1296d42a2a939fc40 [file] [log] [blame]
Thomas Vachuska6b331262015-04-27 11:09:07 -07001<!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>
151D3JS_PLACEHOLDER
152
153 var catalog =
154DATA_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>