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