Class RDoc::Diagram
In: lib/rdoc/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 lib/rdoc/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:     end

Public Instance methods

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 lib/rdoc/diagram.rb, line 48
 48:     def draw
 49:       unless @options.quiet
 50:         $stderr.print "Diagrams: "
 51:         $stderr.flush
 52:       end
 53: 
 54:       @info.each_with_index do |i, file_count|
 55:         @done_modules = {}
 56:         @local_names = find_names(i)
 57:         @global_names = []
 58:         @global_graph = graph = DOT::DOTDigraph.new('name' => 'TopLevel',
 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)
 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:                                       'fontname' => FONT,
 87:                                       'fontsize' => '8',
 88:                                       'bgcolor'  => 'lightcyan1',
 89:                                       'compound' => 'true')
 90: 
 91:           graph << DOT::DOTNode.new('name' => 'node',
 92:                                     'fontname' => FONT,
 93:                                     'color' => 'black',
 94:                                     'fontsize' => 8)
 95:           draw_module(mod, graph, true)
 96:           mod.diagram = convert_to_png("m_#{file_count}_#{count}", 
 97:                                        graph) 
 98:         end
 99:       end
100:       $stderr.puts unless @options.quiet
101:     end

Private Instance methods

[Source]

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

[Source]

     # File lib/rdoc/diagram.rb, line 281
281:     def convert_to_png(file_base, graph)
282:       str = graph.to_s
283:       return @diagram_cache[str] if @diagram_cache[str]
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 << str << "\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:       ret = wrap_in_image_map(src, dot)
304:       @diagram_cache[str] = ret
305:       return ret
306:     end

[Source]

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

[Source]

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

[Source]

     # File lib/rdoc/diagram.rb, line 107
107:     def find_names(mod)
108:       return [mod.full_name] + mod.classes.collect{|cl| cl.full_name} +
109:         mod.modules.collect{|m| find_names(m)}.flatten
110:     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 lib/rdoc/diagram.rb, line 313
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.each do |area|
317:         unless area =~ /^rectangle \((\d+),(\d+)\) \((\d+),(\d+)\) ([\/\w.]+)\s*(.*)/
318:           $stderr.puts "Unexpected output from dot:\n#{area}"
319:           return nil
320:         end
321:         
322:         xs, ys = [$1.to_i, $3.to_i], [$2.to_i, $4.to_i]
323:         url, area_name = $5, $6
324: 
325:         res <<  %{  <area shape="rect" coords="#{xs.min},#{ys.min},#{xs.max},#{ys.max}" }
326:         res <<  %{     href="#{url}" alt="#{area_name}" />\n}
327:       end
328:       res << "</map>\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}">}
332:       return res
333:     end

[Validate]