society 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,22 +1,15 @@
1
1
  Copyright (c) 2014 Coraline Ada Ehmke / Instructure.inc
2
2
 
3
- MIT License
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining
6
- a copy of this software and associated documentation files (the
7
- "Software"), to deal in the Software without restriction, including
8
- without limitation the rights to use, copy, modify, merge, publish,
9
- distribute, sublicense, and/or sell copies of the Software, and to
10
- permit persons to whom the Software is furnished to do so, subject to
11
- the following conditions:
12
-
13
- The above copyright notice and this permission notice shall be
14
- included in all copies or substantial portions of the Software.
15
-
16
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
- NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
- LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
- OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
- WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
3
+ This program is free software: you can redistribute it and/or modify
4
+ it under the terms of the GNU General Public License as published by
5
+ the Free Software Foundation, either version 3 of the License, or
6
+ (at your option) any later version.
7
+
8
+ This program is distributed in the hope that it will be useful,
9
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
+ GNU General Public License for more details.
12
+
13
+ You should have received a copy of the GNU General Public License
14
+ along with this program in the file COPYING.txt. If not, see
15
+ <http://www.gnu.org/licenses/>
data/README.md CHANGED
@@ -39,6 +39,14 @@ and need to pull in updates from the
39
39
  [society-assets](https://github.com/CoralineAda/society-assets) package, you
40
40
  can do so on the command line with `$ bower update`.
41
41
 
42
+ ## Recognition
43
+
44
+ The graph clustering algorithm used in this software is called MCL, described
45
+ in:
46
+
47
+ * Stijn van Dongen, _Graph Clustering by Flow Simulation_, PhD thesis,
48
+ University of Utrecht, May 2000. [micans.org/mcl](http://micans.org/mcl)
49
+
42
50
  ## Contributing
43
51
 
44
52
  Please note that this project is released with a [Contributor Code of Conduct]
data/bower.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "society",
3
3
  "dependencies": {
4
- "society-assets": "1.1.1"
4
+ "society-assets": "1.2.0"
5
5
  }
6
6
  }
@@ -5,6 +5,7 @@ require "active_support/core_ext/string/inflections"
5
5
  require_relative "society/association_processor"
6
6
  require_relative "society/reference_processor"
7
7
  require_relative "society/edge"
8
+ require_relative "society/clusterer"
8
9
  require_relative "society/formatter/graph/json"
9
10
  require_relative "society/formatter/report/html"
10
11
  require_relative "society/formatter/report/json"
@@ -14,8 +15,8 @@ require_relative "society/version"
14
15
 
15
16
  module Society
16
17
 
17
- def self.new(path_to_files)
18
- Society::Parser.for_files(path_to_files)
18
+ def self.new(*paths_to_files)
19
+ Society::Parser.for_files(*paths_to_files)
19
20
  end
20
21
 
21
22
  end
@@ -0,0 +1,188 @@
1
+ # This file has been translated from the `minimcl` perl script, which was
2
+ # sourced from the MCL-edge library, release 14-137. The homepage for MCL-edge
3
+ # is http://micans.org/mcl
4
+ #
5
+ # The original copyright for `minimcl` is as follows:
6
+ #
7
+ # (C) Copyright 2006, 2007, 2008, 2009 Stijn van Dongen
8
+ #
9
+ # This file is part of MCL. You can redistribute and/or modify MCL under the
10
+ # terms of the GNU General Public License; either version 3 of the License or
11
+ # (at your option) any later version. You should have received a copy of the
12
+ # GPL along with MCL, in the file COPYING.
13
+
14
+ module Society
15
+ class Clusterer
16
+
17
+ def initialize(params={})
18
+ @params = DEFAULT_PARAMS.merge(params)
19
+ end
20
+
21
+ # returns an array of arrays of nodes
22
+ def cluster(graph)
23
+ m = matrix_from(graph)
24
+ clusters = mcl(m)
25
+ clusters.map { |index, members| members.keys }
26
+ .sort_by(&:size).reverse
27
+ end
28
+
29
+ private
30
+
31
+ attr_reader :params
32
+
33
+ DEFAULT_PARAMS = {
34
+ inflation: 2.0
35
+ }
36
+
37
+ # TODO: "weights" are ignored right now, but soon will be an attribute of
38
+ # the edge
39
+ def matrix_from(graph)
40
+ matrix = SparseMatrix.new
41
+ graph.edges.each do |edge|
42
+ a = edge.from
43
+ b = edge.to
44
+ matrix[a][b] = 1
45
+ matrix[b][a] = 1
46
+ end
47
+ matrix
48
+ end
49
+
50
+ def mcl(matrix)
51
+ matrix_add_loops(matrix)
52
+ matrix_make_stochastic(matrix)
53
+ chaos = 1.0
54
+ while (chaos > 0.001) do
55
+ sq = matrix_square(matrix)
56
+ chaos = matrix_inflate(sq)
57
+ matrix = sq
58
+ end
59
+ matrix_interpret(matrix)
60
+ end
61
+
62
+ def matrix_square(matrix)
63
+ squared = SparseMatrix.new
64
+ matrix.each do |node, vector|
65
+ squared[node] = matrix_multiply_vector(matrix, vector)
66
+ end
67
+ squared
68
+ end
69
+
70
+ def matrix_multiply_vector(matrix, vector)
71
+ result_vec = SparseVector.new
72
+ vector.each do |entry, val|
73
+ matrix[entry].each do |f, matrix_val|
74
+ result_vec[f] += val * matrix_val
75
+ end
76
+ end
77
+ result_vec
78
+ end
79
+
80
+ def matrix_make_stochastic(matrix)
81
+ matrix_inflate(matrix, 1)
82
+ end
83
+
84
+ def matrix_add_loops(matrix)
85
+ matrix.each do |key,_|
86
+ matrix[key][key] = 1.0
87
+ matrix[key][key] = vector_max(matrix[key])
88
+ end
89
+ end
90
+
91
+ def vector_max(vector)
92
+ vector.values.max || 0.0
93
+ end
94
+
95
+ def vector_sum(vector)
96
+ vector.values.reduce(&:+) || 0.0
97
+ end
98
+
99
+ # prunes small elements as well
100
+ def matrix_inflate(matrix, inflation = params[:inflation])
101
+ chaos = 0.0
102
+ matrix.each do |node, vector|
103
+ sum = 0.0
104
+ sumsq = 0.0
105
+ max = 0.0
106
+ vector.each do |node2, value|
107
+ vector.delete node2 and next if value < 0.00001
108
+
109
+ inflated = value ** inflation
110
+ vector[node2] = inflated
111
+ sum += inflated
112
+ end
113
+ if sum > 0.0
114
+ vector.each do |node2, value|
115
+ vector[node2] /= sum
116
+ sumsq += vector[node2] ** 2
117
+ max = [vector[node2], max].max
118
+ end
119
+ end
120
+ chaos = [max - sumsq, chaos].max
121
+ end
122
+ chaos # only meaningful if input is stochastic
123
+ end
124
+
125
+ # assumes but does not check doubly idempotent matrix.
126
+ # can handle attractor systems of size < 10.
127
+ # recognizes/preserves overlap.
128
+ def matrix_interpret(matrix)
129
+ clusters = SparseMatrix.new
130
+ attrid = {}
131
+ clid = 0
132
+
133
+ # crude removal of small elements
134
+ matrix.each do |n, vec|
135
+ vec.each do |nb, val|
136
+ matrix[n].delete nb if val < 0.1
137
+ end
138
+ end
139
+
140
+ attr = {}
141
+ matrix.each_key do |key|
142
+ attr[key] = 1 if matrix[key].key? key
143
+ end
144
+
145
+ attr.each_key do |a|
146
+ next if attrid.key?(a)
147
+ aa = [a]
148
+ while aa.size > 0 do
149
+ bb = []
150
+ aa.each do |aaa|
151
+ attrid[aaa] = clid
152
+ matrix[aaa].each_key { |akey| bb.push(akey) if attr.key? akey }
153
+ end
154
+ aa = bb.select { |b| !attrid.key?(b) }
155
+ end
156
+ clid += 1
157
+ end
158
+
159
+ matrix.each do |n, val|
160
+ if !attr.key?(n)
161
+ val.keys.select { |x| attr.key? x }.each do |a|
162
+ clusters[attrid[a]][n] += 1
163
+ end
164
+ else
165
+ clusters[attrid[n]][n] += 1
166
+ end
167
+ end
168
+
169
+ clusters
170
+ end
171
+
172
+
173
+ module SparseMatrix
174
+ def self.new
175
+ Hash.new do |hash, key|
176
+ hash[key] = SparseVector.new
177
+ end
178
+ end
179
+ end
180
+
181
+ module SparseVector
182
+ def self.new
183
+ Hash.new(0.0)
184
+ end
185
+ end
186
+ end
187
+ end
188
+
@@ -6,8 +6,7 @@ module Society
6
6
  class JSON
7
7
 
8
8
  def initialize(graph)
9
- @nodes = graph.nodes
10
- @edges = graph.edges
9
+ @graph = graph
11
10
  end
12
11
 
13
12
  def to_json
@@ -22,20 +21,31 @@ module Society
22
21
  from: node_names.index(edge.from),
23
22
  to: node_names.index(edge.to)
24
23
  }
