rails-data-explorer 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. data/CHANGELOG.md +5 -1
  2. data/README.md +11 -0
  3. data/Rakefile +62 -0
  4. data/doc/how_to/release.md +23 -0
  5. data/doc/how_to/trouble_when_packaging_assets.md +8 -0
  6. data/lib/rails-data-explorer-no-rails.rb +42 -0
  7. data/lib/rails-data-explorer.rb +5 -9
  8. data/lib/rails-data-explorer/chart/box_plot.rb +5 -1
  9. data/lib/rails-data-explorer/chart/box_plot_group.rb +22 -5
  10. data/lib/rails-data-explorer/chart/contingency_table.rb +45 -10
  11. data/lib/rails-data-explorer/chart/histogram_categorical.rb +104 -3
  12. data/lib/rails-data-explorer/chart/histogram_quantitative.rb +99 -2
  13. data/lib/rails-data-explorer/chart/histogram_temporal.rb +10 -55
  14. data/lib/rails-data-explorer/chart/parallel_coordinates.rb +4 -0
  15. data/lib/rails-data-explorer/chart/parallel_set.rb +4 -0
  16. data/lib/rails-data-explorer/chart/pie_chart.rb +89 -8
  17. data/lib/rails-data-explorer/chart/scatterplot.rb +110 -8
  18. data/lib/rails-data-explorer/chart/stacked_bar_chart_categorical_percent.rb +133 -14
  19. data/lib/rails-data-explorer/data_series.rb +37 -2
  20. data/lib/rails-data-explorer/data_type/categorical.rb +72 -2
  21. data/lib/rails-data-explorer/data_type/quantitative.rb +41 -12
  22. data/lib/rails-data-explorer/data_type/quantitative/temporal.rb +3 -2
  23. data/lib/rails-data-explorer/exploration.rb +5 -1
  24. data/lib/rails-data-explorer/utils/data_binner.rb +31 -0
  25. data/lib/rails-data-explorer/utils/data_quantizer.rb +66 -0
  26. data/lib/rails_data_explorer.rb +133 -0
  27. data/rails-data-explorer.gemspec +4 -4
  28. data/spec/helper.rb +7 -0
  29. data/spec/helper_no_rails.rb +10 -0
  30. data/spec/rails-data-explorer/data_series_spec.rb +45 -0
  31. data/spec/rails-data-explorer/data_type/categorical_spec.rb +34 -0
  32. data/spec/rails-data-explorer/exploration_spec.rb +55 -0
  33. data/spec/rails-data-explorer/utils/data_binner_spec.rb +29 -0
  34. data/spec/rails-data-explorer/utils/data_quantizer_spec.rb +71 -0
  35. data/vendor/assets/javascripts/packaged/rails-data-explorer.min.js +1 -0
  36. data/vendor/assets/javascripts/rails-data-explorer.js +6 -5
  37. data/vendor/assets/javascripts/{d3.boxplot.js → sources/d3.boxplot.js} +10 -3
  38. data/vendor/assets/javascripts/{d3.parcoords.js → sources/d3.parcoords.js} +1 -1
  39. data/vendor/assets/javascripts/{d3.parsets.js → sources/d3.parsets.js} +3 -3
  40. data/vendor/assets/javascripts/{d3.v3.js → sources/d3.v3.js} +0 -0
  41. data/vendor/assets/javascripts/{nv.d3.js → sources/nv.d3.js} +0 -0
  42. data/vendor/assets/javascripts/sources/vega.js +7040 -0
  43. data/vendor/assets/stylesheets/packaged/rails-data-explorer.min.css +9 -0
  44. data/vendor/assets/stylesheets/rails-data-explorer.css +7 -7
  45. data/vendor/assets/stylesheets/{bootstrap-theme.css → sources/bootstrap-theme.css} +0 -0
  46. data/vendor/assets/stylesheets/{bootstrap.css → sources/bootstrap.css} +0 -0
  47. data/vendor/assets/stylesheets/{d3.boxplot.css → sources/d3.boxplot.css} +0 -0
  48. data/vendor/assets/stylesheets/{d3.parcoords.css → sources/d3.parcoords.css} +0 -0
  49. data/vendor/assets/stylesheets/{d3.parsets.css → sources/d3.parsets.css} +0 -0
  50. data/vendor/assets/stylesheets/{nv.d3.css → sources/nv.d3.css} +0 -0
  51. data/vendor/assets/stylesheets/{rde-default-style.css → sources/rde-default-style.css} +0 -0
  52. metadata +65 -28
