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
@@ -29,7 +29,11 @@ class RailsDataExplorer
29
29
  content_tag(:h2, @title, :class => 'rde-exploration-title panel-title')
30
30
  end +
31
31
  content_tag(:div, :class => 'panel-body') do
32
- @charts.map { |e| e.render }.join.html_safe
32
+ if @charts.any?
33
+ @charts.map { |e| e.render }.join.html_safe
34
+ else
35
+ "No charts are available for this combination of data series."
36
+ end
33
37
  end
34
38
  end.html_safe
35
39
  end
@@ -6,3 +6,34 @@
6
6
  # * 21 - 30
7
7
  # * 31 - 40
8
8
  # * > 40
9
+ class RailsDataExplorer
10
+ module Utils
11
+ class DataBinner
12
+
13
+ def initialize(*args)
14
+ @max = -Float::INFINITY
15
+ @bin_specs = [*args].compact.uniq.sort.map { |e|
16
+ case e
17
+ when Numeric
18
+ @max = [@max, e].max
19
+ { :label => "#{ e.to_s } or less", :lte => e }
20
+ else
21
+ raise "Handle this bin_spec: #{ e.inspect }"
22
+ end
23
+ }
24
+ @bin_specs << { :label => "> #{ @max }", :gt => @max }
25
+ end
26
+
27
+ def bin(value)
28
+ unless value.is_a?(Numeric)
29
+ raise(ArgumentError.new("Wrong type of value, numeric expected, got: #{ value.inspect }"))
30
+ end
31
+ bin = @bin_specs.detect { |bs|
32
+ (bs[:lte] && value <= bs[:lte]) || (bs[:gt] && value > bs[:gt])
33
+ }
34
+ bin[:label]
35
+ end
36
+
37
+ end
38
+ end
39
+ end
@@ -1,2 +1,68 @@
1
1
  # Map a large set of quantitative/temporal/geo input values to a (countable)
2
2
  # smaller set – such as rounding values to some unit of precision.
