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:       @html_suffix = ".html"
42:       if @options.mathml
43:         @html_suffix = ".xhtml"
44:       end
45:     end

Public Instance methods

[Source]

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

[Source]

     # File diagram.rb, line 287
287:     def convert_to_png(file_base, graph, name)
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 << graph.to_s << "\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:       return wrap_in_image_map(src, dot, name)
308:     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 51
 51:     def draw
 52:       unless @options.quiet
 53:         $stderr.print "Diagrams: "
 54:         $stderr.flush
 55:       end
 56: 
 57:       @info.each_with_index do |i, file_count|
 58:         @done_modules = {}
 59:         @local_names = find_names(i)
 60:         @global_names = []
 61:         @global_graph = graph = DOT::DOTDigraph.new('name' => 'TopLevel',
 62:                                     'label' => i.file_absolute_name,
 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, i.name)
 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:                                       'label' => i.full_name,
 91:                                       'fontname' => FONT,
 92:                                       'fontsize' => '8',
 93:                                       'bgcolor'  => 'lightcyan1',
 94:                                       'compound' => 'true')
 95: 
 96:           graph << DOT::DOTNode.new('name' => 'node',
 97:                                     'fontname' => FONT,
 98:                                     'color' => 'black',
 99:                                     'fontsize' => 8)
100:           draw_module(mod, graph, true)
101:           mod.diagram = convert_to_png("m_#{file_count}_#{count}", 
102:                                        graph, 
103:                                        "Module: #{mod.name}")
104:         end
105:       end
106:       $stderr.puts unless @options.quiet
107:     end

[Source]

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

[Source]

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

[Source]

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

[Validate]