unicode_plot 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +20 -0
- data/README.md +44 -0
- data/Rakefile +13 -0
- data/lib/unicode_plot.rb +14 -0
- data/lib/unicode_plot/ascii_canvas.rb +157 -0
- data/lib/unicode_plot/barplot.rb +104 -0
- data/lib/unicode_plot/boxplot.rb +28 -0
- data/lib/unicode_plot/braille_canvas.rb +65 -0
- data/lib/unicode_plot/canvas.rb +160 -0
- data/lib/unicode_plot/lineplot.rb +249 -0
- data/lib/unicode_plot/plot.rb +139 -0
- data/lib/unicode_plot/renderer.rb +234 -0
- data/lib/unicode_plot/styled_printer.rb +91 -0
- data/lib/unicode_plot/value_transformer.rb +43 -0
- data/lib/unicode_plot/version.rb +9 -0
- data/test/helper.rb +2 -0
- data/test/helper/fixture.rb +14 -0
- data/test/helper/with_term.rb +23 -0
- data/test/run-test.rb +12 -0
- data/test/test-barplot.rb +59 -0
- data/test/test-boxplot.rb +48 -0
- data/test/test-canvas.rb +124 -0
- data/test/test-lineplot.rb +132 -0
- data/unicode_plot.gemspec +35 -0
- metadata +118 -0
@@ -0,0 +1,160 @@
|
|
1
|
+
module UnicodePlot
|
2
|
+
class Canvas
|
3
|
+
include BorderPrinter
|
4
|
+
|
5
|
+
def self.create(canvas_type, width, height, **kw)
|
6
|
+
case canvas_type
|
7
|
+
when :ascii
|
8
|
+
AsciiCanvas.new(width, height, **kw)
|
9
|
+
when :braille
|
10
|
+
BrailleCanvas.new(width, height, **kw)
|
11
|
+
else
|
12
|
+
raise ArgumentError, "unknown canvas type: #{canvas_type}"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize(width, height, pixel_width, pixel_height, fill_char,
|
17
|
+
origin_x: 0,
|
18
|
+
origin_y: 0,
|
19
|
+
plot_width: 1,
|
20
|
+
plot_height: 1,
|
21
|
+
x_pixel_per_char: 1,
|
22
|
+
y_pixel_per_char: 1)
|
23
|
+
@width = width
|
24
|
+
@height = height
|
25
|
+
@pixel_width = pixel_width
|
26
|
+
@pixel_height = pixel_height
|
27
|
+
@origin_x = origin_x
|
28
|
+
@origin_y = origin_y
|
29
|
+
@plot_width = plot_width
|
30
|
+
@plot_height = plot_height
|
31
|
+
@x_pixel_per_char = x_pixel_per_char
|
32
|
+
@y_pixel_per_char = y_pixel_per_char
|
33
|
+
@grid = Array.new(@width * @height, fill_char)
|
34
|
+
@colors = Array.new(@width * @height, COLOR_ENCODE[:normal])
|
35
|
+
end
|
36
|
+
|
37
|
+
attr_reader :width
|
38
|
+
attr_reader :height
|
39
|
+
attr_reader :pixel_width
|
40
|
+
attr_reader :pixel_height
|
41
|
+
attr_reader :origin_x
|
42
|
+
attr_reader :origin_y
|
43
|
+
attr_reader :plot_width
|
44
|
+
attr_reader :plot_height
|
45
|
+
attr_reader :x_pixel_per_char
|
46
|
+
attr_reader :y_pixel_per_char
|
47
|
+
|
48
|
+
def show(out)
|
49
|
+
b = BorderMaps::BORDER_SOLID
|
50
|
+
border_length = width
|
51
|
+
|
52
|
+
print_border_top(out, "", border_length, :solid, color: :light_black)
|
53
|
+
out.puts
|
54
|
+
(0 ... height).each do |row_index|
|
55
|
+
print_styled(out, b[:l], color: :light_black)
|
56
|
+
print_row(out, row_index)
|
57
|
+
print_styled(out, b[:r], color: :light_black)
|
58
|
+
out.puts
|
59
|
+
end
|
60
|
+
print_border_bottom(out, "", border_length, :solid, color: :light_black)
|
61
|
+
end
|
62
|
+
|
63
|
+
def print(out)
|
64
|
+
(0 ... height).each do |row_index|
|
65
|
+
print_row(out, row_index)
|
66
|
+
out.puts if row_index < height - 1
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def char_at(x, y)
|
71
|
+
@grid[index_at(x, y)]
|
72
|
+
end
|
73
|
+
|
74
|
+
def color_at(x, y)
|
75
|
+
@colors[index_at(x, y)]
|
76
|
+
end
|
77
|
+
|
78
|
+
def index_at(x, y)
|
79
|
+
return nil unless 0 <= x && x < width && 0 <= y && y < height
|
80
|
+
y * width + x
|
81
|
+
end
|
82
|
+
|
83
|
+
def point!(x, y, color)
|
84
|
+
unless origin_x <= x && x <= origin_x + plot_width &&
|
85
|
+
origin_y <= y && y <= origin_y + plot_height
|
86
|
+
return c
|
87
|
+
end
|
88
|
+
|
89
|
+
plot_offset_x = x - origin_x
|
90
|
+
pixel_x = plot_offset_x.fdiv(plot_width) * pixel_width
|
91
|
+
|
92
|
+
plot_offset_y = y - origin_y
|
93
|
+
pixel_y = pixel_height - plot_offset_y.fdiv(plot_height) * pixel_height
|
94
|
+
|
95
|
+
pixel!(pixel_x.floor, pixel_y.floor, color)
|
96
|
+
end
|
97
|
+
|
98
|
+
def points!(x, y, color = :normal)
|
99
|
+
if x.length != y.length
|
100
|
+
raise ArgumentError, "x and y must be the same length"
|
101
|
+
end
|
102
|
+
unless x.length > 0
|
103
|
+
raise ArgumentError, "x and y must not be empty"
|
104
|
+
end
|
105
|
+
(0 ... x.length).each do |i|
|
106
|
+
point!(x[i], y[i], color)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# digital differential analyzer algorithm
|
111
|
+
def line!(x1, y1, x2, y2, color)
|
112
|
+
if (x1 < origin_x && x2 < origin_x) ||
|
113
|
+
(x1 > origin_x + plot_width && x2 > origin_x + plot_width)
|
114
|
+
return color
|
115
|
+
end
|
116
|
+
if (y1 < origin_y && y2 < origin_y) ||
|
117
|
+
(y1 > origin_y + plot_height && y2 > origin_y + plot_height)
|
118
|
+
return color
|
119
|
+
end
|
120
|
+
|
121
|
+
toff = x1 - origin_x
|
122
|
+
px1 = toff.fdiv(plot_width) * pixel_width
|
123
|
+
toff = x2 - origin_x
|
124
|
+
px2 = toff.fdiv(plot_width) * pixel_width
|
125
|
+
|
126
|
+
toff = y1 - origin_y
|
127
|
+
py1 = pixel_height - toff.fdiv(plot_height) * pixel_height
|
128
|
+
toff = y2 - origin_y
|
129
|
+
py2 = pixel_height - toff.fdiv(plot_height) * pixel_height
|
130
|
+
|
131
|
+
dx = px2 - px1
|
132
|
+
dy = py2 - py1
|
133
|
+
nsteps = dx.abs > dy.abs ? dx.abs : dy.abs
|
134
|
+
inc_x = dx.fdiv(nsteps)
|
135
|
+
inc_y = dy.fdiv(nsteps)
|
136
|
+
|
137
|
+
cur_x = px1
|
138
|
+
cur_y = py1
|
139
|
+
pixel!(cur_x.floor, cur_y.floor, color)
|
140
|
+
1.upto(nsteps) do |i|
|
141
|
+
cur_x += inc_x
|
142
|
+
cur_y += inc_y
|
143
|
+
pixel!(cur_x.floor, cur_y.floor, color)
|
144
|
+
end
|
145
|
+
color
|
146
|
+
end
|
147
|
+
|
148
|
+
def lines!(x, y, color = :normal)
|
149
|
+
if x.length != y.length
|
150
|
+
raise ArgumentError, "x and y must be the same length"
|
151
|
+
end
|
152
|
+
unless x.length > 0
|
153
|
+
raise ArgumentError, "x and y must not be empty"
|
154
|
+
end
|
155
|
+
(0 ... (x.length - 1)).each do |i|
|
156
|
+
line!(x[i], y[i], x[i+1], y[i+1], color)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
@@ -0,0 +1,249 @@
|
|
1
|
+
require 'date'
|
2
|
+
|
3
|
+
module UnicodePlot
|
4
|
+
class GridCanvas < Plot
|
5
|
+
MIN_WIDTH = 5
|
6
|
+
DEFAULT_WIDTH = 40
|
7
|
+
MIN_HEIGHT = 2
|
8
|
+
DEFAULT_HEIGHT = 15
|
9
|
+
|
10
|
+
def initialize(x, y, canvas,
|
11
|
+
width: DEFAULT_WIDTH,
|
12
|
+
height: DEFAULT_HEIGHT,
|
13
|
+
xlim: [0, 0],
|
14
|
+
ylim: [0, 0],
|
15
|
+
grid: true,
|
16
|
+
**kw)
|
17
|
+
unless xlim.length == 2 && ylim.length == 2
|
18
|
+
raise ArgumentError, "xlim and ylim must be 2-length arrays"
|
19
|
+
end
|
20
|
+
width = [width, MIN_WIDTH].max
|
21
|
+
height = [height, MIN_HEIGHT].max
|
22
|
+
min_x, max_x = extend_limits(x, xlim)
|
23
|
+
min_y, max_y = extend_limits(y, ylim)
|
24
|
+
origin_x = min_x
|
25
|
+
origin_y = min_y
|
26
|
+
plot_width = max_x - origin_x
|
27
|
+
plot_height = max_y - origin_y
|
28
|
+
@canvas = Canvas.create(canvas, width, height,
|
29
|
+
origin_x: origin_x,
|
30
|
+
origin_y: origin_y,
|
31
|
+
plot_width: plot_width,
|
32
|
+
plot_height: plot_height)
|
33
|
+
super(**kw)
|
34
|
+
|
35
|
+
min_x_str = (roundable?(min_x) ? min_x.round : min_x).to_s
|
36
|
+
max_x_str = (roundable?(max_x) ? max_x.round : max_x).to_s
|
37
|
+
min_y_str = (roundable?(min_y) ? min_y.round : min_y).to_s
|
38
|
+
max_y_str = (roundable?(max_y) ? max_y.round : max_y).to_s
|
39
|
+
|
40
|
+
annotate_row!(:l, 0, max_y_str, color: :light_black)
|
41
|
+
annotate_row!(:l, height-1, min_y_str, color: :light_black)
|
42
|
+
annotate!(:bl, min_x_str, color: :light_black)
|
43
|
+
annotate!(:br, max_x_str, color: :light_black)
|
44
|
+
|
45
|
+
if grid
|
46
|
+
if min_y < 0 && 0 < max_y
|
47
|
+
step = plot_width.fdiv(width * @canvas.x_pixel_per_char - 1)
|
48
|
+
min_x.step(max_x, by: step) do |i|
|
49
|
+
@canvas.point!(i, 0, :normal)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
if min_x < 0 && 0 < max_x
|
53
|
+
step = plot_height.fdiv(height * @canvas.y_pixel_per_char - 1)
|
54
|
+
min_y.step(max_y, by: step) do |i|
|
55
|
+
@canvas.point!(0, i, :normal)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def origin_x
|
62
|
+
@canvas.origin_x
|
63
|
+
end
|
64
|
+
|
65
|
+
def origin_y
|
66
|
+
@canvas.origin_y
|
67
|
+
end
|
68
|
+
|
69
|
+
def plot_width
|
70
|
+
@canvas.plot_width
|
71
|
+
end
|
72
|
+
|
73
|
+
def plot_height
|
74
|
+
@canvas.plot_height
|
75
|
+
end
|
76
|
+
|
77
|
+
def n_rows
|
78
|
+
@canvas.height
|
79
|
+
end
|
80
|
+
|
81
|
+
def n_columns
|
82
|
+
@canvas.width
|
83
|
+
end
|
84
|
+
|
85
|
+
def points!(x, y, color)
|
86
|
+
@canvas.points!(x, y, color)
|
87
|
+
end
|
88
|
+
|
89
|
+
def lines!(x, y, color)
|
90
|
+
@canvas.lines!(x, y, color)
|
91
|
+
end
|
92
|
+
|
93
|
+
def print_row(out, row_index)
|
94
|
+
@canvas.print_row(out, row_index)
|
95
|
+
end
|
96
|
+
|
97
|
+
def extend_limits(values, limits)
|
98
|
+
mi, ma = limits.minmax.map(&:to_f)
|
99
|
+
if mi == 0 && ma == 0
|
100
|
+
mi, ma = values.minmax.map(&:to_f)
|
101
|
+
end
|
102
|
+
diff = ma - mi
|
103
|
+
if diff == 0
|
104
|
+
ma = mi + 1
|
105
|
+
mi = mi - 1
|
106
|
+
end
|
107
|
+
if limits == [0, 0]
|
108
|
+
plotting_range_narrow(mi, ma)
|
109
|
+
else
|
110
|
+
[mi, ma]
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def plotting_range_narrow(xmin, xmax)
|
115
|
+
diff = xmax - xmin
|
116
|
+
xmax = round_up_subtick(xmax, diff)
|
117
|
+
xmin = round_down_subtick(xmin, diff)
|
118
|
+
[xmin.to_f, xmax.to_f]
|
119
|
+
end
|
120
|
+
|
121
|
+
def round_up_subtick(x, m)
|
122
|
+
if x == 0
|
123
|
+
0.0
|
124
|
+
elsif x > 0
|
125
|
+
x.ceil(ceil_neg_log10(m) + 1)
|
126
|
+
else
|
127
|
+
-(-x).floor(ceil_neg_log10(m) + 1)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def round_down_subtick(x, m)
|
132
|
+
if x == 0
|
133
|
+
0.0
|
134
|
+
elsif x > 0
|
135
|
+
x.floor(ceil_neg_log10(m) + 1)
|
136
|
+
else
|
137
|
+
-(-x).ceil(ceil_neg_log10(m) + 1)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def ceil_neg_log10(x)
|
142
|
+
if roundable?(-Math.log10(x))
|
143
|
+
(-Math.log10(x)).ceil
|
144
|
+
else
|
145
|
+
(-Math.log10(x)).floor
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def roundable?(x)
|
150
|
+
x.to_i == x
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
class Lineplot < GridCanvas
|
155
|
+
def initialize(x, y, canvas,
|
156
|
+
**kw)
|
157
|
+
if x.length != y.length
|
158
|
+
raise ArgumentError, "x and y must be the same length"
|
159
|
+
end
|
160
|
+
unless x.length > 0
|
161
|
+
raise ArgumentError, "x and y must not be empty"
|
162
|
+
end
|
163
|
+
super(x, y, canvas, **kw)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
module_function def lineplot(*args,
|
168
|
+
canvas: :braille,
|
169
|
+
color: :auto,
|
170
|
+
name: "",
|
171
|
+
**kw)
|
172
|
+
case args.length
|
173
|
+
when 1
|
174
|
+
# y only
|
175
|
+
y = Array(args[0])
|
176
|
+
x = Array(1 .. y.length)
|
177
|
+
when 2
|
178
|
+
# x and y
|
179
|
+
x = Array(args[0])
|
180
|
+
y = Array(args[1])
|
181
|
+
else
|
182
|
+
raise ArgumentError, "wrong number of arguments"
|
183
|
+
end
|
184
|
+
|
185
|
+
case x[0]
|
186
|
+
when Time, Date
|
187
|
+
if x[0].is_a? Time
|
188
|
+
d = x.map(&:to_f)
|
189
|
+
else
|
190
|
+
origin = Date.new(1, 1, 1)
|
191
|
+
d = x.map {|xi| xi - origin }
|
192
|
+
end
|
193
|
+
plot = lineplot(d, y, canvas: canvas, color: color, name: name, **kw)
|
194
|
+
xmin, xmax = x.minmax
|
195
|
+
plot.annotate!(:bl, xmin.to_s, color: :light_black)
|
196
|
+
plot.annotate!(:br, xmax.to_s, color: :light_black)
|
197
|
+
plot
|
198
|
+
else
|
199
|
+
plot = Lineplot.new(x, y, canvas, **kw)
|
200
|
+
lineplot!(plot, x, y, color: color, name: name)
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
module_function def lineplot!(plot,
|
205
|
+
*args,
|
206
|
+
color: :auto,
|
207
|
+
name: "")
|
208
|
+
case args.length
|
209
|
+
when 1
|
210
|
+
# y only
|
211
|
+
y = Array(args[0])
|
212
|
+
x = Array(1 .. y.length)
|
213
|
+
when 2
|
214
|
+
# x and y
|
215
|
+
x = Array(args[0])
|
216
|
+
y = Array(args[1])
|
217
|
+
|
218
|
+
if x.length == 1 && y.length == 1
|
219
|
+
# intercept and slope
|
220
|
+
intercept = x[0]
|
221
|
+
slope = y[0]
|
222
|
+
xmin = plot.origin_x
|
223
|
+
xmax = plot.origin_x + plot.plot_width
|
224
|
+
ymin = plot.origin_y
|
225
|
+
ymax = plot.origin_y + plot.plot_height
|
226
|
+
x = [xmin, xmax]
|
227
|
+
y = [intercept + xmin*slope, intercept + xmax*slope]
|
228
|
+
end
|
229
|
+
else
|
230
|
+
raise ArgumentError, "wrong number of arguments"
|
231
|
+
end
|
232
|
+
|
233
|
+
case x[0]
|
234
|
+
when Time, Date
|
235
|
+
if x[0].is_a? Time
|
236
|
+
d = x.map(&:to_f)
|
237
|
+
else
|
238
|
+
origin = Date.new(1, 1, 1)
|
239
|
+
d = x.map {|xi| xi - origin }
|
240
|
+
end
|
241
|
+
lineplot!(plot, d, y, color: color, name: name)
|
242
|
+
else
|
243
|
+
color = color == :auto ? plot.next_color : color
|
244
|
+
plot.annotate!(:r, name.to_s, color: color) unless name.nil? || name == ""
|
245
|
+
plot.lines!(x, y, color)
|
246
|
+
end
|
247
|
+
plot
|
248
|
+
end
|
249
|
+
end
|
@@ -0,0 +1,139 @@
|
|
1
|
+
module UnicodePlot
|
2
|
+
class Plot
|
3
|
+
include StyledPrinter
|
4
|
+
|
5
|
+
DEFAULT_BORDER = :solid
|
6
|
+
DEFAULT_MARGIN = 3
|
7
|
+
DEFAULT_PADDING = 1
|
8
|
+
|
9
|
+
def initialize(title: nil,
|
10
|
+
xlabel: nil,
|
11
|
+
ylabel: nil,
|
12
|
+
border: DEFAULT_BORDER,
|
13
|
+
margin: DEFAULT_MARGIN,
|
14
|
+
padding: DEFAULT_PADDING,
|
15
|
+
labels: true)
|
16
|
+
@title = title
|
17
|
+
@xlabel = xlabel
|
18
|
+
@ylabel = ylabel
|
19
|
+
@border = border
|
20
|
+
@margin = check_margin(margin)
|
21
|
+
@padding = padding
|
22
|
+
@labels_left = {}
|
23
|
+
@colors_left = {}
|
24
|
+
@labels_right = {}
|
25
|
+
@colors_right = {}
|
26
|
+
@decorations = {}
|
27
|
+
@colors_deco = {}
|
28
|
+
@show_labels = labels
|
29
|
+
@auto_color = 0
|
30
|
+
end
|
31
|
+
|
32
|
+
attr_reader :title,
|
33
|
+
:xlabel,
|
34
|
+
:ylabel,
|
35
|
+
:border,
|
36
|
+
:margin,
|
37
|
+
:padding,
|
38
|
+
:labels_left,
|
39
|
+
:colors_left,
|
40
|
+
:labels_right,
|
41
|
+
:colors_right,
|
42
|
+
:decorations,
|
43
|
+
:colors_deco
|
44
|
+
|
45
|
+
def title_given?
|
46
|
+
title && title != ""
|
47
|
+
end
|
48
|
+
|
49
|
+
def xlabel_given?
|
50
|
+
xlabel && xlabel != ""
|
51
|
+
end
|
52
|
+
|
53
|
+
def ylabel_given?
|
54
|
+
ylabel && ylabel != ""
|
55
|
+
end
|
56
|
+
|
57
|
+
def ylabel_length
|
58
|
+
ylabel&.length || 0
|
59
|
+
end
|
60
|
+
|
61
|
+
def show_labels?
|
62
|
+
@show_labels
|
63
|
+
end
|
64
|
+
|
65
|
+
def annotate!(loc, value, color: :normal)
|
66
|
+
case loc
|
67
|
+
when :l
|
68
|
+
(0 ... n_rows).each do |row|
|
69
|
+
if @labels_left.fetch(row, "") == ""
|
70
|
+
@labels_left[row] = value
|
71
|
+
@colors_left[row] = color
|
72
|
+
break
|
73
|
+
end
|
74
|
+
end
|
75
|
+
when :r
|
76
|
+
(0 ... n_rows).each do |row|
|
77
|
+
if @labels_right.fetch(row, "") == ""
|
78
|
+
@labels_right[row] = value
|
79
|
+
@colors_right[row] = color
|
80
|
+
break
|
81
|
+
end
|
82
|
+
end
|
83
|
+
when :t, :b, :tl, :tr, :bl, :br
|
84
|
+
@decorations[loc] = value
|
85
|
+
@colors_deco[loc] = color
|
86
|
+
else
|
87
|
+
raise ArgumentError,
|
88
|
+
"unknown location to annotate (#{loc.inspect} for :t, :b, :l, :r, :tl, :tr, :bl, or :br)"
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def annotate_row!(loc, row_index, value, color: :normal)
|
93
|
+
case loc
|
94
|
+
when :l
|
95
|
+
@labels_left[row_index] = value
|
96
|
+
@colors_left[row_index] = color
|
97
|
+
when :r
|
98
|
+
@labels_right[row_index] = value
|
99
|
+
@colors_right[row_index] = color
|
100
|
+
else
|
101
|
+
raise ArgumentError, "unknown location `#{loc}`, try :l or :r instead"
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def render(out)
|
106
|
+
Renderer.render(out, self)
|
107
|
+
end
|
108
|
+
|
109
|
+
COLOR_CYCLE = [
|
110
|
+
:green,
|
111
|
+
:blue,
|
112
|
+
:red,
|
113
|
+
:magenta,
|
114
|
+
:yellow,
|
115
|
+
:cyan
|
116
|
+
].freeze
|
117
|
+
|
118
|
+
def next_color
|
119
|
+
COLOR_CYCLE[@auto_color]
|
120
|
+
ensure
|
121
|
+
@auto_color = (@auto_color + 1) % COLOR_CYCLE.length
|
122
|
+
end
|
123
|
+
|
124
|
+
def to_s
|
125
|
+
StringIO.open do |sio|
|
126
|
+
render(sio)
|
127
|
+
sio.close
|
128
|
+
sio.string
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
private def check_margin(margin)
|
133
|
+
if margin < 0
|
134
|
+
raise ArgumentError, "margin must be >= 0"
|
135
|
+
end
|
136
|
+
margin
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|