Class RDoc::Diagram
In: diagram.rb
Parent: Object

Draw a set of diagrams representing the modules and classes in the system. We draw one diagram for each file, and one for each toplevel class or module. This means there will be overlap. However, it also means that you‘ll get better context for objects.

To use, simply

  d = Diagram.new(info)   # pass in collection of top level infos
  d.draw

The results will be written to the dot subdirectory. The process also sets the diagram attribute in each object it graphs to the name of the file containing the image. This can be used by output generators to insert images.

Methods

Constants

FONT = "Arial"
DOT_PATH = "dot"

Public Class methods

Pass in the set of top level objects. The method also creates the subdirectory to hold the images

[Source]

    # File diagram.rb, line 36
36:     def initialize(info, options)
37:       @info = info
38:       @options = options
39:       @counter = 0
40:       File.makedirs(DOT_PATH)
41:       @diagram_cache = {}
42:       @html_suffix = ".html"
43:       if @options.mathml
44:         @html_suffix = ".xhtml"
45:       end
46:     end

Public Instance methods

[Source]

     # File diagram.rb, line 172
172:     def add_classes(container, graph, file = nil )
173: 
174:       use_fileboxes = Options.instance.fileboxes
175: 
176:       files = {}
177: 
178:       # create dummy node (needed if empty and for module includes)
179:       if container.full_name
180:         graph << DOT::DOTNode.new('name'     => "#{container.full_name.gsub( /:/,'_' )}",
181:                                   'label'    => "",
182:                                   'width'  => (container.classes.empty? and 
183:                                                container.modules.empty?) ? 
184:                                   '0.75' : '0.01',
185:                                   'height' => '0.01',
186:                                   'shape' => 'plaintext')
187:       end
188:       container.classes.each_with_index do |cl, cl_index|
189:         last_file = cl.in_files[-1].file_relative_name
190: 
191:         if use_fileboxes && !files.include?(last_file)
192:           @counter += 1
193:           files[last_file] =
194:             DOT::DOTSubgraph.new('name'     => "cluster_#{@counter}",
195:                                  'label'    => "#{last_file}",
196:                                  'fontname' => FONT,
197:                                  'color'=>
198:                                  last_file == file ? 'red' : 'black')
199:         end
200: 
201:         next if cl.name == 'Object' || cl.name[0,2] == "<<"
202: 
203:         url = cl.http_url("classes").sub(/\.html$/, @html_suffix)
204:         
205:         label = cl.name.dup
206:         if use_fileboxes && cl.in_files.length > 1
207:           label <<  '\n[' + 
208:                         cl.in_files.collect {|i|
209:                              i.file_relative_name 
210:                         }.sort.join( '\n' ) +
211:                     ']'
212:         end 
213:                 
214:         attrs = {
215:           'name' => "#{cl.full_name.gsub( /:/, '_' )}",
216:           'fontcolor' => 'black',
217:           'style'=>'filled',
218:           'color'=>'palegoldenrod',
219:           'label' => label,
220:           'shape' => 'ellipse',
221:           'URL'   => %{"#{url}"}
222:         }
223: 
224:         c = DOT::DOTNode.new(attrs)
225:         
226:         if use_fileboxes
227:           files[last_file].push c 
228:         else
229:           graph << c
230:         end
231:       end
232:       
233:       if use_fileboxes
234:         files.each_value do |val|
235:           graph << val
236:         end
237:       end
238:       
239:       unless container.classes.empty?
240:         container.classes.each_with_index do |cl, cl_index|
241:           cl.includes.each do |m|
242:             m_full_name = find_full_name(m.name, cl)
243:             if @local_names.include?(m_full_name)
244:               @global_graph << DOT::DOTEdge.new('from' => "#{m_full_name.gsub( /:/,'_' )}",
245:                                       'to' => "#{cl.full_name.gsub( /:/,'_' )}",
246:                                       'ltail' => "cluster_#{m_full_name.gsub( /:/,'_' )}")
247:             else
248:               unless @global_names.include?(m_full_name)
249:                 path = m_full_name.split("::")
250:                 url = File.join('classes', *path) + @html_suffix
251:                 @global_graph << DOT::DOTNode.new('name' => "#{m_full_name.gsub( /:/,'_' )}",
252:                                           'shape' => 'box',
253:                                           'label' => "#{m_full_name}",
254:                                           'URL'   => %{"#{url}"})
255:                 @global_names << m_full_name
256:               end
257:               @global_graph << DOT::DOTEdge.new('from' => "#{m_full_name.gsub( /:/,'_' )}",
258:                                       'to' => "#{cl.full_name.gsub( /:/, '_')}")
259:             end
260:           end
261: 
262:           sclass = cl.superclass
263:           next if sclass.nil? || sclass == 'Object'
264:           sclass_full_name = find_full_name(sclass,cl)
265:           unless @local_names.include?(sclass_full_name) or @global_names.include?(sclass_full_name)
266:             path = sclass_full_name.split("::")
267:             url = File.join('classes', *path) + @html_suffix
268:             @global_graph << DOT::DOTNode.new(
269:                        'name' => "#{sclass_full_name.gsub( /:/, '_' )}",
270:                        'label' => sclass_full_name,
271:                        'URL'   => %{"#{url}"})
272:             @global_names << sclass_full_name
273:           end
274:           @global_graph << DOT::DOTEdge.new('from' => "#{sclass_full_name.gsub( /:/,'_' )}",
275:                                     'to' => "#{cl.full_name.gsub( /:/, '_')}")
276:         end
277:       end
278: 
279:       container.modules.each do |submod|
280:         draw_module(submod, graph)
281:       end
282:       
283:     end

