| 1 | // GNU Guix --- Functional package management for GNU |
| 2 | // Copyright © 2016 Ricardo Wurmus <rekado@elephly.net> |
| 3 | // |
| 4 | // This file is part of GNU Guix. |
| 5 | // |
| 6 | // GNU Guix is free software; you can redistribute it and/or modify it |
| 7 | // under the terms of the GNU General Public License as published by |
| 8 | // the Free Software Foundation; either version 3 of the License, or (at |
| 9 | // your option) any later version. |
| 10 | // |
| 11 | // GNU Guix is distributed in the hope that it will be useful, but |
| 12 | // WITHOUT ANY WARRANTY; without even the implied warranty of |
| 13 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 14 | // GNU General Public License for more details. |
| 15 | // |
| 16 | // You should have received a copy of the GNU General Public License |
| 17 | // along with GNU Guix. If not, see <http://www.gnu.org/licenses/>. |
| 18 | |
| 19 | var outerRadius = Math.max(nodeArray.length * 15, 500) / 2, |
| 20 | innerRadius = outerRadius - Math.min(nodeArray.length * 5, 200), |
| 21 | width = outerRadius * 2, |
| 22 | height = outerRadius * 2, |
| 23 | colors = d3.scale.category20c(), |
| 24 | matrix = []; |
| 25 | |
| 26 | function neighborsOf (node) { |
| 27 | return links.filter(function (e) { |
| 28 | return e.source === node; |
| 29 | }).map(function (e) { |
| 30 | return e.target; |
| 31 | }); |
| 32 | } |
| 33 | |
| 34 | function zoomed () { |
| 35 | zoomer.attr("transform", |
| 36 | "translate(" + d3.event.translate + ")" + |
| 37 | "scale(" + d3.event.scale + ")"); |
| 38 | } |
| 39 | |
| 40 | function fade (opacity, root) { |
| 41 | return function (g, i) { |
| 42 | root.selectAll("g path.chord") |
| 43 | .filter(function (d) { |
| 44 | return d.source.index != i && d.target.index != i; |
| 45 | }) |
| 46 | .transition() |
| 47 | .style("opacity", opacity); |
| 48 | }; |
| 49 | } |
| 50 | |
| 51 | // Now that we have all nodes in an object we can replace each reference |
| 52 | // with the actual node object. |
| 53 | links.forEach(function (link) { |
| 54 | link.target = nodes[link.target]; |
| 55 | link.source = nodes[link.source]; |
| 56 | }); |
| 57 | |
| 58 | // Construct a square matrix for package dependencies |
| 59 | nodeArray.forEach(function (d, index, arr) { |
| 60 | var source = index, |
| 61 | row = matrix[source]; |
| 62 | if (!row) { |
| 63 | row = matrix[source] = []; |
| 64 | for (var i = -1; ++i < arr.length;) row[i] = 0; |
| 65 | } |
| 66 | neighborsOf(d).forEach(function (d) { row[d.index]++; }); |
| 67 | }); |
| 68 | |
| 69 | // chord layout |
| 70 | var chord = d3.layout.chord() |
| 71 | .padding(0.01) |
| 72 | .sortSubgroups(d3.descending) |
| 73 | .sortChords(d3.descending) |
| 74 | .matrix(matrix); |
| 75 | |
| 76 | var arc = d3.svg.arc() |
| 77 | .innerRadius(innerRadius) |
| 78 | .outerRadius(innerRadius + 20); |
| 79 | |
| 80 | var zoom = d3.behavior.zoom() |
| 81 | .scaleExtent([0.1, 10]) |
| 82 | .on("zoom", zoomed); |
| 83 | |
| 84 | var svg = d3.select("body").append("svg") |
| 85 | .attr("width", "100%") |
| 86 | .attr("height", "100%") |
| 87 | .attr('viewBox', '0 0 ' + Math.min(width, height) + ' ' + Math.min(width, height)) |
| 88 | .attr('preserveAspectRatio', 'xMinYMin') |
| 89 | .call(zoom); |
| 90 | |
| 91 | var zoomer = svg.append("g"); |
| 92 | |
| 93 | var container = zoomer.append("g") |
| 94 | .attr("transform", "translate(" + outerRadius + "," + outerRadius + ")"); |
| 95 | |
| 96 | // Group for arcs and labels |
| 97 | var g = container.selectAll(".group") |
| 98 | .data(chord.groups) |
| 99 | .enter().append("g") |
| 100 | .attr("class", "group") |
| 101 | .on("mouseout", fade(1, container)) |
| 102 | .on("mouseover", fade(0.1, container)); |
| 103 | |
| 104 | // Draw one segment per package |
| 105 | g.append("path") |
| 106 | .style("fill", function (d) { return colors(d.index); }) |
| 107 | .style("stroke", function (d) { return colors(d.index); }) |
| 108 | .attr("d", arc); |
| 109 | |
| 110 | // Add circular labels |
| 111 | g.append("text") |
| 112 | .each(function (d) { d.angle = (d.startAngle + d.endAngle) / 2; }) |
| 113 | .attr("dy", ".35em") |
| 114 | .attr("transform", function (d) { |
| 115 | return "rotate(" + (d.angle * 180 / Math.PI - 90) + ")" |
| 116 | + "translate(" + (innerRadius + 26) + ")" |
| 117 | + (d.angle > Math.PI ? "rotate(180)" : ""); |
| 118 | }) |
| 119 | .style("text-anchor", function (d) { return d.angle > Math.PI ? "end" : null; }) |
| 120 | .text(function (d) { return nodeArray[d.index].label; }); |
| 121 | |
| 122 | // Draw chords from source to target; color by source. |
| 123 | container.selectAll(".chord") |
| 124 | .data(chord.chords) |
| 125 | .enter().append("path") |
| 126 | .attr("class", "chord") |
| 127 | .style("stroke", function (d) { return d3.rgb(colors(d.source.index)).darker(); }) |
| 128 | .style("fill", function (d) { return colors(d.source.index); }) |
| 129 | .attr("d", d3.svg.chord().radius(innerRadius)); |