rubyfca 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 1a7c7bc3a45c296b6a2a7447106284ff7cef7871
4
+ data.tar.gz: d5da0d7b0dffe7d8b33dd662133222bbe0720c10
5
+ SHA512:
6
+ metadata.gz: 99a30f945baf45ec345c9ed614b9de725a743784f09136434117db65d6e2a7bef10ccf3cd9131eb28d1d5364efa9bf8d464e9ff939bbe789ef81a0ea2b2b109f
7
+ data.tar.gz: 4642dbe52edc0c57d17f3d8401f8531a29b2019327180114d9d7e38a375d76f858d6801e359036151bc88cdffe7186a17ea55a84754d64b41919afa747c1adeb
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,21 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ .DS_Store
19
+ *.bak
20
+ *.~
21
+ *.log
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+ # gem "trollop"
3
+ # Specify your gem's dependencies in ..gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Yoichiro Hasebe
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,44 @@
1
+ = RubyFCA
2
+
3
+ Command line tool for Formal Concept Analysis (FCA) written in Ruby.
4
+
5
+ == Features
6
+
7
+ * Convert a Conexp's CXT or CSV file and generate a Graphviz DOT file, or a PNG/JPG/EPS image file.
8
+ * Adopt the Ganter algorithm (through its Perl implementation of Fcastone by Uta Priss).
9
+
10
+ == Installation
11
+
12
+ Install the gem:
13
+
14
+ $sudo gem install rubyfca --source http://gemcutter.org
15
+
16
+ == How to Use
17
+
18
+ rubyfca [options] <source file> <output file>
19
+
20
+ where:
21
+ <source file>
22
+ "foo.cxt", "foo.csv"
23
+ <output file>
24
+ "bar.dot", "bar.png", "bar.jpg", or "bar.eps"
25
+ [options]:
26
+ --box, -b: Use box shaped concept nodes
27
+ --full, -f: Do not contract concept labels
28
+ --legend, -l: Print the legend of concept nodes (disabled when using circle node shape) (default: true)
29
+ --coloring, -c: Color concept nodes (default: true)
30
+ --straight, -s: Straighten edges (available when output format is either png, jpg, or eps)
31
+ --help, -h: Show this message
32
+
33
+
34
+ == ToDo
35
+
36
+ * Database connection capability
37
+
38
+ == Links
39
+
40
+ under construction
41
+
42
+ == Copyright
43
+
44
+ Copyright (c) 2009-2012 Yoichiro Hasebe and Kow Kuroda. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
data/bin/rubyfca ADDED
@@ -0,0 +1,108 @@
1
+ #!/usr/bin/env ruby
2
+ # -*- coding: utf-8 -*-
3
+ Encoding.default_external = "UTF-8"
4
+
5
+
6
+ $:.unshift File.join(File.dirname(__FILE__), '..', 'lib')
7
+ require 'rubyfca'
8
+
9
+ ################ parse options ##########
10
+
11
+ opts = Trollop::options do
12
+ version RubyFCA::VERSION
13
+ banner <<-EOS
14
+
15
+ RubuFCA converts Conexp CXT data to Graphviz dot format.
16
+
17
+ Usage:
18
+ rubyfca [options] <source file> <output file>
19
+
20
+ where:
21
+ <source file>
22
+ ".cxt", ".csv"
23
+ <output file>
24
+ ."dot", ".png", ".jpg", or ".eps"
25
+ [options]:
26
+ EOS
27
+
28
+ opt :full, "Do not contract concept labels", :default=> false
29
+ opt :coloring, "Color concept nodes [0 = none (default), 1 = lightblue/pink, 2 = monochrome]", :default => 0
30
+ opt :straight, "Straighten edges (available when output format is either png, jpg, svg, pdf, or eps)", :default => false
31
+ opt :nodesep, "Size of separation between sister nodes (from 0.1 to 5.0)", :default => 0.4
32
+ opt :ranksep, "Size of separation between ranks (from 0.1 to 5.0)", :default => 0.2
33
+ opt :legend, "Print the legend of concept nodes (available only when using circle node shape)", :default => false
34
+ opt :circle, "Use circle shaped concept nodes", :default=> false
35
+
36
+ end
37
+ Trollop::die :coloring, "must be 0, 1, or 2" if (opts[:coloring] > 2 || opts[:coloring] < 0)
38
+ Trollop::die :ranksep, "must be within 0.1 - 5.0" if (opts[:ranksep] < 0.1 || opts[:ranksep] > 5.0)
39
+ Trollop::die :nodesep, "must be within 0.1 - 5.0" if (opts[:nodesep] < 0.1 || opts[:nodesep] > 5.0)
40
+ ############### main program ###############
41
+
42
+ if ARGV.size != 2
43
+ showerror("Input and output files are not set properly", 1)
44
+ end
45
+
46
+ filename1 = ARGV[0] #input filename
47
+ filename2 = ARGV[1] #output filename
48
+
49
+ #
50
+ # extract input and output file types
51
+ #
52
+ input_type = filename1.slice(/\.[^\.]+\z/).split(//)[1..-1].join("")
53
+ output_type = filename2.slice(/\.[^\.]+\z/).split(//)[1..-1].join("")
54
+
55
+ if (input_type !~ /\A(cxt|csv)\z/ || output_type !~ /\A(dot|png|jpg|svg|pdf|eps)\z/)
56
+ showerror("These file extensions are not (yet) supported.", 1)
57
+ end
58
+
59
+ #
60
+ # input data is kept as plain text
61
+ #
62
+ f = File.open(filename1, "r:UTF-8:UTF-8")
63
+
64
+ inputdata = f.read
65
+ inputdata.gsub!(/\r\n?/){"\n"}
66
+ f.close
67
+
68
+ #
69
+ # ask for confirmation of overwriting an exisiting file
70
+ #
71
+ if (File.exist?(filename2) && !opts[:sil])
72
+ print "#{filename2} exists and will be overwritten, OK? [y/n]"
73
+ var1 = STDIN.gets;
74
+ if /y/i !~ var1
75
+ exit;
76
+ end
77
+ end
78
+
79
+ #
80
+ # context data is converted to a hash table
81
+ #
82
+ begin
83
+ ctxt = FormalContext.new(inputdata, input_type, !opts[:full])
84
+ ctxt.calcurate
85
+ # rescue => e
86
+ # puts e
87
+ # showerror("Source data may have problems. Process aborted.", 1)
88
+ end
89
+
90
+ #
91
+ # create the output file
92
+ #
93
+ case output_type
94
+ when "dot"
95
+ File.open(filename2, "w") do |f|
96
+ f.write(ctxt.generate_dot(opts))
97
+ end
98
+ when "png"
99
+ ctxt.generate_img(filename2, "png", opts)
100
+ when "jpg"
101
+ ctxt.generate_img(filename2, "jpg", opts)
102
+ when "svg"
103
+ ctxt.generate_img(filename2, "svg", opts)
104
+ when "pdf"
105
+ ctxt.generate_img(filename2, "pdf", opts)
106
+ when "eps"
107
+ ctxt.generate_img(filename2, "eps", opts)
108
+ end
data/lib/rubyfca.rb ADDED
@@ -0,0 +1,467 @@
1
+ #!/usr/bin/env ruby
2
+ # -*- coding: utf-8 -*-
3
+
4
+ ## lib/rubyfca.rb -- Formal Concept Analysis tool in Ruby
5
+ ## Author:: Yoichiro Hasebe (mailto: yohasebe@gmail.com)
6
+ ## Kow Kuroda (mailto: kuroda@nict.go.jp)
7
+ ## Copyright:: Copyright 2009 Yoichiro Hasebe and Kow Kuroda
8
+ ## License:: GNU GPL version 3
9
+
10
+ $: << File.dirname(__FILE__) + "/rubyfca"
11
+
12
+ require 'csv'
13
+ require 'ruby_graphviz'
14
+ require 'version'
15
+ require 'trollop'
16
+
17
+ private
18
+
19
+ ## Take two arrays each consisting of 0s and 1s and create their logical disjunction
20
+ def create_and_ary(a, b)
21
+ return false if (s = a.size) != b.size
22
+ result = (0..(s-1)).to_a.map{|i| a[i].to_i & b[i].to_i}
23
+ return result
24
+ end
25
+
26
+ ## Take two arrays each consisting of 0s and 1s and create their logical conjunction
27
+ def create_or_ary(a, b)
28
+ return false if (s = a.size) != b.size
29
+ result = (0..(s-1)).to_a.map{|i| a[i].to_i | b[i].to_i}
30
+ return result
31
+ end
32
+
33
+ public
34
+
35
+ ## Take care of errors
36
+ def showerror(sentence, severity)
37
+ if severity == 0
38
+ puts "Warning: #{sentence} The output may not be meaningful."
39
+ elsif severity == 1
40
+ puts "Error: #{sentence} No output generated."
41
+ exit
42
+ end
43
+ end
44
+
45
+ ## Basic structure of the code is the same as Fcastone written in Perl by Uta Priss
46
+ class FormalContext
47
+
48
+ ## Converte cxt data to three basic structures of objects, attributes, and matrix
49
+ def initialize(input, mode, label_contraction = false)
50
+ if input.size == 0
51
+ showerror("File is empty", 1)
52
+ end
53
+ input.gsub!(" ", "&nbsp;")
54
+ # begin
55
+ case mode
56
+ when /cxt\z/
57
+ read_cxt(input)
58
+ when /csv\z/
59
+ read_csv(input)
60
+ end
61
+ # rescue => e
62
+ # showerror("Input data contains a syntax problem.", 1)
63
+ # end
64
+ @label_contraction = label_contraction
65
+ end
66
+
67
+ ## process cxt data
68
+ def read_cxt(input)
69
+ lines = input.split
70
+ t1 = 3
71
+ if (lines[0] !~ /B/i || (2 * lines[1].to_i + lines[2].to_i + t1) != lines.size)
72
+ showerror("Wrong cxt format!", 1)
73
+ end
74
+ @objects = lines[t1..(lines[1].to_i + t1 - 1)]
75
+ @attributes = lines[(lines[1].to_i + t1) .. (lines[1].to_i + lines[2].to_i + t1 - 1)]
76
+ lines = lines[(lines[1].to_i + lines[2].to_i + t1) .. lines.size]
77
+ @matrix = changecrosssymbol("X", "\\.", lines)
78
+ end
79
+
80
+ # process csv data using the standard csv library
81
+ def read_csv(input)
82
+ input = remove_blank(input)
83
+ data = CSV.parse(input)
84
+ @objects = trim_ary(data.transpose.first[1..-1])
85
+ @attributes = trim_ary(data.first[1..-1])
86
+ @matrix = []
87
+ data[1..-1].each do |line|
88
+ @matrix << line[1..-1].collect { |cell| /x/i =~ cell ? 1 : 0 }
89
+ end
90
+ end
91
+
92
+ def remove_blank(input)
93
+ blank_removed = ""
94
+ input.split("\n").each do |line|
95
+ line = line.strip
96
+ unless /\A\s*\z/ =~ line
97
+ blank_removed << line + "\n"
98
+ end
99
+ end
100
+ blank_removed
101
+ end
102
+
103
+ def trim_ary(ary)
104
+ newary = ary.collect do |cell|
105
+ cell.strip
106
+ end
107
+ newary
108
+ end
109
+
110
+ ## Apply a formal concept analysis on the matrix
111
+ def calcurate
112
+ @concepts, @extM, @intM = ganter_alg(@matrix)
113
+ @relM, @reltrans, @rank = create_rel(@intM)
114
+ @gammaM, @muM = gammaMu(@extM, @intM, @matrix)
115
+ end
116
+
117
+ ## This is an implementation of an algorithm described by Bernhard Ganter
118
+ ## in "Two basic algorithms in concept analysis." Technische Hochschule
119
+ ## Darmstadt, FB4-Preprint, 831, 1984.
120
+ def ganter_alg(matrix)
121
+
122
+ m = matrix
123
+
124
+ ## all arrays except @idx are arrays of arrays of 0's and 1's
125
+ idx = []
126
+ extension = []
127
+ intension = []
128
+ ext_A = []
129
+ int_B = []
130
+ endkey = []
131
+ temp = []
132
+
133
+ ## the level in the lattice from the top
134
+ lvl = 0
135
+ ## is lower than the index of leftmost attr
136
+ idx[lvl] = -1
137
+ ## number of attr. and objs
138
+ anzM = m.size
139
+ ## only needed for initialization
140
+ anzG = m[0].size
141
+ ## initialize extA[0] = [1,...,1]
142
+ anzG.times do |i|
143
+ if !ext_A[0]
144
+ ext_A[0] = []
145
+ end
146
+ ext_A[0] << 1
147
+ end
148
+ ## initialize extB[0] = [0,...,0]
149
+ anzM.times do |i|
150
+ if !int_B[0]
151
+ int_B[0] = []
152
+ end
153
+ int_B[0] << 0
154
+ end
155
+ anzCpt = 0
156
+ extension[0] = ext_A[0]
157
+ intension[0] = int_B[0]
158
+ anzM.times do |i|
159
+ endkey << 1
160
+ end
161
+
162
+ ## start of algorithm
163
+ while int_B[lvl] != endkey
164
+ (anzM - 1).downto(0) do |i|
165
+ breakkey = false
166
+ if (int_B[lvl][i] != 1)
167
+ while (i < idx[lvl])
168
+ lvl -= 1
169
+ end
170
+ idx[lvl + 1] = i
171
+ ext_A[lvl + 1] = create_and_ary(ext_A[lvl], m[i])
172
+ 0.upto(i - 1) do |j|
173
+ if(!breakkey && int_B[lvl][j] != 1)
174
+ temp = create_and_ary(ext_A[lvl + 1], m[j])
175
+ if temp == ext_A[lvl + 1]
176
+ breakkey = true
177
+ end
178
+ end
179
+ end
180
+ unless breakkey
181
+ int_B[lvl + 1] = int_B[lvl].dup
182
+ int_B[lvl + 1][i] = 1
183
+ (i+1).upto(anzM - 1) do |k|
184
+ if int_B[lvl + 1][k] != 1
185
+ temp = create_and_ary(ext_A[lvl + 1], m[k])
186
+ if temp == ext_A[lvl + 1]
187
+ int_B[lvl + 1][k] = 1
188
+ end
189
+ end
190
+ end
191
+ lvl += 1
192
+ anzCpt += 1
193
+ extension[anzCpt] = ext_A[lvl]
194
+ intension[anzCpt] = int_B[lvl]
195
+ break
196
+ end
197
+ end
198
+ end
199
+ end
200
+
201
+ a1 = extension[0].join("")
202
+ a2 = extension[1].join("")
203
+ if a1 == a2
204
+ extension.shift
205
+ intension.shift
206
+ anzCpt -= 1
207
+ end
208
+
209
+ c = []
210
+ 0.upto(anzCpt) do |i|
211
+ c[i] = i
212
+ end
213
+
214
+ [c, intension, extension]
215
+ end
216
+
217
+ ## Output arrayconsists of the following:
218
+ ## r (subconcept superconcept relation)
219
+ ## rt (trans. closure of r)
220
+ ## s (ranked concepts)
221
+ def create_rel(intensions)
222
+ anzCpt = intensions.size
223
+ rank = []
224
+ sup_con = []
225
+ r = []
226
+ rt = []
227
+ s = []
228
+
229
+ 0.upto(anzCpt - 1) do |i|
230
+ 0.upto(anzCpt - 1) do |j|
231
+ unless r[i]
232
+ r[i] = []
233
+ end
234
+ r[i][j] = 0;
235
+ unless rt[i]
236
+ rt[i] = []
237
+ end
238
+ rt[i][j] = 0;
239
+ end
240
+ end
241
+
242
+ 1.upto(anzCpt - 1) do |i|
243
+ rank[i] = 1
244
+ (i - 1).downto(0) do |j|
245
+ temp = create_and_ary(intensions[j], intensions[i])
246
+ if temp == intensions[i]
247
+ unless sup_con[i]
248
+ sup_con[i] = []
249
+ end
250
+ sup_con[i] << j
251
+ r[i][j] = 1
252
+ rt[i][j] = 1
253
+ sup_con[i].each do |elem|
254
+ if r[elem][j] == 1
255
+ r[i][j] = 0
256
+ if rank[elem] >= rank [i]
257
+ rank[i] = rank[elem] + 1
258
+ end
259
+ break
260
+ end
261
+ end
262
+ end
263
+ end
264
+ unless s[rank[i]]
265
+ s[rank[i]] = []
266
+ end
267
+ s[rank[i]] << i
268
+ end
269
+ s = s.collect do |i|
270
+ i ? i : [0]
271
+ end
272
+
273
+ [r, rt, s]
274
+ end
275
+
276
+ def gammaMu(extent, intent, cxt)
277
+
278
+ gamma = []
279
+ mu = []
280
+ invcxt = []
281
+ 0.upto(cxt[0].size - 1) do |i|
282
+ 0.upto(cxt.size - 1) do |k|
283
+ invcxt[i] = [] unless invcxt[i]
284
+ invcxt[i][k] = cxt[k][i]
285
+ end
286
+ end
287
+
288
+ 0.upto(intent.size - 1) do |j|
289
+ 0.upto(cxt.size - 1) do |i|
290
+ gamma[i] = [] unless gamma[i]
291
+ if cxt[i] == intent[j]
292
+ gamma[i][j] = 2
293
+ elsif (!@label_contraction && create_or_ary(cxt[i], intent[j]) == cxt[i])
294
+ gamma[i][j] = 1
295
+ else
296
+ gamma[i][j] = 0
297
+ end
298
+ end
299
+
300
+ 0.upto(invcxt.size - 1) do |i|
301
+ # next unless invcxt[i]
302
+ mu[i] = [] unless mu[i]
303
+ if invcxt[i] == extent[j]
304
+ mu[i][j] = 2
305
+ elsif (!@label_contraction && create_or_ary(invcxt[i], extent[j]) == invcxt[i])
306
+ mu[i][j] = 1
307
+ else
308
+ mu[i][j] = 0
309
+ end
310
+ end
311
+ end
312
+
313
+ [gamma, mu]
314
+ end
315
+
316
+ def changecrosssymbol(char1, char2, lns)
317
+ rel = []
318
+ lns.each do |ln|
319
+ ary = []
320
+ elems = ln.split(//)
321
+ elems.each do |elem|
322
+ if /#{char1}/i =~ elem
323
+ ary << 1
324
+ elsif /#{char2}/i =~ elem
325
+ ary << 0
326
+ end
327
+ end
328
+ rel << ary
329
+ end
330
+ rel
331
+ end
332
+
333
+ ## Generate Graphviz dot data (not creating a file)
334
+ ## For options, see 'rubyfca'
335
+ def generate_dot(opts)
336
+ index_max_width = @concepts.size.to_s.split(//).size
337
+ nodesep = opts[:nodesep] ? opts[:nodesep].to_s : "0.4"
338
+ ranksep = opts[:ranksep] ? opts[:ranksep].to_s : "0.2"
339
+ clattice = RubyGraphviz.new("clattice", :rankdir => "", :nodesep => nodesep, :ranksep => ranksep)
340
+
341
+ if opts[:circle] and opts[:legend]
342
+ legend = RubyGraphviz.new("legend", :rankdir => "TB", :lebelloc => "t", :centered => "false")
343
+ legend.node_default(:shape => "plaintext")
344
+ legend.edge_default(:color => "gray60") if opts[:coloring]
345
+ legends = []
346
+ end
347
+
348
+ if opts[:circle]
349
+ clattice.node_default(:shape => "circle", :style => "filled")
350
+ clattice.edge_default(:dir => "none", :minlen => "2")
351
+ clattice.edge_default(:color => "gray60") if opts[:coloring]
352
+ else
353
+ clattice.node_default(:shape => "record", :margin => "0.2,0.055")
354
+ clattice.edge_default(:dir => "none")
355
+ clattice.edge_default(:color => "gray60") if opts[:coloring]
356
+ end
357
+
358
+ 0.upto(@concepts.size - 1) do |i|
359
+ objfull = []
360
+ attrfull = []
361
+ 0.upto(@gammaM.size - 1) do |j|
362
+ if @gammaM[j][i] == 2
363
+ # pointing finger does not appear correctly in eps...
364
+ # obj = opts[:full] ? @objects[j] + " " + [0x261C].pack("U") : @objects[j]
365
+ obj = opts[:full] ? @objects[j] + "*" : @objects[j]
366
+ objfull << obj
367
+ elsif @gammaM[j][i] == 1
368
+ objfull << @objects[j]
369
+ end
370
+ end
371
+ 0.upto(@muM.size - 1) do |k|
372
+ if @muM[k][i] == 2
373
+ # pointing finger does not appear correctly in eps...
374
+ # att = opts[:full] ? @attributes[k] + " " + [0x261C].pack("U") : @attributes[k]
375
+ att = opts[:full] ? @attributes[k] + "*" : @attributes[k]
376
+ attrfull << att
377
+ elsif @muM[k][i] == 1
378
+ attrfull << @attributes[k]
379
+ end
380
+ end
381
+
382
+ concept_id = i + 1
383
+
384
+ attr_str = attrfull.join('<br />')
385
+ attr_str = attr_str == "" ? " " : attr_str
386
+
387
+ if opts[:coloring] == 0 or /\A\s+\z/ =~ attr_str
388
+ attr_color = "white"
389
+ elsif opts[:coloring] == 1
390
+ attr_color = "lightblue"
391
+ elsif opts[:coloring] == 2
392
+ attr_color = "gray87"
393
+ end
394
+
395
+ obj_str = objfull.join('<br />')
396
+ obj_str = obj_str == "" ? " " : obj_str
397
+
398
+ if opts[:coloring] == 0 or /\A\s+\z/ =~ obj_str
399
+ obj_color = "white"
400
+ elsif opts[:coloring] == 1
401
+ obj_color = "pink"
402
+ elsif opts[:coloring] == 2
403
+ obj_color = "gray92"
404
+ end
405
+
406
+ label = "<<table border=\"0\" cellborder=\"1\" cellspacing=\"0\">" +
407
+ "<tr><td balign=\"left\" align=\"left\" bgcolor=\"#{attr_color}\">#{attr_str}</td></tr>" +
408
+ "<tr><td balign=\"left\" align=\"left\" bgcolor=\"#{obj_color}\">#{obj_str}</td></tr>" +
409
+ "</table>>"
410
+
411
+ if opts[:circle] and opts[:legend]
412
+
413
+ leg = "<<table border=\"0\" cellborder=\"1\" cellspacing=\"0\">" +
414
+ "<tr><td rowspan=\"2\">#{concept_id}</td><td balign=\"left\" align=\"left\" bgcolor=\"#{attr_color}\">#{attr_str}</td></tr>" +
415
+ "<tr><td balign=\"left\" align=\"left\" bgcolor=\"#{obj_color}\">#{obj_str}</td></tr>" +
416
+ "</table>>"
417
+
418
+ if !attrfull.empty? or !objfull.empty?
419
+ legend.node("cl#{concept_id}k", :label => concept_id, :style => "invis")
420
+ legend.node("cl#{concept_id}v", :label => leg, :fillcolor => "white")
421
+ legend.rank("cl#{concept_id}k", "cl#{concept_id}v", :style => "invis", :length => "0.0")
422
+ if legends[-1]
423
+ legend.edge("cl#{legends[-1]}k", "cl#{concept_id}k", :style => "invis", :length => "0.0")
424
+ end
425
+ legends << concept_id
426
+ end
427
+ end
428
+
429
+ if opts[:circle]
430
+ clattice.node("c#{i}", :width => "0.5", :fontsize => "14.0", :label => concept_id)
431
+ else
432
+ clattice.node("c#{i}", :label => label, :shape => "plaintext",
433
+ :height => "0.0", :width => "0.0", :margin => "0.0")
434
+ end
435
+ end
436
+
437
+ 0.upto(@relM.size - 1) do |i|
438
+ 0.upto(@relM.size - 1) do |j|
439
+ if @relM[i][j] == 1
440
+ clattice.edge("c#{i}", "c#{j}")
441
+ end
442
+ end
443
+ end
444
+
445
+ clattice.subgraph(legend) if opts[:circle] and opts[:legend]
446
+ clattice.to_dot
447
+ end
448
+
449
+ ## Generate an actual graphic file (Graphviz dot needs to be installed properly)
450
+ def generate_img(outfile, image_type, opts)
451
+ dot = generate_dot(opts)
452
+ isthere_dot = `dot -V 2>&1`
453
+ if isthere_dot !~ /dot.*version/i
454
+ showerror("Graphviz's dot program cannot be found.", 1)
455
+ else
456
+ if opts[:straight]
457
+ cmd = "dot | neato -n -T#{image_type} -o#{outfile} 2>rubyfca.log"
458
+ else
459
+ cmd = "dot -T#{image_type} -o#{outfile} 2>rubyfca.log"
460
+ end
461
+ IO.popen(cmd, 'r+') do |io|
462
+ io.puts dot
463
+ end
464
+ end
465
+ end
466
+ end
467
+