Skip to content

Instantly share code, notes, and snippets.

@miyagawa
Created August 8, 2013 02:47
Show Gist options
  • Save miyagawa/6181016 to your computer and use it in GitHub Desktop.
Save miyagawa/6181016 to your computer and use it in GitHub Desktop.
Generate D3 dependency graph with Carton. Copied from Startopan Demo https://dl.dropboxusercontent.com/u/135035/carton-graph.html
#!/usr/bin/perl
use strict;
use Carton::Environment;
use Carton::Tree;
use Path::Tiny;
my $env = Carton::Environment->build;
$env->snapshot->load;
$env->cpanfile->load;
my(@dists, %seen);
my $dumper; $dumper = sub {
my($dist, $reqs) = @_;
return if $dist && $seen{$dist->name}++;
my @deps = map $env->snapshot->find_or_core($_),
sort $reqs->required_modules;
if ($dist) {
push @dists, { name => $dist->name, prereqs => [ map $_->name, @deps ] };
}
for my $dependency (@deps) {
$dumper->($dependency, $dependency->requirements);
}
};
$dumper->(undef, $env->cpanfile->requirements);
use JSON;
my $prereqs = JSON::encode_json(\@dists);
my $html = path('graph/index.html.tmpl')->slurp;
$html =~ s/__PREREQS__/$prereqs/;
print $html;
<html>
<head>
<style>
.prereq-graph {
margin: 0 auto;
}
.prereq-graph-legend {
font-family: 'Courier New', monospace;
font-weight: 700;
}
.prereq-graph-legend .target {
color: #1f77b4;
}
.prereq-graph-legend .depends-on {
color: #d62728;
}
.prereq-graph-legend .required-by {
color: #2ca02c;
}
path.arc {
fill: #fff;
}
.node {
font-size: 16px;
}
.node:hover {
fill: #1f77b4;
font-weight: 700;
}
.link {
fill: none;
stroke: #1f77b4;
stroke-opacity: .4;
pointer-events: none;
}
.link.source, .link.target {
stroke-opacity: 1;
stroke-width: 2px;
}
.node.target {
fill: #d62728 !important;
font-weight: 700;
}
.link.source {
stroke: #d62728;
}
.node.source {
fill: #2ca02c;
font-weight: 700;
}
.link.target {
stroke: #2ca02c;
}
</style>
</head>
<body>
<div class="prereq-graph"></div>
<script src="d3.v3.min.js" charset="utf-8"></script>
<script>
var rotate = 0;
var m0;
//----------------------------------------------------------------------------
var prereqs = __PREREQS__;
(function(prereqs){
var data = transform(prereqs);
var bundle = d3.layout.bundle();
var maxInnerRadius = 400,
minInnerRadius = 200,
minNameTightness = 15,
longNameLength = 200,
numberOfNodes = data.children.length;
var innerRadius = Math.max(minInnerRadius,
Math.min(maxInnerRadius,
(numberOfNodes * minNameTightness / (2 * Math.PI))));
var outerDiameter = (innerRadius + longNameLength) * 2;
var outerRadius = outerDiameter / 2;
var containerDiv = d3.select(".prereq-graph")
.style("width", outerDiameter + "px");
var cluster = d3.layout.cluster()
.size([360, innerRadius])
.sort(function(a, b) { return d3.ascending(a.key, b.key); });
var nodes = cluster.nodes(data);
var links = linkNodes(nodes);
var splines = bundle(links);
var line = d3.svg.line.radial()
.radius(function(d) { return d.y; })
.angle(function(d) { return d.x / 180 * Math.PI; })
.interpolate("bundle")
.tension(1);
// Remve contents of the container
// This is usually a spinner graphic
containerDiv.selectAll("div").remove();
var svg = containerDiv.append("svg:svg")
.attr("width", outerDiameter)
.attr("height", outerDiameter)
.append("svg:g")
.attr("transform", "translate(" + outerRadius + "," + outerRadius + ")");
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(d) { mouseover(d, svg); } )
.on("mouseout", function(d) { mouseout(d, svg); } );
d3.select(window)
.on("mousedown", function(d) { mousedown(svg, outerRadius); } )
.on("mousemove", function(d) { mousemove(svg, outerRadius); } )
.on("mouseup", function(d) { mouseup(svg, outerRadius); } );
})(prereqs);
//----------------------------------------------------------------------------
function transform (prereqs) {
var map = {};
function find(name, data) {
var node = map[name], i;
if (!node) {
node = map[name] = data || {name: name, children: []};
if (name.length) {
node.parent = find('');
node.parent.children.push(node);
// Literal '.' not allowed in selector,
// so replace it with an underscore.
node.key = name.replace(/\./g, "_");
}
}
return node;
}
prereqs.forEach(function(d) { find(d.name, d); });
return map[""];
}
//----------------------------------------------------------------------------
function linkNodes (nodes) {
var map = {},
links = [];
// Compute a map from name to node.
nodes.forEach(function(d) {
map[d.name] = d;
});
// For each prereq, construct a link from the source to target node.
nodes.forEach(function(d) {
if (d.prereqs) d.prereqs.forEach(function(i) {
links.push({source: map[d.name], target: map[i]});
});
});
return links;
}
//----------------------------------------------------------------------------
function mouseover(d, svg) {
svg.selectAll("path.link.target-" + d.key)
.classed("target", true)
.each(updateNodes(svg, "source", true));
svg.selectAll("path.link.source-" + d.key)
.classed("source", true)
.each(updateNodes(svg, "target", true));
return true;
}
//----------------------------------------------------------------------------
function mouseout(d, svg) {
svg.selectAll("path.link.source-" + d.key)
.classed("source", false)
.each(updateNodes(svg, "target", false));
svg.selectAll("path.link.target-" + d.key)
.classed("target", false)
.each(updateNodes(svg, "source", false));
return true;
}
//----------------------------------------------------------------------------
function mousedown(svg, r) {
m0 = mouse(d3.event, r, r);
d3.selectAll('.prereq-graph').style('cursor', 'move');
d3.event.preventDefault();
return true;
}
//----------------------------------------------------------------------------
function mousemove(svg, r) {
if (m0) {
var m1 = mouse(d3.event, r, r);
rotate += Math.atan2(cross(m0, m1), dot(m0, m1)) * 180 / Math.PI;
if (rotate > 360) rotate -= 360;
else if (rotate < 0) rotate += 360;
svg.attr("transform", "translate(" + r + "," + r + ")rotate(" + rotate + ")")
.selectAll("g.node text")
.attr("dx", function(d) { return (d.x + rotate) % 360 < 180 ? 8 : -8; })
.attr("text-anchor", function(d) { return (d.x + rotate) % 360 < 180 ? "start" : "end"; })
.attr("transform", function(d) { return (d.x + rotate) % 360 < 180 ? null : "rotate(180)"; });
m0 = m1;
}
return true;
}
//----------------------------------------------------------------------------
function mouseup(svg, rx, ry) {
m0 = null;
d3.selectAll('.prereq-graph').style('cursor', 'pointer');
return true;
}
//----------------------------------------------------------------------------
function cross(a, b) {
return a[0] * b[1] - a[1] * b[0];
}
//----------------------------------------------------------------------------
function dot(a, b) {
return a[0] * b[0] + a[1] * b[1];
}
//----------------------------------------------------------------------------
function mouse(e, rx, ry) {
return [e.pageX - rx, e.pageY - ry];
}
//----------------------------------------------------------------------------
function updateNodes(svg, name, value) {
return function(d) {
if (value) this.parentNode.appendChild(this);
svg.select("#node-" + d[name].key).classed(name, value);
};
}
</script>
</body>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment