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