lib/treat/workers/formatters/visualizers/dot.rb
# Visualization of entities in DOT graph format.
class Treat::Workers::Formatters::Visualizers::DOT
require 'date'
DefaultOptions = {
:colors => {},
:features => :all,
:file => nil,
:remove_types => [],
:remove_features => [],
:colors => nil,
:first => true # For internal purposes only.
}
# Create the top-most graph structure
# and delegate the creation of the graph
# nodes to to_dot.
def self.visualize(entity, options = {})
options = DefaultOptions.merge(options)
string = "graph {"
string << self.to_dot(entity, options)
string << "\n}"
if options[:file]
File.open(options[:file], 'w') { |f| f.write(string) }
end
string
end
# dot -Tpdf test4.dot > test4.pdf
def self.to_dot(entity, options)
# Filter out specified types.
match_types = lambda do |t1, t2s|
f = false
t2s.each { |t2| f = true if Treat::Entities.match_types[t1][t2] }
f
end
return '' if match_types.call(entity.type, options[:remove_types])
# Id
string = ''
label = ''
sv = entity.short_value.inspect[1..-2]
string = "\n#{entity.id} ["
label = "#{entity.type.to_s.capitalize}\\n\\\"#{sv}\\\""
label.gsub!(' [...]', " [...] \\n")
# Features
if entity.has_features?
unless options[:features] == :none
label << "\\n"
entity.features.each do |feature, value|
next if options[:remove_features].include?(feature)
if options[:features] == :all ||
options[:features].include?(feature)
if value.is_a?(Treat::Entities::Entity)
label << "\\n#{feature}: \\\"*#{value.id}\\\""
elsif value.is_a?(Struct)
label << "\\n#{feature}: \\n\{ "
value.members.each do |member|
v = value.send(member)
v = v.to_s if v.is_a?(DateTime)
v = "*#{v.id}" if v.is_a?(Treat::Entities::Entity)
v = v ? v.inspect : ' -- '
v = escape(v)
label << "#{member}: #{v},\\n"
end
label = label[0..-4] unless label[-2] == '{'
label << "\},"
elsif value.is_a?(Hash)
label << "\\n#{feature}: \\n\{ "
value.each do |k,v|
v = v ? v.inspect : ' -- '
v = escape(v)
label << "#{k}: #{v},\\n"
end
label = label[0..-4] unless label[-2] == '{'
label << "\},"
elsif value.is_a?(Array)
label << "\\n#{feature}: \\n\[ "
value.each do |e|
if e.is_a?(Treat::Entities::Entity)
v = escape("*#{e.id}")
else
v = escape(e.inspect)
end
label << "#{v},\\n"
end
label = label[0..-4] unless label[-2] == '['
label << " \]"
else
label << "\\n#{feature}: \\\"#{value}\\\""
end
end
end
end
end
color = nil
if options[:colors]
options[:colors].each do |col, lambda|
color = col.to_s if lambda.call(entity)
break if color
end
end
label = label[0..-2] if label[-1] == ','
string << "label=\"#{label}\",color=\"#{color.to_s}\"]"
string.gsub!('\\\\""]', '\\""]')
string.gsub('\"\""]', '\""]')
# Parent-child relationships.
if entity.has_parent?
unless options[:first] == true
string << "\n#{entity.parent.id} -- #{entity.id};"
end
end
# edges.
if entity.has_edges?
entity.edges.each do |edge|
dir = ''
if edge.directed == true
dir = edge.direction == 1 ? 'forward' : 'back'
dir = ",dir=#{dir}"
else
dir = ",dir=both"
end
string << "\n#{entity.id} -- #{edge.target}"
string << "[label=#{edge.type}#{dir}]"
end
end
# Recurse.
options[:first] = false
if entity.has_children?
entity.each do |child|
string << self.to_dot(child, options)
end
end
string
end
def self.escape(v)
v.gsub('[', '\[')
.gsub('{', '\}')
.gsub(']', '\]')
.gsub('}', '\}')
.gsub('"', '\"')
end
end