Created
August 8, 2013 02:47
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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; | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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