spout 0.7.0 → 0.8.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +11 -0
- data/README.md +27 -0
- data/lib/spout/actions.rb +6 -0
- data/lib/spout/commands/json_charts_and_tables.rb +202 -0
- data/lib/spout/helpers/array_statistics.rb +93 -0
- data/lib/spout/helpers/chart_types.rb +395 -0
- data/lib/spout/helpers/table_formatting.rb +52 -0
- data/lib/spout/models/subject.rb +17 -0
- data/lib/spout/tasks/engine.rake +7 -0
- data/lib/spout/templates/gitignore +1 -0
- data/lib/spout/version.rb +2 -2
- data/spout.gemspec +1 -1
- metadata +11 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 55751ba1e111b174e9501fb52b1dd9a484b9ee1c
|
4
|
+
data.tar.gz: ca81a991a37e425a4166dd28e6aeffa183378b1c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6fec0a99eeb2b26af88652c7b6b79b3d5de8b750593aac2196275e1a1d365dda0814f47343ac312adaf9bb41bd29065a5d6dee8092de8927bdd41136d6ffbca0
|
7
|
+
data.tar.gz: 1d80934213ba8633506850aeabc142764f578d6fa727ffe7ac5ac76cc07422b4e0759d53e7200c5c686075077d3a37cd1101d7b650294d147db5a2f38f1e43eb
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,14 @@
|
|
1
|
+
## 0.8.0
|
2
|
+
|
3
|
+
### Enhancements
|
4
|
+
- Added `spout json` command that generates JSON charts and tables of each variable in a dataset
|
5
|
+
- This command requires a .spout.yml file to be specified to identify the following variables:
|
6
|
+
- `visit`: This variable is used to separate subject encounters in a histogram
|
7
|
+
- `charts`: Array of choices, numeric, or integer variables for charts
|
8
|
+
- **Gem Changes**
|
9
|
+
- Updated to colorize 0.7.2
|
10
|
+
- Use of Ruby 2.1.2 is now recommended
|
11
|
+
|
1
12
|
## 0.7.0 (April 16, 2014)
|
2
13
|
|
3
14
|
### Enhancements
|
data/README.md
CHANGED
@@ -170,6 +170,33 @@ For specific variables the following can be used:
|
|
170
170
|
|
171
171
|
Generated graphs are placed in: `./graphs/`
|
172
172
|
|
173
|
+
|
174
|
+
### Generate charts and tables for data in your dataset
|
175
|
+
|
176
|
+
```
|
177
|
+
spout json
|
178
|
+
```
|
179
|
+
|
180
|
+
This command generates JSON charts and tables of each variable in a dataset
|
181
|
+
|
182
|
+
Requires a Spout YAML configuration file, `.spout.yml`, in the root of the data dictionary that defines the variables used to create the charts:
|
183
|
+
|
184
|
+
- `visit`: This variable is used to separate subject encounters in a histogram
|
185
|
+
- `charts`: Array of choices, numeric, or integer variables for charts
|
186
|
+
|
187
|
+
Example `.spout.yml` file:
|
188
|
+
|
189
|
+
```yml
|
190
|
+
visit: visitnumber
|
191
|
+
charts:
|
192
|
+
- age
|
193
|
+
- gender
|
194
|
+
- race
|
195
|
+
```
|
196
|
+
|
197
|
+
This will generate charts and tables for each variable in the dataset plotted against the variables listed under `charts`.
|
198
|
+
|
199
|
+
|
173
200
|
### Export to the Hybrid Data Dictionary format from your JSON repository
|
174
201
|
|
175
202
|
Exporting to a format compatible with [Hybrid](https://github.com/sleepepi/hybrid) is also available.
|
data/lib/spout/actions.rb
CHANGED
@@ -23,6 +23,8 @@ module Spout
|
|
23
23
|
coverage_report(argv)
|
24
24
|
when 'graphs', '-graphs', '--graphs', 'g', '-g'
|
25
25
|
generate_graphs(argv.last(argv.size - 1))
|
26
|
+
when 'json', 'j'
|
27
|
+
generate_charts_and_tables(argv.last(argv.size - 1))
|
26
28
|
else
|
27
29
|
help
|
28
30
|
end
|
@@ -140,6 +142,10 @@ EOT
|
|
140
142
|
system "bundle exec rake spout:graphs #{params_string}"
|
141
143
|
end
|
142
144
|
|
145
|
+
def generate_charts_and_tables(variables)
|
146
|
+
system "bundle exec rake spout:json variables=#{variables.join(',')}"
|
147
|
+
end
|
148
|
+
|
143
149
|
private
|
144
150
|
|
145
151
|
def copy_file(template_file, file_name = '')
|
@@ -0,0 +1,202 @@
|
|
1
|
+
require 'csv'
|
2
|
+
require 'fileutils'
|
3
|
+
require 'rubygems'
|
4
|
+
require 'json'
|
5
|
+
require 'yaml'
|
6
|
+
|
7
|
+
|
8
|
+
require 'spout/models/subject'
|
9
|
+
require 'spout/helpers/array_statistics'
|
10
|
+
require 'spout/helpers/chart_types'
|
11
|
+
|
12
|
+
|
13
|
+
module Spout
|
14
|
+
module Commands
|
15
|
+
class JsonChartsAndTables
|
16
|
+
def initialize(variables)
|
17
|
+
spout_config = YAML.load_file('.spout.yml')
|
18
|
+
|
19
|
+
_visit = ''
|
20
|
+
|
21
|
+
if spout_config.kind_of?(Hash)
|
22
|
+
_visit = spout_config['visit'].to_s.strip
|
23
|
+
|
24
|
+
chart_variables = if spout_config['charts'].kind_of?(Array)
|
25
|
+
spout_config['charts'].collect{|c| c.to_s.strip}.select{|c| c != ''}
|
26
|
+
else
|
27
|
+
[]
|
28
|
+
end
|
29
|
+
else
|
30
|
+
puts "The YAML file needs to be in the following format:"
|
31
|
+
puts "histogram: visitnumber # VISIT_VARIABLE\ncharts:\n - age_s1\n - gender\n - race\n"
|
32
|
+
exit
|
33
|
+
end
|
34
|
+
|
35
|
+
if Spout::Helpers::ChartTypes::get_json(_visit, 'variable') == nil
|
36
|
+
if _visit == ''
|
37
|
+
puts "The visit variable in .spout.yml can't be blank."
|
38
|
+
else
|
39
|
+
puts "Could not find the following visit variable: #{_visit}"
|
40
|
+
end
|
41
|
+
exit
|
42
|
+
end
|
43
|
+
missing_variables = chart_variables.select{|c| Spout::Helpers::ChartTypes::get_json(c, 'variable') == nil}
|
44
|
+
if missing_variables.count > 0
|
45
|
+
puts "Could not find the following chart variable#{'s' unless missing_variables.size == 1}: #{missing_variables.join(', ')}"
|
46
|
+
exit
|
47
|
+
end
|
48
|
+
|
49
|
+
argv_string = variables.join(',')
|
50
|
+
number_of_rows = nil
|
51
|
+
|
52
|
+
if match_data = argv_string.match(/-rows=(\d*)/)
|
53
|
+
number_of_rows = match_data[1].to_i
|
54
|
+
argv_string.gsub!(match_data[0], '')
|
55
|
+
end
|
56
|
+
|
57
|
+
valid_ids = argv_string.split(',').compact.reject{|s| s == ''}
|
58
|
+
|
59
|
+
@visit = _visit
|
60
|
+
|
61
|
+
chart_lookup = { _visit => "Histogram" }
|
62
|
+
|
63
|
+
chart_variables.each do |chart_variable|
|
64
|
+
json = Spout::Helpers::ChartTypes::get_json(chart_variable, 'variable')
|
65
|
+
chart_lookup[chart_variable] = json['display_name']
|
66
|
+
end
|
67
|
+
|
68
|
+
|
69
|
+
|
70
|
+
t = Time.now
|
71
|
+
|
72
|
+
|
73
|
+
version = standard_version
|
74
|
+
|
75
|
+
subjects = []
|
76
|
+
|
77
|
+
FileUtils.mkpath "charts/#{version}"
|
78
|
+
|
79
|
+
csv_files = Dir.glob("csvs/#{version}/*.csv")
|
80
|
+
|
81
|
+
csv_files.each_with_index do |csv_file, index|
|
82
|
+
count = 0
|
83
|
+
puts "Parsing: #{csv_file}"
|
84
|
+
CSV.parse( File.open(csv_file, 'r:iso-8859-1:utf-8'){|f| f.read}, headers: true, header_converters: lambda { |h| h.to_s.downcase } ) do |line|
|
85
|
+
|
86
|
+
row = line.to_hash
|
87
|
+
count += 1
|
88
|
+
puts "Line: #{count}" if (count % 1000 == 0)
|
89
|
+
subjects << Spout::Models::Subject.create do |t|
|
90
|
+
|
91
|
+
t._visit = row[_visit] #.to_s.strip
|
92
|
+
|
93
|
+
row.each do |key,value|
|
94
|
+
unless t.respond_to?(key)
|
95
|
+
t.class.send(:define_method, "#{key}") { instance_variable_get("@#{key}") }
|
96
|
+
t.class.send(:define_method, "#{key}=") { |value| instance_variable_set("@#{key}", value) }
|
97
|
+
end
|
98
|
+
|
99
|
+
unless value == nil
|
100
|
+
t.send("#{key}=", value)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
# puts "Memory Used: " + (`ps -o rss -p #{$$}`.strip.split.last.to_i / 1024).to_s + " MB" if count % 1000 == 0
|
105
|
+
# break if count >= 1000
|
106
|
+
break if number_of_rows != nil and count >= number_of_rows
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
variable_files = Dir.glob('variables/**/*.json')
|
111
|
+
variable_files_count = variable_files.count
|
112
|
+
|
113
|
+
variable_files.each do |variable_file|
|
114
|
+
json = JSON.parse(File.read(variable_file)) rescue json = nil
|
115
|
+
next unless json
|
116
|
+
next unless valid_ids.include?(json["id"].to_s.downcase) or valid_ids.size == 0
|
117
|
+
next unless ["numeric", "integer"].include?(json["type"])
|
118
|
+
method = json['id'].to_s.downcase
|
119
|
+
next unless Spout::Models::Subject.method_defined?(method)
|
120
|
+
|
121
|
+
subjects.each{ |s| s.send(method) != nil ? s.send("#{method}=", s.send("#{method}").to_f) : nil }
|
122
|
+
end
|
123
|
+
|
124
|
+
variable_files.each_with_index do |variable_file, file_index|
|
125
|
+
json = JSON.parse(File.read(variable_file)) rescue json = nil
|
126
|
+
next unless json
|
127
|
+
next unless valid_ids.include?(json["id"].to_s.downcase) or valid_ids.size == 0
|
128
|
+
next unless ["numeric", "integer", "choices"].include?(json["type"])
|
129
|
+
variable_name = json['id'].to_s.downcase
|
130
|
+
next unless Spout::Models::Subject.method_defined?(variable_name)
|
131
|
+
|
132
|
+
puts "#{file_index+1} of #{variable_files_count}: #{variable_file.gsub(/(^variables\/|\.json$)/, '').gsub('/', ' / ')}"
|
133
|
+
|
134
|
+
|
135
|
+
stats = {
|
136
|
+
charts: {},
|
137
|
+
tables: {}
|
138
|
+
}
|
139
|
+
|
140
|
+
chart_types = case json['type'] when 'integer', 'numeric', 'choices'
|
141
|
+
chart_lookup.keys
|
142
|
+
else
|
143
|
+
[]
|
144
|
+
end
|
145
|
+
|
146
|
+
chart_types.each do |chart_type|
|
147
|
+
if chart_type == _visit
|
148
|
+
filtered_subjects = subjects.select{ |s| s.send(chart_type) != nil } # and s.send(variable_name) != nil
|
149
|
+
if filtered_subjects.count > 0
|
150
|
+
stats[:charts][chart_lookup[chart_type].downcase] = Spout::Helpers::ChartTypes::chart_histogram(chart_type, filtered_subjects, json, variable_name)
|
151
|
+
stats[:tables][chart_lookup[chart_type].downcase] = Spout::Helpers::ChartTypes::table_arbitrary(chart_type, filtered_subjects, json, variable_name)
|
152
|
+
end
|
153
|
+
else
|
154
|
+
filtered_subjects = subjects.select{ |s| s.send(chart_type) != nil } # and s.send(variable_name) != nil
|
155
|
+
if filtered_subjects.count > 0
|
156
|
+
stats[:charts][chart_lookup[chart_type].downcase] = Spout::Helpers::ChartTypes::chart_arbitrary(chart_type, filtered_subjects, json, variable_name, visits)
|
157
|
+
stats[:tables][chart_lookup[chart_type].downcase] = visits.collect do |visit_display_name, visit_value|
|
158
|
+
visit_subjects = filtered_subjects.select{ |s| s._visit == visit_value }
|
159
|
+
unknown_subjects = visit_subjects.select{ |s| s.send(variable_name) == nil }
|
160
|
+
(visit_subjects.count > 0 && visit_subjects.count != unknown_subjects.count) ? Spout::Helpers::ChartTypes::table_arbitrary(chart_type, visit_subjects, json, variable_name, visit_display_name) : nil
|
161
|
+
end.compact
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
chart_json_file = File.join('charts', version, "#{json['id']}.json")
|
167
|
+
File.open(chart_json_file, 'w') { |file| file.write( JSON.pretty_generate(stats) + "\n" ) }
|
168
|
+
|
169
|
+
end
|
170
|
+
|
171
|
+
puts "Took #{Time.now - t} seconds."
|
172
|
+
|
173
|
+
|
174
|
+
end
|
175
|
+
|
176
|
+
# [["Visit 1", "1"], ["Visit 2", "2"], ["CVD Outcomes", "3"]]
|
177
|
+
def visits
|
178
|
+
@visits ||= begin
|
179
|
+
Spout::Commands::JsonChartsAndTables::domain_array(@visit)
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
# This is directly from Spout
|
184
|
+
def self.standard_version
|
185
|
+
version = File.open('VERSION', &:readline).strip rescue ''
|
186
|
+
version == '' ? '1.0.0' : version
|
187
|
+
end
|
188
|
+
|
189
|
+
def self.domain_array(variable_name)
|
190
|
+
variable_file = Dir.glob("variables/**/#{variable_name}.json").first
|
191
|
+
json = JSON.parse(File.read(variable_file)) rescue json = nil
|
192
|
+
if json
|
193
|
+
domain_json = Spout::Helpers::ChartTypes::get_domain(json)
|
194
|
+
domain_json ? domain_json.collect{|option_hash| [option_hash['display_name'], option_hash['value']]} : []
|
195
|
+
else
|
196
|
+
[]
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# array_statistics.rb
|
2
|
+
|
3
|
+
class Array
|
4
|
+
def n
|
5
|
+
self.compact.count
|
6
|
+
end
|
7
|
+
|
8
|
+
def mean
|
9
|
+
array = self.compact
|
10
|
+
return nil if array.size == 0
|
11
|
+
array.inject(:+).to_f / array.size
|
12
|
+
end
|
13
|
+
|
14
|
+
def sample_variance
|
15
|
+
array = self.compact
|
16
|
+
m = array.mean
|
17
|
+
sum = array.inject(0){|accum, i| accum +(i-m)**2 }
|
18
|
+
sum / (array.length - 1).to_f
|
19
|
+
end
|
20
|
+
|
21
|
+
def standard_deviation
|
22
|
+
array = self.compact
|
23
|
+
return nil if array.size < 2
|
24
|
+
return Math.sqrt(array.sample_variance)
|
25
|
+
end
|
26
|
+
|
27
|
+
def median
|
28
|
+
array = self.compact.sort
|
29
|
+
return nil if array.size == 0
|
30
|
+
len = array.size
|
31
|
+
len % 2 == 1 ? array[len/2] : (array[len/2 - 1] + array[len/2]).to_f / 2
|
32
|
+
end
|
33
|
+
|
34
|
+
def unknown
|
35
|
+
self.select{|s| s == nil}.count
|
36
|
+
end
|
37
|
+
|
38
|
+
def quartile_sizes
|
39
|
+
quartile_size = self.count / 4
|
40
|
+
quartile_fraction = self.count % 4
|
41
|
+
|
42
|
+
quartile_sizes = [quartile_size] * 4
|
43
|
+
(0..quartile_fraction - 1).to_a.each do |index|
|
44
|
+
quartile_sizes[index] += 1
|
45
|
+
end
|
46
|
+
|
47
|
+
quartile_sizes
|
48
|
+
end
|
49
|
+
|
50
|
+
def quartile_one
|
51
|
+
self[0..(self.quartile_sizes[0] - 1)]
|
52
|
+
end
|
53
|
+
|
54
|
+
def quartile_two
|
55
|
+
sizes = self.quartile_sizes
|
56
|
+
start = sizes[0]
|
57
|
+
stop = start + sizes[1] - 1
|
58
|
+
self[start..stop]
|
59
|
+
end
|
60
|
+
|
61
|
+
def quartile_three
|
62
|
+
sizes = self.quartile_sizes
|
63
|
+
start = sizes[0] + sizes[1]
|
64
|
+
stop = start + sizes[2] - 1
|
65
|
+
self[start..stop]
|
66
|
+
end
|
67
|
+
|
68
|
+
def quartile_four
|
69
|
+
sizes = self.quartile_sizes
|
70
|
+
start = sizes[0] + sizes[1] + sizes[2]
|
71
|
+
stop = start + sizes[3] - 1
|
72
|
+
self[start..stop]
|
73
|
+
end
|
74
|
+
|
75
|
+
def compact_min
|
76
|
+
self.compact.min
|
77
|
+
end
|
78
|
+
|
79
|
+
def compact_max
|
80
|
+
self.compact.max
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
|
85
|
+
module Spout
|
86
|
+
module Helpers
|
87
|
+
class ArrayStatistics
|
88
|
+
def self.calculations
|
89
|
+
[["N", :n, :count], ["Mean", :mean, :decimal], ["StdDev", :standard_deviation, :decimal, "± %s"], ["Median", :median, :decimal], ["Min", :compact_min, :decimal], ["Max", :compact_max, :decimal], ["Unknown", :unknown, :count]]
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,395 @@
|
|
1
|
+
require 'spout/helpers/table_formatting'
|
2
|
+
|
3
|
+
module Spout
|
4
|
+
module Helpers
|
5
|
+
class ChartTypes
|
6
|
+
def self.get_bucket(buckets, value)
|
7
|
+
buckets.each do |b|
|
8
|
+
return "#{b[0].round(1)} to #{b[1].round(1)}" if value >= b[0] and value <= b[1]
|
9
|
+
end
|
10
|
+
nil
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.continuous_buckets(values)
|
14
|
+
return [] if values.count == 0
|
15
|
+
minimum_bucket = values.min
|
16
|
+
maximum_bucket = values.max
|
17
|
+
|
18
|
+
max_buckets = [(maximum_bucket - minimum_bucket), 12].min
|
19
|
+
bucket_size = (maximum_bucket - minimum_bucket) / max_buckets
|
20
|
+
buckets = []
|
21
|
+
(0..(max_buckets-1)).to_a.each do |index|
|
22
|
+
start = minimum_bucket + index * bucket_size
|
23
|
+
stop = start + bucket_size
|
24
|
+
buckets << [start,stop]
|
25
|
+
end
|
26
|
+
buckets
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.get_json(file_name, file_type)
|
30
|
+
file = Dir.glob("#{file_type}s/**/#{file_name}.json").first
|
31
|
+
json = JSON.parse(File.read(file)) rescue json = nil
|
32
|
+
json
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.get_variable(variable_name)
|
36
|
+
get_json(variable_name, 'variable')
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.get_domain(json)
|
40
|
+
get_json(json['domain'], 'domain')
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
def self.chart_arbitrary_choices_by_quartile(chart_type, subjects, json, method)
|
45
|
+
# CHART TYPE IS THE QUARTILE VARIABLE
|
46
|
+
return unless chart_variable_json = get_variable(chart_type)
|
47
|
+
return unless domain_json = get_domain(json)
|
48
|
+
|
49
|
+
title = "#{json['display_name']} by #{chart_variable_json['display_name']}"
|
50
|
+
subtitle = "By Visit"
|
51
|
+
# categories = ["Quartile One", "Quartile Two", "Quartile Three", "Quartile Four"]
|
52
|
+
units = 'percent'
|
53
|
+
series = []
|
54
|
+
|
55
|
+
filtered_subjects = subjects.select{ |s| s.send(method) != nil and s.send(chart_type) != nil }.sort_by(&chart_type.to_sym)
|
56
|
+
|
57
|
+
categories = [:quartile_one, :quartile_two, :quartile_three, :quartile_four].collect do |quartile|
|
58
|
+
bucket = filtered_subjects.send(quartile).collect(&chart_type.to_sym)
|
59
|
+
"#{bucket.min} to #{bucket.max}"
|
60
|
+
end
|
61
|
+
|
62
|
+
domain_json.each do |option_hash|
|
63
|
+
data = [:quartile_one, :quartile_two, :quartile_three, :quartile_four].collect do |quartile|
|
64
|
+
filtered_subjects.send(quartile).select{ |s| s.send(method) == option_hash['value'] }.count
|
65
|
+
end
|
66
|
+
|
67
|
+
series << { name: option_hash['display_name'], data: data } unless filtered_subjects.size == 0
|
68
|
+
end
|
69
|
+
|
70
|
+
{ title: title, subtitle: subtitle, categories: categories, units: units, series: series, stacking: 'percent' }
|
71
|
+
end
|
72
|
+
|
73
|
+
def self.chart_arbitrary_by_quartile(chart_type, subjects, json, method, visits)
|
74
|
+
# CHART TYPE IS THE QUARTILE VARIABLE
|
75
|
+
return unless chart_variable_json = get_variable(chart_type)
|
76
|
+
return chart_arbitrary_choices_by_quartile(chart_type, subjects, json, method) if json['type'] == 'choices'
|
77
|
+
|
78
|
+
title = "#{json['display_name']} by #{chart_variable_json['display_name']}"
|
79
|
+
subtitle = "By Visit"
|
80
|
+
categories = ["Quartile One", "Quartile Two", "Quartile Three", "Quartile Four"]
|
81
|
+
units = json["units"]
|
82
|
+
series = []
|
83
|
+
|
84
|
+
series = []
|
85
|
+
|
86
|
+
visits.each do |visit_display_name, visit_value|
|
87
|
+
data = []
|
88
|
+
filtered_subjects = subjects.select{ |s| s._visit == visit_value and s.send(method) != nil and s.send(chart_type) != nil }.sort_by(&chart_type.to_sym)
|
89
|
+
|
90
|
+
[:quartile_one, :quartile_two, :quartile_three, :quartile_four].each do |quartile|
|
91
|
+
array = filtered_subjects.send(quartile).collect(&method.to_sym)
|
92
|
+
data << { y: (array.mean.round(1) rescue 0.0),
|
93
|
+
stddev: ("%0.1f" % array.standard_deviation rescue ''),
|
94
|
+
median: ("%0.1f" % array.median rescue ''),
|
95
|
+
min: ("%0.1f" % array.min rescue ''),
|
96
|
+
max: ("%0.1f" % array.max rescue ''),
|
97
|
+
n: array.n }
|
98
|
+
end
|
99
|
+
|
100
|
+
series << { name: visit_display_name, data: data } unless filtered_subjects.size == 0
|
101
|
+
end
|
102
|
+
|
103
|
+
{ title: title, subtitle: subtitle, categories: categories, units: units, series: series }
|
104
|
+
end
|
105
|
+
|
106
|
+
def self.table_arbitrary_by_quartile(chart_type, subjects, json, method, subtitle = nil)
|
107
|
+
return table_arbitrary_choices_by_quartile(chart_type, subjects, json, method, subtitle) if json['type'] == 'choices'
|
108
|
+
# CHART TYPE IS THE QUARTILE VARIABLE
|
109
|
+
return unless chart_variable_json = get_variable(chart_type)
|
110
|
+
|
111
|
+
|
112
|
+
headers = [
|
113
|
+
[""] + Spout::Helpers::ArrayStatistics::calculations.collect{|calculation_label, calculation_method| calculation_label} + ["Total"]
|
114
|
+
]
|
115
|
+
|
116
|
+
filtered_subjects = subjects.select{ |s| s.send(method) != nil and s.send(chart_type) != nil }.sort_by(&chart_type.to_sym)
|
117
|
+
|
118
|
+
rows = [:quartile_one, :quartile_two, :quartile_three, :quartile_four].collect do |quartile|
|
119
|
+
bucket = filtered_subjects.send(quartile)
|
120
|
+
row_subjects = bucket.collect(&method.to_sym)
|
121
|
+
data = Spout::Helpers::ArrayStatistics::calculations.collect do |calculation_label, calculation_method, calculation_type, calculation_format|
|
122
|
+
TableFormatting::format_number(row_subjects.send(calculation_method), calculation_type, calculation_format)
|
123
|
+
end
|
124
|
+
|
125
|
+
row_name = if row_subjects.size == 0
|
126
|
+
quartile.to_s.capitalize.gsub('_one', ' One').gsub('_two', ' Two').gsub('_three', ' Three').gsub('_four', ' Four')
|
127
|
+
else
|
128
|
+
"#{bucket.collect(&chart_type.to_sym).min} to #{bucket.collect(&chart_type.to_sym).max} #{chart_variable_json['units']}"
|
129
|
+
end
|
130
|
+
|
131
|
+
[row_name] + data + [{ text: TableFormatting::format_number(row_subjects.count, :count), style: 'font-weight:bold'}]
|
132
|
+
end
|
133
|
+
|
134
|
+
total_values = Spout::Helpers::ArrayStatistics::calculations.collect do |calculation_label, calculation_method, calculation_type, calculation_format|
|
135
|
+
total_count = filtered_subjects.collect(&method.to_sym).send(calculation_method)
|
136
|
+
{ text: TableFormatting::format_number(total_count, calculation_type, calculation_format), style: "font-weight:bold" }
|
137
|
+
end
|
138
|
+
|
139
|
+
footers = [
|
140
|
+
[{ text: "Total", style: "font-weight:bold" }] + total_values + [{ text: TableFormatting::format_number(filtered_subjects.count, :count), style: 'font-weight:bold'}]
|
141
|
+
]
|
142
|
+
|
143
|
+
{ title: "#{chart_variable_json['display_name']} vs #{json['display_name']}", subtitle: subtitle, headers: headers, footers: footers, rows: rows }
|
144
|
+
|
145
|
+
end
|
146
|
+
|
147
|
+
def self.table_arbitrary_choices_by_quartile(chart_type, subjects, json, method, subtitle)
|
148
|
+
# CHART TYPE IS THE QUARTILE VARIABLE
|
149
|
+
return unless chart_variable_json = get_variable(chart_type)
|
150
|
+
return unless domain_json = get_domain(json)
|
151
|
+
|
152
|
+
filtered_subjects = subjects.select{ |s| s.send(method) != nil and s.send(chart_type) != nil }.sort_by(&chart_type.to_sym)
|
153
|
+
|
154
|
+
categories = [:quartile_one, :quartile_two, :quartile_three, :quartile_four].collect do |quartile|
|
155
|
+
bucket = filtered_subjects.send(quartile).collect(&chart_type.to_sym)
|
156
|
+
"#{bucket.min} to #{bucket.max} #{chart_variable_json['units']}"
|
157
|
+
end
|
158
|
+
|
159
|
+
headers = [
|
160
|
+
[""] + categories + ["Total"]
|
161
|
+
]
|
162
|
+
|
163
|
+
rows = []
|
164
|
+
|
165
|
+
rows = domain_json.collect do |option_hash|
|
166
|
+
row_subjects = filtered_subjects.select{ |s| s.send(method) == option_hash['value'] }
|
167
|
+
|
168
|
+
data = [:quartile_one, :quartile_two, :quartile_three, :quartile_four].collect do |quartile|
|
169
|
+
bucket = filtered_subjects.send(quartile).select{ |s| s.send(method) == option_hash['value'] }
|
170
|
+
TableFormatting::format_number(bucket.count, :count)
|
171
|
+
end
|
172
|
+
|
173
|
+
[option_hash['display_name']] + data + [{ text: TableFormatting::format_number(row_subjects.count, :count), style: 'font-weight:bold'}]
|
174
|
+
end
|
175
|
+
|
176
|
+
|
177
|
+
total_values = [:quartile_one, :quartile_two, :quartile_three, :quartile_four].collect do |quartile|
|
178
|
+
{ text: TableFormatting::format_number(filtered_subjects.send(quartile).count, :count), style: "font-weight:bold" }
|
179
|
+
end
|
180
|
+
|
181
|
+
footers = [
|
182
|
+
[{ text: "Total", style: "font-weight:bold" }] + total_values + [{ text: TableFormatting::format_number(filtered_subjects.count, :count), style: 'font-weight:bold'}]
|
183
|
+
]
|
184
|
+
|
185
|
+
{ title: "#{json['display_name']} vs #{chart_variable_json['display_name']}", subtitle: subtitle, headers: headers, footers: footers, rows: rows }
|
186
|
+
end
|
187
|
+
|
188
|
+
|
189
|
+
def self.chart_arbitrary_choices(chart_type, subjects, json, method)
|
190
|
+
return unless chart_variable_json = get_variable(chart_type)
|
191
|
+
return unless chart_variable_domain = Spout::Commands::JsonChartsAndTables::domain_array(chart_type)
|
192
|
+
return unless domain_json = get_domain(json)
|
193
|
+
|
194
|
+
|
195
|
+
title = "#{json['display_name']} by #{chart_variable_json['display_name']}"
|
196
|
+
subtitle = "By Visit"
|
197
|
+
categories = chart_variable_domain.collect{|a| a[0]}
|
198
|
+
units = 'percent'
|
199
|
+
series = []
|
200
|
+
|
201
|
+
|
202
|
+
domain_json.each do |option_hash|
|
203
|
+
domain_values = subjects.select{ |s| s.send(method) == option_hash['value'] }
|
204
|
+
|
205
|
+
data = chart_variable_domain.collect do |display_name, value|
|
206
|
+
domain_values.select{ |s| s.send(chart_type) == value }.count
|
207
|
+
end
|
208
|
+
series << { name: option_hash['display_name'], data: data }
|
209
|
+
end
|
210
|
+
|
211
|
+
{ title: title, subtitle: subtitle, categories: categories, units: units, series: series, stacking: 'percent' }
|
212
|
+
end
|
213
|
+
|
214
|
+
def self.chart_arbitrary(chart_type, subjects, json, method, visits)
|
215
|
+
return unless chart_variable_json = get_variable(chart_type)
|
216
|
+
return unless chart_variable_domain = Spout::Commands::JsonChartsAndTables::domain_array(chart_type)
|
217
|
+
return chart_arbitrary_by_quartile(chart_type, subjects, json, method, visits) if ['numeric', 'integer'].include?(chart_variable_json['type'])
|
218
|
+
|
219
|
+
return chart_arbitrary_choices(chart_type, subjects, json, method) if json['type'] == 'choices'
|
220
|
+
|
221
|
+
title = "#{json['display_name']} by #{chart_variable_json['display_name']}"
|
222
|
+
subtitle = "By Visit"
|
223
|
+
categories = []
|
224
|
+
units = json["units"]
|
225
|
+
series = []
|
226
|
+
|
227
|
+
data = []
|
228
|
+
|
229
|
+
visits.each do |visit_display_name, visit_value|
|
230
|
+
visit_subjects = subjects.select{ |s| s._visit == visit_value and s.send(method) != nil }
|
231
|
+
if visit_subjects.count > 0
|
232
|
+
chart_variable_domain.each_with_index do |(display_name, value), index|
|
233
|
+
values = visit_subjects.select{|s| s.send(chart_type) == value }.collect(&method.to_sym)
|
234
|
+
data[index] ||= []
|
235
|
+
data[index] << (values.mean.round(2) rescue 0.0)
|
236
|
+
end
|
237
|
+
categories << visit_display_name
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
chart_variable_domain.each_with_index do |(display_name, value), index|
|
242
|
+
series << { name: display_name, data: data[index] }
|
243
|
+
end
|
244
|
+
|
245
|
+
{ title: title, subtitle: subtitle, categories: categories, units: units, series: series }
|
246
|
+
end
|
247
|
+
|
248
|
+
|
249
|
+
def self.table_arbitrary(chart_type, subjects, json, method, subtitle = nil)
|
250
|
+
return unless chart_variable_json = get_variable(chart_type)
|
251
|
+
return unless chart_variable_domain = Spout::Commands::JsonChartsAndTables::domain_array(chart_type)
|
252
|
+
return table_arbitrary_by_quartile(chart_type, subjects, json, method, subtitle) if ['numeric', 'integer'].include?(chart_variable_json['type'])
|
253
|
+
return table_arbitrary_choices(chart_type, subjects, json, method, subtitle) if json['type'] == 'choices'
|
254
|
+
|
255
|
+
headers = [
|
256
|
+
[""] + Spout::Helpers::ArrayStatistics::calculations.collect{|calculation_label, calculation_method| calculation_label} + ["Total"]
|
257
|
+
]
|
258
|
+
|
259
|
+
filtered_subjects = subjects.select{ |s| s.send(chart_type) != nil }
|
260
|
+
|
261
|
+
rows = chart_variable_domain.collect do |display_name, value|
|
262
|
+
row_subjects = filtered_subjects.select{ |s| s.send(chart_type) == value }
|
263
|
+
|
264
|
+
row_cells = Spout::Helpers::ArrayStatistics::calculations.collect do |calculation_label, calculation_method, calculation_type, calculation_format|
|
265
|
+
count = row_subjects.collect(&method.to_sym).send(calculation_method)
|
266
|
+
(count == 0 && calculation_method == :count) ? { text: '-', class: 'text-muted' } : TableFormatting::format_number(count, calculation_type, calculation_format)
|
267
|
+
end
|
268
|
+
|
269
|
+
[display_name] + row_cells + [{ text: TableFormatting::format_number(row_subjects.count, :count), style: 'font-weight:bold'}]
|
270
|
+
end
|
271
|
+
|
272
|
+
total_values = Spout::Helpers::ArrayStatistics::calculations.collect do |calculation_label, calculation_method, calculation_type, calculation_format|
|
273
|
+
total_count = filtered_subjects.collect(&method.to_sym).send(calculation_method)
|
274
|
+
{ text: TableFormatting::format_number(total_count, calculation_type, calculation_format), style: "font-weight:bold" }
|
275
|
+
end
|
276
|
+
|
277
|
+
footers = [
|
278
|
+
[{ text: "Total", style: "font-weight:bold" }] + total_values + [{ text: TableFormatting::format_number(filtered_subjects.count, :count), style: 'font-weight:bold'}]
|
279
|
+
]
|
280
|
+
|
281
|
+
{ title: "#{chart_variable_json['display_name']} vs #{json['display_name']}", subtitle: subtitle, headers: headers, footers: footers, rows: rows }
|
282
|
+
|
283
|
+
end
|
284
|
+
|
285
|
+
def self.table_arbitrary_choices(chart_type, subjects, json, method, subtitle)
|
286
|
+
return unless chart_variable_json = get_variable(chart_type)
|
287
|
+
return unless chart_variable_domain = Spout::Commands::JsonChartsAndTables::domain_array(chart_type)
|
288
|
+
return unless domain_json = get_domain(json)
|
289
|
+
|
290
|
+
headers = [
|
291
|
+
[""] + chart_variable_domain.collect{|display_name, value| display_name} + ["Total"]
|
292
|
+
]
|
293
|
+
|
294
|
+
filtered_subjects = subjects.select{ |s| s.send(chart_type) != nil }
|
295
|
+
|
296
|
+
rows = domain_json.collect do |option_hash|
|
297
|
+
row_subjects = filtered_subjects.select{ |s| s.send(method) == option_hash['value'] }
|
298
|
+
row_cells = chart_variable_domain.collect do |display_name, value|
|
299
|
+
count = row_subjects.select{ |s| s.send(chart_type) == value }.count
|
300
|
+
count > 0 ? TableFormatting::format_number(count, :count) : { text: '-', class: 'text-muted' }
|
301
|
+
end
|
302
|
+
|
303
|
+
total = row_subjects.count
|
304
|
+
|
305
|
+
[option_hash['display_name']] + row_cells + [total == 0 ? { text: '-', class: 'text-muted' } : { text: TableFormatting::format_number(total, :count), style: 'font-weight:bold'}]
|
306
|
+
end
|
307
|
+
|
308
|
+
if filtered_subjects.select{|s| s.send(method) == nil }.count > 0
|
309
|
+
unknown_values = chart_variable_domain.collect do |display_name, value|
|
310
|
+
{ text: TableFormatting::format_number(filtered_subjects.select{ |s| s.send(chart_type) == value and s.send(method) == nil }.count, :count), class: 'text-muted' }
|
311
|
+
end
|
312
|
+
rows << [{ text: 'Unknown', class: 'text-muted'}] + unknown_values + [ { text: filtered_subjects.select{|s| s.send(method) == nil}.count.to_s, style: 'font-weight:bold', class: 'text-muted' } ]
|
313
|
+
end
|
314
|
+
|
315
|
+
|
316
|
+
|
317
|
+
total_values = chart_variable_domain.collect do |display_name, value|
|
318
|
+
total_count = filtered_subjects.select{|s| s.send(chart_type) == value }.count
|
319
|
+
{ text: (total_count == 0 ? "-" : TableFormatting::format_number(total_count, :count)), style: "font-weight:bold" }
|
320
|
+
end
|
321
|
+
|
322
|
+
footers = [
|
323
|
+
[{ text: "Total", style: "font-weight:bold" }] + total_values + [{ text: TableFormatting::format_number(filtered_subjects.count, :count), style: 'font-weight:bold'}]
|
324
|
+
]
|
325
|
+
|
326
|
+
{ title: "#{json['display_name']} vs #{chart_variable_json['display_name']}", subtitle: subtitle, headers: headers, footers: footers, rows: rows }
|
327
|
+
end
|
328
|
+
|
329
|
+
|
330
|
+
def self.chart_histogram_choices(chart_type, subjects, json, method)
|
331
|
+
return unless domain_json = get_domain(json)
|
332
|
+
return unless chart_variable_json = get_variable(chart_type)
|
333
|
+
return unless chart_variable_domain = Spout::Commands::JsonChartsAndTables::domain_array(chart_type)
|
334
|
+
|
335
|
+
|
336
|
+
title = "#{json['display_name']}"
|
337
|
+
subtitle = "By Visit"
|
338
|
+
|
339
|
+
categories = domain_json.collect{|option_hash| option_hash['display_name']}
|
340
|
+
|
341
|
+
units = "Subjects"
|
342
|
+
series = []
|
343
|
+
|
344
|
+
chart_variable_domain.each do |display_name, value|
|
345
|
+
visit_subjects = subjects.select{ |s| s.send(chart_type) == value and s.send(method) != nil }.collect(&method.to_sym)
|
346
|
+
next unless visit_subjects.size > 0
|
347
|
+
|
348
|
+
data = []
|
349
|
+
|
350
|
+
domain_json.each do |option_hash|
|
351
|
+
data << visit_subjects.select{ |v| v == option_hash['value'] }.count
|
352
|
+
end
|
353
|
+
|
354
|
+
series << { name: display_name, data: data }
|
355
|
+
end
|
356
|
+
|
357
|
+
{ title: title, subtitle: subtitle, categories: categories, units: units, series: series }
|
358
|
+
end
|
359
|
+
|
360
|
+
def self.chart_histogram(chart_type, subjects, json, method)
|
361
|
+
return chart_histogram_choices(chart_type, subjects, json, method) if json['type'] == 'choices'
|
362
|
+
return unless chart_variable_json = get_variable(chart_type)
|
363
|
+
return unless chart_variable_domain = Spout::Commands::JsonChartsAndTables::domain_array(chart_type)
|
364
|
+
|
365
|
+
title = "#{json['display_name']}"
|
366
|
+
subtitle = "By Visit"
|
367
|
+
categories = []
|
368
|
+
units = "Subjects"
|
369
|
+
series = []
|
370
|
+
|
371
|
+
all_subject_values = subjects.collect(&method.to_sym).compact.sort
|
372
|
+
return nil if all_subject_values.count == 0
|
373
|
+
buckets = continuous_buckets(all_subject_values)
|
374
|
+
|
375
|
+
categories = buckets.collect{|b| "#{b[0].round(1)} to #{b[1].round(1)}"}
|
376
|
+
|
377
|
+
chart_variable_domain.each do |display_name, value|
|
378
|
+
visit_subjects = subjects.select{ |s| s.send(chart_type) == value and s.send(method) != nil }.collect(&method.to_sym).sort
|
379
|
+
next unless visit_subjects.size > 0
|
380
|
+
|
381
|
+
data = []
|
382
|
+
|
383
|
+
visit_subjects.group_by{|v| get_bucket(buckets, v) }.each do |key, values|
|
384
|
+
data[categories.index(key)] = values.count if categories.index(key)
|
385
|
+
end
|
386
|
+
|
387
|
+
series << { name: display_name, data: data }
|
388
|
+
end
|
389
|
+
|
390
|
+
{ title: title, subtitle: subtitle, categories: categories, units: units, series: series }
|
391
|
+
end
|
392
|
+
|
393
|
+
end
|
394
|
+
end
|
395
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Spout
|
2
|
+
module Helpers
|
3
|
+
class TableFormatting
|
4
|
+
|
5
|
+
# def initialize(number)
|
6
|
+
# @number = number
|
7
|
+
# end
|
8
|
+
|
9
|
+
def self.number_with_delimiter(number, delimiter = ",")
|
10
|
+
number.to_s.reverse.scan(/(?:\d*\.)?\d{1,3}-?/).join(',').reverse
|
11
|
+
end
|
12
|
+
|
13
|
+
# type: :count or :decimal
|
14
|
+
def self.format_number(number, type, format = nil)
|
15
|
+
if number == nil
|
16
|
+
format_nil(number)
|
17
|
+
elsif type == :count
|
18
|
+
format_count(number)
|
19
|
+
else
|
20
|
+
format_decimal(number, format)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.format_nil(number)
|
25
|
+
'-'
|
26
|
+
end
|
27
|
+
|
28
|
+
# count:
|
29
|
+
# 0 -> '-'
|
30
|
+
# 10 -> '10'
|
31
|
+
# 1000 -> '1,000'
|
32
|
+
# Input (Numeric) -> Output (String)
|
33
|
+
def self.format_count(number)
|
34
|
+
(number == 0 || number == nil) ? '-' : number_with_delimiter(number)
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
# decimal:
|
39
|
+
# 0 -> '0.0'
|
40
|
+
# 10 -> '10.0'
|
41
|
+
# -50.2555 -> '-50.3'
|
42
|
+
# 1000 -> '1,000.0'
|
43
|
+
# 12412423.42252525 -> '12,412,423.4'
|
44
|
+
# Input (Numeric) -> Output (String)
|
45
|
+
def self.format_decimal(number, format)
|
46
|
+
number = self.number_with_delimiter(number.round(1))
|
47
|
+
number = format % number if format
|
48
|
+
number
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
data/lib/spout/tasks/engine.rake
CHANGED
@@ -131,6 +131,13 @@ namespace :spout do
|
|
131
131
|
Spout::Commands::Graphs.new(types, variable_ids, sizes)
|
132
132
|
end
|
133
133
|
|
134
|
+
desc 'Generate JSON charts and tables'
|
135
|
+
task :json do
|
136
|
+
require 'spout/commands/json_charts_and_tables'
|
137
|
+
variables = ENV['variables'].to_s.split(',').collect{|s| s.to_s.downcase}
|
138
|
+
Spout::Commands::JsonChartsAndTables.new(variables)
|
139
|
+
end
|
140
|
+
|
134
141
|
end
|
135
142
|
|
136
143
|
class SpoutCoverageResult
|
data/lib/spout/version.rb
CHANGED
data/spout.gemspec
CHANGED
@@ -29,7 +29,7 @@ Gem::Specification.new do |spec|
|
|
29
29
|
spec.add_dependency "rake"
|
30
30
|
spec.add_dependency "turn"
|
31
31
|
spec.add_dependency "json"
|
32
|
-
spec.add_dependency "colorize", "~> 0.
|
32
|
+
spec.add_dependency "colorize", "~> 0.7.2"
|
33
33
|
|
34
34
|
spec.add_development_dependency "bundler", "~> 1.3"
|
35
35
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: spout
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.8.0.beta1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Remo Mueller
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-
|
11
|
+
date: 2014-05-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rake
|
@@ -58,14 +58,14 @@ dependencies:
|
|
58
58
|
requirements:
|
59
59
|
- - "~>"
|
60
60
|
- !ruby/object:Gem::Version
|
61
|
-
version: 0.
|
61
|
+
version: 0.7.2
|
62
62
|
type: :runtime
|
63
63
|
prerelease: false
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
66
|
- - "~>"
|
67
67
|
- !ruby/object:Gem::Version
|
68
|
-
version: 0.
|
68
|
+
version: 0.7.2
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
70
|
name: bundler
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
@@ -98,7 +98,12 @@ files:
|
|
98
98
|
- lib/spout/actions.rb
|
99
99
|
- lib/spout/application.rb
|
100
100
|
- lib/spout/commands/graphs.rb
|
101
|
+
- lib/spout/commands/json_charts_and_tables.rb
|
102
|
+
- lib/spout/helpers/array_statistics.rb
|
103
|
+
- lib/spout/helpers/chart_types.rb
|
104
|
+
- lib/spout/helpers/table_formatting.rb
|
101
105
|
- lib/spout/hidden_reporter.rb
|
106
|
+
- lib/spout/models/subject.rb
|
102
107
|
- lib/spout/support/javascripts/data.js
|
103
108
|
- lib/spout/support/javascripts/highcharts-convert.js
|
104
109
|
- lib/spout/support/javascripts/highcharts-more.js
|
@@ -142,9 +147,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
142
147
|
version: '0'
|
143
148
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
144
149
|
requirements:
|
145
|
-
- - "
|
150
|
+
- - ">"
|
146
151
|
- !ruby/object:Gem::Version
|
147
|
-
version:
|
152
|
+
version: 1.3.1
|
148
153
|
requirements: []
|
149
154
|
rubyforge_project:
|
150
155
|
rubygems_version: 2.2.2
|