@@ -17,6 +17,7 @@ class RailsDataExplorer
17
17
 
18
18
  x_ds = x_candidates.first
19
19
  y_ds = (y_candidates - [x_ds]).first
20
+ return false if x_ds.nil? || y_ds.nil?
20
21
 
21
22
  # initialize data_matrix
22
23
  data_matrix = { :_sum => { :_sum => 0 } }
@@ -38,25 +39,30 @@ class RailsDataExplorer
38
39
  data_matrix[:_sum][:_sum] += 1
39
40
  }
40
41
 
41
- x_sorted_keys = x_ds.uniq_vals.sort { |a,b|
42
- data_matrix[b][:_sum] <=> data_matrix[a][:_sum]
43
- }
44
- y_sorted_keys = y_ds.uniq_vals.sort { |a,b|
45
- data_matrix[:_sum][b] <=> data_matrix[:_sum][a]
46
- }
42
+ x_sorted_keys = x_ds.uniq_vals.sort(
43
+ &x_ds.label_sorter(
44
+ nil,
45
+ lambda { |a,b| data_matrix[b][:_sum] <=> data_matrix[a][:_sum] }
46
+ )
47
+ )
48
+ y_sorted_keys = y_ds.uniq_vals.sort(
49
+ &y_ds.label_sorter(
50
+ nil,
51
+ lambda { |a,b| data_matrix[:_sum][b] <=> data_matrix[:_sum][a] }
52
+ )
53
+ )
47
54
 
48
55
  values = case @data_set.dimensions_count
49
56
  when 2
