rubyfca 0.2.11 → 0.3.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.
- checksums.yaml +7 -0
- data/.gitignore +6 -1
- data/Gemfile +3 -3
- data/LICENSE +674 -20
- data/README.md +129 -0
- data/Rakefile +11 -0
- data/bin/rubyfca +59 -53
- data/lib/rubyfca/ruby_graphviz.rb +20 -23
- data/lib/rubyfca/version.rb +3 -1
- data/lib/rubyfca.rb +147 -141
- data/rubyfca.gemspec +18 -10
- data/samples/fca-source-result.png +0 -0
- data/samples/numbers-result-compact.png +0 -0
- data/samples/numbers-result.png +0 -0
- data/samples/numbers-source.png +0 -0
- data/samples/sample_01.svg +202 -0
- data/samples/sample_02.svg +178 -0
- data/samples/sample_03.svg +244 -0
- data/samples/xlsx-sample.png +0 -0
- data/test/rubyfca_test.rb +55 -4
- data/test/test_expected/test_result_01.svg +202 -0
- data/test/test_expected/test_result_02.svg +178 -0
- data/test/test_expected/test_result_03.svg +481 -0
- data/test/test_expected/test_result_04.svg +314 -0
- data/test/test_input/test_data.csv +7 -0
- data/test/{test_data.cxt → test_input/test_data.cxt} +1 -1
- data/test/test_input/test_data.xlsx +0 -0
- data/test/test_input/test_data_numbers.xlsx +0 -0
- data/test_data_numbers.svg +272 -0
- data/test_data_numbers_a.svg +272 -0
- metadata +87 -27
- data/README.rdoc +0 -44
- data/lib/rubyfca/trollop.rb +0 -739
- data/rubyfca.log +0 -0
- data/test/test_helper.rb +0 -10
- /data/test/{test_data.csv → test_data_.csv} +0 -0
data/lib/rubyfca.rb
CHANGED
@@ -1,40 +1,39 @@
|
|
1
|
-
|
2
|
-
# -*- coding: utf-8 -*-
|
1
|
+
# frozen_string_literal: true
|
3
2
|
|
4
3
|
## lib/rubyfca.rb -- Formal Concept Analysis tool in Ruby
|
5
4
|
## Author:: Yoichiro Hasebe (mailto: yohasebe@gmail.com)
|
6
|
-
## Kow Kuroda (mailto: kuroda@nict.go.jp)
|
7
|
-
## Copyright:: Copyright 2009 Yoichiro Hasebe and Kow Kuroda
|
5
|
+
## Kow Kuroda (mailto: kuroda@nict.go.jp)
|
6
|
+
## Copyright:: Copyright 2009-2023 Yoichiro Hasebe and Kow Kuroda
|
8
7
|
## License:: GNU GPL version 3
|
9
8
|
|
10
|
-
|
9
|
+
require "csv"
|
10
|
+
require "optimist"
|
11
|
+
require "roo"
|
11
12
|
|
12
|
-
|
13
|
-
|
14
|
-
require 'version'
|
15
|
-
require 'trollop'
|
13
|
+
require_relative "./rubyfca/ruby_graphviz"
|
14
|
+
require_relative "./rubyfca/version"
|
16
15
|
|
17
16
|
private
|
18
17
|
|
19
|
-
## Take two arrays each consisting of 0s and 1s and create their logical disjunction
|
18
|
+
## Take two arrays each consisting of 0s and 1s and create their logical disjunction
|
20
19
|
def create_and_ary(a, b)
|
21
20
|
return false if (s = a.size) != b.size
|
22
|
-
|
23
|
-
|
21
|
+
|
22
|
+
(0..(s - 1)).to_a.map { |i| a[i].to_i & b[i].to_i }
|
24
23
|
end
|
25
24
|
|
26
25
|
## Take two arrays each consisting of 0s and 1s and create their logical conjunction
|
27
26
|
def create_or_ary(a, b)
|
28
27
|
return false if (s = a.size) != b.size
|
29
|
-
|
30
|
-
|
28
|
+
|
29
|
+
(0..(s - 1)).to_a.map { |i| a[i].to_i | b[i].to_i }
|
31
30
|
end
|
32
31
|
|
33
32
|
public
|
34
33
|
|
35
34
|
## Take care of errors
|
36
35
|
def showerror(sentence, severity)
|
37
|
-
if severity
|
36
|
+
if severity.zero?
|
38
37
|
puts "Warning: #{sentence} The output may not be meaningful."
|
39
38
|
elsif severity == 1
|
40
39
|
puts "Error: #{sentence} No output generated."
|
@@ -44,26 +43,27 @@ end
|
|
44
43
|
|
45
44
|
## Basic structure of the code is the same as Fcastone written in Perl by Uta Priss
|
46
45
|
class FormalContext
|
47
|
-
|
48
46
|
## Converte cxt data to three basic structures of objects, attributes, and matrix
|
49
|
-
def initialize(input, mode, label_contraction
|
50
|
-
if input.
|
51
|
-
|
52
|
-
end
|
53
|
-
input.gsub!(" ", " ")
|
47
|
+
def initialize(input, mode, label_contraction: false)
|
48
|
+
showerror("File is empty", 1) if input.empty?
|
49
|
+
input = input.gsub(" ", " ")
|
54
50
|
begin
|
55
51
|
case mode
|
56
52
|
when /cxt\z/
|
57
53
|
read_cxt(input)
|
58
54
|
when /csv\z/
|
59
55
|
read_csv(input)
|
56
|
+
when /xlsx\z/
|
57
|
+
read_xlsx(input)
|
60
58
|
end
|
61
|
-
rescue => e
|
62
|
-
|
59
|
+
rescue StandardError => e
|
60
|
+
pp e.message
|
61
|
+
pp e.backtrace
|
62
|
+
showerror("Input data contains a syntactic problem.", 1)
|
63
63
|
end
|
64
64
|
@label_contraction = label_contraction
|
65
65
|
end
|
66
|
-
|
66
|
+
|
67
67
|
## process cxt data
|
68
68
|
def read_cxt(input)
|
69
69
|
lines = input.split
|
@@ -71,26 +71,41 @@ class FormalContext
|
|
71
71
|
if (lines[0] !~ /B/i || (2 * lines[1].to_i + lines[2].to_i + t1) != lines.size)
|
72
72
|
showerror("Wrong cxt format!", 1)
|
73
73
|
end
|
74
|
-
@objects
|
75
|
-
@attributes = lines[(lines[1].to_i + t1)
|
76
|
-
lines
|
77
|
-
@matrix
|
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
78
|
end
|
79
|
-
|
79
|
+
|
80
80
|
# process csv data using the standard csv library
|
81
81
|
def read_csv(input)
|
82
82
|
input = remove_blank(input)
|
83
83
|
data = CSV.parse(input)
|
84
|
-
@objects = trim_ary(data.transpose.first[1
|
85
|
-
@attributes = trim_ary(data.first[1
|
84
|
+
@objects = trim_ary(data.transpose.first[1..])
|
85
|
+
@attributes = trim_ary(data.first[1..])
|
86
|
+
@matrix = []
|
87
|
+
data[1..].each do |line|
|
88
|
+
@matrix << line[1..].collect { |cell| /x/i =~ cell ? 1 : 0 }
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# process xlsx data using the roo gem
|
93
|
+
def read_xlsx(file_path)
|
94
|
+
xlsx = Roo::Spreadsheet.open(file_path, extension: :xlsx)
|
95
|
+
@objects = xlsx.sheet(0).column(1)[1..]
|
96
|
+
@attributes = xlsx.sheet(0).row(1)[1..]
|
86
97
|
@matrix = []
|
87
|
-
|
88
|
-
|
98
|
+
(2..xlsx.sheet(0).last_row).each do |row|
|
99
|
+
matrix_row = []
|
100
|
+
(2..xlsx.sheet(0).last_column).each do |col|
|
101
|
+
matrix_row << (xlsx.sheet(0).cell(row, col) =~ /x/i ? 1 : 0)
|
102
|
+
end
|
103
|
+
@matrix << matrix_row
|
89
104
|
end
|
90
105
|
end
|
91
|
-
|
106
|
+
|
92
107
|
def remove_blank(input)
|
93
|
-
blank_removed = ""
|
108
|
+
blank_removed = +""
|
94
109
|
input.split("\n").each do |line|
|
95
110
|
line = line.strip
|
96
111
|
unless /\A\s*\z/ =~ line
|
@@ -99,26 +114,22 @@ class FormalContext
|
|
99
114
|
end
|
100
115
|
blank_removed
|
101
116
|
end
|
102
|
-
|
117
|
+
|
103
118
|
def trim_ary(ary)
|
104
|
-
|
105
|
-
cell.strip
|
106
|
-
end
|
107
|
-
newary
|
119
|
+
ary.map(&:strip)
|
108
120
|
end
|
109
|
-
|
121
|
+
|
110
122
|
## Apply a formal concept analysis on the matrix
|
111
|
-
def calcurate
|
123
|
+
def calcurate
|
112
124
|
@concepts, @extM, @intM = ganter_alg(@matrix)
|
113
125
|
@relM, @reltrans, @rank = create_rel(@intM)
|
114
126
|
@gammaM, @muM = gammaMu(@extM, @intM, @matrix)
|
115
127
|
end
|
116
128
|
|
117
|
-
## This is an implementation of an algorithm described by Bernhard Ganter
|
129
|
+
## This is an implementation of an algorithm described by Bernhard Ganter
|
118
130
|
## in "Two basic algorithms in concept analysis." Technische Hochschule
|
119
131
|
## Darmstadt, FB4-Preprint, 831, 1984.
|
120
132
|
def ganter_alg(matrix)
|
121
|
-
|
122
133
|
m = matrix
|
123
134
|
|
124
135
|
## all arrays except @idx are arrays of arrays of 0's and 1's
|
@@ -129,25 +140,25 @@ class FormalContext
|
|
129
140
|
int_B = []
|
130
141
|
endkey = []
|
131
142
|
temp = []
|
132
|
-
|
143
|
+
|
133
144
|
## the level in the lattice from the top
|
134
|
-
lvl = 0
|
145
|
+
lvl = 0
|
135
146
|
## is lower than the index of leftmost attr
|
136
|
-
idx[lvl] = -1
|
147
|
+
idx[lvl] = -1
|
137
148
|
## number of attr. and objs
|
138
|
-
anzM = m.size
|
139
|
-
## only needed for initialization
|
149
|
+
anzM = m.size
|
150
|
+
## only needed for initialization
|
140
151
|
anzG = m[0].size
|
141
152
|
## initialize extA[0] = [1,...,1]
|
142
|
-
anzG.times do
|
143
|
-
|
153
|
+
anzG.times do
|
154
|
+
unless ext_A[0]
|
144
155
|
ext_A[0] = []
|
145
156
|
end
|
146
157
|
ext_A[0] << 1
|
147
158
|
end
|
148
159
|
## initialize extB[0] = [0,...,0]
|
149
|
-
anzM.times do
|
150
|
-
|
160
|
+
anzM.times do
|
161
|
+
unless int_B[0]
|
151
162
|
int_B[0] = []
|
152
163
|
end
|
153
164
|
int_B[0] << 0
|
@@ -155,7 +166,7 @@ class FormalContext
|
|
155
166
|
anzCpt = 0
|
156
167
|
extension[0] = ext_A[0]
|
157
168
|
intension[0] = int_B[0]
|
158
|
-
anzM.times do
|
169
|
+
anzM.times do
|
159
170
|
endkey << 1
|
160
171
|
end
|
161
172
|
|
@@ -164,13 +175,11 @@ class FormalContext
|
|
164
175
|
(anzM - 1).downto(0) do |i|
|
165
176
|
breakkey = false
|
166
177
|
if (int_B[lvl][i] != 1)
|
167
|
-
while (i < idx[lvl])
|
168
|
-
lvl -= 1
|
169
|
-
end
|
178
|
+
lvl -= 1 while (i < idx[lvl])
|
170
179
|
idx[lvl + 1] = i
|
171
180
|
ext_A[lvl + 1] = create_and_ary(ext_A[lvl], m[i])
|
172
181
|
0.upto(i - 1) do |j|
|
173
|
-
if(!breakkey && int_B[lvl][j] != 1)
|
182
|
+
if (!breakkey && int_B[lvl][j] != 1)
|
174
183
|
temp = create_and_ary(ext_A[lvl + 1], m[j])
|
175
184
|
if temp == ext_A[lvl + 1]
|
176
185
|
breakkey = true
|
@@ -180,7 +189,7 @@ class FormalContext
|
|
180
189
|
unless breakkey
|
181
190
|
int_B[lvl + 1] = int_B[lvl].dup
|
182
191
|
int_B[lvl + 1][i] = 1
|
183
|
-
(i+1).upto(anzM - 1) do |k|
|
192
|
+
(i + 1).upto(anzM - 1) do |k|
|
184
193
|
if int_B[lvl + 1][k] != 1
|
185
194
|
temp = create_and_ary(ext_A[lvl + 1], m[k])
|
186
195
|
if temp == ext_A[lvl + 1]
|
@@ -194,7 +203,7 @@ class FormalContext
|
|
194
203
|
intension[anzCpt] = int_B[lvl]
|
195
204
|
break
|
196
205
|
end
|
197
|
-
end
|
206
|
+
end
|
198
207
|
end
|
199
208
|
end
|
200
209
|
|
@@ -214,7 +223,7 @@ class FormalContext
|
|
214
223
|
[c, intension, extension]
|
215
224
|
end
|
216
225
|
|
217
|
-
## Output arrayconsists of the following:
|
226
|
+
## Output arrayconsists of the following:
|
218
227
|
## r (subconcept superconcept relation)
|
219
228
|
## rt (trans. closure of r)
|
220
229
|
## s (ranked concepts)
|
@@ -231,11 +240,11 @@ class FormalContext
|
|
231
240
|
unless r[i]
|
232
241
|
r[i] = []
|
233
242
|
end
|
234
|
-
r[i][j] = 0
|
243
|
+
r[i][j] = 0
|
235
244
|
unless rt[i]
|
236
245
|
rt[i] = []
|
237
246
|
end
|
238
|
-
rt[i][j] = 0
|
247
|
+
rt[i][j] = 0
|
239
248
|
end
|
240
249
|
end
|
241
250
|
|
@@ -265,16 +274,15 @@ class FormalContext
|
|
265
274
|
s[rank[i]] = []
|
266
275
|
end
|
267
276
|
s[rank[i]] << i
|
268
|
-
end
|
277
|
+
end
|
269
278
|
s = s.collect do |i|
|
270
|
-
i
|
279
|
+
i || [0]
|
271
280
|
end
|
272
281
|
|
273
282
|
[r, rt, s]
|
274
|
-
end
|
283
|
+
end
|
275
284
|
|
276
285
|
def gammaMu(extent, intent, cxt)
|
277
|
-
|
278
286
|
gamma = []
|
279
287
|
mu = []
|
280
288
|
invcxt = []
|
@@ -288,31 +296,31 @@ class FormalContext
|
|
288
296
|
0.upto(intent.size - 1) do |j|
|
289
297
|
0.upto(cxt.size - 1) do |i|
|
290
298
|
gamma[i] = [] unless gamma[i]
|
291
|
-
if cxt[i] == intent[j]
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
299
|
+
gamma[i][j] = if cxt[i] == intent[j]
|
300
|
+
2
|
301
|
+
elsif (!@label_contraction && create_or_ary(cxt[i], intent[j]) == cxt[i])
|
302
|
+
1
|
303
|
+
else
|
304
|
+
0
|
305
|
+
end
|
298
306
|
end
|
299
|
-
|
307
|
+
|
300
308
|
0.upto(invcxt.size - 1) do |i|
|
301
309
|
# next unless invcxt[i]
|
302
310
|
mu[i] = [] unless mu[i]
|
303
|
-
if invcxt[i] == extent[j]
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
311
|
+
mu[i][j] = if invcxt[i] == extent[j]
|
312
|
+
2
|
313
|
+
elsif (!@label_contraction && create_or_ary(invcxt[i], extent[j]) == invcxt[i])
|
314
|
+
1
|
315
|
+
else
|
316
|
+
0
|
317
|
+
end
|
310
318
|
end
|
311
319
|
end
|
312
|
-
|
320
|
+
|
313
321
|
[gamma, mu]
|
314
322
|
end
|
315
|
-
|
323
|
+
|
316
324
|
def changecrosssymbol(char1, char2, lns)
|
317
325
|
rel = []
|
318
326
|
lns.each do |ln|
|
@@ -333,36 +341,36 @@ class FormalContext
|
|
333
341
|
## Generate Graphviz dot data (not creating a file)
|
334
342
|
## For options, see 'rubyfca'
|
335
343
|
def generate_dot(opts)
|
336
|
-
index_max_width = @concepts.size.to_s.split(//).size
|
337
|
-
|
338
|
-
|
339
|
-
clattice = RubyGraphviz.new("clattice", :
|
340
|
-
|
341
|
-
if opts[:circle]
|
342
|
-
legend
|
343
|
-
legend.node_default(:
|
344
|
-
legend.edge_default(:
|
344
|
+
# index_max_width = @concepts.size.to_s.split(//).size
|
345
|
+
nodesep = opts[:nodesep] ? opts[:nodesep].to_s : "0.4"
|
346
|
+
ranksep = opts[:ranksep] ? opts[:ranksep].to_s : "0.2"
|
347
|
+
clattice = RubyGraphviz.new("clattice", rankdir: "", nodesep: nodesep, ranksep: ranksep)
|
348
|
+
|
349
|
+
if opts[:circle] && opts[:legend]
|
350
|
+
legend = RubyGraphviz.new("legend", rankdir: "TB", lebelloc: "t", centered: "false")
|
351
|
+
legend.node_default(shape: "plaintext")
|
352
|
+
legend.edge_default(color: "gray60") if opts[:coloring]
|
345
353
|
legends = []
|
346
354
|
end
|
347
355
|
|
348
356
|
if opts[:circle]
|
349
|
-
clattice.node_default(:
|
350
|
-
clattice.edge_default(:
|
351
|
-
clattice.edge_default(:
|
357
|
+
clattice.node_default(shape: "circle", style: "filled")
|
358
|
+
clattice.edge_default(dir: "none", minlen: "2")
|
359
|
+
clattice.edge_default(color: "gray60") if opts[:coloring]
|
352
360
|
else
|
353
|
-
clattice.node_default(:
|
354
|
-
clattice.edge_default(:
|
355
|
-
clattice.edge_default(:
|
361
|
+
clattice.node_default(shape: "record", margin: "0.2,0.055")
|
362
|
+
clattice.edge_default(dir: "none")
|
363
|
+
clattice.edge_default(color: "gray60") if opts[:coloring]
|
356
364
|
end
|
357
|
-
|
365
|
+
|
358
366
|
0.upto(@concepts.size - 1) do |i|
|
359
367
|
objfull = []
|
360
|
-
attrfull = []
|
368
|
+
attrfull = []
|
361
369
|
0.upto(@gammaM.size - 1) do |j|
|
362
370
|
if @gammaM[j][i] == 2
|
363
|
-
|
371
|
+
# pointing finger does not appear correctly in eps...
|
364
372
|
# obj = opts[:full] ? @objects[j] + " " + [0x261C].pack("U") : @objects[j]
|
365
|
-
obj = opts[:full] ? @objects[j] + "*" : @objects[j]
|
373
|
+
obj = opts[:full] ? @objects[j].to_s + "*" : @objects[j].to_s
|
366
374
|
objfull << obj
|
367
375
|
elsif @gammaM[j][i] == 1
|
368
376
|
objfull << @objects[j]
|
@@ -370,9 +378,9 @@ class FormalContext
|
|
370
378
|
end
|
371
379
|
0.upto(@muM.size - 1) do |k|
|
372
380
|
if @muM[k][i] == 2
|
373
|
-
|
381
|
+
# pointing finger does not appear correctly in eps...
|
374
382
|
# att = opts[:full] ? @attributes[k] + " " + [0x261C].pack("U") : @attributes[k]
|
375
|
-
att = opts[:full] ? @attributes[k] + "*" : @attributes[k]
|
383
|
+
att = opts[:full] ? @attributes[k].to_s + "*" : @attributes[k].to_s
|
376
384
|
attrfull << att
|
377
385
|
elsif @muM[k][i] == 1
|
378
386
|
attrfull << @attributes[k]
|
@@ -381,21 +389,21 @@ class FormalContext
|
|
381
389
|
|
382
390
|
concept_id = i + 1
|
383
391
|
|
384
|
-
attr_str = attrfull.join(
|
392
|
+
attr_str = attrfull.join("<br />")
|
385
393
|
attr_str = attr_str == "" ? " " : attr_str
|
386
|
-
|
387
|
-
if opts[:coloring]
|
394
|
+
|
395
|
+
if opts[:coloring].zero? || /\A\s+\z/ =~ attr_str
|
388
396
|
attr_color = "white"
|
389
397
|
elsif opts[:coloring] == 1
|
390
398
|
attr_color = "lightblue"
|
391
399
|
elsif opts[:coloring] == 2
|
392
400
|
attr_color = "gray87"
|
393
401
|
end
|
394
|
-
|
395
|
-
obj_str = objfull.join(
|
402
|
+
|
403
|
+
obj_str = objfull.join("<br />")
|
396
404
|
obj_str = obj_str == "" ? " " : obj_str
|
397
405
|
|
398
|
-
if opts[:coloring]
|
406
|
+
if opts[:coloring].zero? || /\A\s+\z/ =~ obj_str
|
399
407
|
obj_color = "white"
|
400
408
|
elsif opts[:coloring] == 1
|
401
409
|
obj_color = "pink"
|
@@ -403,37 +411,36 @@ class FormalContext
|
|
403
411
|
obj_color = "gray92"
|
404
412
|
end
|
405
413
|
|
406
|
-
label = "<<table border=\"0\" cellborder=\"1\" cellspacing=\"0\">"
|
407
|
-
|
408
|
-
|
409
|
-
|
414
|
+
label = "<<table border=\"0\" cellborder=\"1\" cellspacing=\"0\">" \
|
415
|
+
"<tr><td balign=\"left\" align=\"left\" bgcolor=\"#{attr_color}\">#{attr_str}</td></tr>" \
|
416
|
+
"<tr><td balign=\"left\" align=\"left\" bgcolor=\"#{obj_color}\">#{obj_str}</td></tr>" \
|
417
|
+
"</table>>"
|
410
418
|
|
411
|
-
if opts[:circle]
|
419
|
+
if opts[:circle] && opts[:legend]
|
412
420
|
|
413
|
-
leg = "<<table border=\"0\" cellborder=\"1\" cellspacing=\"0\">"
|
414
|
-
|
415
|
-
|
416
|
-
|
421
|
+
leg = "<<table border=\"0\" cellborder=\"1\" cellspacing=\"0\">" \
|
422
|
+
"<tr><td rowspan=\"2\">#{concept_id}</td><td balign=\"left\" align=\"left\" bgcolor=\"#{attr_color}\">#{attr_str}</td></tr>" \
|
423
|
+
"<tr><td balign=\"left\" align=\"left\" bgcolor=\"#{obj_color}\">#{obj_str}</td></tr>" \
|
424
|
+
"</table>>"
|
417
425
|
|
418
|
-
if !attrfull.empty?
|
419
|
-
legend.node("cl#{concept_id}k", :
|
420
|
-
legend.node("cl#{concept_id}v", :
|
421
|
-
legend.rank("cl#{concept_id}k", "cl#{concept_id}v", :
|
426
|
+
if !attrfull.empty? || !objfull.empty?
|
427
|
+
legend.node("cl#{concept_id}k", label: concept_id, style: "invis")
|
428
|
+
legend.node("cl#{concept_id}v", label: leg, fillcolor: "white")
|
429
|
+
legend.rank("cl#{concept_id}k", "cl#{concept_id}v", style: "invis", length: "0.0")
|
422
430
|
if legends[-1]
|
423
|
-
legend.edge("cl#{legends[-1]}k", "cl#{concept_id}k", :
|
431
|
+
legend.edge("cl#{legends[-1]}k", "cl#{concept_id}k", style: "invis", length: "0.0")
|
424
432
|
end
|
425
433
|
legends << concept_id
|
426
434
|
end
|
427
435
|
end
|
428
|
-
|
436
|
+
|
429
437
|
if opts[:circle]
|
430
|
-
clattice.node("c#{i}", :
|
438
|
+
clattice.node("c#{i}", width: "0.5", fontsize: "14.0", label: concept_id)
|
431
439
|
else
|
432
|
-
clattice.node("c#{i}", :label
|
433
|
-
:height => "0.0", :width => "0.0", :margin => "0.0")
|
440
|
+
clattice.node("c#{i}", label: label, shape: "plaintext", height: "0.0", width: "0.0", margin: "0.0")
|
434
441
|
end
|
435
442
|
end
|
436
|
-
|
443
|
+
|
437
444
|
0.upto(@relM.size - 1) do |i|
|
438
445
|
0.upto(@relM.size - 1) do |j|
|
439
446
|
if @relM[i][j] == 1
|
@@ -441,11 +448,11 @@ class FormalContext
|
|
441
448
|
end
|
442
449
|
end
|
443
450
|
end
|
444
|
-
|
445
|
-
clattice.subgraph(legend) if opts[:circle]
|
451
|
+
|
452
|
+
clattice.subgraph(legend) if opts[:circle] && opts[:legend]
|
446
453
|
clattice.to_dot
|
447
454
|
end
|
448
|
-
|
455
|
+
|
449
456
|
## Generate an actual graphic file (Graphviz dot needs to be installed properly)
|
450
457
|
def generate_img(outfile, image_type, opts)
|
451
458
|
dot = generate_dot(opts)
|
@@ -453,15 +460,14 @@ class FormalContext
|
|
453
460
|
if isthere_dot !~ /dot.*version/i
|
454
461
|
showerror("Graphviz's dot program cannot be found.", 1)
|
455
462
|
else
|
456
|
-
if opts[:straight]
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
IO.popen(cmd,
|
463
|
+
cmd = if opts[:straight]
|
464
|
+
"dot | neato -n -T#{image_type} -o#{outfile} 2>rubyfca.log"
|
465
|
+
else
|
466
|
+
"dot -T#{image_type} -o#{outfile} 2>rubyfca.log"
|
467
|
+
end
|
468
|
+
IO.popen(cmd, "r+") do |io|
|
462
469
|
io.puts dot
|
463
470
|
end
|
464
471
|
end
|
465
|
-
end
|
472
|
+
end
|
466
473
|
end
|
467
|
-
|
data/rubyfca.gemspec
CHANGED
@@ -1,17 +1,25 @@
|
|
1
|
-
#
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require File.expand_path("lib/rubyfca/version", __dir__)
|
3
4
|
|
4
5
|
Gem::Specification.new do |gem|
|
6
|
+
gem.name = "rubyfca"
|
7
|
+
gem.version = RubyFCA::VERSION
|
5
8
|
gem.authors = ["Yoichiro Hasebe", "Kow Kuroda"]
|
6
9
|
gem.email = ["yohasebe@gmail.com"]
|
7
|
-
gem.summary
|
8
|
-
gem.description
|
9
|
-
gem.homepage
|
10
|
+
gem.summary = "Command line FCA tool written in Ruby"
|
11
|
+
gem.description = "Command line Formal Concept Analysis (FCA) tool written in Ruby"
|
12
|
+
gem.homepage = "http://github.com/yohasebe/rubyfca"
|
10
13
|
|
11
|
-
gem.
|
12
|
-
gem.
|
13
|
-
gem.
|
14
|
-
gem.
|
14
|
+
gem.required_ruby_version = ">= 2.6.10"
|
15
|
+
gem.licenses = ["GPL-3.0"]
|
16
|
+
gem.files = `git ls-files`.split("\n")
|
17
|
+
gem.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
|
15
18
|
gem.require_paths = ["lib"]
|
16
|
-
|
19
|
+
|
20
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
21
|
+
|
22
|
+
gem.add_dependency "optimist"
|
23
|
+
gem.add_dependency "roo"
|
24
|
+
gem.add_development_dependency "minitest"
|
17
25
|
end
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|