rails-data-explorer 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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