25
- end
24
+ end,
25
+ clusters: clusters_of_indices
26
26
  }
27
27
  end
28
28
 
29
29
  private
30
30
 
31
- attr_reader :nodes, :edges
31
+ attr_reader :graph
32
32
 
33
33
  def node_names
34
- @node_names ||= nodes.map(&:full_name).uniq
34
+ @node_names ||= graph.nodes.map(&:full_name).uniq
35
35
  end
36
36
 
37
37
  def named_edges
38
- @named_edges ||= edges.map { |edge| Edge.new(from: edge.from.full_name, to: edge.to.full_name) }
38
+ @named_edges ||= graph.edges.map { |edge| Edge.new(from: edge.from.full_name, to: edge.to.full_name) }
39
+ end
40
+
41
+ def clusters_of_indices
42
+ Society::Clusterer.new.cluster(graph_of_names).map do |cluster|
43
+ cluster.map { |name| node_names.index(name) }
44
+ end
45
+ end
46
+
47
+ def graph_of_names
48
+ ObjectGraph.new(nodes: node_names, edges: named_edges)
39
49
  end
40
50
 
41
51
  end
@@ -5,27 +5,28 @@ module Society
5
5
 
6
6
  attr_reader :json_data, :output_path
7
7
 
8
- def initialize(json_data:, output_path: default_output_path)
8
+ def initialize(json_data:, output_path: nil)
9
9
  @json_data = json_data
