spout 0.7.0 → 0.8.0.beta1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 700e0115db7d7231bd257fe56a4d7f4bff17ce01
4
- data.tar.gz: e82a19b250ef2d068efa93d6acb9dc215e18c09b
3
+ metadata.gz: 55751ba1e111b174e9501fb52b1dd9a484b9ee1c
4
+ data.tar.gz: ca81a991a37e425a4166dd28e6aeffa183378b1c
5
5
  SHA512:
6
- metadata.gz: a65d3873dcf0ab0c85673ac04761f97ba838e6a3415b02b5fa86d8024a2415b412267b50d1f818df02062dd7be48ae9d7a6c04a538af6fe17b1397c45670898f
7
- data.tar.gz: 96843da45ebe630f2c4ba2eee3aa276f5c7735cc2fe731d082b09ff1ad209941123475de16e5b1d7e76d779d13483dc2857c68ef8c9a491347ec8e3e726c239d
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
@@ -0,0 +1,17 @@
1
+ # subject.rb
2
+
3
+ module Spout
4
+ module Models
5
+
6
+ class Subject
7
+ attr_accessor :_visit
8
+
9
+ def self.create(&block)
10
+ subject = self.new
11
+ yield subject if block_given?
12
+ subject
13
+ end
14
+
15
+ end
16
+ end
17
+ end
@@ -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
@@ -9,3 +9,4 @@
9
9
  /coverage
10
10
  /csvs
11
11
  /graphs
12
+ /charts
data/lib/spout/version.rb CHANGED
@@ -1,9 +1,9 @@
1
1
  module Spout
2
2
  module VERSION #:nodoc:
3
3
  MAJOR = 0
4
- MINOR = 7
4
+ MINOR = 8
5
5
  TINY = 0
6
- BUILD = nil # nil, "pre", "rc", "rc2"
6
+ BUILD = "beta1" # nil, "pre", "rc", "rc2"
7
7
 
8
8
  STRING = [MAJOR, MINOR, TINY, BUILD].compact.join('.')
9
9
  end
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.6.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.7.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-04-16 00:00:00.000000000 Z
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.6.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.6.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: '0'
152
+ version: 1.3.1
148
153
  requirements: []
149
154
  rubyforge_project:
150
155
  rubygems_version: 2.2.2