50
57
  y_sorted_keys.map { |y_val|
51
- {
52
- key: y_val,
53
- values: x_sorted_keys.map { |x_val|
54
- {
55
- x: x_val,
56
- y: (data_matrix[x_val][y_val] / data_matrix[x_val][:_sum].to_f) }
58
+ x_sorted_keys.map { |x_val|
59
+ {
60
+ x: x_val,
61
+ y: (data_matrix[x_val][y_val] / data_matrix[x_val][:_sum].to_f) * 100,
62
+ c: y_val
57
63
  }
58
64
  }
59
- }
65
+ }.flatten
60
66
  else
61
67
  raise(ArgumentError.new("Exactly two data series required for contingency table."))
62
68
  end
@@ -72,8 +78,115 @@ class RailsDataExplorer
72
78
  def render
73
79
  return '' unless render?
74
80
  ca = compute_chart_attrs
81
+ return '' unless ca
82
+ render_vega(ca)
83
+ end
84
+
85
+ def render_vega(ca)
75
86
  %(
76
- <div class="rde-chart rde-bar-chart">
87
+ <div class="rde-chart rde-stacked-bar-chart-categorical-percent">
88
+ <h3 class="rde-chart-title">Stacked Bar Chart</h3>
89
+ <div id="#{ dom_id }"></div>
90
+ <script type="text/javascript">
91
+ (function() {
92
+ var spec = {
93
+ "width": 800,
94
+ "height": 200,
95
+ "padding": {"top": 10, "left": 50, "bottom": 50, "right": 100},
96
+ "data": [
97
+ {
98
+ "name": "table",
99
+ "values": #{ ca[:values].to_json }
100
+ },
101
+ {
102
+ "name": "stats",
103
+ "source": "table",
104
+ "transform": [
105
+ {"type": "facet", "keys": ["data.x"]},
106
+ {"type": "stats", "value": "data.y"}
107
+ ]
108
+ }
109
+ ],
110
+ "scales": [
111
+ {
112
+ "name": "x",
113
+ "type": "ordinal",
114
+ "range": "width",
115
+ "domain": {"data": "table", "field": "data.x"}
116
+ },
117
+ {
118
+ "name": "y",
119
+ "type": "linear",
120
+ "range": "height",
121
+ "nice": true,
122
+ "domain": {"data": "stats", "field": "sum"}
123
+ },
124
+ {
125
+ "name": "color",
126
+ "type": "ordinal",
127
+ "range": "category10"
128
+ }
129
+ ],
130
+ "axes": [
131
+ {
132
+ "type": "x",
133
+ "scale": "x",
134
+ "title": "#{ ca[:x_axis_label] }",
135
+ "format": #{ ca[:x_axis_tick_format] },
136
+ },
137
+ {
138
+ "type": "y",
139
+ "scale": "y",
140
+ "title": "#{ ca[:y_axis_label] }",
141
+ "format": #{ ca[:y_axis_tick_format] },
142
+ }
143
+ ],
144
+ "marks": [
145
+ {
146
+ "type": "group",
147
+ "from": {
148
+ "data": "table",
149
+ "transform": [
150
+ {"type": "facet", "keys": ["data.c"]},
151
+ {"type": "stack", "point": "data.x", "height": "data.y"}
152
+ ]
153
+ },
154
+ "marks": [
155
+ {
156
+ "type": "rect",
157
+ "properties": {
158
+ "enter": {
159
+ "x": {"scale": "x", "field": "data.x"},
160
+ "width": {"scale": "x", "band": true, "offset": -1},
161
+ "y": {"scale": "y", "field": "y"},
162
+ "y2": {"scale": "y", "field": "y2"},
163
+ "fill": {"scale": "color", "field": "data.c"}
164
+ },
165
+ }
166
+ }
167
+ ]
168
+ }
169
+ ],
170
+ "legends": [
171
+ {
172
+ "fill": "color",
173
+ }
174
+ ],
175
+ };
176
+
177
+ vg.parse.spec(spec, function(chart) {
178
+ var view = chart({ el:"##{ dom_id }" }).update();
179
+ });
180
+
181
+ })();
182
+ </script>
183
+ </div>
184
+ )
185
+ end
186
+
187
+ def render_nvd3(ca)
188
+ %(
189
+ <div class="rde-chart rde-stacked-bar-chart-categorical-percent">
77
190
  <h3 class="rde-chart-title">Stacked Bar Chart</h3>
78
191
  <div id="#{ dom_id }", style="height: 200px;">
79
192
  <svg></svg>
@@ -98,6 +211,12 @@ class RailsDataExplorer
98
211
 
99
212
  chart.multibar.stacked(true);
100
213
  chart.showControls(false);
214
+ chart.tooltipContent(
215
+ function(key, x, y, e, graph) {
216
+ return '<p>' + key + '</p>' + '<p>' + y + ' of ' + x + '</p>'
217
+ }
218
+ );
219
+
101
220
 
102
221
  d3.select('##{ dom_id } svg')
103
222
  .datum(data)
@@ -8,6 +8,19 @@ class RailsDataExplorer
8
8
  delegate :available_chart_types, :to => :data_type, :prefix => false
9
9
  delegate :available_chart_roles, :to => :data_type, :prefix => false
10
10
 
11
+ # Any data series with a dynamic range greater than this is considered
12
+ # having a large dynamic range
13
+ # We consider dynamic range the ratio between the largest and the smallest value.
14
+ def self.large_dynamic_range_cutoff
15
+ 1000.0
16
+ end
17
+
18
+ # Any data series with more than this uniq vals is considered having many
19
+ # uniq values.
20
+ def self.many_uniq_vals_cutoff
21
+ 20
22
+ end
23
+
11
24
  # options: :chart_roles, :data_type (all optional)
12
25
  def initialize(_name, _values, options={})
13
26
  options = { chart_roles: [], data_type: nil }.merge(options)
@@ -53,6 +66,10 @@ class RailsDataExplorer
53
66
  data_type.axis_tick_format(values)
54
67
  end
55
68
 
69
+ def axis_scale
70
+ data_type.axis_scale(self)
71
+ end
72
+
56
73
  def uniq_vals
57
74
  @uniq_vals = values.uniq
58
75
  end
@@ -69,6 +86,23 @@ class RailsDataExplorer
69
86
  @max_val = values.compact.max
70
87
  end
71
88
 
89
+ # Used to decide whether we can render certain chart types
90
+ def has_many_uniq_vals?
91
+ uniq_vals_count > self.class.many_uniq_vals_cutoff
92
+ end
93
+
94
+ def dynamic_range
95
+ max_val / [min_val, max_val].min.to_f
96
+ end
97
+
98
+ def has_large_dynamic_range?
99
+ dynamic_range > self.class.large_dynamic_range_cutoff
100
+ end
101
+
102
+ def label_sorter(label_val_key, value_sorter)
103
+ data_type.label_sorter(label_val_key, self, value_sorter)
104
+ end
105
+
72
106
  private
73
107
 
74
108
  # @param[Array<Symbol>] chart_role_overrides, :x, :y, :color
@@ -94,14 +128,15 @@ class RailsDataExplorer
94
128
 
95
129
  def init_data_type(data_type_override)
96
130
  if data_type_override.nil?
97
- case values.first
131
+ first_value = values.detect { |e| !e.nil? }
132
+ case first_value
98
133
  when Integer, Bignum, Fixnum
99
134
  DataType::Quantitative::Integer.new
100
135
  when Float
101
136
  DataType::Quantitative::Decimal.new
102
137
  when String
103
138
  DataType::Categorical.new
104
- when DateTime, ActiveSupport::TimeWithZone
139
+ when Time, DateTime, ActiveSupport::TimeWithZone
105
140
  DataType::Quantitative::Temporal.new
106
141
  else
107
142
  raise(ArgumentError.new("Can't infer data type for value: #{ values.first.class.inspect }"))
@@ -70,14 +70,22 @@ class RailsDataExplorer
70
70
  m << { :label => "#{ k.to_s }_percent", :value => (v / total_count.to_f) * 100, :ruby_formatter => ruby_formatters[:percent] }
71
71
  m
72
72
  }.sort { |a,b| b[:value] <=> a[:value] }
73
- r.insert(0, { :label => 'Total_count', :value => total_count, :ruby_formatter => ruby_formatters[:integer] })
74
- r.insert(0, { :label => 'Total_percent', :value => 100, :ruby_formatter => ruby_formatters[:percent] })
73
+ r.insert(0, { :label => '[Total]_count', :value => total_count, :ruby_formatter => ruby_formatters[:integer] })
74
+ r.insert(0, { :label => '[Total]_percent', :value => 100, :ruby_formatter => ruby_formatters[:percent] })
75
75
  r
76
76
  end
77
77
 
78
78
  # Returns an OpenStruct that describes a statistics table.
79
79
  def descriptive_statistics_table(values)
80
80
  desc_stats = descriptive_statistics(values)
81
+ if desc_stats.length < DataSeries.many_uniq_vals_cutoff
82
+ descriptive_statistics_table_horizontal(desc_stats)
83
+ else
84
+ descriptive_statistics_table_vertical(desc_stats)
85
+ end
86
+ end
87
+
88
+ def descriptive_statistics_table_horizontal(desc_stats)
81
89
  labels = desc_stats.map { |e| e[:label].gsub(/_count|_percent/, '') }.uniq
82
90
  table = OpenStruct.new(
83
91
  :rows => []
@@ -108,10 +116,72 @@ class RailsDataExplorer
108
116
  table
109
117
  end
110
118
 
119
+ def descriptive_statistics_table_vertical(desc_stats)
120
+ labels = desc_stats.map { |e| e[:label].gsub(/_count|_percent/, '') }.uniq
121
+ table = OpenStruct.new(
122
+ :rows => []
123
+ )
124
+ table.rows << OpenStruct.new(
125
+ :css_class => 'rde-column_header',
126
+ :tag => :tr,
127
+ :cells => %w[Value Count Percent].map { |label|
128
+ OpenStruct.new(:value => label, :tag => :th, :css_class => 'rde-cell-label')
129
+ }
130
+ )
131
+ labels.each { |label|
132
+ count_stat = desc_stats.detect { |e| "#{ label }_count" == e[:label] }
133
+ percent_stat = desc_stats.detect { |e| "#{ label }_percent" == e[:label] }
134
+ table.rows << OpenStruct.new(
135
+ :css_class => 'rde-data_row',
136
+ :tag => :tr,
137
+ :cells => [
138
+ OpenStruct.new(:value => label, :tag => :td, :css_class => 'rde-cell-value'),
139
+ OpenStruct.new(
140
+ :value => count_stat[:value],
141
+ :ruby_formatter => count_stat[:ruby_formatter],
142
+ :tag => :td,
143
+ :css_class => 'rde-cell-value'
144
+ ),
145
+ OpenStruct.new(
146
+ :value => percent_stat[:value],
147
+ :ruby_formatter => percent_stat[:ruby_formatter],
148
+ :tag => :td,
149
+ :css_class => 'rde-cell-value'
150
+ ),
151
+ ]
152
+ )
153
+ }
154
+ table
155
+ end
156
+
111
157
  def axis_tick_format(values)
112
158
  %(function(d) { return d })
113
159
  end
114
160
 
161
+ # @param[Symbol, nil] label_val_key the hash key to use to get the label value during sort (sent to a,b)
162
+ # @param[DataSeries] data_series the ds that contains the uniq vals
163
+ # @param[Proc] value_sorter the sorting proc to use if not sorted numerically
164
+ # @return[Proc] a Proc that will be used by #sort
165
+ def label_sorter(label_val_key, data_series, value_sorter)
166
+ if data_series.uniq_vals.any? { |e| e.to_s =~ /^\d+/ }
167
+ # Sort numerical categories by key ASC
168
+ lambda { |a,b|
169
+ number_extractor = lambda { |val|
170
+ str = label_val_key ? val[label_val_key] : val
171
+ number = str.gsub(/^[^\d]*/, '').to_f
172
+ number += 1 if str =~ /^>/ # increase highest threshold by one for proper sorting
173
+ number
174
+ }
175
+ a_number = number_extractor.call(a)
176
+ b_number = number_extractor.call(b)
177
+ a_number <=> b_number
178
+ }
179
+ else
180
+ # Use provided value sorter
181
+ value_sorter
182
+ end
183
+ end
184
+
115
185
  end
116
186
  end
117
187
  end
@@ -6,12 +6,12 @@ class RailsDataExplorer
6
6
 
7
7
  def all_available_chart_types
8
8
  [
9
- {
10
- chart_class: Chart::BoxPlot,
11
- chart_roles: [:y],
12
- dimensions_count_min: 1,
13
- dimensions_count_max: 1
14
- },
9
+ # {
10
+ # chart_class: Chart::BoxPlot,
11
+ # chart_roles: [:y],
12
+ # dimensions_count_min: 1,
13
+ # dimensions_count_max: 1
14
+ # },
15
15
  {
16
16
  chart_class: Chart::HistogramQuantitative,
17
17
  chart_roles: [:x],
@@ -44,34 +44,54 @@ class RailsDataExplorer
44
44
  end
45
45
 
46
46
  def descriptive_statistics(values)
47
- stats = ::DescriptiveStatistics::Stats.new(values)
47
+ non_nil_values = values.find_all { |e| !(e.nil? || Float::NAN == e) }
48
+ stats = ::DescriptiveStatistics::Stats.new(non_nil_values)
48
49
  ruby_formatters = {
49
- :integer => Proc.new { |v| number_with_delimiter(v.round) },
50
- :decimal => Proc.new { |v| number_with_precision(v, :precision => 3, :significant => true, :strip_insignificant_zeros => true, :delimiter => ',') },
51
- :pass_through => Proc.new { |v| v },
50
+ :integer => Proc.new { |v|
51
+ v.nil? ? 'Null' : number_with_delimiter(v.round)
52
+ },
53
+ :decimal => Proc.new { |v|
54
+ case
55
+ when v.nil?
56
+ 'Null'
57
+ when v.is_a?(Float) && v.nan?
58
+ 'NaN'
59
+ else
60
+ number_with_precision(
61
+ v,
62
+ :precision => 3,
63
+ :significant => true,
64
+ :strip_insignificant_zeros => true,
65
+ :delimiter => ','
66
+ )
67
+ end
68
+ },
69
+ :pass_through => Proc.new { |v| (v.nil? || Float::NAN == v) ? 'NaN' : v },
52
70
  }
53
71
  [
54
72
  { :label => 'Min', :value => stats.min, :ruby_formatter => ruby_formatters[:decimal], :table_row => 1 },
55
73
  { :label => '1%ile', :value => stats.value_from_percentile(1), :ruby_formatter => ruby_formatters[:decimal], :table_row => 1 },
74
+ { :label => '5%ile', :value => stats.value_from_percentile(5), :ruby_formatter => ruby_formatters[:decimal], :table_row => 1 },
56
75
  { :label => '10%ile', :value => stats.value_from_percentile(10), :ruby_formatter => ruby_formatters[:decimal], :table_row => 1 },
57
76
  { :label => '25%ile', :value => stats.value_from_percentile(25), :ruby_formatter => ruby_formatters[:decimal], :table_row => 1 },
58
77
  { :label => 'Median', :value => stats.median, :ruby_formatter => ruby_formatters[:decimal], :table_row => 1 },
59
78
  { :label => '75%ile', :value => stats.value_from_percentile(75), :ruby_formatter => ruby_formatters[:decimal], :table_row => 1 },
60
79
  { :label => '90%ile', :value => stats.value_from_percentile(90), :ruby_formatter => ruby_formatters[:decimal], :table_row => 1 },
80
+ { :label => '95%ile', :value => stats.value_from_percentile(95), :ruby_formatter => ruby_formatters[:decimal], :table_row => 1 },
61
81
  { :label => '99%ile', :value => stats.value_from_percentile(99), :ruby_formatter => ruby_formatters[:decimal], :table_row => 1 },
62
82
  { :label => 'Max', :value => stats.max, :ruby_formatter => ruby_formatters[:decimal], :table_row => 1 },
63
- { :label => '', :value => '', :ruby_formatter => ruby_formatters[:pass_through], :table_row => 1 },
64
83
 
65
84
  { :label => 'Range', :value => stats.range, :ruby_formatter => ruby_formatters[:decimal], :table_row => 2 },
66
85
  { :label => 'Mean', :value => stats.mean, :ruby_formatter => ruby_formatters[:decimal], :table_row => 2 },
67
86
  { :label => 'Mode', :value => stats.mode, :ruby_formatter => ruby_formatters[:decimal], :table_row => 2 },
68
87
  { :label => 'Count', :value => values.length, :ruby_formatter => ruby_formatters[:integer], :table_row => 2 },
69
- { :label => 'Sum', :value => values.inject(0) { |m,e| m += e }, :ruby_formatter => ruby_formatters[:decimal], :table_row => 2 },
88
+ { :label => 'Sum', :value => non_nil_values.inject(0) { |m,e| m += e }, :ruby_formatter => ruby_formatters[:decimal], :table_row => 2 },
70
89
  { :label => 'Variance', :value => stats.variance, :ruby_formatter => ruby_formatters[:decimal], :table_row => 2 },
71
90
  { :label => 'Std. dev.', :value => stats.standard_deviation, :ruby_formatter => ruby_formatters[:decimal], :table_row => 2 },
72
91
  { :label => 'Rel. std. dev.', :value => stats.relative_standard_deviation, :ruby_formatter => ruby_formatters[:decimal], :table_row => 2 },
73
92
  { :label => 'Skewness', :value => stats.skewness, :ruby_formatter => ruby_formatters[:decimal], :table_row => 2 },
74
93
  { :label => 'Kurtosis', :value => stats.kurtosis, :ruby_formatter => ruby_formatters[:decimal], :table_row => 2 },
94
+ { :label => '', :value => '', :ruby_formatter => ruby_formatters[:pass_through], :table_row => 2 },
75
95
  ]
76
96
  end
77
97
 
@@ -104,6 +124,15 @@ class RailsDataExplorer
104
124
  raise "Implement me in sub_class"
105
125
  end
106
126
 
127
+ def axis_scale(data_series)
128
+ # Log scales can't handle 0 values
129
+ if data_series.min_val > 0.0 && data_series.has_large_dynamic_range?
130
+ 'd3.scale.log'
131
+ else
132
+ 'd3.scale.linear'
133
+ end
134
+ end
135
+
107
136
  end
108
137
  end
109
138
  end
@@ -26,10 +26,11 @@ class RailsDataExplorer
26
26
  end
27
27
 
28
28
  def descriptive_statistics(values)
29
+ non_nil_values = values.find_all { |e| !e.nil? }
29
30
  ruby_formatter = Proc.new { |v| v.nil? ? '' : v.strftime('%a, %b %e, %Y, %l:%M:%S %p %Z') }
30
31
  [
31
- { :label => 'Min', :value => values.min, :ruby_formatter => ruby_formatter },
32
- { :label => 'Max', :value => values.max, :ruby_formatter => ruby_formatter },
32
+ { :label => 'Min', :value => non_nil_values.min, :ruby_formatter => ruby_formatter },
33
+ { :label => 'Max', :value => non_nil_values.max, :ruby_formatter => ruby_formatter },
33
34
  { :label => 'Count', :value => values.length, :ruby_formatter => Proc.new { |e| number_with_delimiter(e) } },
34
35
  ]
35
36
  end