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