Last active
March 6, 2017 13:57
-
-
Save timblair/ffc4cc7eb2556800d4aa87903c7597f5 to your computer and use it in GitHub Desktop.
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
require "gviz" | |
require "optparse" | |
# Usage: $0 PINFILE [OPTIONS] | |
# | |
# -i, --highlight APPS Highlight apps that depend on these | |
# -x, --exclude APPS Exclude these apps | |
# -o, --only APPS Only show dependencies and dependants of these | |
# -s, --hide-solitary Hide apps with no dependecies or dependants | |
# -f, --out-file FILE The filename to write output to (default: out.png) | |
# -h, --help Show this help message | |
class Graph | |
def initialize(source) | |
@source = source | |
@nodes = {} | |
self.load | |
end | |
def load | |
instance_eval(File.read(@source)) | |
end | |
# Called as part of the Pinfile DSL from the instance_eval above. | |
def process(params) | |
label, deps = params, [] | |
if params.is_a? Hash | |
label = params.keys.first | |
deps = Array(params.values.first) | |
end | |
safe_id = Node.safe_id(label) | |
@nodes[safe_id] = Node.new(label, deps) | |
end | |
def render(options = {}) | |
vis = Gviz.new | |
vis.global(rankdir: "RL", rankseq: "equally") | |
[:highlight, :exclude, :only].each do |opt| | |
options[opt] = (options[opt] || []).map { |a| Node.safe_id(a) } | |
end | |
@nodes.each do |id, node| | |
next if options[:exclude].include?(id) | |
next if options[:loners] == false && | |
node.dependencies.empty? && dependants_of(id).empty? | |
# For `only` options we need to check both dependants and dependees so we | |
# can include the correct nodes and labels for each side. | |
next if !options[:only].empty? && | |
!options[:only].include?(id) && | |
(options[:only] & node.dependencies).empty? && | |
(options[:only] & dependants_of(id)).empty? | |
opts = { label: node.label } | |
if options[:highlight].include?(id) | |
opts[:peripheries] = 2 | |
opts[:penwidth] = 2 | |
opts[:color] = node.color | |
opts[:label] = "<B>#{opts[:label]}</B>" | |
end | |
fill = [] | |
options[:highlight].each do |h| | |
fill << @nodes[h].color if node.dependencies.include?(h) | |
end | |
if !fill.empty? | |
opts[:fillcolor] = fill.join(":") | |
if fill.size > 1 | |
opts[:style] = "wedged" | |
else | |
opts[:style] = "filled" | |
end | |
end | |
vis.node(id, opts) | |
deps = node.dependencies - options[:exclude] | |
if !options[:only].empty? && !options[:only].include?(id) | |
deps &= options[:only] | |
end | |
vis.route(id => deps) | |
end | |
vis | |
end | |
def dependants_of(id) | |
@nodes.values.inject([]) do |arr, node| | |
arr << node.safe_id if node.dependencies.include?(id) | |
arr | |
end | |
end | |
class Node | |
attr_reader :label, :dependencies | |
def initialize(label, deps = []) | |
@label = label.to_s | |
@dependencies = deps.map { |d| self.class.safe_id(d) } | |
end | |
def self.safe_id(label) | |
label.to_s.gsub("_", "").gsub("-", "").to_sym | |
end | |
def safe_id | |
@safe_id ||= self.class.safe_id(@label) | |
end | |
# Generate a random colour based on the provided object's String | |
# representation. | |
def color | |
hash = safe_id.to_s.gsub(" ", "").to_i(36) | |
hex_code = "%06x" % (hash & 0xffffff) | |
"##{hex_code}" | |
end | |
end | |
end | |
options = { out: "out" } | |
OptionParser.new do |opts| | |
opts.banner = "Usage: #{$0} PINFILE [OPTIONS]" | |
opts.separator "" | |
opts.on("-iAPPS", "--highlight APPS", "Highlight apps that depend on these") do |arg| | |
options[:highlight] = arg.split(",") | |
end | |
opts.on("-xAPPS", "--exclude APPS", "Exclude these apps") do |arg| | |
options[:exclude] = arg.split(",") | |
end | |
opts.on("-oAPPS", "--only APPS", "Only show dependencies and dependants of these") do |arg| | |
options[:only] = arg.split(",") | |
end | |
opts.on("-s", "--hide-solitary", "Hide apps with no dependecies or dependants") do |arg| | |
options[:loners] = false | |
end | |
opts.on("-fFILE", "--out-file FILE", "The filename to write output to (default: #{options[:out]}.png)") do |arg| | |
options[:out] = arg.gsub(/\.png$/i, "") | |
end | |
opts.on("-h", "--help", "Show this help message") do |arg| | |
puts opts | |
exit | |
end | |
opts.separator "" | |
end.parse! | |
graph = Graph.new(ARGV.first) | |
vis = graph.render(options) | |
vis.save(options[:out], :png) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment