charma 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +12 -0
- data/.rspec +3 -0
- data/.rubocop.yml +74 -0
- data/.travis.yml +7 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +48 -0
- data/LICENSE.txt +21 -0
- data/README.md +43 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/charma.gemspec +33 -0
- data/examples/bar_charts.pdf +4273 -0
- data/examples/bar_charts.rb +67 -0
- data/examples/line_charts.pdf +5645 -0
- data/examples/line_charts.rb +106 -0
- data/examples/runall.rb +6 -0
- data/examples/violin_charts.pdf +8020 -0
- data/examples/violin_charts.rb +65 -0
- data/lib/charma/bar_chart.rb +75 -0
- data/lib/charma/chart.rb +188 -0
- data/lib/charma/document.rb +27 -0
- data/lib/charma/error.rb +5 -0
- data/lib/charma/line_chart.rb +141 -0
- data/lib/charma/page.rb +58 -0
- data/lib/charma/rect.rb +52 -0
- data/lib/charma/version.rb +5 -0
- data/lib/charma/violin_chart.rb +121 -0
- data/lib/charma.rb +14 -0
- metadata +143 -0
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'charma'
|
4
|
+
|
5
|
+
SampleMaker = Struct.new(:size) do
|
6
|
+
def r
|
7
|
+
n=0.5
|
8
|
+
4.times.inject(0){ |acc,| acc + rand(-n..n) }
|
9
|
+
end
|
10
|
+
|
11
|
+
def samples(*peaks)
|
12
|
+
srand(0)
|
13
|
+
Array.new(size){
|
14
|
+
r + peaks.sample
|
15
|
+
}
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
foobar = SampleMaker.new(20000)
|
20
|
+
|
21
|
+
small = SampleMaker.new(6)
|
22
|
+
|
23
|
+
Charma::Document.new{ |doc|
|
24
|
+
[
|
25
|
+
{
|
26
|
+
title: "Lorem ipsum",
|
27
|
+
series:[
|
28
|
+
{
|
29
|
+
name: "foo",
|
30
|
+
y: Array.new(4){ |n| foobar.samples(n+2, (n+2)*2) }
|
31
|
+
},
|
32
|
+
{
|
33
|
+
name: "bar",
|
34
|
+
y: Array.new(4){ |n| foobar.samples(n+2, (n+2)*2, 6) }
|
35
|
+
},
|
36
|
+
],
|
37
|
+
x_ticks: %w(Q1 Q2 Q3 Q4),
|
38
|
+
},
|
39
|
+
{
|
40
|
+
title: "Small size sample",
|
41
|
+
series:[
|
42
|
+
{
|
43
|
+
name: "foo",
|
44
|
+
y: Array.new(4){ |n| small.samples(n+2, (n+2)*2) }
|
45
|
+
},
|
46
|
+
{
|
47
|
+
name: "bar",
|
48
|
+
y: Array.new(4){ |n| small.samples(n+2, (n+2)*2, 6) }
|
49
|
+
},
|
50
|
+
],
|
51
|
+
x_ticks: %w(Q1 Q2 Q3 Q4),
|
52
|
+
},
|
53
|
+
].each do |opts|
|
54
|
+
doc.new_page do |page|
|
55
|
+
case opts
|
56
|
+
when Hash
|
57
|
+
page.add_violinchart( opts )
|
58
|
+
when Array
|
59
|
+
opts.each{ |o| page.add_violinchart( o ) }
|
60
|
+
else
|
61
|
+
raise "unexpected input #{opts.inspect}"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
}.render( File.basename(__FILE__, ".*")+".pdf" )
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Charma
|
4
|
+
class BarChart < Chart
|
5
|
+
def initialize(opts)
|
6
|
+
super(opts)
|
7
|
+
end
|
8
|
+
|
9
|
+
def draw_bar(pdf, rect, yrange, ys, cols)
|
10
|
+
ratio = 0.75
|
11
|
+
_, bars, = rect.hsplit( (1-ratio)/2, ratio, (1-ratio)/2 )
|
12
|
+
bar_rects = bars.hsplit(*Array.new(ys.size,1))
|
13
|
+
bar_rects.zip(ys, cols) do |bar, y, col|
|
14
|
+
ay = abs_y_positoin(y, bar, yrange)
|
15
|
+
zero = abs_y_positoin(0, bar, yrange)
|
16
|
+
b, t = [ ay, zero ].minmax
|
17
|
+
rc = Rect.new( bar.x, t, bar.w, (t-b) )
|
18
|
+
fill_rect( pdf, rc, col )
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def render_chart(pdf, rect, yrange)
|
23
|
+
stroke_rect(pdf, rect)
|
24
|
+
y_values = values(:y).transpose
|
25
|
+
bar_areas = rect.hsplit(*Array.new(y_values.size,1))
|
26
|
+
cols = if y_values.first.size==1
|
27
|
+
colors(y_values.size).map{ |e| [e] }
|
28
|
+
else
|
29
|
+
[colors(y_values.first.size)] * y_values.size
|
30
|
+
end
|
31
|
+
y_values.zip(bar_areas, cols).each do |ys, rc, c|
|
32
|
+
draw_bar(pdf, rc, yrange, ys, c)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def render_xticks(pdf, area)
|
37
|
+
rects = area.hsplit(*Array.new(@opts[:x_ticks].size){ 1 }).map{ |rc0|
|
38
|
+
rc0.hsplit(1,8,1)[1]
|
39
|
+
}
|
40
|
+
draw_samesize_texts( pdf, rects, @opts[:x_ticks], valign: :top )
|
41
|
+
end
|
42
|
+
|
43
|
+
def bar_range
|
44
|
+
ymin = [0, values(:y).flatten.min * 1.1].min
|
45
|
+
ymax = [0, values(:y).flatten.max * 1.1].max
|
46
|
+
[ ymin, ymax ]
|
47
|
+
end
|
48
|
+
|
49
|
+
def render( pdf, rect )
|
50
|
+
stroke_rect(pdf, rect)
|
51
|
+
title_text = @opts[:title]
|
52
|
+
title, main, ticks, bottom = rect.vsplit(
|
53
|
+
(title_text ? 1 : 0),
|
54
|
+
7,
|
55
|
+
(@opts[:x_ticks] ? 0.5 : 0),
|
56
|
+
(bottom_legend? ? 0.5 : 0))
|
57
|
+
draw_text( pdf, title, title_text ) if title_text
|
58
|
+
hratio = [(@opts[:y_label] ? 1 : 0), 1, 10]
|
59
|
+
ylabel, yticks, chart = main.hsplit(*hratio)
|
60
|
+
yrange = @opts[:y_range] || bar_range
|
61
|
+
render_chart(pdf, chart, yrange)
|
62
|
+
if @opts[:y_label]
|
63
|
+
render_rottext(pdf, ylabel, @opts[:y_label] )
|
64
|
+
end
|
65
|
+
if @opts[:x_ticks]
|
66
|
+
_, _, xticks = ticks.hsplit(*hratio)
|
67
|
+
render_xticks(pdf, xticks)
|
68
|
+
end
|
69
|
+
yvalues = tick_values(:y, yrange)
|
70
|
+
render_yticks(pdf, yticks, yrange, yvalues)
|
71
|
+
render_y_grid(pdf, chart, yrange, yvalues)
|
72
|
+
render_legend(pdf, bottom) if bottom_legend?
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
data/lib/charma/chart.rb
ADDED
@@ -0,0 +1,188 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Charma
|
4
|
+
class Chart
|
5
|
+
def initialize(opts)
|
6
|
+
@opts = opts
|
7
|
+
end
|
8
|
+
|
9
|
+
def stroke_rect( pdf, rect )
|
10
|
+
pdf.stroke{
|
11
|
+
pdf.rectangle( [rect.x, rect.y], rect.w, rect.h )
|
12
|
+
}
|
13
|
+
end
|
14
|
+
|
15
|
+
def values(sym)
|
16
|
+
@opts[:series].map{ |s|
|
17
|
+
s[sym].map(&:to_f)
|
18
|
+
}
|
19
|
+
end
|
20
|
+
|
21
|
+
def bottom_legend?
|
22
|
+
has_legend?
|
23
|
+
end
|
24
|
+
|
25
|
+
def has_legend?
|
26
|
+
@opts[:series].any?{ |s| ! s[:name].nil? }
|
27
|
+
end
|
28
|
+
|
29
|
+
def scale_type(sym)
|
30
|
+
key = :"#{sym}_scale"
|
31
|
+
case @opts[key]
|
32
|
+
when :log10
|
33
|
+
:log10
|
34
|
+
else
|
35
|
+
:linear
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def scale_value(axis, v)
|
40
|
+
case scale_type(axis)
|
41
|
+
when :log10
|
42
|
+
Math.log10(v)
|
43
|
+
else
|
44
|
+
v
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def unscale_value(axis, v)
|
49
|
+
case scale_type(axis)
|
50
|
+
when :log10
|
51
|
+
10.0**v
|
52
|
+
else
|
53
|
+
v
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def abs_x_positoin(v, rc, xrange)
|
58
|
+
rx, min, max = [ v, *xrange ].map{ |e| scale_value(:x, e) }
|
59
|
+
(rx-min) * rc.w / (max-min) + rc.x
|
60
|
+
end
|
61
|
+
|
62
|
+
def abs_y_positoin(v, rc, yrange)
|
63
|
+
ry, min, max = [ v, *yrange ].map{ |e| scale_value(:y, e) }
|
64
|
+
(ry-min) * rc.h / (max-min) + rc.bottom
|
65
|
+
end
|
66
|
+
|
67
|
+
def fill_rect( pdf, rect, col )
|
68
|
+
pdf.save_graphics_state do
|
69
|
+
pdf.fill{
|
70
|
+
pdf.fill_color( col )
|
71
|
+
pdf.rectangle( [rect.x, rect.y], rect.w, rect.h )
|
72
|
+
}
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def draw_text( pdf, rect, text, opts = {} )
|
77
|
+
pdf.text_box( text,
|
78
|
+
at:rect.topleft,
|
79
|
+
width:rect.w,
|
80
|
+
height:rect.h,
|
81
|
+
align: (opts[:align] || :center),
|
82
|
+
valign: (opts[:valign] || :center),
|
83
|
+
size: (opts[:size] || rect.h),
|
84
|
+
overflow: :shrink_to_fit )
|
85
|
+
end
|
86
|
+
|
87
|
+
def colors(n)
|
88
|
+
case n
|
89
|
+
when 0, 1
|
90
|
+
return ["666666"]
|
91
|
+
else
|
92
|
+
f = ->(t0){
|
93
|
+
t = t0 % 3
|
94
|
+
v = case t
|
95
|
+
when 0..1 then t
|
96
|
+
when 1..2 then 2-t
|
97
|
+
else 0
|
98
|
+
end
|
99
|
+
"%02x" % (v**0.5*255).round
|
100
|
+
}
|
101
|
+
Array.new(n){ |i|
|
102
|
+
t = i*3.0/n+0.5
|
103
|
+
[f[t],f[t+1],f[t+2]].join
|
104
|
+
}
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def draw_samesize_texts( pdf, rects, texts, opts={} )
|
109
|
+
pdf.save_graphics_state do
|
110
|
+
size = texts.zip(rects).map{ |txt,rc|
|
111
|
+
w = pdf.width_of(txt, size:1)
|
112
|
+
h = pdf.height_of(txt, size:1)
|
113
|
+
[rc.w.to_f/w ,rc.h.to_f/h].min
|
114
|
+
}.min
|
115
|
+
texts.zip(rects).each do |txt, rc|
|
116
|
+
draw_text( pdf, rc, txt, size:size, **opts )
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def render_rottext( pdf, rect, text )
|
122
|
+
pdf.rotate(90, origin: rect.center) do
|
123
|
+
rc = rect.rot90
|
124
|
+
w = pdf.width_of(text, size:1)
|
125
|
+
h = pdf.height_of(text, size:1)
|
126
|
+
size = [rc.w.to_f/w ,rc.h.to_f/h].min
|
127
|
+
pdf.draw_text( text, size:size, at:rc.bottomleft )
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def render_legend( pdf, rect )
|
132
|
+
names = @opts[:series].map.with_index{ |e,ix| e[:name] || "series #{ix}" }
|
133
|
+
rects = rect.hsplit( *([1]*names.size) )
|
134
|
+
name_areas, bar_areas = rects.map{ |rc| rc.hsplit(1,1) }.transpose
|
135
|
+
draw_samesize_texts( pdf, name_areas, names, align: :right )
|
136
|
+
cols = colors(names.size)
|
137
|
+
bar_areas.zip(cols).each do |rc0, col|
|
138
|
+
_, rc1, = rc0.vsplit(1,1,1)
|
139
|
+
rc, = rc1.hsplit(2,1)
|
140
|
+
fill_rect( pdf, rc, col )
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def tick_unit(v)
|
145
|
+
base = (10**Math.log10(v).round).to_f
|
146
|
+
man = v/base
|
147
|
+
return 0.5*base if man<0.6
|
148
|
+
return base if man<1.2
|
149
|
+
base*2
|
150
|
+
end
|
151
|
+
|
152
|
+
def tick_values(axis, range)
|
153
|
+
min, max = range.minmax.map{ |e| scale_value( axis, e ) }
|
154
|
+
unit = tick_unit((max - min) * 0.1)
|
155
|
+
i_low = (min / unit).ceil
|
156
|
+
i_hi = (max / unit).floor
|
157
|
+
(i_low..i_hi).map{ |i| unscale_value( axis, i*unit ) }
|
158
|
+
end
|
159
|
+
|
160
|
+
def render_yticks(pdf, area, yrange, yvalues)
|
161
|
+
h = (area.h / yvalues.size) * 0.7
|
162
|
+
rects = yvalues.map{ |v|
|
163
|
+
abs_y = abs_y_positoin( v, area, yrange )
|
164
|
+
Rect.new( area.x, abs_y + h/2, area.w*0.9, h )
|
165
|
+
}
|
166
|
+
svalues = yvalues.map{ |v| "%g " % v }
|
167
|
+
draw_samesize_texts( pdf, rects, svalues, align: :right )
|
168
|
+
end
|
169
|
+
|
170
|
+
def render_y_grid(pdf, area, yrange, yvalues)
|
171
|
+
pdf.save_graphics_state do
|
172
|
+
pdf.line_width = 0.5
|
173
|
+
yvalues.each do |v|
|
174
|
+
if v==0
|
175
|
+
pdf.stroke_color "000000"
|
176
|
+
pdf.undash
|
177
|
+
else
|
178
|
+
pdf.stroke_color "888888"
|
179
|
+
pdf.dash([2,2])
|
180
|
+
end
|
181
|
+
abs_y = abs_y_positoin( v, area, yrange )
|
182
|
+
pdf.stroke_horizontal_line area.x, area.right, at: abs_y
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
end
|
188
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Charma
|
4
|
+
class Document
|
5
|
+
def initialize( &block )
|
6
|
+
@pages = []
|
7
|
+
block[self]
|
8
|
+
end
|
9
|
+
|
10
|
+
def new_page(&block)
|
11
|
+
page = Page.new(self)
|
12
|
+
block[page]
|
13
|
+
@pages.push page
|
14
|
+
end
|
15
|
+
|
16
|
+
def render( filename )
|
17
|
+
raise "no page added" if @pages.empty?
|
18
|
+
opts = @pages.first.create_opts
|
19
|
+
Prawn::Document.generate(filename, opts) do |pdf|
|
20
|
+
@pages.each.with_index do |page,ix|
|
21
|
+
pdf.start_new_page(page.create_opts) if ix != 0
|
22
|
+
page.render(pdf)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/charma/error.rb
ADDED
@@ -0,0 +1,141 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Charma
|
4
|
+
class LineChart < Chart
|
5
|
+
def initialize(opts)
|
6
|
+
super(opts)
|
7
|
+
end
|
8
|
+
|
9
|
+
def scaled_values(sym)
|
10
|
+
@opts[:series].map{ |e|
|
11
|
+
v = e[sym]
|
12
|
+
if v
|
13
|
+
e[sym].map{ |v| scale_value(sym, v) }
|
14
|
+
else
|
15
|
+
(1..e[:y].size).map(&:to_f)
|
16
|
+
end
|
17
|
+
}
|
18
|
+
end
|
19
|
+
|
20
|
+
def calc_range( sym )
|
21
|
+
r0 = scaled_values(sym).flatten.minmax
|
22
|
+
dist = r0[1] - r0[0]
|
23
|
+
delta = dist==0 ? 1 : dist*0.1
|
24
|
+
raw_range =
|
25
|
+
if 0<=r0[0]
|
26
|
+
[[r0[0]-delta, 0].max, r0[1]+delta]
|
27
|
+
else
|
28
|
+
[r0[0]-delta, r0[1]+delta]
|
29
|
+
end
|
30
|
+
raw_range.map{ |e| unscale_value( sym, e ) }
|
31
|
+
end
|
32
|
+
|
33
|
+
def render_series( pdf, rect, xrange, yrange, s)
|
34
|
+
xs = s[:x] || [*1..s[:y].size]
|
35
|
+
ys = s[:y]
|
36
|
+
points = xs.zip(ys).map{ |x,y|
|
37
|
+
[
|
38
|
+
abs_x_positoin( x, rect, xrange ),
|
39
|
+
abs_y_positoin( y, rect, yrange )
|
40
|
+
]
|
41
|
+
}
|
42
|
+
pdf.stroke do
|
43
|
+
pdf.move_to( *points.first )
|
44
|
+
points.drop(1).each do |x,y|
|
45
|
+
pdf.line_to(x,y)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def render_chart(pdf, rect, xrange, yrange)
|
51
|
+
stroke_rect(pdf, rect)
|
52
|
+
cols = colors(@opts[:series].size)
|
53
|
+
pdf.save_graphics_state do
|
54
|
+
pdf.line_width( 4 )
|
55
|
+
@opts[:series].zip(cols).each do |s, col|
|
56
|
+
pdf.stroke_color( col )
|
57
|
+
render_series( pdf, rect, xrange, yrange, s)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def has_x_ticks?
|
63
|
+
if @opts[:x_ticks].nil?
|
64
|
+
!! @opts[:series].first[:x]
|
65
|
+
else
|
66
|
+
!! @opts[:x_ticks]
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def render_xticks(pdf, area, xrange, xticks)
|
71
|
+
xtick_texts = @opts[:x_ticks] || xticks.map{ |e| "%g" % e }
|
72
|
+
w = area.w*0.7 / xticks.size
|
73
|
+
rects = xticks.map{ |rx|
|
74
|
+
ax = abs_x_positoin( rx, area, xrange )
|
75
|
+
Rect.new( ax-w/2, area.y, w, area.h )
|
76
|
+
}
|
77
|
+
draw_samesize_texts( pdf, rects, xtick_texts, valign: :top )
|
78
|
+
end
|
79
|
+
|
80
|
+
def render_x_grid(pdf, area, xrange, xvalues)
|
81
|
+
pdf.save_graphics_state do
|
82
|
+
pdf.line_width = 0.5
|
83
|
+
xvalues.each do |v|
|
84
|
+
if v==0
|
85
|
+
pdf.stroke_color "000000"
|
86
|
+
pdf.undash
|
87
|
+
else
|
88
|
+
pdf.stroke_color "888888"
|
89
|
+
pdf.dash([2,2])
|
90
|
+
end
|
91
|
+
abs_x = abs_x_positoin( v, area, xrange )
|
92
|
+
pdf.stroke_vertical_line area.y, area.bottom, at: abs_x
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def tick_values(axis, range)
|
98
|
+
ticks = @opts[:"#{axis}_ticks"]
|
99
|
+
if @opts[:series].first[axis] || !ticks
|
100
|
+
super(axis, range)
|
101
|
+
else
|
102
|
+
(1..ticks.size).map(&:to_f)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def render( pdf, rect )
|
107
|
+
stroke_rect(pdf, rect)
|
108
|
+
title_text = @opts[:title]
|
109
|
+
title, main, ticks, bottom = rect.vsplit(
|
110
|
+
(title_text ? 1 : 0),
|
111
|
+
7,
|
112
|
+
(has_x_ticks? ? 0.5 : 0),
|
113
|
+
(bottom_legend? ? 0.5 : 0))
|
114
|
+
draw_text( pdf, title, title_text ) if title_text
|
115
|
+
hratio = [(@opts[:y_label] ? 1 : 0), 1, 10]
|
116
|
+
ylabel, yticks, chart = main.hsplit(*hratio)
|
117
|
+
xrange = @opts[:x_range] || calc_range(:x)
|
118
|
+
yrange = @opts[:y_range] || calc_range(:y)
|
119
|
+
render_chart(pdf, chart, xrange, yrange)
|
120
|
+
xvalues = tick_values(:x, xrange )
|
121
|
+
if has_x_ticks?
|
122
|
+
_, _, xticks = ticks.hsplit(*hratio)
|
123
|
+
render_xticks(pdf, xticks, xrange, xvalues)
|
124
|
+
end
|
125
|
+
render_x_grid(pdf, chart, xrange, xvalues)
|
126
|
+
render_legend(pdf, bottom) if bottom_legend?
|
127
|
+
yvalues = tick_values(:y, yrange)
|
128
|
+
render_yticks(pdf, yticks, yrange, yvalues)
|
129
|
+
render_y_grid(pdf, chart, yrange, yvalues)
|
130
|
+
|
131
|
+
# end
|
132
|
+
# if @opts[:x_ticks]
|
133
|
+
# _, _, xticks = ticks.hsplit(*hratio)
|
134
|
+
# render_xticks(pdf, xticks)
|
135
|
+
# end
|
136
|
+
# yvalues = ytick_values(yrange)
|
137
|
+
# render_yticks(pdf, yticks, yrange, yvalues)
|
138
|
+
# render_y_grid(pdf, chart, yrange, yvalues)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
data/lib/charma/page.rb
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Charma
|
4
|
+
class Page
|
5
|
+
def initialize( doc )
|
6
|
+
@doc = doc
|
7
|
+
@graphs = []
|
8
|
+
end
|
9
|
+
|
10
|
+
def create_opts
|
11
|
+
{
|
12
|
+
page_size: "A4",
|
13
|
+
page_layout: :landscape,
|
14
|
+
}
|
15
|
+
end
|
16
|
+
|
17
|
+
def split_rect( rc, pos, size )
|
18
|
+
horz = ([10,1]*size[0])[0..-2]
|
19
|
+
vrect = rc.hsplit( *horz ).select.with_index{ |_,ix| ix.even? }[pos[0]]
|
20
|
+
vert = ([10,1]*size[1])[0..-2]
|
21
|
+
vrect.vsplit( *vert ).select.with_index{ |_,ix| ix.even? }[pos[1]]
|
22
|
+
end
|
23
|
+
|
24
|
+
def area(mb, ix)
|
25
|
+
t = Rect.new(mb.left, mb.top, mb.width, mb.height)
|
26
|
+
c = @graphs.size
|
27
|
+
case c
|
28
|
+
when 1
|
29
|
+
t
|
30
|
+
when 2..3
|
31
|
+
split_rect( t, [ix,0], [c,1] )
|
32
|
+
else
|
33
|
+
w = Math.sqrt(c).ceil
|
34
|
+
h = (c.to_f/w).ceil
|
35
|
+
split_rect( t, ix.divmod(w).reverse, [w, h] )
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def render(pdf)
|
40
|
+
pdf.stroke_axis
|
41
|
+
@graphs.each.with_index do |g,ix|
|
42
|
+
g.render( pdf, area(pdf.margin_box, ix) )
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def add_barchart(opts)
|
47
|
+
@graphs.push BarChart.new(opts)
|
48
|
+
end
|
49
|
+
|
50
|
+
def add_linechart(opts)
|
51
|
+
@graphs.push LineChart.new(opts)
|
52
|
+
end
|
53
|
+
|
54
|
+
def add_violinchart(opts)
|
55
|
+
@graphs.push ViolinChart.new(opts)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
data/lib/charma/rect.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Charma
|
4
|
+
Rect = Struct.new( :x, :y, :w, :h ) do
|
5
|
+
def vsplit( *rel_hs )
|
6
|
+
rel_sum = rel_hs.sum
|
7
|
+
abs_y = y.to_f
|
8
|
+
rel_hs.map{ |rel_h|
|
9
|
+
abs_h = rel_h.to_f * h / rel_sum
|
10
|
+
rc = Rect.new( x, abs_y, w, abs_h )
|
11
|
+
abs_y -= abs_h
|
12
|
+
rc
|
13
|
+
}
|
14
|
+
end
|
15
|
+
|
16
|
+
def hsplit( *rel_ws )
|
17
|
+
rel_sum = rel_ws.sum
|
18
|
+
abs_x = x.to_f
|
19
|
+
rel_ws.map{ |rel_w|
|
20
|
+
abs_w = rel_w.to_f * w / rel_sum
|
21
|
+
rc = Rect.new( abs_x, y, abs_w, h )
|
22
|
+
abs_x += abs_w
|
23
|
+
rc
|
24
|
+
}
|
25
|
+
end
|
26
|
+
|
27
|
+
def center
|
28
|
+
[x+w/2, y-h/2]
|
29
|
+
end
|
30
|
+
|
31
|
+
def rot90
|
32
|
+
cx, cy = center
|
33
|
+
Rect.new( cx-h/2, cy+w/2, h, w )
|
34
|
+
end
|
35
|
+
|
36
|
+
def right
|
37
|
+
x+w
|
38
|
+
end
|
39
|
+
|
40
|
+
def bottom
|
41
|
+
y-h
|
42
|
+
end
|
43
|
+
|
44
|
+
def topleft
|
45
|
+
[x,y]
|
46
|
+
end
|
47
|
+
|
48
|
+
def bottomleft
|
49
|
+
[x, y-h]
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|