graph 1.2.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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