Skip to content

Instantly share code, notes, and snippets.

@omegahm
Last active June 8, 2020 12:41
Show Gist options
  • Save omegahm/23f8474603d356091404 to your computer and use it in GitHub Desktop.
Save omegahm/23f8474603d356091404 to your computer and use it in GitHub Desktop.
Generate nice Dependency Hierarchy Graph of Gems

Usage

  • Download above file
  • Run chmod +x graph.rb
  • Run with ./graph.rb PATH_TO_GEMFILE.LOCK
  • Open gem_graph.html

Screenshot

Screenshot

#!/usr/bin/env ruby
require 'json'
gemfile = ARGV[0] || 'Gemfile.lock'
gems = {}
last_gem = ''
name = ''
File.readlines(gemfile).each do |line|
next unless line.start_with?(' ' * 4) # 4 spaces makes it a gem or dependency
line = line.gsub(/\(.*?$/, '')
name = line.strip
gems[name] ||= { name: name, dependencies: [] }
case line
when /^\s{6}/ # 6 spaces, it's a dependency
gems[last_gem][:dependencies] << name
when /^\s{4}[^\s]/ # 4 spaces, it's a gem
last_gem = name
end
end
data = gems.map { |_, value| value }
File.write('gem_graph.html', <<-HTML
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Gem Graph</title>
<style>
body {
font-family: Helvetica;
padding: 0 10px;
}
path.arc {
fill: #fff;
}
.node {
font-size: 12px;
cursor: pointer;
}
.node:hover {
fill: #1f77b4;
}
.link {
fill: none;
stroke: #1f77b4;
stroke-opacity: .5;
pointer-events: none;
}
.link.source, .link.target {
stroke-opacity: 1;
stroke-width: 2px;
}
.node.target {
fill: #d62728 !important;
}
.link.source {
stroke: #d62728;
}
.node.source {
fill: #2ca02c;
}
.link.target {
stroke: #2ca02c;
}
</style>
</head>
<body>
<h1>Gemfile visualization</h1>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script>
// YOUR JSON
var data = JSON.parse('#{data.to_json}');
// WIDTH AND HEIGHT CONFIGURATION
var rx = 576,
ry = 432;
// GENERATING THE ACTUAL HIERARCHY GRAPH
var cluster = d3.layout.cluster()
.size([360, ry - 120])
.sort(function(a, b) {
return d3.ascending(a.key.toLowerCase(), b.key.toLowerCase());
});
var bundle = d3.layout.bundle();
var nodes = cluster.nodes(hierarchyRoot(data)),
links = dependencies(nodes),
splines = bundle(links);
var svg = d3.select("body").insert("div")
.style("-webkit-backface-visibility", "hidden")
.append("svg:svg")
.style("width", rx * 2 + "px")
.style("height", ry * 2 + "px")
.append("svg:g")
.attr("transform", "translate(" + ry + "," + ry + ")");
svg.append("svg:path")
.attr("class", "arc")
.attr("d", d3.svg.arc().outerRadius(ry - 120).innerRadius(0).startAngle(0).endAngle(2 * Math.PI));
var line = d3.svg.line.radial()
.interpolate("bundle")
.tension(.85)
.radius(function(d) { return d.y; })
.angle(function(d) { return d.x / 180 * Math.PI; });
var path = svg.selectAll("path.link")
.data(links)
.enter().append("svg:path")
.attr("class", function(d) { return "link source-" + d.source.key + " target-" + d.target.key; })
.attr("d", function(d, i) { return line(splines[i]); });
svg.selectAll("g.node")
.data(nodes.filter(function(n) { return !n.children; }))
.enter().append("svg:g")
.attr("class", "node")
.attr("id", function(d) { return "node-" + d.key; })
.attr("transform", function(d) { return "rotate(" + (d.x - 90) + ") translate(" + d.y + ")"; })
.append("svg:text")
.attr("dx", function(d) { return d.x < 180 ? 8 : -8; })
.attr("dy", ".31em")
.attr("text-anchor", function(d) { return d.x < 180 ? "start" : "end"; })
.attr("transform", function(d) { return d.x < 180 ? null : "rotate(180)"; })
.text(function(d) { return d.name; })
.on("mouseover", function(node) { mouseHover(node, true); })
.on("mouseout", function(node) { mouseHover(node, false); });
// UTILITY FUNCTIONS
function mouseHover(node, enable) {
svg.selectAll("path.link.target-" + node.key)
.classed("target", enable)
.each(updateNodes("source", enable));
svg.selectAll("path.link.source-" + node.key)
.classed("source", enable)
.each(updateNodes("target", enable));
}
function updateNodes(name, value) {
return function(d) {
if (value) {
this.parentNode.appendChild(this);
}
svg.select("#node-" + d[name].key).classed(name, value);
};
}
function hierarchyRoot(gems) {
var rootElement = [];
gems.forEach(function(d) {
d.key = d.name.replace('.', '-');
rootElement.push(d);
});
return { children: rootElement };
}
function dependencies(nodes) {
var map = {},
deps = [];
nodes.forEach(function(d) {
map[d.name] = d;
});
nodes.forEach(function(d) {
if (d.dependencies) {
d.dependencies.forEach(function(dep) {
deps.push({
source: map[d.name],
target: map[dep]
});
});
}
});
return deps;
}
</script>
</body>
</html>
HTML
)
@Lubdhak
Copy link

Lubdhak commented Jun 5, 2020

prefered over bundle viz

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment