graph 1.2.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/graph.rb CHANGED
@@ -1,130 +1,165 @@
1
1
  #!/usr/local/bin/ruby -w
2
2
 
3
3
  ##
4
- # Graph is a type of hash that outputs in graphviz's dot format.
4
+ # Graph models directed graphs and subgraphs and outputs in graphviz's
5
+ # dot format.
6
+
7
+ class Graph
8
+ VERSION = "2.0.0" # :nodoc:
9
+
10
+ LIGHT_COLORS = %w(gray lightblue lightcyan lightgray lightpink
11
+ lightslategray lightsteelblue white)
12
+
13
+ # WTF -- can't be %w() because of a bug in rcov
14
+ BOLD_COLORS = ["black", "brown", "mediumblue", "blueviolet",
15
+ "orange", "magenta", "darkgreen", "maroon",
16
+ "violetred", "purple", "greenyellow", "deeppink",
17
+ "midnightblue", "firebrick", "darkturquoise",
18
+ "mediumspringgreen", "chartreuse", "navy",
19
+ "lightseagreen", "chocolate", "lawngreen", "green",
20
+ "indigo", "darkgoldenrod", "darkviolet", "red",
21
+ "springgreen", "saddlebrown", "mediumvioletred",
22
+ "goldenrod", "tomato", "cyan", "forestgreen",
23
+ "darkorchid", "crimson", "coral", "deepskyblue",
24
+ "seagreen", "peru", "turquoise", "orangered",
25
+ "dodgerblue", "sienna", "limegreen", "royalblue",
26
+ "darkorange", "blue"]
27
+
28
+ SHAPES = %w(Mcircle Mdiamond Msquare box box3d circle component
29
+ diamond doublecircle doubleoctagon egg ellipse folder
30
+ hexagon house invhouse invtrapezium invtriangle none
31
+ note octagon parallelogram pentagon plaintext point
32
+ polygon rect rectangle septagon square tab trapezium
33
+ triangle tripleoctagon)
34
+
35
+ STYLES = %w(dashed dotted solid invis bold filled diagonals rounded)
36
+
37
+ STYLES.each do |name|
38
+ define_method(name) { style name }
39
+ end
40
+
41
+ (BOLD_COLORS + LIGHT_COLORS).each do |name|
42
+ define_method(name) { color name }
43
+ end
5
44
 
6
- class Graph < Hash
7
- VERSION = '1.2.0' # :nodoc:
45
+ SHAPES.each do |name|
46
+ define_method(name.downcase) { shape name }
47
+ end
8
48
 
9
49
  ##
10
- # A Hash of arrays of attributes for each node. Eg:
11
- #
12
- # graph.attribs["a"] << "color = red"
13
- #
14
- # Will color node "a" red.
50
+ # A parent graph, if any. Only used for subgraphs.
15
51
 
16
- attr_reader :attribs
52
+ attr_accessor :graph
17
53
 
18
54
  ##
19
- # An array of the order of traversal / definition of the nodes.
20
- #
21
- # You (generally) should leave this alone.
55
+ # The name of the graph. Optional for graphs and subgraphs. Prefix
56
+ # the name of a subgraph with "cluster" for subgraph that is boxed.
22
57
 
23
- attr_reader :order
58
+ attr_accessor :name
24
59
 
25
60
  ##
26
- # An array of attributes to add to the front of the graph source. Eg:
27
- #
28
- # graph.prefix << "ratio = 1.5"
61
+ # Global attributes for edges in this graph.
29
62
 
30
- attr_reader :prefix
63
+ attr_reader :edge_attribs
31
64
 
32
65
  ##
33
- # A Hash of a Hashes of Arrays of attributes for an edge. Eg:
34
- #
35
- # graph.edge["a"]["b"] << "color = blue"
36
- #
37
- # Will color the edge between a and b blue.
66
+ # The hash of hashes of edges in this graph. Use #[] or #node to create edges.
38
67
 
39
- attr_reader :edge
68
+ attr_reader :edges
40
69
 
41
- def []= key, val # :nodoc:
42
- @order << key unless self.has_key? key
43
- super
44
- end
70
+ ##
71
+ # Global attributes for this graph.
45
72
 
46
- def clear # :nodoc:
47
- super
48
- @prefix.clear
49
- @order.clear
50
- @attribs.clear
51
- @edge.clear
52
- end
73
+ attr_reader :graph_attribs
53
74
 
