charty 0.1.5.dev → 0.2.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +71 -0
- data/.github/workflows/nmatrix.yml +67 -0
- data/.github/workflows/pycall.yml +86 -0
- data/Dockerfile.dev +9 -1
- data/Gemfile +18 -0
- data/README.md +176 -9
- data/Rakefile +4 -5
- data/charty.gemspec +10 -1
- data/examples/Gemfile +1 -0
- data/examples/active_record.ipynb +1 -1
- data/examples/daru.ipynb +1 -1
- data/examples/iris_dataset.ipynb +1 -1
- data/examples/nmatrix.ipynb +1 -1
- data/examples/{numo-narray.ipynb → numo_narray.ipynb} +1 -1
- data/examples/palette.rb +71 -0
- data/examples/sample.png +0 -0
- data/examples/sample_bokeh.ipynb +156 -0
- data/examples/sample_google_chart.ipynb +229 -68
- data/examples/sample_images/bar_bokeh.html +85 -0
- data/examples/sample_images/barh_bokeh.html +85 -0
- data/examples/sample_images/box_plot_bokeh.html +85 -0
- data/examples/sample_images/curve_bokeh.html +85 -0
- data/examples/sample_images/curve_with_function_bokeh.html +85 -0
- data/examples/sample_images/hist_gruff.png +0 -0
- data/examples/sample_images/scatter_bokeh.html +85 -0
- data/examples/sample_pyplot.ipynb +40 -38
- data/images/penguins_body_mass_g_flipper_length_mm_scatter_plot.png +0 -0
- data/images/penguins_body_mass_g_flipper_length_mm_species_scatter_plot.png +0 -0
- data/images/penguins_body_mass_g_flipper_length_mm_species_sex_scatter_plot.png +0 -0
- data/images/penguins_species_body_mass_g_bar_plot_h.png +0 -0
- data/images/penguins_species_body_mass_g_bar_plot_v.png +0 -0
- data/images/penguins_species_body_mass_g_box_plot_h.png +0 -0
- data/images/penguins_species_body_mass_g_box_plot_v.png +0 -0
- data/images/penguins_species_body_mass_g_sex_bar_plot_v.png +0 -0
- data/images/penguins_species_body_mass_g_sex_box_plot_v.png +0 -0
- data/lib/charty.rb +14 -1
- data/lib/charty/backend_methods.rb +8 -0
- data/lib/charty/backends.rb +80 -0
- data/lib/charty/backends/bokeh.rb +32 -26
- data/lib/charty/backends/google_charts.rb +267 -0
- data/lib/charty/backends/gruff.rb +102 -83
- data/lib/charty/backends/plotly.rb +685 -0
- data/lib/charty/backends/pyplot.rb +586 -92
- data/lib/charty/backends/rubyplot.rb +82 -74
- data/lib/charty/backends/unicode_plot.rb +79 -0
- data/lib/charty/index.rb +213 -0
- data/lib/charty/linspace.rb +1 -1
- data/lib/charty/missing_value_support.rb +14 -0
- data/lib/charty/plot_methods.rb +184 -0
- data/lib/charty/plotter.rb +48 -40
- data/lib/charty/plotters.rb +11 -0
- data/lib/charty/plotters/abstract_plotter.rb +183 -0
- data/lib/charty/plotters/bar_plotter.rb +201 -0
- data/lib/charty/plotters/box_plotter.rb +79 -0
- data/lib/charty/plotters/categorical_plotter.rb +380 -0
- data/lib/charty/plotters/count_plotter.rb +7 -0
- data/lib/charty/plotters/estimation_support.rb +84 -0
- data/lib/charty/plotters/random_support.rb +25 -0
- data/lib/charty/plotters/relational_plotter.rb +518 -0
- data/lib/charty/plotters/scatter_plotter.rb +104 -0
- data/lib/charty/plotters/vector_plotter.rb +6 -0
- data/lib/charty/statistics.rb +114 -0
- data/lib/charty/table.rb +80 -3
- data/lib/charty/table_adapters.rb +25 -0
- data/lib/charty/table_adapters/active_record_adapter.rb +63 -0
- data/lib/charty/table_adapters/base_adapter.rb +69 -0
- data/lib/charty/table_adapters/daru_adapter.rb +70 -0
- data/lib/charty/table_adapters/datasets_adapter.rb +49 -0
- data/lib/charty/table_adapters/hash_adapter.rb +224 -0
- data/lib/charty/table_adapters/narray_adapter.rb +76 -0
- data/lib/charty/table_adapters/nmatrix_adapter.rb +67 -0
- data/lib/charty/table_adapters/pandas_adapter.rb +81 -0
- data/lib/charty/util.rb +20 -0
- data/lib/charty/vector.rb +69 -0
- data/lib/charty/vector_adapters.rb +183 -0
- data/lib/charty/vector_adapters/array_adapter.rb +109 -0
- data/lib/charty/vector_adapters/daru_adapter.rb +171 -0
- data/lib/charty/vector_adapters/narray_adapter.rb +187 -0
- data/lib/charty/vector_adapters/nmatrix_adapter.rb +37 -0
- data/lib/charty/vector_adapters/numpy_adapter.rb +168 -0
- data/lib/charty/vector_adapters/pandas_adapter.rb +200 -0
- data/lib/charty/version.rb +1 -1
- metadata +179 -10
- data/.travis.yml +0 -11
- data/lib/charty/backends/google_chart.rb +0 -167
- data/lib/charty/plotter_adapter.rb +0 -17
@@ -0,0 +1,104 @@
|
|
1
|
+
module Charty
|
2
|
+
module Plotters
|
3
|
+
class ScatterPlotter < RelationalPlotter
|
4
|
+
def initialize(data: nil, variables: {}, **options, &block)
|
5
|
+
x, y, color, style, size = variables.values_at(:x, :y, :color, :style, :size)
|
6
|
+
super(x, y, color, style, size, data: data, **options, &block)
|
7
|
+
end
|
8
|
+
|
9
|
+
attr_reader :alpha, :legend
|
10
|
+
|
11
|
+
def alpha=(val)
|
12
|
+
case val
|
13
|
+
when nil, :auto, 0..1
|
14
|
+
@alpha = val
|
15
|
+
when "auto"
|
16
|
+
@alpha = val.to_sym
|
17
|
+
when Numeric
|
18
|
+
raise ArgumentError,
|
19
|
+
"the given alpha is out of bounds " +
|
20
|
+
"(%p for nil, :auto, or number 0..1)" % val
|
21
|
+
else
|
22
|
+
raise ArgumentError,
|
23
|
+
"invalid value of alpha " +
|
24
|
+
"(%p for nil, :auto, or number in 0..1)" % val
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def legend=(val)
|
29
|
+
case val
|
30
|
+
when :auto, :brief, :full, false
|
31
|
+
@legend = val
|
32
|
+
when "auto", "brief", "full"
|
33
|
+
@legend = val.to_sym
|
34
|
+
else
|
35
|
+
raise ArgumentError,
|
36
|
+
"invalid value of legend (%p for :auto, :brief, :full, or false)" % val
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
attr_reader :line_width, :edge_color
|
41
|
+
|
42
|
+
def line_width=(val)
|
43
|
+
@line_width = check_number(val, :line_width, allow_nil: true)
|
44
|
+
end
|
45
|
+
|
46
|
+
def edge_color=(val)
|
47
|
+
@line_width = check_color(val, :edge_color, allow_nil: true)
|
48
|
+
end
|
49
|
+
|
50
|
+
private def render_plot(backend, **)
|
51
|
+
draw_points(backend)
|
52
|
+
annotate_axes(backend)
|
53
|
+
end
|
54
|
+
|
55
|
+
private def draw_points(backend)
|
56
|
+
map_color(palette: palette, order: color_order, norm: color_norm)
|
57
|
+
map_size(sizes: sizes, order: size_order, norm: size_norm)
|
58
|
+
map_style(markers: markers, order: marker_order)
|
59
|
+
|
60
|
+
data = @plot_data.drop_na
|
61
|
+
|
62
|
+
# TODO: shold pass key_color to backend's scatter method.
|
63
|
+
# In pyplot backend, it is passed as color parameter.
|
64
|
+
|
65
|
+
x = data[:x]
|
66
|
+
y = data[:y]
|
67
|
+
color = data[:color] if @variables.key?(:color)
|
68
|
+
style = data[:style] if @variables.key?(:style)
|
69
|
+
size = data[:size] if @variables.key?(:size)
|
70
|
+
|
71
|
+
# TODO: key_color
|
72
|
+
backend.scatter(
|
73
|
+
x, y, @variables,
|
74
|
+
color: color, color_mapper: @color_mapper,
|
75
|
+
style: style, style_mapper: @style_mapper,
|
76
|
+
size: size, size_mapper: @size_mapper,
|
77
|
+
legend: legend
|
78
|
+
)
|
79
|
+
end
|
80
|
+
|
81
|
+
private def annotate_axes(backend)
|
82
|
+
xlabel = self.variables[:x]
|
83
|
+
ylabel = self.variables[:y]
|
84
|
+
backend.set_xlabel(xlabel) unless xlabel.nil?
|
85
|
+
backend.set_ylabel(ylabel) unless ylabel.nil?
|
86
|
+
|
87
|
+
if legend
|
88
|
+
add_legend_data(backend)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
private def add_legend_data(backend)
|
93
|
+
# TODO: Legend Support
|
94
|
+
verbosity = legend
|
95
|
+
verbosity = :auto if verbosity == true
|
96
|
+
|
97
|
+
titles = Util.filter_map([:color, :size, :style]) do |v|
|
98
|
+
variables[v] if variables.key?(v)
|
99
|
+
end
|
100
|
+
legend_title = titles.length == 1 ? titles[0] : ""
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
module Charty
|
2
|
+
module Statistics
|
3
|
+
begin
|
4
|
+
require "enumerable/statistics"
|
5
|
+
|
6
|
+
def self.mean(enum)
|
7
|
+
enum.mean
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.stdev(enum, population: false)
|
11
|
+
enum.stdev(population: population)
|
12
|
+
end
|
13
|
+
rescue LoadError
|
14
|
+
def self.mean(enum)
|
15
|
+
xs = enum.to_a
|
16
|
+
xs.sum / xs.length.to_f
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.stdev(enum, population: false)
|
20
|
+
xs = enum.to_a
|
21
|
+
n = xs.length
|
22
|
+
mean = xs.sum.to_f / n
|
23
|
+
ddof = population ? 0 : 1
|
24
|
+
var = xs.map {|x| (x - mean)**2 }.sum / (n - ddof)
|
25
|
+
Math.sqrt(var)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.bootstrap(vector, n_boot: 2000, func: :mean, units: nil, random: nil)
|
30
|
+
n = vector.size
|
31
|
+
random = Charty::Plotters::RandomSupport.check_random(random)
|
32
|
+
func = Charty::Plotters::EstimationSupport.check_estimator(func)
|
33
|
+
|
34
|
+
if units
|
35
|
+
return structured_bootstrap(vector, n_boot, units, func, random)
|
36
|
+
end
|
37
|
+
|
38
|
+
if defined?(Pandas::Series) || defined?(Numpy::NDArray)
|
39
|
+
boot_dist = bootstrap_optimized_for_pycall(vector, n_boot, random, func)
|
40
|
+
return boot_dist if boot_dist
|
41
|
+
end
|
42
|
+
|
43
|
+
boot_dist = Array.new(n_boot) do |i|
|
44
|
+
resampler = Array.new(n) { random.rand(n) }
|
45
|
+
|
46
|
+
w ||= vector.values_at(*resampler)
|
47
|
+
|
48
|
+
case func
|
49
|
+
when :mean
|
50
|
+
mean(w)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
boot_dist
|
55
|
+
end
|
56
|
+
|
57
|
+
private_class_method def self.bootstrap_optimized_for_pycall(vector, n_boot, random, func)
|
58
|
+
case
|
59
|
+
when vector.is_a?(Charty::Vector)
|
60
|
+
bootstrap_optimized_for_pycall(vector.data, n_boot, random, func)
|
61
|
+
|
62
|
+
when defined?(Pandas::Series) && vector.is_a?(Pandas::Series) || vector.is_a?(Numpy::NDArray)
|
63
|
+
# numpy is also available when pandas is available
|
64
|
+
n = vector.size
|
65
|
+
resampler = Numpy.empty(n, dtype: Numpy.intp)
|
66
|
+
Array.new(n_boot) do |i|
|
67
|
+
# TODO: Use Numo and MemoryView to reduce execution time
|
68
|
+
# resampler = Numo::Int64.new(n).rand(n)
|
69
|
+
# w = Numpy.take(vector, resampler)
|
70
|
+
n.times {|i| resampler[i] = random.rand(n) }
|
71
|
+
w = vector.take(resampler)
|
72
|
+
|
73
|
+
case func
|
74
|
+
when :mean
|
75
|
+
w.mean
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
private_class_method def self.structured_bootstrap(vector, n_boot, units, func, random)
|
82
|
+
raise NotImplementedError,
|
83
|
+
"structured bootstrapping has not been supported yet"
|
84
|
+
end
|
85
|
+
|
86
|
+
def self.bootstrap_ci(*vectors, which, n_boot: 2000, func: :mean, units: nil, random: nil)
|
87
|
+
boot = bootstrap(*vectors, n_boot: n_boot, func: func, units: units, random: random)
|
88
|
+
q = [50 - which / 2, 50 + which / 2]
|
89
|
+
if boot.respond_to?(:percentile)
|
90
|
+
boot.percentile(q)
|
91
|
+
else
|
92
|
+
percentile(boot, q)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# TODO: optimize with introselect algorithm
|
97
|
+
def self.percentile(a, q)
|
98
|
+
return mean(a) if a.size == 0
|
99
|
+
|
100
|
+
a = a.sort
|
101
|
+
n = a.size
|
102
|
+
q.map do |x|
|
103
|
+
x = n * (x / 100.0)
|
104
|
+
i = x.floor
|
105
|
+
if i == n-1
|
106
|
+
a[i]
|
107
|
+
else
|
108
|
+
t = x - i
|
109
|
+
(1-t)*a[i] + t*a[i+1]
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
data/lib/charty/table.rb
CHANGED
@@ -1,11 +1,69 @@
|
|
1
|
+
require 'forwardable'
|
1
2
|
|
2
3
|
module Charty
|
4
|
+
class ColumnAccessor
|
5
|
+
def initialize(adapter)
|
6
|
+
@adapter = adapter
|
7
|
+
end
|
8
|
+
|
9
|
+
def [](column_name)
|
10
|
+
@adapter[nil, column_name]
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
3
14
|
class Table
|
4
|
-
|
5
|
-
|
15
|
+
extend Forwardable
|
16
|
+
include MissingValueSupport
|
17
|
+
|
18
|
+
def initialize(data, **kwargs)
|
19
|
+
adapter_class = TableAdapters.find_adapter_class(data)
|
20
|
+
if kwargs.empty?
|
21
|
+
@adapter = adapter_class.new(data)
|
22
|
+
else
|
23
|
+
@adapter = adapter_class.new(data, **kwargs)
|
24
|
+
end
|
25
|
+
|
26
|
+
@column_cache = {}
|
6
27
|
end
|
7
28
|
|
8
|
-
attr_reader :
|
29
|
+
attr_reader :adapter
|
30
|
+
|
31
|
+
def_delegators :adapter, :length, :column_length
|
32
|
+
|
33
|
+
def_delegators :adapter, :columns, :columns=
|
34
|
+
def_delegators :adapter, :index, :index=
|
35
|
+
|
36
|
+
def_delegator :@adapter, :column_names
|
37
|
+
def_delegator :@adapter, :data, :raw_data
|
38
|
+
|
39
|
+
def ==(other)
|
40
|
+
return true if equal?(other)
|
41
|
+
|
42
|
+
case other
|
43
|
+
when Charty::Table
|
44
|
+
adapter == other.adapter
|
45
|
+
else
|
46
|
+
super
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def empty?
|
51
|
+
length == 0
|
52
|
+
end
|
53
|
+
|
54
|
+
def [](key)
|
55
|
+
key = case key
|
56
|
+
when Symbol
|
57
|
+
key
|
58
|
+
else
|
59
|
+
String.try_convert(key).to_sym
|
60
|
+
end
|
61
|
+
if @column_cache.key?(key)
|
62
|
+
@column_cache[key]
|
63
|
+
else
|
64
|
+
@column_cache[key] = @adapter[nil, key]
|
65
|
+
end
|
66
|
+
end
|
9
67
|
|
10
68
|
def to_a(x=nil, y=nil, z=nil)
|
11
69
|
case
|
@@ -39,5 +97,24 @@ module Charty
|
|
39
97
|
i += 1
|
40
98
|
end
|
41
99
|
end
|
100
|
+
|
101
|
+
def drop_na
|
102
|
+
# TODO: Must implement this method in each adapter
|
103
|
+
missing_index = index.select do |i|
|
104
|
+
column_names.any? do |key|
|
105
|
+
missing_value?(self[key][i])
|
106
|
+
end
|
107
|
+
end
|
108
|
+
if missing_index.empty?
|
109
|
+
self
|
110
|
+
else
|
111
|
+
select_index = index.to_a - missing_index
|
112
|
+
new_data = column_names.map { |key|
|
113
|
+
vals = select_index.map {|i| self[key][i] }
|
114
|
+
[key, vals]
|
115
|
+
}.to_h
|
116
|
+
Charty::Table.new(new_data, index: select_index)
|
117
|
+
end
|
118
|
+
end
|
42
119
|
end
|
43
120
|
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Charty
|
2
|
+
module TableAdapters
|
3
|
+
@adapters = {}
|
4
|
+
|
5
|
+
def self.register(name, adapter_class)
|
6
|
+
@adapters[name] = adapter_class
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.find_adapter_class(data)
|
10
|
+
@adapters.each_value do |adapter_class|
|
11
|
+
return adapter_class if adapter_class.supported?(data)
|
12
|
+
end
|
13
|
+
raise ArgumentError, "Unsupported data class: #{data.class}"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
require_relative 'table_adapters/base_adapter'
|
19
|
+
require_relative 'table_adapters/hash_adapter'
|
20
|
+
require_relative 'table_adapters/narray_adapter'
|
21
|
+
require_relative 'table_adapters/datasets_adapter'
|
22
|
+
require_relative 'table_adapters/daru_adapter'
|
23
|
+
require_relative 'table_adapters/active_record_adapter'
|
24
|
+
require_relative 'table_adapters/nmatrix_adapter'
|
25
|
+
require_relative 'table_adapters/pandas_adapter'
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module Charty
|
2
|
+
module TableAdapters
|
3
|
+
class ActiveRecordAdapter < BaseAdapter
|
4
|
+
TableAdapters.register(:active_record, self)
|
5
|
+
|
6
|
+
def self.supported?(data)
|
7
|
+
defined?(ActiveRecord::Relation) && data.is_a?(ActiveRecord::Relation)
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(data)
|
11
|
+
@data = check_type(ActiveRecord::Relation, data, :data)
|
12
|
+
@column_names = @data.column_names.freeze
|
13
|
+
self.columns = Index.new(@column_names)
|
14
|
+
self.index = RangeIndex.new(0 ... length)
|
15
|
+
end
|
16
|
+
|
17
|
+
attr_reader :data, :column_names
|
18
|
+
|
19
|
+
def_delegators :data, :size
|
20
|
+
|
21
|
+
alias length size
|
22
|
+
|
23
|
+
def column_length
|
24
|
+
column_names.length
|
25
|
+
end
|
26
|
+
|
27
|
+
def [](row, column)
|
28
|
+
fetch_records unless @columns_cache
|
29
|
+
if row
|
30
|
+
@columns_cache[resolve_column_index(column)][row]
|
31
|
+
else
|
32
|
+
column_data = @columns_cache[resolve_column_index(column)]
|
33
|
+
Vector.new(column_data, index: index, name: column)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
private def resolve_column_index(column)
|
38
|
+
case column
|
39
|
+
when String, Symbol
|
40
|
+
index = column_names.index(column.to_s)
|
41
|
+
unless index
|
42
|
+
raise IndexError, "invalid column name: #{column.inspect}"
|
43
|
+
end
|
44
|
+
index
|
45
|
+
when Integer
|
46
|
+
column
|
47
|
+
else
|
48
|
+
message = "column must be String or Integer: #{column.inspect}"
|
49
|
+
raise ArgumentError, message
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
private def fetch_records
|
54
|
+
@columns_cache = @data.pluck(*column_names).transpose
|
55
|
+
end
|
56
|
+
|
57
|
+
private def check_type(type, data, name)
|
58
|
+
return data if data.is_a?(type)
|
59
|
+
raise TypeError, "#{name} must be a #{type}"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require "forwardable"
|
2
|
+
|
3
|
+
module Charty
|
4
|
+
module TableAdapters
|
5
|
+
class BaseAdapter
|
6
|
+
extend Forwardable
|
7
|
+
include Enumerable
|
8
|
+
|
9
|
+
attr_reader :columns
|
10
|
+
|
11
|
+
def columns=(values)
|
12
|
+
@columns = check_and_convert_index(values, :columns, column_length)
|
13
|
+
end
|
14
|
+
|
15
|
+
def column_names
|
16
|
+
columns.to_a
|
17
|
+
end
|
18
|
+
|
19
|
+
attr_reader :index
|
20
|
+
|
21
|
+
def index=(values)
|
22
|
+
@index = check_and_convert_index(values, :index, length)
|
23
|
+
end
|
24
|
+
|
25
|
+
def ==(other)
|
26
|
+
case other
|
27
|
+
when BaseAdapter
|
28
|
+
return false if columns != other.columns
|
29
|
+
return false if index != other.index
|
30
|
+
compare_data_equality(other)
|
31
|
+
else
|
32
|
+
false
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def compare_data_equality(other)
|
37
|
+
columns.each do |name|
|
38
|
+
if self[nil, name] != other[nil, name]
|
39
|
+
return false
|
40
|
+
end
|
41
|
+
end
|
42
|
+
true
|
43
|
+
end
|
44
|
+
|
45
|
+
private def check_and_convert_index(values, name, expected_length)
|
46
|
+
case values
|
47
|
+
when Index, Range
|
48
|
+
else
|
49
|
+
unless (ary = Array.try_convert(values))
|
50
|
+
raise ArgumentError, "invalid object for %s: %p" % [name, values]
|
51
|
+
end
|
52
|
+
values = ary
|
53
|
+
end
|
54
|
+
if expected_length != values.size
|
55
|
+
raise ArgumentError,
|
56
|
+
"invalid length for %s (%d for %d)" % [name, values.size, expected_length]
|
57
|
+
end
|
58
|
+
case values
|
59
|
+
when Index
|
60
|
+
values
|
61
|
+
when Range
|
62
|
+
RangeIndex.new(values)
|
63
|
+
when Array
|
64
|
+
Index.new(values)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|