Class RDoc::Diagram
In: diagram.rb
doc-tmp/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

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 37
37:     def initialize(info, options)
38:       @info = info
39:       @options = options
40:       @counter = 0
41:       FileUtils.mkdir_p(DOT_PATH)
42:       @diagram_cache = {}
43:       @html_suffix = ".html"
44:       if @options.mathml
45:         @html_suffix = ".xhtml"
46:       end
47:     end

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

[Source]

    # File doc-tmp/rdoc/diagram.rb, line 37
37:     def initialize(info, options)
38:       @info = info
39:       @options = options
40:       @counter = 0
41:       FileUtils.mkdir_p(DOT_PATH)
42:       @diagram_cache = {}
43:       @html_suffix = ".html"
44:       if @options.mathml
45:         @html_suffix = ".xhtml"
46:       end
47:     end

Public Instance methods

[Source]

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

[Source]

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

[Source]

     # File doc-tmp/rdoc/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.split($/).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

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 doc-tmp/rdoc/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.split($/).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]