rubyfca 0.2.2

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.
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
+