10
10
  @output_path = output_path
11
11
  end
12
12
 
13
13
  def write
14
- prepare_output_directory
15
- write_json_data
14
+ if output_path
15
+ prepare_output_directory
16
+ write_json_data
17
+ else
18
+ puts json_data
19
+ end
16
20
  end
17
21
 
18
22
  private
19
23
 
20
- def default_output_path
21
- File.join('doc', 'society', timestamp, 'society_graph.json')
22
- end
23
-
24
24
  def timestamp
25
25
  @timestamp ||= Time.now.strftime("%Y_%m_%d_%H_%M_%S")
26
26
  end
27
27
 
28
28
  def prepare_output_directory
29
+ raise "No output path was specified" if output_path.nil?
29
30
  directory_path = File.split(output_path).first
30
31
  FileUtils.mkpath directory_path
31
32
  end
@@ -6,6 +6,10 @@
6
6
  fill: #eee;
7
7
  }
8
8
 
9
+ .society-cell-outline {
10
+ stroke: #fff;
11
+ }
12
+
9
13
  .society-heatmap-select,
10
14
  .society-network-toggle {
11
15
  display: block;
@@ -235,9 +235,22 @@
235
235
  };
236
236
 
237
237
  Heatmap.prototype.transformData = function(data) {
238
+ var _findCluster = function(nodeIndex) {
239
+ var clusterId = null, i;
240
+ for (i = 0; i < data.clusters.length; i++) {
241
+ if (data.clusters[i].indexOf(nodeIndex) != -1) {
242
+ clusterId = i;
243
+ break;
244
+ }
245
+ }
246
+ return clusterId;
247
+ };
248
+
249
+ var findCluster = data.clusters ? _findCluster : function() { return 0; };
250
+
238
251
  return {
239
- nodes: data.nodes.map(function(node) {
240
- return { name: node.name, group: 1 };
252
+ nodes: data.nodes.map(function(node, index) {
253
+ return { name: node.name, group: findCluster(index) };
241
254
  }),
242
255
  links: data.edges.map(function(edge) {
243
256
  return { source: edge.from, target: edge.to, value: 1 };
@@ -264,7 +277,6 @@
264
277
  .attr("width", this.width + this.margin.left + this.margin.right)
265
278
  .attr("class", "society-graph")
266
279
  .attr("height", this.height + this.margin.top + this.margin.bottom)
267
- .style("margin-left", -this.margin.left + "px")
268
280
  .append("g")
269
281
  .attr("transform", "translate(" + this.margin.left + "," + this.margin.top + ")");
270
282
 
@@ -312,6 +324,7 @@
312
324
  .each(row);
313
325
 
314
326
  row.append("line")
327
+ .attr("class", "society-cell-outline")
315
328
  .attr("x2", this.width);
316
329
 
317
330
  row.append("text")
@@ -328,6 +341,7 @@
328
341
  .attr("transform", function(d, i) { return "translate(" + x(i) + ")rotate(-90)"; });
329
342
 
330
343
  column.append("line")
344
+ .attr("class", "society-cell-outline")
331
345
  .attr("x1", -this.width);
332
346
 
333
347
  column.append("text")
@@ -363,17 +377,17 @@
363
377
  function order(value) {
364
378
  x.domain(orders[value]);
365
379
 
366
- var t = coOccurrenceSvg.transition().duration(2500);
380
+ var t = coOccurrenceSvg.transition().duration(1000);
367
381
 
368
382
  t.selectAll(".society-row")
369
- .delay(function(d, i) { return x(i) * 4; })
383
+ .delay(function(d, i) { return x(i) * 0.4; })
370
384
  .attr("transform", function(d, i) { return "translate(0," + x(i) + ")"; })
371
385
  .selectAll(".society-cell")
372
- .delay(function(d) { return x(d.x) * 4; })
386
+ .delay(function(d) { return x(d.x) * 0.4; })
373
387
  .attr("x", function(d) { return x(d.x); });
374
388
 
375
389
  t.selectAll(".society-column")
376
- .delay(function(d, i) { return x(i) * 4; })
390
+ .delay(function(d, i) { return x(i) * 0.4; })
377
391
  .attr("transform", function(d, i) { return "translate(" + x(i) + ")rotate(-90)"; });
378
392
  }
379
393
 
@@ -11,4 +11,4 @@ module Society
11
11
 
12
12
  end
13
13
 
14
- end
14
+ end
@@ -2,8 +2,8 @@ module Society
2
2
 
3
3
  class Parser
4
4
 
5
- def self.for_files(file_path)
6
- new(::Analyst.for_files(file_path))
5
+ def self.for_files(*file_paths)
6
+ new(::Analyst.for_files(*file_paths))
7
7
  end
8
8
 
9
9
  def self.for_source(source)