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.tar.gz.sig +0 -0
- data/History.txt +36 -0
- data/Manifest.txt +6 -0
- data/README.txt +21 -18
- data/Rakefile +18 -0
- data/bin/graph +6 -1
- data/gallery/cluster.rb +66 -0
- data/gallery/fsm.rb +49 -0
- data/gallery/ruby_exceptions.rb +20 -0
- data/gallery/simple_example.rb +19 -0
- data/gallery/unix.rb +115 -0
- data/lib/dep_analyzer.rb +36 -6
- data/lib/graph.rb +328 -108
- data/lib/homebrew_analyzer.rb +19 -0
- data/lib/rake_analyzer.rb +19 -22
- data/lib/rubygems_analyzer.rb +25 -4
- data/test/test_graph.rb +227 -90
- metadata +41 -27
- metadata.gz.sig +1 -0
data/lib/graph.rb
CHANGED
@@ -1,130 +1,165 @@
|
|
1
1
|
#!/usr/local/bin/ruby -w
|
2
2
|
|
3
3
|
##
|
4
|
-
# Graph
|
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
|
-
|
7
|
-
|
45
|
+
SHAPES.each do |name|
|
46
|
+
define_method(name.downcase) { shape name }
|
47
|
+
end
|
8
48
|
|
9
49
|
##
|
10
|
-
# A
|
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
|
-
|
52
|
+
attr_accessor :graph
|
17
53
|
|
18
54
|
##
|
19
|
-
#
|
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
|
-
|
58
|
+
attr_accessor :name
|
24
59
|
|
25
60
|
##
|
26
|
-
#
|
27
|
-
#
|
28
|
-
# graph.prefix << "ratio = 1.5"
|
61
|
+
# Global attributes for edges in this graph.
|
29
62
|
|
30
|
-
attr_reader :
|
63
|
+
attr_reader :edge_attribs
|
31
64
|
|
32
65
|
##
|
33
|
-
#
|
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 :
|
68
|
+
attr_reader :edges
|
40
69
|
|
41
|
-
|
42
|
-
|
43
|
-
super
|
44
|
-
end
|
70
|
+
##
|
71
|
+
# Global attributes for this graph.
|
45
72
|
|
46
|
-
|
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
|
-
#
|
76
|
+
# Global attributes for nodes in this graph.
|
56
77
|
|
57
|
-
|
58
|
-
(keys + values).flatten.uniq
|
59
|
-
end
|
78
|
+
attr_reader :node_attribs
|
60
79
|
|
61
80
|
##
|
62
|
-
#
|
63
|
-
# good for longer text nodes.
|
81
|
+
# The hash of nodes in this graph. Use #[] or #node to create nodes.
|
64
82
|
|
65
|
-
|
66
|
-
|
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
|
-
#
|
111
|
+
# Push a subgraph into the current graph. Sets the subgraph's graph to self.
|
71
112
|
|
72
|
-
def
|
73
|
-
|
74
|
-
|
75
|
-
end
|
113
|
+
def << subgraph
|
114
|
+
subgraphs << subgraph
|
115
|
+
subgraph.graph = self
|
76
116
|
end
|
77
117
|
|
78
118
|
##
|
79
|
-
#
|
119
|
+
# Access a node by name
|
80
120
|
|
81
|
-
def
|
82
|
-
|
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
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
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
|
-
#
|
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
|
105
|
-
|
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
|
-
#
|
140
|
+
# Shortcut method to create a new colorscheme Attribute instance.
|
114
141
|
|
115
|
-
def
|
116
|
-
|
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
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
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
|
-
|
136
|
-
|
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
|
-
#
|
179
|
+
# Shortcut method to create a new fillcolor Attribute instance.
|
143
180
|
|
144
|
-
def
|
145
|
-
|
181
|
+
def fillcolor n
|
182
|
+
Attribute.new "fillcolor = #{n}"
|
146
183
|
end
|
147
184
|
|
148
185
|
##
|
149
|
-
#
|
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
|
152
|
-
|
189
|
+
def font name, size=nil
|
190
|
+
Attribute.new "fontname = #{name.inspect}" +
|
191
|
+
(size ? ", fontsize = #{size}" : "")
|
153
192
|
end
|
154
193
|
|
155
194
|
##
|
156
|
-
#
|
195
|
+
# Shortcut method to set the graph's label. Usually used with subgraphs.
|
157
196
|
|
158
|
-
def
|
159
|
-
|
197
|
+
def label name
|
198
|
+
graph_attribs << "label = \"#{name}\""
|
160
199
|
end
|
161
200
|
|
162
201
|
##
|
163
|
-
#
|
202
|
+
# Access a node by name, supplying an optional label
|
164
203
|
|
165
|
-
def
|
166
|
-
|
167
|
-
|
168
|
-
|
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=
|
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
|
-
|
263
|
+
|
264
|
+
type = graph ? "subgraph" : "digraph"
|
265
|
+
result << "#{type} #{name}"
|
188
266
|
result << " {"
|
189
267
|
|
190
|
-
|
268
|
+
graph_attribs.each do |line|
|
191
269
|
result << " #{line};"
|
192
270
|
end
|
193
271
|
|
194
|
-
|
195
|
-
result << "
|
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
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
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
|