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