54
75
  ##
55
- # Return all nodes (aka the set of all keys and values).
76
+ # Global attributes for nodes in this graph.
56
77
 
57
- def nodes
58
- (keys + values).flatten.uniq
59
- end
78
+ attr_reader :node_attribs
60
79
 
61
80
  ##
62
- # A convenience method to set every node to be a box. Especially
63
- # good for longer text nodes.
81
+ # The hash of nodes in this graph. Use #[] or #node to create nodes.
64
82
 
65
- def boxes
66
- prefix << "node [ shape = box ]"
83
+ attr_reader :nodes
84
+
85
+ ##
86
+ # An array of subgraphs.
87
+
88
+ attr_reader :subgraphs
89
+
90
+ ##
91
+ # Creates a new graph object. Optional name and parent graph are
92
+ # available. Also takes an optional block for DSL-like use.
93
+
94
+ def initialize name = nil, graph = nil, &block
95
+ @name = name
96
+ @graph = graph
97
+ graph << self if graph
98
+ @nodes = Hash.new { |h,k| h[k] = Node.new self, k }
99
+ @edges = Hash.new { |h,k|
100
+ h[k] = Hash.new { |h2, k2| h2[k2] = Edge.new self, self[k], self[k2] }
101
+ }
102
+ @graph_attribs = []
103
+ @node_attribs = []
104
+ @edge_attribs = []
105
+ @subgraphs = []
106
+
107
+ instance_eval(&block) if block
67
108
  end
68
109
 
69
110
  ##
70
- # A convenience method to set an attribute on all nodes.
111
+ # Push a subgraph into the current graph. Sets the subgraph's graph to self.
71
112
 
72
- def global_attrib attrib
73
- nodes.each do |key|
74
- attribs[key] << attrib
75
- end
113
+ def << subgraph
114
+ subgraphs << subgraph
115
+ subgraph.graph = self
76
116
  end
77
117
 
78
118
  ##
79
- # Returns a Hash with a count of the outgoing edges for each node.
119
+ # Access a node by name
80
120
 
81
- def counts
82
- result = Hash.new 0
83
- each_pair do |from, to|
84
- result[from] += 1
85
- end
86
- result
121
+ def [] name
122
+ nodes[name]
87
123
  end
88
124
 
89
- def delete key # :nodoc:
90
- @order.delete key
91
- # TODO: prolly needs to go clean up attribs
92
- super
125
+ ##
126
+ # A convenience method to set the global node attributes to use boxes.
127
+
128
+ def boxes
129
+ node_attribs << shape("box")
93
130
  end
94
131
 
95
132
  ##
96
- # Overrides Hash#each_pair to go over each _edge_. Eg:
97
- #
98
- # g["a"] << "b"
99
- # g["a"] << "b"
100
- # g.each_pair { |from, to| ... }
101
- #
102
- # goes over a -> b *twice*.
133
+ # Shortcut method to create a new color Attribute instance.
103
134
 
104
- def each_pair
105
- @order.each do |from|
106
- self[from].each do |to|
107
- yield from, to
108
- end
109
- end
135
+ def color color
136
+ Attribute.new "color = #{color}"
110
137
  end
111
138
 
112
139
  ##
113
- # Deletes nodes that have less than minimum number of outgoing edges.
140
+ # Shortcut method to create a new colorscheme Attribute instance.
114
141
 
115
- def filter_size minimum
116
- counts.each do |node, count|
117
- next unless count < minimum
118
- delete node
119
- end
142
+ def colorscheme name
143
+ Attribute.new "colorscheme = #{name}"
120
144
  end
121
145
 
122
- def initialize # :nodoc:
123
- super { |h,k| h[k] = [] }
124
- @prefix = []
125
- @order = []
126
- @attribs = Hash.new { |h,k| h[k] = [] }
127
- @edge = Hash.new { |h,k| h[k] = Hash.new { |h2,k2| h2[k2] = [] } }
146
+ ##
147
+ # Define one or more edges.
148
+ #
149
+ # edge "a", "b", "c", ...
150
+ #
151
+ # is equivalent to:
152
+ #
153
+ # edge "a", "b"
154
+ # edge "b", "c"
155
+ # ...
156
+
157
+ def edge(*names)
158
+ last = nil
159
+ names.each_cons(2) do |from, to|
160
+ last = self[from][to]
161
+ end
162
+ last
128
163
  end