[Source]

     # File diagram.rb, line 285
285:     def convert_to_png(file_base, graph)
286:       str = graph.to_s
287:       return @diagram_cache[str] if @diagram_cache[str]
288:       op_type = Options.instance.image_format
289:       dotfile = File.join(DOT_PATH, file_base)
290:       src = dotfile + ".dot"
291:       dot = dotfile + "." + op_type
292: 
293:       unless @options.quiet
294:         $stderr.print "."
295:         $stderr.flush
296:       end
297: 
298:       File.open(src, 'w+' ) do |f|
299:         f << str << "\n"
300:       end
301:       
302:       system "dot", "-T#{op_type}", src, "-o", dot
303: 
304:       # Now construct the imagemap wrapper around
305:       # that png
306: 
307:       ret = wrap_in_image_map(src, dot)
308:       @diagram_cache[str] = ret
309:       return ret
310:     end

Draw the diagrams. We traverse the files, drawing a diagram for each. We also traverse each top-level class and module in that file drawing a diagram for these too.

[Source]

     # File diagram.rb, line 52
 52:     def draw
 53:       unless @options.quiet
 54:         $stderr.print "Diagrams: "
 55:         $stderr.flush
 56:       end
 57: 
 58:       @info.each_with_index do |i, file_count|
 59:         @done_modules = {}
 60:         @local_names = find_names(i)
 61:         @global_names = []
 62:         @global_graph = graph = DOT::DOTDigraph.new('name' => 'TopLevel',
 63:                                     'fontname' => FONT,
 64:                                     'fontsize' => '8',
 65:                                     'bgcolor'  => 'lightcyan1',
 66:                                     'compound' => 'true')
 67:         
 68:         # it's a little hack %) i'm too lazy to create a separate class
 69:         # for default node
 70:         graph << DOT::DOTNode.new('name' => 'node',
 71:                                   'fontname' => FONT,
 72:                                   'color' => 'black',
 73:                                   'fontsize' => 8)
 74:         
 75:         i.modules.each do |mod|
 76:           draw_module(mod, graph, true, i.file_relative_name)
 77:         end
 78:         add_classes(i, graph, i.file_relative_name)
 79: 
 80:         i.diagram = convert_to_png("f_#{file_count}", graph)
 81:         
 82:         # now go through and document each top level class and
 83:         # module independently
 84:         i.modules.each_with_index do |mod, count|
 85:           @done_modules = {}
 86:           @local_names = find_names(mod)
 87:           @global_names = []
 88: 
 89:           @global_graph = graph = DOT::DOTDigraph.new('name' => 'TopLevel',
 90:                                       'fontname' => FONT,
 91:                                       'fontsize' => '8',
 92:                                       'bgcolor'  => 'lightcyan1',
 93:                                       'compound' => 'true')
 94: 
 95:           graph << DOT::DOTNode.new('name' => 'node',
 96:                                     'fontname' => FONT,
 97:                                     'color' => 'black',
 98:                                     'fontsize' => 8)
 99:           draw_module(mod, graph, true)
