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.
- data/CHANGELOG.md +5 -1
- data/README.md +11 -0
- data/Rakefile +62 -0
- data/doc/how_to/release.md +23 -0
- data/doc/how_to/trouble_when_packaging_assets.md +8 -0
- data/lib/rails-data-explorer-no-rails.rb +42 -0
- data/lib/rails-data-explorer.rb +5 -9
- data/lib/rails-data-explorer/chart/box_plot.rb +5 -1
- data/lib/rails-data-explorer/chart/box_plot_group.rb +22 -5
- data/lib/rails-data-explorer/chart/contingency_table.rb +45 -10
- data/lib/rails-data-explorer/chart/histogram_categorical.rb +104 -3
- data/lib/rails-data-explorer/chart/histogram_quantitative.rb +99 -2
- data/lib/rails-data-explorer/chart/histogram_temporal.rb +10 -55
- data/lib/rails-data-explorer/chart/parallel_coordinates.rb +4 -0
- data/lib/rails-data-explorer/chart/parallel_set.rb +4 -0
- data/lib/rails-data-explorer/chart/pie_chart.rb +89 -8
- data/lib/rails-data-explorer/chart/scatterplot.rb +110 -8
- data/lib/rails-data-explorer/chart/stacked_bar_chart_categorical_percent.rb +133 -14
- data/lib/rails-data-explorer/data_series.rb +37 -2
- data/lib/rails-data-explorer/data_type/categorical.rb +72 -2
- data/lib/rails-data-explorer/data_type/quantitative.rb +41 -12
- data/lib/rails-data-explorer/data_type/quantitative/temporal.rb +3 -2
- data/lib/rails-data-explorer/exploration.rb +5 -1
- data/lib/rails-data-explorer/utils/data_binner.rb +31 -0
- data/lib/rails-data-explorer/utils/data_quantizer.rb +66 -0
- data/lib/rails_data_explorer.rb +133 -0
- data/rails-data-explorer.gemspec +4 -4
- data/spec/helper.rb +7 -0
- data/spec/helper_no_rails.rb +10 -0
- data/spec/rails-data-explorer/data_series_spec.rb +45 -0
- data/spec/rails-data-explorer/data_type/categorical_spec.rb +34 -0
- data/spec/rails-data-explorer/exploration_spec.rb +55 -0
- data/spec/rails-data-explorer/utils/data_binner_spec.rb +29 -0
- data/spec/rails-data-explorer/utils/data_quantizer_spec.rb +71 -0
- data/vendor/assets/javascripts/packaged/rails-data-explorer.min.js +1 -0
- data/vendor/assets/javascripts/rails-data-explorer.js +6 -5
- data/vendor/assets/javascripts/{d3.boxplot.js → sources/d3.boxplot.js} +10 -3
- data/vendor/assets/javascripts/{d3.parcoords.js → sources/d3.parcoords.js} +1 -1
- data/vendor/assets/javascripts/{d3.parsets.js → sources/d3.parsets.js} +3 -3
- data/vendor/assets/javascripts/{d3.v3.js → sources/d3.v3.js} +0 -0
- data/vendor/assets/javascripts/{nv.d3.js → sources/nv.d3.js} +0 -0
- data/vendor/assets/javascripts/sources/vega.js +7040 -0
- data/vendor/assets/stylesheets/packaged/rails-data-explorer.min.css +9 -0
- data/vendor/assets/stylesheets/rails-data-explorer.css +7 -7
- data/vendor/assets/stylesheets/{bootstrap-theme.css → sources/bootstrap-theme.css} +0 -0
- data/vendor/assets/stylesheets/{bootstrap.css → sources/bootstrap.css} +0 -0
- data/vendor/assets/stylesheets/{d3.boxplot.css → sources/d3.boxplot.css} +0 -0
- data/vendor/assets/stylesheets/{d3.parcoords.css → sources/d3.parcoords.css} +0 -0
- data/vendor/assets/stylesheets/{d3.parsets.css → sources/d3.parsets.css} +0 -0
- data/vendor/assets/stylesheets/{nv.d3.css → sources/nv.d3.css} +0 -0
- data/vendor/assets/stylesheets/{rde-default-style.css → sources/rde-default-style.css} +0 -0
- 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.
|
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
|
data/rails-data-explorer.gemspec
CHANGED
@@ -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
|
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
|
data/spec/helper.rb
ADDED
@@ -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
|