129
164
 
130
165
  ##
@@ -132,76 +167,261 @@ class Graph < Hash
132
167
 
133
168
  def invert
134
169
  result = self.class.new
135
- each_pair do |from, to|
136
- result[to] << from
170
+ edges.each do |from, h|
171
+ h.each do |to, edge|
172
+ result[to][from]
173
+ end
137
174
  end
138
175
  result
139
176
  end
140
177
 
141
178
  ##
142
- # Returns a list of keys sorted by number of outgoing edges.
179
+ # Shortcut method to create a new fillcolor Attribute instance.
143
180
 
144
- def keys_by_count
145
- counts.sort_by { |key, count| -count }.map {|key, count| key }
181
+ def fillcolor n
182
+ Attribute.new "fillcolor = #{n}"
146
183
  end
147
184
 
148
185
  ##
149
- # Specify the orientation of the graph. Defaults to the graphviz default "TB".
186
+ # Shortcut method to create a new font Attribute instance. You can
187
+ # pass in both the name and an optional font size.
150
188
 
151
- def orient dir = "TB"
152
- prefix << "rankdir = #{dir}"
189
+ def font name, size=nil
190
+ Attribute.new "fontname = #{name.inspect}" +
191
+ (size ? ", fontsize = #{size}" : "")
153
192
  end
154
193
 
155
194
  ##
156
- # Really just an alias for #orient but with "LR" as the default value.
195
+ # Shortcut method to set the graph's label. Usually used with subgraphs.
157
196
 
158
- def rotate dir = "LR"
159
- orient dir
197
+ def label name
198
+ graph_attribs << "label = \"#{name}\""
160
199
  end
161
200
 
162
201
  ##
163
- # Remove all duplicate edges.
202
+ # Access a node by name, supplying an optional label
164
203
 
165
- def normalize
166
- each do |k,v|
167
- v.uniq!
168
- end
204
+ def node name, label = nil
205
+ n = nodes[name]
206
+ n.label label if label
207
+ n
208
+ end
209
+
210
+ ##
211
+ # Shortcut method to specify the orientation of the graph. Defaults
212
+ # to the graphviz default "TB".
213
+
214
+ def orient dir = "TB"
215
+ graph_attribs << "rankdir = #{dir}"
216
+ end
217
+
218
+ ##
219
+ # Shortcut method to specify the orientation of the graph. Defaults to "LR".
220
+
221
+ def rotate dir = "LR"
222
+ orient dir
169
223
  end
170
224
 
171
225
  ##
172
226
  # Saves out both a dot file to path and an image for the specified type.
173
227
  # Specify type as nil to skip exporting an image.
174
228
 
175
- def save path, type="png"
229
+ def save path, type = nil
176
230
  File.open "#{path}.dot", "w" do |f|
177
231
  f.write self.to_s
178
232
  end
179
233
  system "dot -T#{type} #{path}.dot > #{path}.#{type}" if type
180
234
  end
181
235
 
236
+ ##
237
+ # Shortcut method to create a new shape Attribute instance.
238
+
239
+ def shape shape
240
+ Attribute.new "shape = #{shape}"
241
+ end
242
+
243
+ ##
244
+ # Shortcut method to create a new style Attribute instance.
245
+
246
+ def style name
247
+ Attribute.new "style = #{name}"
248
+ end
249
+
250
+ ##
251
+ # Shortcut method to create a subgraph in the current graph. Use
252
+ # with the top-level +digraph+ method in block form for a graph DSL.
253
+
254
+ def subgraph name = nil, &block
255
+ Graph.new name, self, &block
256
+ end
257
+
182
258
  ##
183
259
  # Outputs a graphviz graph.
184
260
 
185
261
  def to_s
186
262
  result = []
187
- result << "digraph absent"
263
+
264
+ type = graph ? "subgraph" : "digraph"
265
+ result << "#{type} #{name}"
188
266
  result << " {"
189
267
 
190
- @prefix.each do |line|
268
+ graph_attribs.each do |line|
191
269
  result << " #{line};"
192
270
  end
193
271
 
194
- @attribs.sort.each do |node, attribs|
195
- result << " #{node.inspect} [ #{attribs.join ','} ];"
272
+ unless node_attribs.empty? then
273
+ result << " node [ #{node_attribs.join(", ")} ];"
274
+ end
275
+
276
+ unless edge_attribs.empty? then
277
+ result << " edge [ #{edge_attribs.join(", ")} ];"
196
278
  end