3
+ class RailsDataExplorer
4
+ module Utils
5
+ class DataQuantizer
6
+
7
+ attr_accessor :number_of_bins, :delta
8
+
9
+ def initialize(data_series, options = {})
10
+ @options = {
11
+ :nice => true,
12
+ :type => 'midtread', # 'midtread' or 'midrise'
13
+ :number_of_bins => 100, # assuming 800px wide chart, 8px per bin
14
+ :delta => nil,
15
+ }.merge(options)
16
+ @data_series = data_series
17
+ @number_of_bins = @options[:number_of_bins]
18
+ init_attrs
19
+ end
20
+
21
+ def init_attrs
22
+ # Compute boundaries
23
+ if @options[:nice]
24
+ range = @data_series.max_val - @data_series.min_val
25
+ rounding_factor = 10.0 ** Math.log10(range).floor
26
+ @min_val = (@data_series.min_val / rounding_factor).floor * rounding_factor
27
+ @max_val = (@data_series.max_val / rounding_factor).ceil * rounding_factor
28
+ else
29
+ @min_val = @data_series.min_val
30
+ @max_val = @data_series.max_val
31
+ end
32
+ # Compute delta
33
+ @delta = if @options[:delta]
34
+ @options[:delta]
35
+ else
36
+ (@max_val - @min_val) / @number_of_bins.to_f
37
+ end
38
+ end
39
+
40
+ def values
41
+ @values ||= (
42
+ case @options[:type]
43
+ when 'midrise'
44
+ @data_series.values.map { |e|
45
+ index_of_quantized_value = ((e - @min_val) / @delta).round
46
+ (
47
+ (index_of_quantized_value * @delta) +
48
+ (@delta / 2.0) +
49
+ @min_val
50
+ )
51
+ }
52
+ when 'midtread'
53
+ @data_series.values.map { |e|
54
+ index_of_quantized_value = ((e - @min_val) / @delta).round
55
+ (
56
+ (index_of_quantized_value * @delta) +
57
+ @min_val
58
+ )
59
+ }
60
+ else
61
+ raise "Handle this type: #{ @options[:type].inspect }"
62
+ end
63
+ )
64
+ end
65
+
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,133 @@
1
+ class RailsDataExplorer
2
+
3
+ attr_accessor :output_buffer # required for content_tag
4
+ include ActionView::Helpers::TagHelper
5
+
6
+ attr_reader :explorations
7
+
8
+ def initialize(data_collection, data_series_specs)
9
+ @explorations = []
10
+ univariate = []
11
+ bivariate = {}
12
+ multivariate = {}
13
+
14
+ data_series_specs.each do |data_series_spec|
15
+ ds_spec = {
16
+ :univariate => true,
17
+ :bivariate => true,
18
+ }.merge(data_series_spec)
19
+ univariate << ds_spec.dup if ds_spec[:univariate]
20
+
21
+ if ds_spec[:bivariate]
22
+ [*ds_spec[:bivariate]].each { |group_key|
23
+ group_key = group_key.to_s
24
+ bivariate[group_key] ||= []
25
+ bivariate[group_key] << ds_spec.dup
26
+ }
27
+ end
28
+
29
+ if ds_spec[:multivariate]
30
+ [*ds_spec[:multivariate]].each { |group_key|
31
+ group_key = group_key.to_s
32
+ multivariate[group_key] ||= []
33
+ multivariate[group_key] << ds_spec.dup
34
+ }
35
+ end
36
+ end
37
+
38
+ univariate.uniq.compact.each { |data_series_spec|
39
+ @explorations << Exploration.new(
40
+ data_series_spec[:name],
41
+ data_collection.map(&data_series_spec[:data_method]),
42
+ )
43
+ }
44
+
45
+ bivariate.each { |group_key, bv_data_series_specs|
46
+ next unless group_key # skip if key is falsey
47
+ bv_data_series_specs.uniq.compact.combination(2) { |ds_specs_pair|
48
+ @explorations << build_exploration_from_data_series_specs(
49
+ data_collection,
50
+ ds_specs_pair
51
+ )
52
+ }
53
+ }
54
+
55
+ multivariate.each { |group_key, mv_data_series_specs|
56
+ next unless group_key # skip key `false` or `nil`
57
+ ds_specs = mv_data_series_specs.uniq.compact
58
+ @explorations << build_exploration_from_data_series_specs(
59
+ data_collection,
60
+ ds_specs
61
+ )
62
+ }
63
+ end
64
+
65
+ def render
66
+ expls = separate_explorations_with_and_without_charts
67
+ r = render_toc(expls[:with])
68
+ r << render_charts(expls[:with])
69
+ r << render_explorations_without_charts(expls[:without])
70
+ r
71
+ end
72
+
73
+ private
74
+
75
+ def build_exploration_from_data_series_specs(data_collection, ds_specs)
76
+ Exploration.new(
77
+ ds_specs.map { |e| e[:name] }.sort.join(' vs. '),
78
+ ds_specs.map { |ds_spec|
79
+ {
80
+ :name => ds_spec[:name],
81
+ :values => data_collection.map(&ds_spec[:data_method])
82
+ }
83
+ }
84
+ )
85
+ end
86
+
87
+ def render_toc(expls)
88
+ content_tag(:div, :class => 'rde panel panel-primary') do
89
+ content_tag(:div, :class => 'panel-heading') do
90
+ content_tag(:h2, "Table of contents", :class => 'rde-exploration-title panel-title')
91
+ end +
92
+ content_tag(:div, :class => 'panel-body') do
93
+ content_tag(:ul, :class => 'rde-table_of_contents') do
94
+ expls.map { |expl|
95
+ content_tag(
96
+ :li,
97
+ %(<a href="##{ expl.dom_id }">#{ expl.title }</a>).html_safe
98
+ )
99
+ }.join.html_safe
100
+ end
101
+ end
102
+ end
103
+ end
104
+
105
+ def render_charts(expls)
106
+ expls.map { |e| e.render }.join.html_safe
107
+ end
108
+
109
+ def render_explorations_without_charts(expls)
110
+ return '' if expls.empty?
111
+ content_tag(:div, :class => 'rde panel panel-default') do
112
+ content_tag(:div, :class => 'panel-heading') do
113
+ content_tag(:h2, "Explorations without charts", :class => 'rde-exploration-title panel-title')
114
+ end +
115
+ content_tag(:div, :class => 'panel-body') do
116
+ content_tag(:p, "There are no charts available for the following explorations:") +
117
+ content_tag(:ul) do
118
+ expls.map { |expl|
119
+ content_tag(:li, expl.title).html_safe
120
+ }.join.html_safe
121
+ end
122
+ end
123
+ end
124
+ end
125
+
126
+ def separate_explorations_with_and_without_charts
127
+ explorations.inject({ :with => [], :without => [] }) { |m, e|
128
+ m[e.charts.any? ? :with : :without] << e
129
+ m
130
+ }
131
+ end
132
+
133
+ end
@@ -3,7 +3,7 @@ $:.push File.expand_path('../lib', __FILE__)
3
3
 
4
4
  Gem::Specification.new do |gem|
5
5
  gem.name = 'rails-data-explorer'
6
- gem.version = '0.0.1'
6
+ gem.version = '0.1.0'
7
7
  gem.platform = Gem::Platform::RUBY
8
8
 
9
9
  gem.authors = ['Jo Hund']
@@ -16,15 +16,15 @@ Gem::Specification.new do |gem|
16
16
  gem.files = `git ls-files`.split("\n")
17
17
  gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
18
 
19
- # really it's only ActiveSupport and ActionView
20
- gem.add_dependency 'actionview', '>= 3.0.0'
21
19
  gem.add_dependency 'color'
22
20
  gem.add_dependency 'descriptive-statistics'
23
21
  gem.add_dependency 'distribution'
24
22
  gem.add_dependency 'interpolate'
25
23
 
24
+ gem.add_development_dependency 'actionview', '>= 3.0.0'
26
25
  gem.add_development_dependency 'bundler', ['>= 1.0.0']
27
- gem.add_development_dependency 'rake', ['>= 0']
28
26
  gem.add_development_dependency 'minitest'
29
27
  gem.add_development_dependency 'minitest-spec-expect'
28
+ gem.add_development_dependency 'rake', ['>= 0']
29
+ gem.add_development_dependency 'yui-compressor'
30
30
  end
@@ -0,0 +1,7 @@
1
+ # dependencies
2
+ require 'rubygems'
3
+ require 'minitest'
4
+ require 'minitest/autorun'
5
+
6
+ # Gem under test
7
+ require 'rails-data-explorer'
@@ -0,0 +1,10 @@
1
+ # dependencies
2
+ require 'rubygems'
3
+ require 'minitest'
4
+ require 'minitest/autorun'
5
+
6
+ # Gem under test
7
+ require 'rails-data-explorer-no-rails'
8
+
9
+ # To silence deprecation notice
10
+ I18n.enforce_available_locales = false
@@ -0,0 +1,45 @@
1
+ require_relative '../helper_no_rails'
2
+
3
+ class RailsDataExplorer
4
+ describe DataSeries do
5
+
6
+ describe "#initialize" do
7
+
8
+ [
9
+ [['a'], DataType::Categorical],
10
+ [[nil, 'a'], DataType::Categorical],
11
+ [[1.0], DataType::Quantitative::Decimal],
12
+ [[1], DataType::Quantitative::Integer],
13
+ [[Time.now], DataType::Quantitative::Temporal],
14
+ ].each_with_index { |(values, xpect), idx|
15
+ it "detects the datatype #{ idx } correctly" do
16
+ DataSeries.new("name", values).data_type.must_be_instance_of xpect
17
+ end
18
+ }
19
+
20
+ end
21
+
22
+ describe "value accessors" do
23
+
24
+ let(:ds) { DataSeries.new("name", ['b', 'a', 'a', 'c']) }
25
+
26
+ it "computes uniq_vals" do
27
+ ds.uniq_vals.must_equal ['b', 'a', 'c']
28
+ end
29
+
30
+ it "computes uniq_vals_count" do
31
+ ds.uniq_vals_count.must_equal 3
32
+ end
33
+
34
+ it "computes min_val" do
35
+ ds.min_val.must_equal 'a'
36
+ end
37
+
38
+ it "computes max_val" do
39
+ ds.max_val.must_equal 'c'
40
+ end
41
+
42
+ end
43
+
44
+ end
45
+ end
@@ -0,0 +1,34 @@
1
+ require_relative '../../helper_no_rails'
2
+
3
+ class RailsDataExplorer
4
+ class DataType
5
+ describe Categorical do
6
+
7
+ let(:dt) { Categorical.new }
8
+ let(:values) { ['a', 'a', 'b', 'c'] }
9
+
10
+ describe "#descriptive_statistics" do
11
+
12
+ let(:desc_stats) {
13
+ dt.descriptive_statistics(values)
14
+ }
15
+
16
+ it "computes count for each uniq val" do
17
+ desc_stats.detect{ |e| 'a_count' == e[:label] }[:value].must_equal 2
18
+ end
19
+
20
+ it "computes percent for each uniq val" do
21
+ desc_stats.detect{ |e| 'a_percent' == e[:label] }[:value].must_equal 50.0
22
+ end
23
+
24
+ it "computes total count" do
25
+ desc_stats.detect{ |e| 'Total_count' == e[:label] }[:value].must_equal 4
26
+ end
27
+ end
28
+
29
+ describe "#available_chart_types" do
30
+ end
31
+
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,55 @@
1
+ require_relative '../helper_no_rails'
2
+
3
+ class RailsDataExplorer
4
+ describe Exploration do
5
+
6
+ describe "integration test" do
7
+
8
+ def check_render_expectations(rendered_string, options)
9
+ r = true
10
+
11
+ if options[:has_charts]
12
+ options[:has_charts].each { |chart_name|
13
+ # TODO: use Nokogiri to test that it is a div class
14
+ rendered_string.must_match(
15
+ Regexp.new(Regexp.escape("rde-#{ chart_name }"))
16
+ )
17
+ }
18
+ end
19
+
20
+ r
21
+ end
22
+
23
+ [
24
+ [
25
+ ['Univariate Integer data', [nil, 1, 2, 3]],
26
+ { :has_charts => ['histogram-quantitative', 'descriptive-statistics-table'] }
27
+ ],
28
+ [
29
+ ['Univariate Decimal data', [nil, 1.0, 2.0, 3.0]],
30
+ { :has_charts => ['histogram-quantitative', 'descriptive-statistics-table'] }
31
+ ],
32
+ [
33
+ ['Univariate Temporal data', [nil, Time.now]],
34
+ { :has_charts => ['histogram-temporal', 'descriptive-statistics-table'] }
35
+ ],
36
+ [
37
+ ['Univariate Categorical data', [nil, 'a', 'b', 'c']],
38
+ { :has_charts => ['histogram-categorical', 'pie-chart', 'descriptive-statistics-table'] }
39
+ ],
40
+ ].each { |(args, xpect_options)|
41
+ title, data_set_or_array, chart_specs = args
42
+
43
+ it "renders #{ title } correctly" do
44
+ check_render_expectations(
45
+ Exploration.new(*args).render,
46
+ xpect_options
47
+ )
48
+ end
49
+
50
+ }
51
+
52
+ end
53
+
54
+ end
55
+ end
@@ -0,0 +1,29 @@
1
+ require_relative '../../helper_no_rails'
2
+
3
+ class RailsDataExplorer
4
+ module Utils
5
+ describe DataBinner do
6
+
7
+ describe '#bin' do
8
+
9
+ [
10
+ [
11
+ '1',
12
+ [1,2,3],
13
+ [0,1,2,3,4],
14
+ ["1 or less", "1 or less", "2 or less", "3 or less", "> 3"]
15
+ ],
16
+ ].each do |(name, bin_specs, vals, xpect)|
17
+
18
+ it "bins #{ name }" do
19
+ db = DataBinner.new(*bin_specs)
20
+ vals.map{ |e| db.bin(e) }.must_equal xpect
21
+ end
22
+
23
+ end
24
+
25
+ end
26
+
27
+ end
28
+ end
29
+ end