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 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