197
279
 
198
- each_pair do |from, to|
199
- edge = @edge[from][to].join ", "
200
- edge = " [ #{edge} ]" unless edge.empty?
201
- result << " #{from.inspect} -> #{to.inspect}#{edge};"
280
+ subgraphs.each do |line|
281
+ result << " #{line};"
282
+ end
283
+
284
+ nodes.each do |name, node|
285
+ result << " #{node};" if graph or not node.attributes.empty?
286
+ end
287
+
288
+ edges.each do |from, deps|
289
+ deps.each do |to, edge|
290
+ result << " #{edge};"
291
+ end
202
292
  end
203
293
 
204
294
  result << " }"
205
295
  result.join "\n"
206
296
  end
297
+
298
+ ##
299
+ # An attribute for a graph, node, or edge. Really just a composable
300
+ # string (via #+) with a convenience method #<< that allows you to
301
+ # "paint" nodes and edges with this attribute.
302
+
303
+ class Attribute < Struct.new :attr
304
+ ##
305
+ # "Paint" graphs, nodes, and edges with this attribute.
306
+ #
307
+ # red << node1 << node2 << node3
308
+ #
309
+ # is the same as:
310
+ #
311
+ # node1.attributes << red
312
+ # node2.attributes << red
313
+ # node3.attributes << red
314
+
315
+ def << thing
316
+ thing.attributes << self
317
+ self
318
+ end
319
+
320
+ ##
321
+ # Returns the attribute in string form.
322
+
323
+ alias :to_s :attr
324
+
325
+ ##
326
+ # Compose a new attribute from two existing attributes:
327
+ #
328
+ # bad_nodes = red + filled + diamond
329
+
330
+ def + style
331
+ Attribute.new "#{self}, #{style}"
332
+ end
333
+ end
334
+
335
+ ##
336
+ # An edge in a graph.
337
+
338
+ class Edge < Struct.new :graph, :from, :to, :attributes
339
+
340
+ ##
341
+ # Create a new edge in +graph+ from +from+ to +to+.
342
+
343
+ def initialize graph, from, to
344
+ super graph, from, to, []
345
+ end
346
+
347
+ ##
348
+ # Shortcut method to set the label attribute on an edge.
349
+
350
+ def label name
351
+ attributes << "label = \"#{name}\""
352
+ self
353
+ end
354
+
355
+ ##
356
+ # Returns the edge in dot syntax.
357
+
358
+ def to_s
359
+ fromto = "%p -> %p" % [from.name, to.name]
360
+ if attributes.empty? then
361
+ fromto
362
+ else
363
+ "%-20s [ %-20s ]" % [fromto, attributes.join(',')]
364
+ end
365
+ end
366
+ end
367
+
368
+ ##
369
+ # Nodes in the graph.
370
+
371
+ class Node < Struct.new :graph, :name, :attributes
372
+
373
+ ##
374
+ # Create a new Node. Takes a parent graph and a name.
375
+
376
+ def initialize graph, name
377
+ super graph, name, []
378
+ end
379
+
380
+ ##
381
+ # Shortcut method to set the node's label.
382
+
383
+ def label name
384
+ attributes << "label = #{name.inspect}"
385
+ end
386
+
387
+ ##
388
+ # Create a new node with +name+ and an edge between them pointing
389
+ # from self to the new node.
390
+
391
+ def >> name
392
+ self[name] # creates node and edge
393
+ self
394
+ end
395
+
396
+ alias :"<<" :">>"
397
+
398
+ ##
399
+ # Returns the edge between self and +dep_name+.
400
+
401
+ def [] dep_name
402
+ graph.edges[name][dep_name]
403
+ end
404
+
405
+ ##
406
+ # Returns the node in dot syntax.
407
+
408
+ def to_s
409
+ if attributes.empty? then
410
+ "#{name.inspect}"
411
+ else
412
+ "%-20p [ %-20s ]" % [name, attributes.join(',')]
413
+ end
414
+ end
415
+ end
416
+ end
417
+
418
+ ##
419
+ # Convenience method to create a new graph. Used for DSL-style:
420
+ #
421
+ # g = digraph do
422
+ # edge "a", "b", "c"
423
+ # end
424
+
425
+ def digraph name = nil, &block
426
+ Graph.new name, &block
207
427
  end