1 # A wonderful hack by to draw package diagrams using the dot package.
2 # Originally written by Jah, team Enticla.
4 # You must have the V1.7 or later in your path
5 # http://www.research.att.com/sw/tools/graphviz/
12 # Draw a set of diagrams representing the modules and classes in the
13 # system. We draw one diagram for each file, and one for each toplevel
14 # class or module. This means there will be overlap. However, it also
15 # means that you'll get better context for objects.
19 # d = Diagram.new(info) # pass in collection of top level infos
22 # The results will be written to the +dot+ subdirectory. The process
23 # also sets the +diagram+ attribute in each object it graphs to
24 # the name of the file containing the image. This can be used
25 # by output generators to insert images.
34 # Pass in the set of top level objects. The method also creates the
35 # subdirectory to hold the images
37 def initialize(info, options)
41 FileUtils.mkdir_p(DOT_PATH)
46 # Draw the diagrams. We traverse the files, drawing a diagram for each. We
47 # also traverse each top-level class and module in that file drawing a
48 # diagram for these too.
52 $stderr.print "Diagrams: "
56 @info.each_with_index do |i, file_count|
58 @local_names = find_names(i)
60 @global_graph = graph = DOT::Digraph.new('name' => 'TopLevel',
63 'bgcolor' => 'lightcyan1',
66 # it's a little hack %) i'm too lazy to create a separate class
68 graph << DOT::Node.new('name' => 'node',
73 i.modules.each do |mod|
74 draw_module(mod, graph, true, i.file_relative_name)
76 add_classes(i, graph, i.file_relative_name)
78 i.diagram = convert_to_png("f_#{file_count}", graph)
80 # now go through and document each top level class and
81 # module independently
82 i.modules.each_with_index do |mod, count|
84 @local_names = find_names(mod)
87 @global_graph = graph = DOT::Digraph.new('name' => 'TopLevel',
90 'bgcolor' => 'lightcyan1',
93 graph << DOT::Node.new('name' => 'node',
97 draw_module(mod, graph, true)
98 mod.diagram = convert_to_png("m_#{file_count}_#{count}",
102 $stderr.puts unless @options.quiet
108 return [mod.full_name] + mod.classes.collect{|cl| cl.full_name} +
109 mod.modules.collect{|m| find_names(m)}.flatten
112 def find_full_name(name, mod)
114 return full_name if @local_names.include?(full_name)
115 mod_path = mod.full_name.split('::')[0..-2]
117 until mod_path.empty?
118 full_name = mod_path.pop + '::' + full_name
119 return full_name if @local_names.include?(full_name)
125 def draw_module(mod, graph, toplevel = false, file = nil)
126 return if @done_modules[mod.full_name] and not toplevel
129 url = mod.http_url("classes")
130 m = DOT::Subgraph.new('name' => "cluster_#{mod.full_name.gsub( /:/,'_' )}",
135 'URL' => %{"#{url}"},
136 'fillcolor' => toplevel ? 'palegreen1' : 'palegreen3')
138 @done_modules[mod.full_name] = m
139 add_classes(mod, m, file)
142 unless mod.includes.empty?
143 mod.includes.each do |inc|
144 m_full_name = find_full_name(inc.name, mod)
145 if @local_names.include?(m_full_name)
146 @global_graph << DOT::Edge.new('from' => "#{m_full_name.gsub( /:/,'_' )}",
147 'to' => "#{mod.full_name.gsub( /:/,'_' )}",
148 'ltail' => "cluster_#{m_full_name.gsub( /:/,'_' )}",
149 'lhead' => "cluster_#{mod.full_name.gsub( /:/,'_' )}")
151 unless @global_names.include?(m_full_name)
152 path = m_full_name.split("::")
153 url = File.join('classes', *path) + ".html"
154 @global_graph << DOT::Node.new('name' => "#{m_full_name.gsub( /:/,'_' )}",
156 'label' => "#{m_full_name}",
157 'URL' => %{"#{url}"})
158 @global_names << m_full_name
160 @global_graph << DOT::Edge.new('from' => "#{m_full_name.gsub( /:/,'_' )}",
161 'to' => "#{mod.full_name.gsub( /:/,'_' )}",
162 'lhead' => "cluster_#{mod.full_name.gsub( /:/,'_' )}")
168 def add_classes(container, graph, file = nil )
170 use_fileboxes = @options.fileboxes
174 # create dummy node (needed if empty and for module includes)
175 if container.full_name
176 graph << DOT::Node.new('name' => "#{container.full_name.gsub( /:/,'_' )}",
178 'width' => (container.classes.empty? and
179 container.modules.empty?) ?
182 'shape' => 'plaintext')
185 container.classes.each_with_index do |cl, cl_index|
186 last_file = cl.in_files[-1].file_relative_name
188 if use_fileboxes && !files.include?(last_file)
191 DOT::Subgraph.new('name' => "cluster_#{@counter}",
192 'label' => "#{last_file}",
195 last_file == file ? 'red' : 'black')
198 next if cl.name == 'Object' || cl.name[0,2] == "<<"
200 url = cl.http_url("classes")
203 if use_fileboxes && cl.in_files.length > 1
205 cl.in_files.collect {|i|
207 }.sort.join( '\n' ) +
212 'name' => "#{cl.full_name.gsub( /:/, '_' )}",
213 'fontcolor' => 'black',
215 'color'=>'palegoldenrod',
217 'shape' => 'ellipse',
221 c = DOT::Node.new(attrs)
224 files[last_file].push c
231 files.each_value do |val|
236 unless container.classes.empty?
237 container.classes.each_with_index do |cl, cl_index|
238 cl.includes.each do |m|
239 m_full_name = find_full_name(m.name, cl)
240 if @local_names.include?(m_full_name)
241 @global_graph << DOT::Edge.new('from' => "#{m_full_name.gsub( /:/,'_' )}",
242 'to' => "#{cl.full_name.gsub( /:/,'_' )}",
243 'ltail' => "cluster_#{m_full_name.gsub( /:/,'_' )}")
245 unless @global_names.include?(m_full_name)
246 path = m_full_name.split("::")
247 url = File.join('classes', *path) + ".html"
248 @global_graph << DOT::Node.new('name' => "#{m_full_name.gsub( /:/,'_' )}",
250 'label' => "#{m_full_name}",
251 'URL' => %{"#{url}"})
252 @global_names << m_full_name
254 @global_graph << DOT::Edge.new('from' => "#{m_full_name.gsub( /:/,'_' )}",
255 'to' => "#{cl.full_name.gsub( /:/, '_')}")
259 sclass = cl.superclass
260 next if sclass.nil? || sclass == 'Object'
261 sclass_full_name = find_full_name(sclass,cl)
262 unless @local_names.include?(sclass_full_name) or @global_names.include?(sclass_full_name)
263 path = sclass_full_name.split("::")
264 url = File.join('classes', *path) + ".html"
265 @global_graph << DOT::Node.new('name' => "#{sclass_full_name.gsub( /:/, '_' )}",
266 'label' => sclass_full_name,
267 'URL' => %{"#{url}"})
268 @global_names << sclass_full_name
270 @global_graph << DOT::Edge.new('from' => "#{sclass_full_name.gsub( /:/,'_' )}",
271 'to' => "#{cl.full_name.gsub( /:/, '_')}")
275 container.modules.each do |submod|
276 draw_module(submod, graph)
281 def convert_to_png(file_base, graph)
283 return @diagram_cache[str] if @diagram_cache[str]
284 op_type = @options.image_format
285 dotfile = File.join(DOT_PATH, file_base)
286 src = dotfile + ".dot"
287 dot = dotfile + "." + op_type
289 unless @options.quiet
294 File.open(src, 'w+' ) do |f|
298 system "dot", "-T#{op_type}", src, "-o", dot
300 # Now construct the imagemap wrapper around
303 ret = wrap_in_image_map(src, dot)
304 @diagram_cache[str] = ret
309 # Extract the client-side image map from dot, and use it to generate the
310 # imagemap proper. Return the whole <map>..<img> combination, suitable for
311 # inclusion on the page
313 def wrap_in_image_map(src, dot)
314 res = %{<map id="map" name="map">\n}
315 dot_map = `dot -Tismap #{src}`
316 dot_map.split($/).each do |area|
317 unless area =~ /^rectangle \((\d+),(\d+)\) \((\d+),(\d+)\) ([\/\w.]+)\s*(.*)/
318 $stderr.puts "Unexpected output from dot:\n#{area}"
322 xs, ys = [$1.to_i, $3.to_i], [$2.to_i, $4.to_i]
323 url, area_name = $5, $6
325 res << %{ <area shape="rect" coords="#{xs.min},#{ys.min},#{xs.max},#{ys.max}" }
326 res << %{ href="#{url}" alt="#{area_name}" />\n}
329 # map_file = src.sub(/.dot/, '.map')
330 # system("dot -Timap #{src} -o #{map_file}")
331 res << %{<img src="#{dot}" usemap="#map" border="0" alt="#{dot}">}