society 1.0.0 → 1.1.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.
@@ -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)