| 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.
Pass in the set of top level objects. The method also creates the subdirectory to hold the images
# 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
# 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
# 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
# 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
# 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
# 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.
# 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.
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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
# 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