IS_JRUBY = (defined?( JRUBY_VERSION ) != nil)
IS_CYGWIN = ((RUBY_PLATFORM =~ /cygwin/) != nil)
require 'tempfile'
require 'graphviz/utils'
require 'graphviz/node'
require 'graphviz/edge'
require 'graphviz/attrs'
require 'graphviz/constants'
require 'graphviz/elements'
require 'graphviz/dot_script'
require 'graphviz/dot2ruby'
require 'graphviz/types'
require 'graphviz/core_ext'
class GraphViz
include GraphViz::Constants
include GraphViz::Utils
public
@@format = nil @format = nil
@filename = nil
@output = nil
@@prog = "dot"
@prog = nil
@@path = []
@path = nil
@@errors = 1
@errors = nil
@@extlibs = []
@extlibs = nil
@name = nil
@graph = nil
@node = nil
@edge = nil
attr_accessor :graph
alias_method :graph_attrs, :graph
attr_accessor :node
alias_method :node_attrs, :node
attr_accessor :edge
alias_method :edge_attrs, :edge
@elements_order = nil
def add_node( xNodeName, hOpts = {} )
if xNodeName.kind_of? Array
raise ArgumentError, "use `add_nodes' to add several nodes at the same time"
end
return add_nodes(xNodeName, hOpts)
end
def add_nodes(node_name, options = {})
if node_name.kind_of? Array
node_name.each { |n| add_nodes(n, options.clone) }
else
node = @hoNodes[node_name]
if node.nil?
@hoNodes[node_name] = GraphViz::Node::new( node_name, self )
@hoNodes[node_name].index = @elements_order.size_of( "node" )
unless options.keys.include?(:label) or options.keys.include?("label")
options[:label] = node_name
end
@elements_order.push( {
"type" => "node",
"name" => node_name,
"value" => @hoNodes[node_name]
} )
node = @hoNodes[node_name]
end
options.each do |xKey, xValue|
@hoNodes[node_name][xKey.to_s] = xValue
end
return node
end
end
def get_node( xNodeName, &block )
node = @hoNodes[xNodeName] || nil
yield( node ) if( block and node )
return node
end
def find_node(name)
root = root_graph
return root.search_node(name)
end
def search_node(name)
n = get_node(name)
return n unless n.nil?
each_graph { |_, g|
n = g.search_node(name)
return n unless n.nil?
}
return nil
end
def enumerate_nodes
nodes = @hoNodes.keys
each_graph { |_, g|
child_nodes = g.enumerate_nodes
nodes += child_nodes
}
return nodes
end
def get_node_at_index( index )
element = @elements_order[index, "node"]
(element.nil?) ? nil : element["value"]
end
def each_node( &block )
if block_given?
@hoNodes.each do |name, node|
yield( name, node )
end
else
return( @hoNodes )
end
end
def node_count
@hoNodes.size
end
def add_edge( oNodeOne, oNodeTwo, hOpts = {} )
if oNodeTwo.kind_of? Array or oNodeOne.kind_of? Array
raise ArgumentError, "use `add_edges' to add several edges at the same time"
end
add_edges(oNodeOne, oNodeTwo, hOpts)
end
def add_edges( node_one, node_two, options = {} )
if( node_one.class == Array )
node_one.each do |no|
add_edges( no, node_two, options )
end
else
if( node_two.class == Array )
node_two.each do |nt|
add_edges( node_one, nt, options )
end
else
edge = GraphViz::Edge::new( node_one, node_two, self )
edge.index = @elements_order.size_of( "edge" )
options.each do |xKey, xValue|
edge[xKey.to_s] = xValue
end
@elements_order.push( {
"type" => "edge",
"value" => edge
} )
@loEdges.push( edge )
return( edge )
end
end
end
def each_edge( &block )
if block_given?
@loEdges.each do |edge|
yield(edge)
end
else
return @loEdges
end
end
def edge_count
@loEdges.size
end
def get_edge_at_index( index )
element = @elements_order[index, "edge"]
(element.nil?) ? nil : element["value"]
end
def add_graph( xGraphName = nil, hOpts = {}, &block )
if xGraphName.kind_of?(GraphViz)
xGraphID = xGraphName.id
@hoGraphs[xGraphID] = xGraphName.clone
@hoGraphs[xGraphID].type = @oGraphType
@hoGraphs[xGraphID].pg = self
xGraphName = xGraphID
else
if xGraphName.kind_of?(Hash)
hOpts = xGraphName
xGraphName = nil
end
if xGraphName.nil?
xGraphID = String.random(11)
xGraphName = ""
else
xGraphID = xGraphName
end
@hoGraphs[xGraphID] = GraphViz::new( xGraphName, {:parent => self, :type => @oGraphType}, &block )
hOpts.each do |xKey, xValue|
@hoGraphs[xGraphID][xKey.to_s] = xValue
end
end
@elements_order.push( {
"type" => "graph",
"name" => xGraphName,
"value" => @hoGraphs[xGraphID]
} )
return( @hoGraphs[xGraphID] )
end
alias :subgraph :add_graph
def get_graph( xGraphName, &block )
graph = @hoGraphs[xGraphName] || nil
yield( graph ) if( block and graph )
return graph
end
def each_graph( &block )
if block_given?
@hoGraphs.each do |name, graph|
yield( name, graph )
end
else
return @hoGraphs
end
end
def add(h)
if h.kind_of? Hash
h.each do |node, data|
add_hash_edge(node, data)
end
end
end
def type
@oGraphType
end
def type=(x) @oGraphType = x
end
def graph_count
@hoGraphs.size
end
def method_missing( idName, *args, &block ) xName = idName.id2name
rCod = nil
if block
rCod = add_graph( xName, args[0]||{} )
yield( rCod )
else
if @hoNodes.keys.include?( xName )
if( args[0] )
return { xName => args[0] }
else
return( @hoNodes[xName] )
end
end
return( @hoGraphs[xName] ) if @hoGraphs.keys.include?( xName )
rCod = add_nodes( xName, args[0]||{} )
end
return rCod
end
def []=( xAttrName, xValue )
xValue = xValue.to_s if xValue.class == Symbol
@graph[xAttrName] = xValue
end
def []( xAttrName )
if Hash === xAttrName
xAttrName.each do |key, value|
self[key] = value
end
else
attr = @graph[xAttrName]
if attr.nil?
return nil
else
return( @graph[xAttrName].clone )
end
end
end
def each_attribute(&b)
@graph.each do |k,v|
yield(k,v)
end
end
def each_attribut(&b)
warn "`GraphViz#each_attribut` is deprecated, please use `GraphViz#each_attribute`"
each_attribute(&b)
end
def to_graph
graph = self.clone
graph.pg = nil
return graph
end
def output( hOpts = {} )
xDOTScript = DOTScript.new
lNotHugly = []
append_attributes_and_types(xDOTScript)
xDOTScript << "}"
if has_parent_graph?
xDOTScript.make_subgraph("#{GraphViz.escape(@name, :unquote_empty => true)}")
else
hOutput = {}
hOpts.each do |xKey, xValue|
xValue = xValue.to_s unless xValue.nil? or [Class, TrueClass, FalseClass].include?(xValue.class)
case xKey.to_s
when "use"
if PROGRAMS.index( xValue ).nil?
raise ArgumentError, "can't use '#{xValue}'"
end
@prog = xValue
when "path"
@path = xValue && xValue.split( "," ).map{ |x| x.strip }
when "errors"
@errors = xValue
when "extlib"
@extlibs = xValue.split( "," ).map{ |x| x.strip }
when "scale"
@scale = xValue
when "inverty"
@inverty = xValue
when "no_layout"
@no_layout = xValue
when "reduce"
@reduce_graph = xValue
when "Lg"
@Lg = xValue
when "LO"
@LO = xValue
when "Ln"
@Ln = xValue
when "LU"
@LU = xValue
when "LC"
@LC = xValue
when "LT"
@LT = xValue
when "nothugly"
begin
require 'graphviz/nothugly'
@nothugly = true
rescue LoadError
warn "You must install ruby-xslt or libxslt-ruby to use nothugly option!"
@nothugly = false
end
else
if FORMATS.index( xKey.to_s ).nil?
raise ArgumentError, "output format '#{xValue}' invalid"
end
hOutput[xKey.to_s] = xValue
end
end
@output = hOutput if hOutput.size > 0
xStict = ((@strict && @oGraphType == "digraph") ? "strict " : "")
xDOTScript.prepend(
"#{xStict}#{@oGraphType} #{GraphViz.escape(@name, :unquote_empty => true)} {"
)
xOutputString = (@filename == String ||
@output.any? {|format, file| file == String })
xOutput = ""
if @format.to_s == "none" or @output.any? {|fmt, fn| fmt.to_s == "none"}
if xOutputString
xOutput << xDOTScript
else
xFileName = @output["none"] || @filename
open( xFileName, "w" ) do |h|
h.puts xDOTScript
end
end
end
if (@format.to_s != "none" and not @format.nil?) or (@output.any? {|format, file| format != "none" } and @output.size > 0)
t = Tempfile::open( File.basename(__FILE__) )
t.print( xDOTScript )
t.close
cmd = find_executable( @prog, @path )
if cmd == nil
raise StandardError, "GraphViz not installed or #{@prog} not in PATH. Install GraphViz or use the 'path' option"
end
xOutputWithFile = []
xOutputWithoutFile = []
unless @format.nil? or @format == "none"
lNotHugly << @filename if @format.to_s == "svg" and @nothugly
if @filename.nil? or @filename == String
xOutputWithoutFile = ["-T#{@format}"]
else
xOutputWithFile = ["-T#{@format}", "-o#{@filename}"]
end
end
@output.each_except( :key => ["none"] ) do |format, file|
lNotHugly << file if format.to_s == "svg" and @nothugly
if file.nil? or file == String
xOutputWithoutFile += ["-T#{format}"]
else
xOutputWithFile += ["-T#{format}", "-o#{file}"]
end
end
xExternalLibraries = @extlibs.map { |lib| "-l#{lib}" }
xOtherOptions = []
xOtherOptions << "-s#{@scale}" if @scale
xOtherOptions << "-y" if @inverty
xOtherOptions << "-n#{@no_layout}" if @no_layout
xOtherOptions << "-x" if @reduce_graph
xOtherOptions << "-Lg" if @Lg
xOtherOptions << "-LO" if @LO
xOtherOptions << "-Ln#{@Ln}" if @Ln
xOtherOptions << "-LU#{@LU}" if @LU
xOtherOptions << "-LC#{@LC}" if @LC
xOtherOptions << "-LT#{@LT}" if @LT
tmpPath = if IS_JRUBY
t.path
elsif IS_CYGWIN
begin
IO.popen("cygpath", "-w", t.path).chomp
rescue
warn "cygpath is not installed!"
t.path
end
else
t.path
end
xCmd = [cmd, "-q#{@errors}"] +
xExternalLibraries +
xOtherOptions +
xOutputWithFile +
xOutputWithoutFile +
[tmpPath]
xOutput << output_from_command( xCmd )
end
lNotHugly.each do |f|
if f.nil? or f == String
xOutput = GraphViz.nothugly( xOutput, false )
else
GraphViz.nothugly( f, true )
end
end
if xOutputString
xOutput
else
print xOutput
end
end
end
alias :save :output
def append_attributes_and_types(script)
script_data = DOTScriptData.new
@elements_order.each { |kElement|
is_new_type = script_data.type != kElement["type"]
if is_new_type
script << script_data unless script_data.type.nil? or script_data.empty?
script_data = DOTScriptData.new(kElement["type"])
end
kElement["value"] or raise ArgumentError, "#{kElement["name"]} is nil!"
case kElement["type"]
when "graph_attr", "node_attr", "edge_attr"
script_data.add_attribute(kElement["name"], kElement["value"].to_gv)
when "node", "graph"
script << kElement["value"].output()
when "edge"
script << " " + kElement["value"].output( @oGraphType )
else
raise ArgumentError,
"Don't know what to do with element type '#{kElement['type']}'"
end
}
script << script_data unless script_data.type.nil? or script_data.empty?
end
def to_s
self.output(:none => String)
end
def name
@name.clone
end
alias :id :name
def <<( oNode )
raise( ArgumentError, "Edge between root graph and node or cluster not allowed!" ) if self.pg.nil?
if( oNode.class == Array )
oNode.each do |no|
self << no
end
else
return GraphViz::commonGraph( oNode, self ).add_edges( self, oNode )
end
end
alias :> :<<
alias :- :<<
alias :>> :<<
def pg @oParentGraph
end
def pg=(x) @oParentGraph = x
end
def root_graph
return( (self.pg.nil?) ? self : self.pg.root_graph )
end
def self.commonGraph( o1, o2 ) g1 = o1.pg
g2 = o2.pg
return o1 if g1.nil?
return o2 if g2.nil?
return g1 if g1.object_id == g2.object_id
return GraphViz::commonGraph( g1, g2 )
end
def set_position( xType, xKey, xValue ) @elements_order.push( {
"type" => "#{xType}_attr",
"name" => xKey,
"value" => xValue
} )
end
def self.default( hOpts )
hOpts.each do |k, v|
case k.to_s
when "use"
@@prog = v
when "path"
@@path = v.split( "," ).map{ |x| x.strip }
when "errors"
@@errors = v
when "extlibs"
@@extlibs = v.split( "," ).map{ |x| x.strip }
else
warn "Invalid option #{k}!"
end
end
end
def self.options( hOpts )
GraphViz::default( hOpts )
end
def self.parse( xFile, hOpts = {}, &block )
graph = Dot2Ruby::new( hOpts[:path], nil, nil ).eval( xFile )
yield( graph ) if( block and graph )
return graph
end
def self.parse_string( str, hOpts = {}, &block )
graph = Dot2Ruby::new(hOpts[:path], nil, nil).eval_string(str)
yield(graph) if(block and graph)
return graph
end
def complete
GraphViz.parse_string( root_graph.output( :dot => String ) )
end
def complete!
complete
end
def directed?
not((/digraph/ =~ "bla digraph bla").nil?)
end
def has_parent_graph?
not @oParentGraph.nil?
end
private
@hoNodes = nil
@loEdges = nil
@hoGraphs = nil
@oParentGraph = nil
@oGraphType = nil
def initialize( xGraphName, hOpts = {}, &block )
@filename = nil
@name = xGraphName.to_s
@format = @@format
@prog = @@prog
@path = @@path
@errors = @@errors
@extlibs = @@extlibs
@output = {}
@nothugly = false
@strict = false
@scale = nil
@inverty = nil
@no_layout = nil
@reduce_graph = nil
@Lg = nil
@LO = nil
@Ln = nil
@LU = nil
@LC = nil
@LT = nil
@elements_order = GraphViz::Elements::new()
@oParentGraph = nil
@oGraphType = "digraph"
@hoNodes = Hash::new()
@loEdges = Array::new()
@hoGraphs = Hash::new()
@node = GraphViz::Attrs::new( self, "node", NODESATTRS )
@edge = GraphViz::Attrs::new( self, "edge", EDGESATTRS )
@graph = GraphViz::Attrs::new( self, "graph", GRAPHSATTRS )
hOpts.each do |xKey, xValue|
case xKey.to_s
when "use"
if PROGRAMS.index( xValue.to_s ).nil?
raise ArgumentError, "can't use '#{xValue}'"
end
@prog = xValue.to_s
when "parent"
@oParentGraph = xValue
when "type"
if GRAPHTYPE.index( xValue.to_s ).nil?
raise ArgumentError, "graph type '#{xValue}' unknow"
end
@oGraphType = xValue.to_s
when "path"
@path = xValue.split( "," ).map{ |x| x.strip }
when "strict"
@strict = (xValue ? true : false)
when "errors"
@errors = xValue
when "extlibs"
@extlibs = xValue.split( "," ).map{ |x| x.strip }
else
self[xKey.to_s] = xValue.to_s
end
end
yield( self ) if( block )
end
def add_hash_edge(node, hash)
if hash.kind_of? Hash
hash.each do |nt, data|
add_edges(node, nt)
add_hash_edge(nt, data)
end
else
add_edges(node, hash)
end
end
def self.graph( xGraphName, hOpts = {}, &block )
new( xGraphName, hOpts.symbolize_keys.merge( {:type => "graph"} ), &block )
end
def self.digraph( xGraphName, hOpts = {}, &block )
new( xGraphName, hOpts.symbolize_keys.merge( {:type => "digraph"} ), &block )
end
def self.strict_digraph( xGraphName, hOpts = {}, &block )
new( xGraphName, hOpts.symbolize_keys.merge( {:type => "digraph", :strict => true} ), &block )
end
def self.generate(num_nodes, num_edges, directed = false, weight_range = (1..1))
g = nil
if directed
g = GraphViz.digraph(:G)
else
g = GraphViz.graph(:G)
end
nodes = (1..num_nodes).map{ |e| e.to_s }
g.add_nodes(nodes)
edges = []
nodes.each do |head|
nodes.each do |tail|
if (directed and head != tail) or (head < tail)
edges << {:head => head, :tail => tail, :weight => weight_range.to_a.shuffle[0]}
end
end
end
edges.shuffle!
(num_edges - 1).times do |i|
g.add_edges(edges[i][:head], edges[i][:tail], :label => edges[i][:weight].to_s, :weight => edges[i][:weight])
end
return g
end
def self.escape(str, opts = {} ) options = {
:force => false,
:unquote_empty => false,
}.merge(opts)
reserved_words = %w{node edge graph digraph subgraph strict}
if (options[:force] or str.match( /\A[a-zA-Z_]+[a-zA-Z0-9_]*\Z/ ).nil? or reserved_words.include?(str.downcase))
unless options[:unquote_empty] and str.size == 0
'"' + str.gsub('"', '\\"').gsub("\n", '\\\\n') + '"'
end
else
str
end
end
end