rails-data-explorer 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +10 -0
- data/CHANGELOG.md +3 -0
- data/Gemfile +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +52 -0
- data/Rakefile +18 -0
- data/lib/rails-data-explorer.rb +44 -0
- data/lib/rails-data-explorer/action_view_extension.rb +12 -0
- data/lib/rails-data-explorer/active_record_extension.rb +14 -0
- data/lib/rails-data-explorer/chart.rb +52 -0
- data/lib/rails-data-explorer/chart/box_plot.rb +79 -0
- data/lib/rails-data-explorer/chart/box_plot_group.rb +109 -0
- data/lib/rails-data-explorer/chart/contingency_table.rb +189 -0
- data/lib/rails-data-explorer/chart/descriptive_statistics_table.rb +22 -0
- data/lib/rails-data-explorer/chart/descriptive_statistics_table_group.rb +0 -0
- data/lib/rails-data-explorer/chart/histogram_categorical.rb +73 -0
- data/lib/rails-data-explorer/chart/histogram_quantitative.rb +73 -0
- data/lib/rails-data-explorer/chart/histogram_temporal.rb +78 -0
- data/lib/rails-data-explorer/chart/multi_dimensional_charts.rb +1 -0
- data/lib/rails-data-explorer/chart/parallel_coordinates.rb +89 -0
- data/lib/rails-data-explorer/chart/parallel_set.rb +65 -0
- data/lib/rails-data-explorer/chart/pie_chart.rb +67 -0
- data/lib/rails-data-explorer/chart/scatterplot.rb +120 -0
- data/lib/rails-data-explorer/chart/scatterplot_matrix.rb +1 -0
- data/lib/rails-data-explorer/chart/stacked_bar_chart_categorical_percent.rb +120 -0
- data/lib/rails-data-explorer/data_series.rb +115 -0
- data/lib/rails-data-explorer/data_set.rb +127 -0
- data/lib/rails-data-explorer/data_type.rb +34 -0
- data/lib/rails-data-explorer/data_type/categorical.rb +117 -0
- data/lib/rails-data-explorer/data_type/geo.rb +1 -0
- data/lib/rails-data-explorer/data_type/quantitative.rb +109 -0
- data/lib/rails-data-explorer/data_type/quantitative/decimal.rb +13 -0
- data/lib/rails-data-explorer/data_type/quantitative/integer.rb +13 -0
- data/lib/rails-data-explorer/data_type/quantitative/temporal.rb +62 -0
- data/lib/rails-data-explorer/engine.rb +24 -0
- data/lib/rails-data-explorer/exploration.rb +89 -0
- data/lib/rails-data-explorer/statistics/pearsons_chi_squared_independence_test.rb +75 -0
- data/lib/rails-data-explorer/statistics/rng_category.rb +37 -0
- data/lib/rails-data-explorer/statistics/rng_gaussian.rb +24 -0
- data/lib/rails-data-explorer/statistics/rng_power_law.rb +21 -0
- data/lib/rails-data-explorer/utils/color_scale.rb +33 -0
- data/lib/rails-data-explorer/utils/data_binner.rb +8 -0
- data/lib/rails-data-explorer/utils/data_encoder.rb +2 -0
- data/lib/rails-data-explorer/utils/data_quantizer.rb +2 -0
- data/lib/rails-data-explorer/utils/value_formatter.rb +41 -0
- data/rails-data-explorer.gemspec +30 -0
- data/vendor/assets/javascripts/d3.boxplot.js +302 -0
- data/vendor/assets/javascripts/d3.parcoords.js +585 -0
- data/vendor/assets/javascripts/d3.parsets.js +663 -0
- data/vendor/assets/javascripts/d3.v3.js +9294 -0
- data/vendor/assets/javascripts/nv.d3.js +14369 -0
- data/vendor/assets/javascripts/rails-data-explorer.js +19 -0
- data/vendor/assets/stylesheets/bootstrap-theme.css +346 -0
- data/vendor/assets/stylesheets/bootstrap.css +1727 -0
- data/vendor/assets/stylesheets/d3.boxplot.css +20 -0
- data/vendor/assets/stylesheets/d3.parcoords.css +34 -0
- data/vendor/assets/stylesheets/d3.parsets.css +34 -0
- data/vendor/assets/stylesheets/nv.d3.css +769 -0
- data/vendor/assets/stylesheets/rails-data-explorer.css +21 -0
- data/vendor/assets/stylesheets/rde-default-style.css +42 -0
- metadata +250 -0
@@ -0,0 +1,22 @@
|
|
1
|
+
class RailsDataExplorer
|
2
|
+
class Chart
|
3
|
+
class DescriptiveStatisticsTable < Chart
|
4
|
+
|
5
|
+
def initialize(_data_set, options = {})
|
6
|
+
@data_set = _data_set
|
7
|
+
@options = {}.merge(options)
|
8
|
+
end
|
9
|
+
|
10
|
+
def render
|
11
|
+
return '' unless render?
|
12
|
+
content_tag(:div, :id => dom_id, :class => 'rde-chart rde-descriptive-statistics-table') do
|
13
|
+
@data_set.data_series.map { |data_series|
|
14
|
+
content_tag(:h3, "Descriptive Statistics", :class => 'rde-chart-title') +
|
15
|
+
render_html_table(data_series.descriptive_statistics_table)
|
16
|
+
}.join.html_safe
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
File without changes
|
@@ -0,0 +1,73 @@
|
|
1
|
+
class RailsDataExplorer
|
2
|
+
class Chart
|
3
|
+
class HistogramCategorical < Chart
|
4
|
+
|
5
|
+
def initialize(_data_set, options = {})
|
6
|
+
@data_set = _data_set
|
7
|
+
@options = {}.merge(options)
|
8
|
+
end
|
9
|
+
|
10
|
+
def compute_chart_attrs
|
11
|
+
x_ds = @data_set.data_series.first
|
12
|
+
# compute histogram
|
13
|
+
h = x_ds.values.inject(Hash.new(0)) { |m,e| m[e] += 1; m }
|
14
|
+
{
|
15
|
+
values: h.map { |k,v| { x: k, y: v } }.sort { |a,b| b[:y] <=> a[:y] },
|
16
|
+
x_axis_label: x_ds.name,
|
17
|
+
x_axis_tick_format: "",
|
18
|
+
y_axis_label: 'Frequency',
|
19
|
+
y_axis_tick_format: "d3.format('r')",
|
20
|
+
}
|
21
|
+
end
|
22
|
+
|
23
|
+
def render
|
24
|
+
return '' unless render?
|
25
|
+
ca = compute_chart_attrs
|
26
|
+
%(
|
27
|
+
<div class="rde-chart rde-histogram">
|
28
|
+
<h3 class="rde-chart-title">Histogram</h3>
|
29
|
+
<div id="#{ dom_id }", style="height: 200px;">
|
30
|
+
<svg></svg>
|
31
|
+
</div>
|
32
|
+
<script type="text/javascript">
|
33
|
+
(function() {
|
34
|
+
var data = [
|
35
|
+
{
|
36
|
+
values: #{ ca[:values].to_json },
|
37
|
+
key: '#{ ca[:x_axis_label] }'
|
38
|
+
}
|
39
|
+
];
|
40
|
+
|
41
|
+
nv.addGraph(function() {
|
42
|
+
var chart = nv.models.discreteBarChart()
|
43
|
+
;
|
44
|
+
|
45
|
+
chart.xAxis
|
46
|
+
.axisLabel('#{ ca[:x_axis_label] }')
|
47
|
+
.tickFormat(#{ ca[:x_axis_tick_format] })
|
48
|
+
;
|
49
|
+
|
50
|
+
chart.yAxis
|
51
|
+
.axisLabel('#{ ca[:y_axis_label] }')
|
52
|
+
.tickFormat(#{ ca[:y_axis_tick_format] })
|
53
|
+
;
|
54
|
+
|
55
|
+
d3.select('##{ dom_id } svg')
|
56
|
+
.datum(data)
|
57
|
+
.transition().duration(100)
|
58
|
+
.call(chart)
|
59
|
+
;
|
60
|
+
|
61
|
+
nv.utils.windowResize(chart.update);
|
62
|
+
|
63
|
+
return chart;
|
64
|
+
});
|
65
|
+
})();
|
66
|
+
</script>
|
67
|
+
</div>
|
68
|
+
)
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
class RailsDataExplorer
|
2
|
+
class Chart
|
3
|
+
class HistogramQuantitative < Chart
|
4
|
+
|
5
|
+
def initialize(_data_set, options = {})
|
6
|
+
@data_set = _data_set
|
7
|
+
@options = {}.merge(options)
|
8
|
+
end
|
9
|
+
|
10
|
+
def compute_chart_attrs
|
11
|
+
x_ds = @data_set.data_series.first
|
12
|
+
# compute histogram
|
13
|
+
h = x_ds.values.inject(Hash.new(0)) { |m,e| m[e] += 1; m }
|
14
|
+
{
|
15
|
+
values: h.map { |k,v| { x: k, y: v } },
|
16
|
+
x_axis_label: x_ds.name,
|
17
|
+
x_axis_tick_format: x_ds.axis_tick_format,
|
18
|
+
y_axis_label: 'Frequency',
|
19
|
+
y_axis_tick_format: "d3.format('r')",
|
20
|
+
}
|
21
|
+
end
|
22
|
+
|
23
|
+
def render
|
24
|
+
return '' unless render?
|
25
|
+
ca = compute_chart_attrs
|
26
|
+
%(
|
27
|
+
<div class="rde-chart rde-histogram">
|
28
|
+
<h3 class="rde-chart-title">Histogram</h3>
|
29
|
+
<div id="#{ dom_id }", style="height: 200px;">
|
30
|
+
<svg></svg>
|
31
|
+
</div>
|
32
|
+
<script type="text/javascript">
|
33
|
+
(function() {
|
34
|
+
var data = [
|
35
|
+
{
|
36
|
+
values: #{ ca[:values].to_json },
|
37
|
+
key: '#{ ca[:x_axis_label] }'
|
38
|
+
}
|
39
|
+
];
|
40
|
+
|
41
|
+
nv.addGraph(function() {
|
42
|
+
var chart = nv.models.historicalBarChart()
|
43
|
+
;
|
44
|
+
|
45
|
+
chart.xAxis
|
46
|
+
.axisLabel('#{ ca[:x_axis_label] }')
|
47
|
+
.tickFormat(#{ ca[:x_axis_tick_format] })
|
48
|
+
;
|
49
|
+
|
50
|
+
chart.yAxis
|
51
|
+
.axisLabel('#{ ca[:y_axis_label] }')
|
52
|
+
.tickFormat(#{ ca[:y_axis_tick_format] })
|
53
|
+
;
|
54
|
+
|
55
|
+
d3.select('##{ dom_id } svg')
|
56
|
+
.datum(data)
|
57
|
+
.transition().duration(100)
|
58
|
+
.call(chart)
|
59
|
+
;
|
60
|
+
|
61
|
+
nv.utils.windowResize(chart.update);
|
62
|
+
|
63
|
+
return chart;
|
64
|
+
});
|
65
|
+
})();
|
66
|
+
</script>
|
67
|
+
</div>
|
68
|
+
)
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
class RailsDataExplorer
|
2
|
+
class Chart
|
3
|
+
class HistogramTemporal < Chart
|
4
|
+
|
5
|
+
def initialize(_data_set, options = {})
|
6
|
+
@data_set = _data_set
|
7
|
+
@options = {}.merge(options)
|
8
|
+
end
|
9
|
+
|
10
|
+
def compute_chart_attrs
|
11
|
+
x_ds = @data_set.data_series.first
|
12
|
+
# compute histogram
|
13
|
+
h = x_ds.values.inject(Hash.new(0)) { |m,e|
|
14
|
+
# Round to day
|
15
|
+
key = (e.beginning_of_day).to_i * 1000
|
16
|
+
m[key] += 1
|
17
|
+
m
|
18
|
+
}
|
19
|
+
{
|
20
|
+
values: h.map { |k,v| { x: k, y: v } },
|
21
|
+
x_axis_label: x_ds.name,
|
22
|
+
x_axis_tick_format: x_ds.axis_tick_format,
|
23
|
+
y_axis_label: 'Frequency',
|
24
|
+
y_axis_tick_format: "d3.format('r')",
|
25
|
+
}
|
26
|
+
end
|
27
|
+
|
28
|
+
def render
|
29
|
+
return '' unless render?
|
30
|
+
ca = compute_chart_attrs
|
31
|
+
%(
|
32
|
+
<div class="rde-chart rde-histogram">
|
33
|
+
<h3 class="rde-chart-title">Histogram</h3>
|
34
|
+
<div id="#{ dom_id }", style="height: 200px;">
|
35
|
+
<svg></svg>
|
36
|
+
</div>
|
37
|
+
<script type="text/javascript">
|
38
|
+
(function() {
|
39
|
+
var data = [
|
40
|
+
{
|
41
|
+
values: #{ ca[:values].to_json },
|
42
|
+
key: '#{ ca[:x_axis_label] }'
|
43
|
+
}
|
44
|
+
];
|
45
|
+
|
46
|
+
nv.addGraph(function() {
|
47
|
+
var chart = nv.models.historicalBarChart()
|
48
|
+
;
|
49
|
+
|
50
|
+
chart.xAxis
|
51
|
+
.axisLabel('#{ ca[:x_axis_label] }')
|
52
|
+
.tickFormat(#{ ca[:x_axis_tick_format] })
|
53
|
+
;
|
54
|
+
|
55
|
+
chart.yAxis
|
56
|
+
.axisLabel('#{ ca[:y_axis_label] }')
|
57
|
+
.tickFormat(#{ ca[:y_axis_tick_format] })
|
58
|
+
;
|
59
|
+
|
60
|
+
d3.select('##{ dom_id } svg')
|
61
|
+
.datum(data)
|
62
|
+
.transition().duration(100)
|
63
|
+
.call(chart)
|
64
|
+
;
|
65
|
+
|
66
|
+
nv.utils.windowResize(chart.update);
|
67
|
+
|
68
|
+
return chart;
|
69
|
+
});
|
70
|
+
})();
|
71
|
+
</script>
|
72
|
+
</div>
|
73
|
+
)
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
# http://dc-js.github.io/dc.js/
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# TODO: add :color chart_role (test first if it makes sense, e.g., for 'pay')
|
2
|
+
class RailsDataExplorer
|
3
|
+
class Chart
|
4
|
+
class ParallelCoordinates < Chart
|
5
|
+
|
6
|
+
def initialize(_data_set, options = {})
|
7
|
+
@data_set = _data_set
|
8
|
+
@options = {}.merge(options)
|
9
|
+
end
|
10
|
+
|
11
|
+
def render
|
12
|
+
return '' unless render?
|
13
|
+
ca = compute_chart_attrs
|
14
|
+
%(
|
15
|
+
<div class="rde-chart rde-parallel-coordinates">
|
16
|
+
<h3 class="rde-chart-title">Parallel coordinates</h3>
|
17
|
+
<div id="#{ dom_id }" class="rde-chart-parallel-coordinates parcoords" style="height: 400px; width: 100%"></div>
|
18
|
+
<script type="text/javascript">
|
19
|
+
(function() {
|
20
|
+
var parcoords = d3.parcoords()("##{ dom_id }")
|
21
|
+
.dimensions(#{ ca[:dimensions ].to_json })
|
22
|
+
.types(#{ ca[:types].to_json })
|
23
|
+
.alpha(#{ ca[:alpha] })
|
24
|
+
;
|
25
|
+
|
26
|
+
parcoords.data(#{ ca[:values].to_json })
|
27
|
+
.render()
|
28
|
+
.createAxes() // has to come before other methods that rely on axes (e.g., brushable)
|
29
|
+
// .shadows() // they don't redraw after reordering, so I'm turning them off for now.
|
30
|
+
.reorderable()
|
31
|
+
.brushable()
|
32
|
+
;
|
33
|
+
|
34
|
+
})();
|
35
|
+
</script>
|
36
|
+
</div>
|
37
|
+
)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Render ParallelCoordinates only when there is at least one data series
|
41
|
+
# with DataType Quantitative. If it's all Categorical, then ParallelSet
|
42
|
+
# is much better suited.
|
43
|
+
def render?
|
44
|
+
@data_set.data_series.any? { |ds|
|
45
|
+
ds.data_type.is_a?(RailsDataExplorer::DataType::Quantitative)
|
46
|
+
}
|
47
|
+
end
|
48
|
+
|
49
|
+
def compute_chart_attrs
|
50
|
+
dimension_data_series = @data_set.data_series.find_all { |ds|
|
51
|
+
(ds.chart_roles[Chart::ParallelCoordinates] & [:dimension, :any]).any?
|
52
|
+
}
|
53
|
+
dimension_names = dimension_data_series.map(&:name)
|
54
|
+
number_of_values = dimension_data_series.first.values.length
|
55
|
+
dimension_values = number_of_values.times.map do |idx|
|
56
|
+
dimension_data_series.inject({}) { |m,ds|
|
57
|
+
m[ds.name] = if ds.data_type.is_a?(RailsDataExplorer::DataType::Quantitative::Temporal)
|
58
|
+
ds.values[idx].to_i * 1000
|
59
|
+
else
|
60
|
+
ds.values[idx]
|
61
|
+
end
|
62
|
+
m
|
63
|
+
}
|
64
|
+
end
|
65
|
+
dimension_types = dimension_data_series.inject({}) { |m,ds|
|
66
|
+
m[ds.name] = case ds.data_type
|
67
|
+
when RailsDataExplorer::DataType::Categorical
|
68
|
+
'string'
|
69
|
+
when RailsDataExplorer::DataType::Quantitative::Temporal
|
70
|
+
'date'
|
71
|
+
when RailsDataExplorer::DataType::Quantitative::Integer,
|
72
|
+
RailsDataExplorer::DataType::Quantitative::Decimal
|
73
|
+
'number'
|
74
|
+
else
|
75
|
+
raise "Unhandled data_type: #{ ds.data_type.inspect }"
|
76
|
+
end
|
77
|
+
m
|
78
|
+
}
|
79
|
+
{
|
80
|
+
:dimensions => dimension_names,
|
81
|
+
:values => dimension_values,
|
82
|
+
:types => dimension_types,
|
83
|
+
:alpha => 1 / ([Math.log([number_of_values, 2].max), 10].min) # from 1.0 to 0.1
|
84
|
+
}
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# http://www.jasondavies.com/parallel-sets/
|
2
|
+
# Suitable when all data series are categorical
|
3
|
+
class RailsDataExplorer
|
4
|
+
class Chart
|
5
|
+
class ParallelSet < Chart
|
6
|
+
|
7
|
+
def initialize(_data_set, options = {})
|
8
|
+
@data_set = _data_set
|
9
|
+
@options = {}.merge(options)
|
10
|
+
end
|
11
|
+
|
12
|
+
def compute_chart_attrs
|
13
|
+
dimension_data_series = @data_set.data_series.find_all { |ds|
|
14
|
+
(ds.chart_roles[Chart::ParallelCoordinates] & [:dimension, :any]).any?
|
15
|
+
}
|
16
|
+
number_of_values = dimension_data_series.first.values.length
|
17
|
+
dimension_names = dimension_data_series.map(&:name)
|
18
|
+
dimension_values = number_of_values.times.map do |idx|
|
19
|
+
dimension_data_series.inject({}) { |m,ds|
|
20
|
+
m[ds.name] = if ds.data_type.is_a?(RailsDataExplorer::DataType::Quantitative::Temporal)
|
21
|
+
ds.values[idx].to_i * 1000
|
22
|
+
else
|
23
|
+
ds.values[idx]
|
24
|
+
end
|
25
|
+
m
|
26
|
+
}
|
27
|
+
end
|
28
|
+
{
|
29
|
+
:dimensions => dimension_names,
|
30
|
+
:values => dimension_values
|
31
|
+
}
|
32
|
+
end
|
33
|
+
|
34
|
+
def render
|
35
|
+
return '' unless render?
|
36
|
+
ca = compute_chart_attrs
|
37
|
+
%(
|
38
|
+
<div class="rde-chart rde-parallel-set">
|
39
|
+
<h3 class="rde-chart-title">Parallel Set</h3>
|
40
|
+
<div id="#{ dom_id }" class="rde-chart-parallel-set" style="height: 600px; width: 100%"></div>
|
41
|
+
<script type="text/javascript">
|
42
|
+
(function() {
|
43
|
+
var parset = d3.parsets()
|
44
|
+
.dimensions(#{ ca[:dimensions ].to_json })
|
45
|
+
;
|
46
|
+
|
47
|
+
var vis = d3.select("##{ dom_id }")
|
48
|
+
.append("svg")
|
49
|
+
.attr("width", parset.width())
|
50
|
+
.attr("height", parset.height())
|
51
|
+
;
|
52
|
+
|
53
|
+
vis.datum(#{ ca[:values].to_json })
|
54
|
+
.call(parset)
|
55
|
+
;
|
56
|
+
|
57
|
+
})();
|
58
|
+
</script>
|
59
|
+
</div>
|
60
|
+
)
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
class RailsDataExplorer
|
2
|
+
class Chart
|
3
|
+
class PieChart < Chart
|
4
|
+
|
5
|
+
def initialize(_data_set, options = {})
|
6
|
+
@data_set = _data_set
|
7
|
+
@options = {}.merge(options)
|
8
|
+
end
|
9
|
+
|
10
|
+
def compute_chart_attrs
|
11
|
+
x_ds = @data_set.data_series.first
|
12
|
+
total_count = x_ds.values.length
|
13
|
+
# compute histogram
|
14
|
+
h = x_ds.values.inject(Hash.new(0)) { |m,e| m[e] += 1; m }
|
15
|
+
{
|
16
|
+
values: h.map { |k,v|
|
17
|
+
{ x: k, y: (v / total_count.to_f) }
|
18
|
+
}.sort { |a,b|
|
19
|
+
b[:y] <=> a[:y]
|
20
|
+
},
|
21
|
+
x_axis_label: x_ds.name,
|
22
|
+
x_axis_tick_format: "",
|
23
|
+
y_axis_label: 'Frequency',
|
24
|
+
y_axis_tick_format: "d3.format('r')",
|
25
|
+
}
|
26
|
+
end
|
27
|
+
|
28
|
+
def render
|
29
|
+
return '' unless render?
|
30
|
+
ca = compute_chart_attrs
|
31
|
+
%(
|
32
|
+
<div class="rde-chart rde-pie-chart">
|
33
|
+
<h3 class="rde-chart-title">Pie Chart</h3>
|
34
|
+
<div id="#{ dom_id }", style="height: 400px; width: 400px;">
|
35
|
+
<svg></svg>
|
36
|
+
</div>
|
37
|
+
<script type="text/javascript">
|
38
|
+
(function() {
|
39
|
+
var data = #{ ca[:values].to_json };
|
40
|
+
|
41
|
+
nv.addGraph(function() {
|
42
|
+
var chart = nv.models.pieChart()
|
43
|
+
;
|
44
|
+
|
45
|
+
chart.valueFormat(d3.format('.1%'))
|
46
|
+
.donut(true)
|
47
|
+
;
|
48
|
+
|
49
|
+
d3.select('##{ dom_id } svg')
|
50
|
+
.datum(data)
|
51
|
+
.transition().duration(100)
|
52
|
+
.call(chart)
|
53
|
+
;
|
54
|
+
|
55
|
+
nv.utils.windowResize(chart.update);
|
56
|
+
|
57
|
+
return chart;
|
58
|
+
});
|
59
|
+
})();
|
60
|
+
</script>
|
61
|
+
</div>
|
62
|
+
)
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|