100:           mod.diagram = convert_to_png("m_#{file_count}_#{count}", 
101:                                        graph) 
102:         end
103:       end
104:       $stderr.puts unless @options.quiet
105:     end

[Source]

     # File diagram.rb, line 129
129:     def draw_module(mod, graph, toplevel = false, file = nil)
130:       return if  @done_modules[mod.full_name] and not toplevel
131: 
132:       @counter += 1
133:       url = mod.http_url("classes").sub(/\.html$/, @html_suffix)
134:       m = DOT::DOTSubgraph.new('name' => "cluster_#{mod.full_name.gsub( /:/,'_' )}",
135:                                'label' => mod.name,
136:                                'fontname' => FONT,
137:                                'color' => 'blue', 
138:                                'style' => 'filled', 
139:                                'URL'   => %{"#{url}"},
140:                                'fillcolor' => toplevel ? 'palegreen1' : 'palegreen3')
141:       
142:       @done_modules[mod.full_name] = m
143:       add_classes(mod, m, file)
144:       graph << m
145: 
146:       unless mod.includes.empty?
147:         mod.includes.each do |m|
148:           m_full_name = find_full_name(m.name, mod)
149:           if @local_names.include?(m_full_name)
150:             @global_graph << DOT::DOTEdge.new('from' => "#{m_full_name.gsub( /:/,'_' )}",
151:                                       'to' => "#{mod.full_name.gsub( /:/,'_' )}",
152:                                       'ltail' => "cluster_#{m_full_name.gsub( /:/,'_' )}",
153:                                       'lhead' => "cluster_#{mod.full_name.gsub( /:/,'_' )}")
154:           else
155:             unless @global_names.include?(m_full_name)
156:               path = m_full_name.split("::")
157:               url = File.join('classes', *path) + @html_suffix
158:               @global_graph << DOT::DOTNode.new('name' => "#{m_full_name.gsub( /:/,'_' )}",
159:                                         'shape' => 'box',
160:                                         'label' => "#{m_full_name}",
161:                                         'URL'   => %{"#{url}"})
162:               @global_names << m_full_name
163:             end
164:             @global_graph << DOT::DOTEdge.new('from' => "#{m_full_name.gsub( /:/,'_' )}",
165:                                       'to' => "#{mod.full_name.gsub( /:/,'_' )}",
166:                                       'lhead' => "cluster_#{mod.full_name.gsub( /:/,'_' )}")
167:           end
168:         end
169:       end
170:     end

[Source]

     # File diagram.rb, line 116
116:     def find_full_name(name, mod)
117:       full_name = name.dup
118:       return full_name if @local_names.include?(full_name)
119:       mod_path = mod.full_name.split('::')[0..-2]
120:       unless mod_path.nil?
121:         until mod_path.empty?
122:           full_name = mod_path.pop + '::' + full_name
123:           return full_name if @local_names.include?(full_name)
124:         end
125:       end
126:       return name
127:     end

[Source]

     # File diagram.rb, line 111
111:     def find_names(mod)
112:       return [mod.full_name] + mod.classes.collect{|cl| cl.full_name} +
113:         mod.modules.collect{|m| find_names(m)}.flatten
114:     end

Extract the client-side image map from dot, and use it to generate the imagemap proper. Return the whole <map>..<img> combination, suitable for inclusion on the page

[Source]

     # File diagram.rb, line 317
317:     def wrap_in_image_map(src, dot)
318:       res = %{<map id="map" name="map">\n}
319:       dot_map = `dot -Tismap #{src}`
320:       dot_map.each do |area|
321:         unless area =~ /^rectangle \((\d+),(\d+)\) \((\d+),(\d+)\) ([\/\w.]+)\s*(.*)/
322:           $stderr.puts "Unexpected output from dot:\n#{area}"
323:           return nil
324:         end
325:         
326:         xs, ys = [$1.to_i, $3.to_i], [$2.to_i, $4.to_i]
327:         url, area_name = $5, $6
328: 
329:         res <<  %{  <area shape="rect" coords="#{xs.min},#{ys.min},#{xs.max},#{ys.max}" }
330:         res <<  %{     href="#{url}" alt="#{area_name}" />\n}
331:       end
332:       res << "</map>\n"
333: #      map_file = src.sub(/.dot/, '.map')
334: #      system("dot -Timap #{src} -o #{map_file}")
335:       res << %{<img src="#{dot}" usemap="#map" border="0" alt="#{dot}">}
336:       return res
337:     end

[Validate]