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:     end

Public Instance methods

[Source]

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

[Source]

     # File diagram.rb, line 283
283:     def convert_to_png(file_base, graph, name)
284:       op_type = Options.instance.image_format
285:       dotfile = File.join(DOT_PATH, file_base)
286:       src = dotfile + ".dot"
287:       dot = dotfile + "." + op_type
288: 
289:       unless @options.quiet
290:         $stderr.print "."
291:         $stderr.flush
292:       end
293: 
294:       File.open(src, 'w+' ) do |f|
295:         f << graph.to_s << "\n"
296:       end
297:       
298:       system "dot", "-T#{op_type}", src, "-o", dot
299: 
300:       # Now construct the imagemap wrapper around
301:       # that png
302: 
303:       return wrap_in_image_map(src, dot, name)
304:     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 47
 47:     def draw
 48:       unless @options.quiet
 49:         $stderr.print "Diagrams: "
 50:         $stderr.flush
 51:       end
 52: 
 53:       @info.each_with_index do |i, file_count|
 54:         @done_modules = {}
 55:         @local_names = find_names(i)
 56:         @global_names = []
 57:         @global_graph = graph = DOT::DOTDigraph.new('name' => 'TopLevel',
 58:                                     'label' => i.file_absolute_name,
 59:                                     'fontname' => FONT,
 60:                                     'fontsize' => '8',
 61:                                     'bgcolor'  => 'lightcyan1',
 62:                                     'compound' => 'true')
 63:         
 64:         # it's a little hack %) i'm too lazy to create a separate class
 65:         # for default node
 66:         graph << DOT::DOTNode.new('name' => 'node',
 67:                                   'fontname' => FONT,
 68:                                   'color' => 'black',
 69:                                   'fontsize' => 8)
 70:         
 71:         i.modules.each do |mod|
 72:           draw_module(mod, graph, true, i.file_relative_name)
 73:         end
 74:         add_classes(i, graph, i.file_relative_name)
 75: 
 76:         i.diagram = convert_to_png("f_#{file_count}", graph, i.name)
 77:         
 78:         # now go through and document each top level class and
 79:         # module independently
 80:         i.modules.each_with_index do |mod, count|
 81:           @done_modules = {}
 82:           @local_names = find_names(mod)
 83:           @global_names = []
 84: 
 85:           @global_graph = graph = DOT::DOTDigraph.new('name' => 'TopLevel',
 86:                                       'label' => i.full_name,
 87:                                       'fontname' => FONT,
 88:                                       'fontsize' => '8',
 89:                                       'bgcolor'  => 'lightcyan1',
 90:                                       'compound' => 'true')
 91: 
 92:           graph << DOT::DOTNode.new('name' => 'node',
 93:                                     'fontname' => FONT,
 94:                                     'color' => 'black',
 95:                                     'fontsize' => 8)
 96:           draw_module(mod, graph, true)
 97:           mod.diagram = convert_to_png("m_#{file_count}_#{count}", 
 98:                                        graph, 
 99:                                        "Module: #{mod.name}")
100:         end
101:       end
102:       $stderr.puts unless @options.quiet
103:     end

[Source]

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

[Source]

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

[Source]

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

[Validate]