rubyvis 0.1.2 → 0.1.3
Sign up to get free protection for your applications and to get access to all the features.
- data.tar.gz.sig +0 -0
- data/History.txt +13 -1
- data/Manifest.txt +19 -0
- data/examples/antibiotics/antibiotics.rb +96 -0
- data/examples/antibiotics/antibiotics_data.rb +20 -0
- data/examples/area.rb +103 -0
- data/examples/bar_column_chart.rb +55 -0
- data/examples/barley/barley.rb +29 -19
- data/examples/barley/barley_data.rb +122 -0
- data/examples/crimea/crimea_grouped_bar.rb +59 -0
- data/examples/dot.rb +19 -0
- data/examples/first.rb +1 -6
- data/examples/line.rb +84 -0
- data/examples/pie_and_donut.rb +38 -0
- data/examples/scatterplot.rb +55 -0
- data/examples/second.rb +3 -4
- data/examples/third.rb +1 -1
- data/lib/rubyvis.rb +3 -1
- data/lib/rubyvis/color/color.rb +31 -3
- data/lib/rubyvis/color/colors.rb +3 -3
- data/lib/rubyvis/format/number.rb +80 -10
- data/lib/rubyvis/internals.rb +11 -5
- data/lib/rubyvis/javascript_behaviour.rb +1 -0
- data/lib/rubyvis/mark.rb +43 -20
- data/lib/rubyvis/mark/anchor.rb +1 -1
- data/lib/rubyvis/mark/area.rb +15 -13
- data/lib/rubyvis/mark/bar.rb +2 -2
- data/lib/rubyvis/mark/dot.rb +85 -0
- data/lib/rubyvis/mark/label.rb +1 -1
- data/lib/rubyvis/mark/line.rb +7 -6
- data/lib/rubyvis/mark/panel.rb +0 -1
- data/lib/rubyvis/mark/rule.rb +5 -4
- data/lib/rubyvis/mark/wedge.rb +124 -0
- data/lib/rubyvis/nest.rb +158 -0
- data/lib/rubyvis/scale.rb +4 -0
- data/lib/rubyvis/scale/log.rb +55 -0
- data/lib/rubyvis/scale/ordinal.rb +34 -11
- data/lib/rubyvis/scale/quantitative.rb +17 -3
- data/lib/rubyvis/scene/svg_area.rb +197 -0
- data/lib/rubyvis/scene/svg_dot.rb +67 -0
- data/lib/rubyvis/scene/svg_label.rb +17 -15
- data/lib/rubyvis/scene/svg_line.rb +0 -2
- data/lib/rubyvis/scene/svg_rule.rb +2 -2
- data/lib/rubyvis/scene/svg_scene.rb +8 -1
- data/lib/rubyvis/scene/svg_wedge.rb +56 -0
- data/lib/rubyvis/sceneelement.rb +2 -1
- data/spec/bar_spec.rb +27 -3
- data/spec/label_spec.rb +1 -1
- data/spec/nest_spec.rb +41 -0
- data/spec/panel_spec.rb +1 -1
- data/spec/scale_linear_spec.rb +2 -2
- data/spec/scale_ordinal_spec.rb +81 -0
- data/spec/spec.opts +0 -1
- metadata +24 -3
- metadata.gz.sig +0 -0
data/lib/rubyvis/scale.rb
CHANGED
@@ -12,6 +12,9 @@ module Rubyvis
|
|
12
12
|
def self.ordinal(*args)
|
13
13
|
Ordinal.new(*args)
|
14
14
|
end
|
15
|
+
def self.log(*args)
|
16
|
+
Log.new(*args)
|
17
|
+
end
|
15
18
|
def self.interpolator(start,_end)
|
16
19
|
if start.is_a? Numeric
|
17
20
|
return lambda {|t| t*(_end-start)+start}
|
@@ -32,3 +35,4 @@ end
|
|
32
35
|
require 'rubyvis/scale/quantitative.rb'
|
33
36
|
require 'rubyvis/scale/linear.rb'
|
34
37
|
require 'rubyvis/scale/ordinal.rb'
|
38
|
+
require 'rubyvis/scale/log.rb'
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Rubyvis
|
2
|
+
|
3
|
+
class Scale::Log < Rubyvis::Scale::Quantitative
|
4
|
+
def initialize(*args)
|
5
|
+
super(*args)
|
6
|
+
@b=nil
|
7
|
+
@_p=nil
|
8
|
+
|
9
|
+
base(10)
|
10
|
+
end
|
11
|
+
def log(x)
|
12
|
+
|
13
|
+
Math.log(x+1e15) / @_p
|
14
|
+
end
|
15
|
+
def pow(y)
|
16
|
+
@b**y
|
17
|
+
end
|
18
|
+
def base(v=nil)
|
19
|
+
if v
|
20
|
+
@b=v
|
21
|
+
@_p=Math.log(@b)
|
22
|
+
transform(lambda {|x| log(x)}, lambda {|x| pow(x)})
|
23
|
+
return self
|
24
|
+
end
|
25
|
+
return @b
|
26
|
+
end
|
27
|
+
def ticks
|
28
|
+
d = domain
|
29
|
+
n = d[0] < 0,
|
30
|
+
i = (n ? -log(-d[0]) : log(d[0])).floor
|
31
|
+
j = (n ? -log(-d[1]) : log(d[1])).ceil
|
32
|
+
ticks = [];
|
33
|
+
if n
|
34
|
+
ticks.push(-pow(-i))
|
35
|
+
(i..j).each {|ii|
|
36
|
+
((b-1)...0).each {|k|
|
37
|
+
ticks.push(-pow(-ii) * k)
|
38
|
+
}
|
39
|
+
}
|
40
|
+
else
|
41
|
+
(i...j).each {|ii|
|
42
|
+
(1...b).each {|k|
|
43
|
+
ticks.push(pow(i) * k)
|
44
|
+
}
|
45
|
+
}
|
46
|
+
ticks.push(pow(i));
|
47
|
+
end
|
48
|
+
|
49
|
+
#for (i = 0; ticks[i] < d[0]; i++); // strip small values
|
50
|
+
#for (j = ticks.length; ticks[j - 1] > d[1]; j--); // strip big values
|
51
|
+
#return ticks.slice(i, j);
|
52
|
+
ticks
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -1,14 +1,19 @@
|
|
1
1
|
module Rubyvis
|
2
2
|
class Scale::Ordinal
|
3
|
-
include Rubyvis::Scale
|
4
|
-
|
3
|
+
#include Rubyvis::Scale
|
4
|
+
attr_reader :range_band
|
5
5
|
def initialize(*args)
|
6
6
|
@d=[] # domain
|
7
7
|
@i={}
|
8
8
|
@r=[]
|
9
|
+
@range_band=nil
|
9
10
|
@band=0
|
10
11
|
domain(*args)
|
11
12
|
end
|
13
|
+
def to_proc
|
14
|
+
that=self
|
15
|
+
lambda {|*args| args[0] ? that.scale(args[0]) : nil }
|
16
|
+
end
|
12
17
|
def scale(x)
|
13
18
|
if @i[x].nil?
|
14
19
|
@d.push(x)
|
@@ -17,15 +22,33 @@ module Rubyvis
|
|
17
22
|
@r[@i[x] % @r.size]
|
18
23
|
end
|
19
24
|
def domain(*arguments)
|
20
|
-
array,f=arguments[0],arguments[1]
|
25
|
+
array, f=arguments[0],arguments[1]
|
21
26
|
if(arguments.size>0)
|
22
|
-
|
27
|
+
array= (array.is_a? Array) ? ((arguments.size>1) ? pv.map(array,f) : array) : arguments.dup
|
23
28
|
@d=array.uniq
|
24
|
-
i=pv.numerate(d)
|
29
|
+
@i=pv.numerate(@d)
|
25
30
|
return self
|
26
31
|
end
|
27
32
|
@d
|
28
33
|
end
|
34
|
+
def split_banded(*arguments)
|
35
|
+
min,max,band=arguments
|
36
|
+
band=1 if (arguments.size < 3)
|
37
|
+
if (band < 0)
|
38
|
+
|
39
|
+
n = self.domain().size
|
40
|
+
total = -band * n
|
41
|
+
remaining = max - min - total
|
42
|
+
padding = remaining / (n + 1).to_f
|
43
|
+
@r = pv.range(min + padding, max, padding - band);
|
44
|
+
@range_band = -band;
|
45
|
+
else
|
46
|
+
step = (max - min) / (self.domain().size + (1 - band))
|
47
|
+
@r = pv.range(min + step * (1 - band), max, step);
|
48
|
+
@range_band = step * band;
|
49
|
+
end
|
50
|
+
return self
|
51
|
+
end
|
29
52
|
def range(*arguments)
|
30
53
|
array, f = arguments[0],arguments[1]
|
31
54
|
if(arguments.size>0)
|
@@ -33,7 +56,7 @@ module Rubyvis
|
|
33
56
|
if @r[0].is_a? String
|
34
57
|
@r=@r.map {|i| pv.color(i)}
|
35
58
|
end
|
36
|
-
self
|
59
|
+
return self
|
37
60
|
end
|
38
61
|
@r
|
39
62
|
end
|
@@ -42,11 +65,11 @@ module Rubyvis
|
|
42
65
|
@r=pv.range(min+step.quo(2),max,step)
|
43
66
|
self
|
44
67
|
end
|
45
|
-
def by(
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
68
|
+
def by(f)
|
69
|
+
that=self
|
70
|
+
lambda {|*args|
|
71
|
+
that.scale(f.js_apply(self,args))
|
72
|
+
}
|
50
73
|
end
|
51
74
|
end
|
52
75
|
end
|
@@ -11,13 +11,22 @@ module Rubyvis
|
|
11
11
|
@n=false
|
12
12
|
@f=Rubyvis.identity # default forward transformation
|
13
13
|
@g=Rubyvis.identity
|
14
|
-
@tick_format=lambda {|x|
|
14
|
+
@tick_format=lambda {|x|
|
15
|
+
if x.is_a? Numeric
|
16
|
+
(x.to_f-x.to_i==0) ? x.to_i : x.to_f
|
17
|
+
else
|
18
|
+
""
|
19
|
+
end
|
20
|
+
}
|
15
21
|
domain(*args)
|
16
22
|
end
|
17
23
|
def new_date(x=nil)
|
18
24
|
x.nil? ? Time.new() : Time.at(x)
|
19
25
|
end
|
20
|
-
|
26
|
+
def to_proc
|
27
|
+
that=self
|
28
|
+
lambda {|*args| args[0] ? that.scale(args[0]) : nil }
|
29
|
+
end
|
21
30
|
def scale(x)
|
22
31
|
x=x.to_f
|
23
32
|
j=Rubyvis.search(@d, x)
|
@@ -251,10 +260,15 @@ module Rubyvis
|
|
251
260
|
end
|
252
261
|
start = (min.quo(step)).ceil * step
|
253
262
|
_end = (max.quo(step)).floor * step
|
254
|
-
@tick_format= pv.Format.number
|
263
|
+
@tick_format= pv.Format.number.fraction_digits([0, -(pv.log(step, 10) + 0.01).floor].max)
|
255
264
|
ticks = pv.range(start, _end + step, step);
|
256
265
|
return reverse ? ticks.reverse() : ticks;
|
257
266
|
end
|
267
|
+
|
268
|
+
def tick_format
|
269
|
+
@tick_format
|
270
|
+
end
|
271
|
+
|
258
272
|
def nice
|
259
273
|
if (@d.size!=2)
|
260
274
|
return self;
|
@@ -0,0 +1,197 @@
|
|
1
|
+
module Rubyvis
|
2
|
+
module SvgScene
|
3
|
+
def self.area(scenes)
|
4
|
+
e = scenes._g.elements[1]
|
5
|
+
return e if scenes.size==0
|
6
|
+
s=scenes[0]
|
7
|
+
# segmented */
|
8
|
+
return self.area_segment(scenes) if (s.segmented)
|
9
|
+
# visible
|
10
|
+
#
|
11
|
+
return e if (!s.visible)
|
12
|
+
|
13
|
+
fill = s.fill_style
|
14
|
+
stroke = s.stroke_style
|
15
|
+
|
16
|
+
return e if (fill.opacity==0 and stroke.opacity==0)
|
17
|
+
# /** @private Computes the straight path for the range [i, j]. */
|
18
|
+
path=lambda {|i,j|
|
19
|
+
p1 = []
|
20
|
+
p2 = [];
|
21
|
+
k=j
|
22
|
+
(i..k).each {|i|
|
23
|
+
si = scenes[i]
|
24
|
+
sj = scenes[j]
|
25
|
+
pi = "#{si.left},#{si.top}"
|
26
|
+
pj = "#{(sj.left + sj.width)},#{(sj.top + sj.height)}"
|
27
|
+
|
28
|
+
#/* interpolate */
|
29
|
+
if (i < k)
|
30
|
+
sk = scenes[i + 1]
|
31
|
+
sl = scenes[j - 1]
|
32
|
+
case (s.interpolate)
|
33
|
+
when "step-before"
|
34
|
+
pi = pi+"V#{sk.top}"
|
35
|
+
pj = pj+"H#{sl.left + sl.width}"
|
36
|
+
|
37
|
+
when "step-after"
|
38
|
+
pi = pi+"H#{sk.left}"
|
39
|
+
pj = pj+"V#{sl.top + sl.height}"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
p1.push(pi);
|
44
|
+
p2.push(pj);
|
45
|
+
j=j-1
|
46
|
+
}
|
47
|
+
(p1+p2).join("L");
|
48
|
+
}
|
49
|
+
|
50
|
+
# @private Computes the curved path for the range [i, j]. */
|
51
|
+
path_curve=lambda {|i, j|
|
52
|
+
pointsT = []
|
53
|
+
pointsB = []
|
54
|
+
pathT=nil
|
55
|
+
pathB=nil
|
56
|
+
|
57
|
+
|
58
|
+
k=j
|
59
|
+
(i..k).each {|i|
|
60
|
+
sj = scenes[j];
|
61
|
+
pointsT.push(scenes[i])
|
62
|
+
pointsB.push(OpenStruct.new({:left=> sj.left + sj.width, :top=> sj.top + sj.height}))
|
63
|
+
j=j-1
|
64
|
+
}
|
65
|
+
|
66
|
+
if (s.interpolate == "basis")
|
67
|
+
pathT = pv.SvgScene.curve_basis(pointsT);
|
68
|
+
pathB = pv.SvgScene.curve_basis(pointsB);
|
69
|
+
elsif (s.interpolate == "cardinal")
|
70
|
+
pathT = pv.SvgScene.curve_cardinal(pointsT, s.tension);
|
71
|
+
pathB = pv.SvgScene.curve_cardinal(pointsB, s.tension);
|
72
|
+
elsif # monotone
|
73
|
+
pathT = pv.SvgScene.curve_monotone(pointsT);
|
74
|
+
pathB = pv.SvgScene.curve_monotone(pointsB);
|
75
|
+
end
|
76
|
+
|
77
|
+
"#{pointsT[0].left },#{ pointsT[0].top }#{ pathT }L#{ pointsB[0].left},#{pointsB[0].top}#{pathB}"
|
78
|
+
}
|
79
|
+
|
80
|
+
#/* points */
|
81
|
+
d = []
|
82
|
+
si=nil
|
83
|
+
sj=nil
|
84
|
+
i=0
|
85
|
+
while(i<scenes.size)
|
86
|
+
si = scenes[i]
|
87
|
+
continue if (si.width==0 and si.height==0)
|
88
|
+
j=0
|
89
|
+
(i+1).upto(scenes.size-1) {|jj|
|
90
|
+
j=jj; sj = scenes[jj];
|
91
|
+
break if (si.width==0 and si.height==0)
|
92
|
+
}
|
93
|
+
|
94
|
+
i=i-1 if (i!=0 and (s.interpolate != "step-after"))
|
95
|
+
j=j+1 if ((j < scenes.size) and (s.interpolate != "step-before"))
|
96
|
+
d.push(((j - i > 2 and (s.interpolate == "basis" or s.interpolate == "cardinal" or s.interpolate == "monotone")) ? path_curve : path).call(i, j - 1))
|
97
|
+
i = j - 1
|
98
|
+
i+=1
|
99
|
+
end
|
100
|
+
|
101
|
+
return e if d.size==0
|
102
|
+
|
103
|
+
e = self.expect(e, "path", {
|
104
|
+
"shape-rendering"=> s.antialias ? nil : "crispEdges",
|
105
|
+
"pointer-events"=> s.events,
|
106
|
+
"cursor"=> s.cursor,
|
107
|
+
"d"=> "M" + d.join("ZM") + "Z",
|
108
|
+
"fill"=> fill.color,
|
109
|
+
"fill-opacity"=> fill.opacity==0 ? nil : fill.opacity,
|
110
|
+
"stroke"=> stroke.color,
|
111
|
+
"stroke-opacity"=> stroke.opacity==0 ? nil : stroke.opacity,
|
112
|
+
"stroke-width"=> stroke.opacity!=0 ? s.line_width / self.scale : nil
|
113
|
+
});
|
114
|
+
return self.append(e, scenes, 0);
|
115
|
+
end
|
116
|
+
|
117
|
+
def self.area_segment(scenes)
|
118
|
+
|
119
|
+
e = scenes._g.elements[1]
|
120
|
+
s = scenes[0]
|
121
|
+
pathsT=[]
|
122
|
+
pathsB=[]
|
123
|
+
|
124
|
+
if (s.interpolate == "basis" or s.interpolate == "cardinal" or s.interpolate == "monotone")
|
125
|
+
pointsT = []
|
126
|
+
pointsB = []
|
127
|
+
n=scenes.size
|
128
|
+
n.times {|i|
|
129
|
+
sj = scenes[n - i - 1]
|
130
|
+
pointsT.push(scenes[i])
|
131
|
+
pointsB.push(OpenStruct.new({:left=> sj.left + sj.width, :top=> sj.top + sj.height}));
|
132
|
+
}
|
133
|
+
|
134
|
+
if (s.interpolate == "basis")
|
135
|
+
pathT = pv.SvgScene.curve_basis_segments(pointsT);
|
136
|
+
pathB = pv.SvgScene.curve_basis_segments(pointsB);
|
137
|
+
elsif (s.interpolate == "cardinal")
|
138
|
+
pathT = pv.SvgScene.curve_cardinal_segments(pointsT, s.tension);
|
139
|
+
pathB = pv.SvgScene.curve_cardinal_segments(pointsB, s.tension);
|
140
|
+
elsif # monotone
|
141
|
+
pathT = pv.SvgScene.curve_monotone_segments(pointsT);
|
142
|
+
pathB = pv.SvgScene.curve_monotone_segments(pointsB);
|
143
|
+
end
|
144
|
+
end
|
145
|
+
n=scenes.size-1
|
146
|
+
n.times {|i|
|
147
|
+
|
148
|
+
s1 = scenes[i]
|
149
|
+
s2 = scenes[i + 1]
|
150
|
+
|
151
|
+
# /* visible */
|
152
|
+
next if (!s1.visible or !s2.visible)
|
153
|
+
|
154
|
+
fill = s.fill_style
|
155
|
+
stroke = s.stroke_style
|
156
|
+
next e if (fill.opacity==0 and stroke.opacity==0)
|
157
|
+
d=nil
|
158
|
+
if (pathsT)
|
159
|
+
pathT = pathsT[i]
|
160
|
+
pb=pathsB[n - i - 1]
|
161
|
+
pathB = "L" + pb[1,pb.size-1]
|
162
|
+
d = pathT + pathB + "Z";
|
163
|
+
else
|
164
|
+
#/* interpolate */
|
165
|
+
si = s1
|
166
|
+
sj = s2
|
167
|
+
|
168
|
+
case (s1.interpolate)
|
169
|
+
when "step-before"
|
170
|
+
si = s2
|
171
|
+
when "step-after"
|
172
|
+
sj = s1
|
173
|
+
end
|
174
|
+
|
175
|
+
|
176
|
+
#/* path */
|
177
|
+
d = "M#{s1.left},#{si.top}L#{s2.left},#{sj.top }L#{s2.left + s2.width},#{sj.top + sj.height}L#{s1.left + s1.width},#{si.top + si.height}Z"
|
178
|
+
end
|
179
|
+
|
180
|
+
e = self.expect(e, "path", {
|
181
|
+
"shape-rendering"=> s1.antialias ? null : "crispEdges",
|
182
|
+
"pointer-events"=> s1.events,
|
183
|
+
"cursor"=> s1.cursor,
|
184
|
+
"d"=> d,
|
185
|
+
"fill"=> fill.color,
|
186
|
+
"fill-opacity"=> fill.opacity==0 ? nil : fill.opacity,
|
187
|
+
"stroke"=> stroke.color,
|
188
|
+
"stroke-opacity"=> stroke.opacity==0 ? nil : stroke.opacity,
|
189
|
+
"stroke-width"=> stroke.opacity!=0 ? s1.line_width / self.scale : nil
|
190
|
+
});
|
191
|
+
e = self.append(e, scenes, i);
|
192
|
+
}
|
193
|
+
return e;
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module Rubyvis
|
2
|
+
module SvgScene
|
3
|
+
def self.dot(scenes)
|
4
|
+
e = scenes._g.elements[1]
|
5
|
+
scenes.each_with_index {|s,i|
|
6
|
+
s = scenes[i];
|
7
|
+
|
8
|
+
# visible */
|
9
|
+
next if !s.visible
|
10
|
+
fill = s.fill_style
|
11
|
+
stroke = s.stroke_style
|
12
|
+
next if (fill.opacity==0 and stroke.opacity==0)
|
13
|
+
|
14
|
+
#/* points */
|
15
|
+
radius = s.shape_radius
|
16
|
+
path = nil
|
17
|
+
case s.shape
|
18
|
+
when 'cross'
|
19
|
+
path = "M#{-radius},#{-radius}L#{radius},#{radius}M#{radius},#{ -radius}L#{ -radius},#{radius}"
|
20
|
+
when "triangle"
|
21
|
+
h = radius
|
22
|
+
w = radius * 1.1547; # // 2 / Math.sqrt(3)
|
23
|
+
path = "M0,#{h}L#{w},#{-h} #{-w},#{-h}Z"
|
24
|
+
when "diamond"
|
25
|
+
radius=radius* Math::sqrt(2)
|
26
|
+
path = "M0,#{-radius}L#{radius},0 0,#{radius} #{-radius},0Z";
|
27
|
+
when "square"
|
28
|
+
path = "M#{-radius},#{-radius}L#{radius},#{-radius} #{radius},#{radius} #{-radius},#{radius}Z"
|
29
|
+
when "tick"
|
30
|
+
path = "M0,0L0,#{-s.shapeSize}"
|
31
|
+
when "bar"
|
32
|
+
path = "M0,#{s.shape_size / 2.0}L0,#{-(s.shapeSize / 2.0)}"
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
#/* Use <circle> for circles, <path> for everything else. */
|
37
|
+
svg = {
|
38
|
+
"shape-rendering"=> s.antialias ? nil : "crispEdges",
|
39
|
+
"pointer-events"=> s.events,
|
40
|
+
"cursor"=> s.cursor,
|
41
|
+
"fill"=> fill.color,
|
42
|
+
"fill-opacity"=> (fill.opacity==0) ? nil :fill.opacity,
|
43
|
+
"stroke"=> stroke.color,
|
44
|
+
"stroke-opacity"=> (stroke.opacity==0) ? nil : stroke.opacity,
|
45
|
+
"stroke-width"=> (stroke.opacity!=0) ? s.line_width / self.scale : nil
|
46
|
+
}
|
47
|
+
|
48
|
+
if (path)
|
49
|
+
svg["transform"] = "translate(#{s.left},#{s.top})"
|
50
|
+
if (s.shape_angle)
|
51
|
+
svg["transform"] += " rotate(#{180 * s.shape_angle / Math.PI})";
|
52
|
+
end
|
53
|
+
svg["d"] = path
|
54
|
+
e = self.expect(e, "path", svg);
|
55
|
+
else
|
56
|
+
svg["cx"] = s.left;
|
57
|
+
svg["cy"] = s.top;
|
58
|
+
svg["r"] = radius;
|
59
|
+
e = self.expect(e, "circle", svg);
|
60
|
+
end
|
61
|
+
e = self.append(e, scenes, i);
|
62
|
+
}
|
63
|
+
return e
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|