rust 0.11 → 0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/rust/core/csv.rb +4 -4
- data/lib/rust/core/manual.rb +89 -0
- data/lib/rust/core/rust.rb +28 -5
- data/lib/rust/core/types/dataframe.rb +69 -2
- data/lib/rust/core/types/datatype.rb +2 -2
- data/lib/rust/core/types/factor.rb +4 -0
- data/lib/rust/core/types/language.rb +33 -1
- data/lib/rust/core/types/list.rb +2 -0
- data/lib/rust/core/types/matrix.rb +8 -0
- data/lib/rust/core.rb +50 -0
- data/lib/rust/external/ggplot2/core.rb +171 -0
- data/lib/rust/external/ggplot2/geoms.rb +83 -0
- data/lib/rust/external/ggplot2/plot_builder.rb +292 -0
- data/lib/rust/external/ggplot2/scale.rb +12 -0
- data/lib/rust/external/ggplot2/themes.rb +458 -0
- data/lib/rust/external/ggplot2.rb +116 -0
- data/lib/rust/models/regression.rb +113 -10
- data/lib/rust/stats/probabilities.rb +22 -1
- data/lib/rust.rb +19 -0
- metadata +10 -3
@@ -0,0 +1,458 @@
|
|
1
|
+
require 'json'
|
2
|
+
require_relative 'core'
|
3
|
+
|
4
|
+
module Rust::Plots::GGPlot
|
5
|
+
class Theme < Layer
|
6
|
+
def self.from_h(options)
|
7
|
+
starting = options.delete('_starting')
|
8
|
+
options = options.map do |key, value|
|
9
|
+
if value.is_a?(Hash)
|
10
|
+
case value.delete('_type').split("::").last
|
11
|
+
when 'TextElement'
|
12
|
+
[key, TextElement.new(**value)]
|
13
|
+
when 'RectElement'
|
14
|
+
[key, RectElement.new(**value)]
|
15
|
+
when 'TextElement'
|
16
|
+
[key, TextElement.new(**value)]
|
17
|
+
when 'LineElement'
|
18
|
+
[key, LineElement.new(**value)]
|
19
|
+
end
|
20
|
+
else
|
21
|
+
[key, value]
|
22
|
+
end
|
23
|
+
end.to_h
|
24
|
+
|
25
|
+
return Theme.new(starting, **options)
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.load(filename)
|
29
|
+
json = JSON.parse(File.read(filename))
|
30
|
+
return self.from_h(json.to_h)
|
31
|
+
end
|
32
|
+
|
33
|
+
def initialize(starting, **options)
|
34
|
+
super("theme", **options)
|
35
|
+
if starting
|
36
|
+
@starting = "theme_" + starting
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def to_R
|
41
|
+
result = super do |options, arguments|
|
42
|
+
[
|
43
|
+
options.map { |k, v| [k.to_s.gsub("_", "."), v] }.to_h,
|
44
|
+
arguments
|
45
|
+
]
|
46
|
+
end
|
47
|
+
|
48
|
+
result = Rust::Function.new(@starting).to_R + " + " + result if @starting
|
49
|
+
|
50
|
+
return result
|
51
|
+
end
|
52
|
+
|
53
|
+
def to_h
|
54
|
+
options = @options.clone
|
55
|
+
|
56
|
+
options['_starting'] = @starting.sub("theme_", "") if @starting
|
57
|
+
options = options.map do |key, value|
|
58
|
+
[key, value.is_a?(Theme::Element) ? value.to_h : value]
|
59
|
+
end.to_h
|
60
|
+
|
61
|
+
return options
|
62
|
+
end
|
63
|
+
|
64
|
+
def save(path, force = false)
|
65
|
+
if !force && FileTest.exist?(path)
|
66
|
+
raise "File already existing."
|
67
|
+
end
|
68
|
+
|
69
|
+
File.open(path, "w") do |f|
|
70
|
+
f.write(
|
71
|
+
JSON.pretty_generate(
|
72
|
+
self.to_h
|
73
|
+
)
|
74
|
+
)
|
75
|
+
end
|
76
|
+
|
77
|
+
return true
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
class ExistingTheme < Layer
|
82
|
+
end
|
83
|
+
|
84
|
+
class Theme::Element
|
85
|
+
attr_reader :options
|
86
|
+
|
87
|
+
def initialize(**options)
|
88
|
+
@options = options
|
89
|
+
end
|
90
|
+
|
91
|
+
def r_function
|
92
|
+
raise "Not implemented for generic theme element"
|
93
|
+
end
|
94
|
+
|
95
|
+
def to_R
|
96
|
+
options = @options.map { |k, v| [k.to_s.gsub("_", "."), v] }.to_h
|
97
|
+
|
98
|
+
function = Rust::Function.new(self.r_function)
|
99
|
+
function.options = Rust::Options.from_hash(options)
|
100
|
+
|
101
|
+
return function.to_R
|
102
|
+
end
|
103
|
+
|
104
|
+
def to_h
|
105
|
+
hash = @options.clone
|
106
|
+
hash['_type'] = self.class.name
|
107
|
+
return hash
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
class Theme::TextElement < Theme::Element
|
112
|
+
def r_function
|
113
|
+
return "element_text"
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
class Theme::LineElement < Theme::Element
|
118
|
+
def r_function
|
119
|
+
return "element_line"
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
class Theme::RectElement < Theme::Element
|
124
|
+
def r_function
|
125
|
+
return "element_rect"
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
class Theme::BlankElement < Theme::Element
|
130
|
+
def r_function
|
131
|
+
return "element_blank"
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
class ThemeComponentBuilder
|
136
|
+
def initialize(namespace=nil)
|
137
|
+
@namespace = namespace
|
138
|
+
@options = {}
|
139
|
+
end
|
140
|
+
|
141
|
+
def option(key, value)
|
142
|
+
key = "#@namespace.#{key}" if @namespace
|
143
|
+
@options[key] = value
|
144
|
+
|
145
|
+
return self
|
146
|
+
end
|
147
|
+
|
148
|
+
def [](key)
|
149
|
+
key = "#@namespace.#{key}" if @namespace
|
150
|
+
return @options[key]
|
151
|
+
end
|
152
|
+
|
153
|
+
def line_el(value)
|
154
|
+
if value.is_a?(Theme::LineElement) || value.is_a?(Theme::BlankElement)
|
155
|
+
return value
|
156
|
+
elsif value.is_a?(Hash)
|
157
|
+
return Theme::LineElement.new(**value)
|
158
|
+
elsif !value
|
159
|
+
return Theme::BlankElement.new
|
160
|
+
else
|
161
|
+
raise "Expected line or hash"
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
def rect_el(value)
|
166
|
+
if value.is_a?(Theme::RectElement) || value.is_a?(Theme::BlankElement)
|
167
|
+
return value
|
168
|
+
elsif value.is_a?(Hash)
|
169
|
+
return Theme::RectElement.new(**value)
|
170
|
+
elsif !value
|
171
|
+
return Theme::BlankElement.new
|
172
|
+
else
|
173
|
+
raise "Expected rect or hash"
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
def text_el(value)
|
178
|
+
if value.is_a?(Theme::TextElement) || value.is_a?(Theme::BlankElement)
|
179
|
+
return value
|
180
|
+
elsif value.is_a?(Hash)
|
181
|
+
return Theme::TextElement.new(**value)
|
182
|
+
elsif !value
|
183
|
+
return Theme::BlankElement.new
|
184
|
+
else
|
185
|
+
raise "Expected text or hash"
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
def unit_el(value)
|
190
|
+
numeric = nil
|
191
|
+
unit = nil
|
192
|
+
|
193
|
+
if input.is_a?(String)
|
194
|
+
numeric, unit = *input.scan(/^([0-9.]+)([A-Za-z]+)/).flatten
|
195
|
+
|
196
|
+
raise "Unclear numeric part in #{input}" unless numeric
|
197
|
+
raise "Unclear unit part in #{input}" unless unit
|
198
|
+
elsif input.is_a?(Numeric)
|
199
|
+
numeric = input
|
200
|
+
unit = "npc"
|
201
|
+
end
|
202
|
+
|
203
|
+
raise "Unable to handle #{input}" unless numeric && unit
|
204
|
+
|
205
|
+
function = Rust::Function.new("units")
|
206
|
+
function.arguments = Rust::Arguments.new([numeric, unit])
|
207
|
+
|
208
|
+
return function.to_R
|
209
|
+
end
|
210
|
+
|
211
|
+
def alignment_el(value)
|
212
|
+
if value.is_a?(String) || value.is_a?(Symbol)
|
213
|
+
case value.to_s.downcase
|
214
|
+
when 'left'
|
215
|
+
value = 1
|
216
|
+
when 'right'
|
217
|
+
value = 0
|
218
|
+
else
|
219
|
+
value = 1
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
return value
|
224
|
+
end
|
225
|
+
|
226
|
+
def numeric_el(value)
|
227
|
+
raise "Expected number" unless value.is_a?(Numeric)
|
228
|
+
return value
|
229
|
+
end
|
230
|
+
|
231
|
+
def build
|
232
|
+
@options
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
class ThemeBuilder < ThemeComponentBuilder
|
237
|
+
def initialize(starting = nil)
|
238
|
+
super("plot")
|
239
|
+
@starting = starting
|
240
|
+
end
|
241
|
+
|
242
|
+
def background(value)
|
243
|
+
self.option('background', rect_el(value))
|
244
|
+
end
|
245
|
+
|
246
|
+
def title(value)
|
247
|
+
self.option('title', text_el(value))
|
248
|
+
end
|
249
|
+
|
250
|
+
def axis
|
251
|
+
builder = ThemeAxisBuilder.new
|
252
|
+
yield builder
|
253
|
+
|
254
|
+
@options.merge!(builder.build)
|
255
|
+
return self
|
256
|
+
end
|
257
|
+
|
258
|
+
def legend
|
259
|
+
builder = ThemeLegendBuilder.new
|
260
|
+
yield builder
|
261
|
+
|
262
|
+
@options.merge!(builder.build)
|
263
|
+
return self
|
264
|
+
end
|
265
|
+
|
266
|
+
def panel
|
267
|
+
builder = ThemePanelBuilder.new
|
268
|
+
yield builder
|
269
|
+
|
270
|
+
@options.merge!(builder.build)
|
271
|
+
return self
|
272
|
+
end
|
273
|
+
|
274
|
+
def build
|
275
|
+
return Theme.new(@starting, **@options)
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
class ThemeAxisBuilder < ThemeComponentBuilder
|
280
|
+
def initialize
|
281
|
+
super("axis")
|
282
|
+
end
|
283
|
+
|
284
|
+
def line(value)
|
285
|
+
self.option('line', line_el(value))
|
286
|
+
end
|
287
|
+
|
288
|
+
def text(value)
|
289
|
+
self.option('text', text_el(value))
|
290
|
+
end
|
291
|
+
|
292
|
+
def text_x(value)
|
293
|
+
self.option('text.x', text_el(value))
|
294
|
+
end
|
295
|
+
|
296
|
+
def text_y(value)
|
297
|
+
self.option('text.y', text_el(value))
|
298
|
+
end
|
299
|
+
|
300
|
+
def title(value)
|
301
|
+
self.option('title', text_el(value))
|
302
|
+
end
|
303
|
+
|
304
|
+
def title_x(value)
|
305
|
+
self.option('title.x', text_el(value))
|
306
|
+
end
|
307
|
+
|
308
|
+
def title_y(value)
|
309
|
+
self.option('title.y', text_el(value))
|
310
|
+
end
|
311
|
+
|
312
|
+
def ticks(value)
|
313
|
+
self.option('ticks', line_el(value))
|
314
|
+
end
|
315
|
+
|
316
|
+
def ticks_length(value)
|
317
|
+
self.option('ticks.length', unit_el(value))
|
318
|
+
end
|
319
|
+
end
|
320
|
+
|
321
|
+
class ThemeLegendBuilder < ThemeComponentBuilder
|
322
|
+
def initialize
|
323
|
+
super("legend")
|
324
|
+
end
|
325
|
+
|
326
|
+
def position(value)
|
327
|
+
self.option('position', value)
|
328
|
+
end
|
329
|
+
|
330
|
+
def justification(value)
|
331
|
+
self.option('justification', value)
|
332
|
+
end
|
333
|
+
|
334
|
+
def background(value)
|
335
|
+
self.option('background', rect_el(value))
|
336
|
+
end
|
337
|
+
|
338
|
+
def key_background(value)
|
339
|
+
self.option('key', rect_el(value))
|
340
|
+
end
|
341
|
+
|
342
|
+
def key_size(value)
|
343
|
+
self.option('key.size', unit_el(value))
|
344
|
+
end
|
345
|
+
|
346
|
+
def key_height(value)
|
347
|
+
self.option('key.height', unit_el(value))
|
348
|
+
end
|
349
|
+
|
350
|
+
def key_width(value)
|
351
|
+
self.option('key.width', unit_el(value))
|
352
|
+
end
|
353
|
+
|
354
|
+
def margin(value)
|
355
|
+
self.option('margin', unit_el(value))
|
356
|
+
end
|
357
|
+
|
358
|
+
def text(value)
|
359
|
+
self.option('text', text_el(value))
|
360
|
+
end
|
361
|
+
|
362
|
+
def text_align(value)
|
363
|
+
self.option('text.align', alignment_el(value))
|
364
|
+
end
|
365
|
+
|
366
|
+
def title(value)
|
367
|
+
self.option('title', text_el(value))
|
368
|
+
end
|
369
|
+
|
370
|
+
def title_align(value)
|
371
|
+
self.option('key.size', alignment_el(value))
|
372
|
+
end
|
373
|
+
end
|
374
|
+
|
375
|
+
class ThemePanelBuilder < ThemeComponentBuilder
|
376
|
+
def initialize
|
377
|
+
super("panel")
|
378
|
+
end
|
379
|
+
|
380
|
+
def background(value)
|
381
|
+
self.option('background', rect_el(value))
|
382
|
+
end
|
383
|
+
|
384
|
+
def border(value)
|
385
|
+
self.option('border', rect_el(value))
|
386
|
+
end
|
387
|
+
|
388
|
+
def grid_major(value)
|
389
|
+
self.option('grid.major', line_el(value))
|
390
|
+
end
|
391
|
+
|
392
|
+
def grid_major_x(value)
|
393
|
+
self.option('grid.major.x', line_el(value))
|
394
|
+
end
|
395
|
+
|
396
|
+
def grid_major_y(value)
|
397
|
+
self.option('grid.major.y', line_el(value))
|
398
|
+
end
|
399
|
+
|
400
|
+
def grid_minor(value)
|
401
|
+
self.option('grid.minor', line_el(value))
|
402
|
+
end
|
403
|
+
|
404
|
+
def grid_minor_x(value)
|
405
|
+
self.option('grid.minor.x', line_el(value))
|
406
|
+
end
|
407
|
+
|
408
|
+
def grid_minor_y(value)
|
409
|
+
self.option('grid.minor.y', line_el(value))
|
410
|
+
end
|
411
|
+
|
412
|
+
def aspect_ratio(value)
|
413
|
+
self.option('aspect.ratio', numeric_el(value))
|
414
|
+
end
|
415
|
+
|
416
|
+
def margin(value)
|
417
|
+
self.option('margin', unit_el(value))
|
418
|
+
end
|
419
|
+
|
420
|
+
def margin_x(value)
|
421
|
+
self.option('margin.x', unit_el(value))
|
422
|
+
end
|
423
|
+
|
424
|
+
def margin_y(value)
|
425
|
+
self.option('margin.y', unit_el(value))
|
426
|
+
end
|
427
|
+
end
|
428
|
+
|
429
|
+
class ThemeCollection
|
430
|
+
def self.ggtech(name = "google")
|
431
|
+
Rust.prerequisite("ricardo-bion/ggtech", true)
|
432
|
+
|
433
|
+
return ExistingTheme.new("theme_tech", theme: name)
|
434
|
+
end
|
435
|
+
|
436
|
+
def self.ggdark(style = "classic")
|
437
|
+
Rust.prerequisite("ggdark")
|
438
|
+
|
439
|
+
return ExistingTheme.new("dark_theme_#{style}")
|
440
|
+
end
|
441
|
+
end
|
442
|
+
|
443
|
+
self.default_theme = ThemeBuilder.new("bw").
|
444
|
+
title(face: 'bold', size: 12).
|
445
|
+
legend do |legend|
|
446
|
+
legend.background(fill: 'white', size: 4, colour: 'white')
|
447
|
+
legend.position([0, 1])
|
448
|
+
legend.justification([0, 1])
|
449
|
+
end.
|
450
|
+
axis do |axis|
|
451
|
+
axis.ticks(colour: 'grey70', size: 0.2)
|
452
|
+
end.
|
453
|
+
panel do |panel|
|
454
|
+
panel.grid_major(colour: 'grey70', size: 0.2)
|
455
|
+
panel.grid_minor(Theme::BlankElement.new)
|
456
|
+
end.
|
457
|
+
build
|
458
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
require_relative 'ggplot2/core'
|
2
|
+
require_relative 'ggplot2/geoms'
|
3
|
+
require_relative 'ggplot2/themes'
|
4
|
+
require_relative 'ggplot2/plot_builder'
|
5
|
+
require_relative 'ggplot2/scale'
|
6
|
+
|
7
|
+
Rust::Manual.register(:ggplot2, "ggplot2", "Informations on the wrapper of the popular ggplot2 plotting library for R.")
|
8
|
+
|
9
|
+
Rust::Manual.for(:ggplot2).register("Introduction", /intro/,
|
10
|
+
<<-EOS
|
11
|
+
bind_ggplot! # Avoid using long module names to reach Rust::Plots::GGPlot (simply includes this module)
|
12
|
+
|
13
|
+
# Best with a dataframe, but not necessary. If you have it...
|
14
|
+
df = Rust.toothgrowth
|
15
|
+
plot = PlotBuilder.for_dataframe(df). # Use a dataframe (symbols will be variable names)
|
16
|
+
labeled("Example plot"). # "labeled" sets the label to the last set aesthetic item (x, y, or title, in this case)
|
17
|
+
with_x(:len).labeled("X data from df"). # Set all the aesthetics (x, y, ...)
|
18
|
+
with_y(:dose).labeled("Y data from df").
|
19
|
+
draw_points. # Set the geometries to plot (based on the plot type)
|
20
|
+
build # Returns the plot ready to use
|
21
|
+
plot.show # Show the plot in a window
|
22
|
+
plot.save("output.pdf", width: 5, height: 4) # Save the plot, width, height etc. are optional
|
23
|
+
|
24
|
+
# If you don't have a dataframe...
|
25
|
+
plot2 = PlotBuilder.new.
|
26
|
+
with_x([1,2,3]).labeled("X data from df").
|
27
|
+
with_y([3,4,5]).labeled("Y data from df").
|
28
|
+
draw_points.
|
29
|
+
build
|
30
|
+
plot2.show
|
31
|
+
EOS
|
32
|
+
)
|
33
|
+
|
34
|
+
Rust::Manual.for(:ggplot2).register("Scatter plots", /scatter/,
|
35
|
+
<<-EOS
|
36
|
+
bind_ggplot!
|
37
|
+
df = Rust.toothgrowth
|
38
|
+
plot = PlotBuilder.for_dataframe(df).
|
39
|
+
with_x(:len).labeled("X data").
|
40
|
+
with_y(:dose).labeled("Y data").
|
41
|
+
draw_points. # To draw points
|
42
|
+
draw_lines. # To draw lines (keep both to draw both)
|
43
|
+
build
|
44
|
+
plot.show
|
45
|
+
EOS
|
46
|
+
)
|
47
|
+
|
48
|
+
Rust::Manual.for(:ggplot2).register("Bar plots", /bar/,
|
49
|
+
<<-EOS
|
50
|
+
bind_ggplot!
|
51
|
+
df = Rust.toothgrowth
|
52
|
+
plot = PlotBuilder.for_dataframe(df).
|
53
|
+
with_x(:len).labeled("X data").
|
54
|
+
with_fill(:supp).labeled("Legend"). # Use with_fill or with_color for stacked plots
|
55
|
+
draw_bars. # To draw bars
|
56
|
+
build
|
57
|
+
plot.show
|
58
|
+
EOS
|
59
|
+
)
|
60
|
+
|
61
|
+
Rust::Manual.for(:ggplot2).register("Box plots", /box/,
|
62
|
+
<<-EOS
|
63
|
+
bind_ggplot!
|
64
|
+
df = Rust.toothgrowth
|
65
|
+
plot = PlotBuilder.for_dataframe(df).
|
66
|
+
with_y(:len).labeled("Data to boxplot").
|
67
|
+
with_group(:supp).labeled("Groups"). # Groups to plot
|
68
|
+
draw_boxplot.
|
69
|
+
build
|
70
|
+
plot.show
|
71
|
+
EOS
|
72
|
+
)
|
73
|
+
|
74
|
+
Rust::Manual.for(:ggplot2).register("Histograms", /hist/,
|
75
|
+
<<-EOS
|
76
|
+
bind_ggplot!
|
77
|
+
df = Rust.toothgrowth
|
78
|
+
plot = PlotBuilder.for_dataframe(df).
|
79
|
+
with_x(:len).labeled("Data to plot").
|
80
|
+
with_fill(:supp).labeled("Color"). # Use with_fill or with_color for multiple plots
|
81
|
+
draw_histogram.
|
82
|
+
build
|
83
|
+
plot.show
|
84
|
+
EOS
|
85
|
+
)
|
86
|
+
|
87
|
+
Rust::Manual.for(:ggplot2).register("Themes", /them/,
|
88
|
+
<<-EOS
|
89
|
+
bind_ggplot!
|
90
|
+
df = Rust.toothgrowth
|
91
|
+
# The method with_theme allows to change theme options. The method can be called
|
92
|
+
# several times, each time the argument does not overwrite the previous options,
|
93
|
+
# unless they are specified again (in that case, the last specified ones win).
|
94
|
+
plot = PlotBuilder.for_dataframe(df).
|
95
|
+
with_x(:len).labeled("X data").
|
96
|
+
with_y(:dose).labeled("Y data").
|
97
|
+
draw_points.
|
98
|
+
with_theme(
|
99
|
+
ThemeBuilder.new('bw').
|
100
|
+
title(face: 'bold', size: 12). # Each method sets the property for the related element
|
101
|
+
legend do |legend| # Legend and other parts can be set like this
|
102
|
+
legend.position(:left) # Puts the legend on the left
|
103
|
+
end.
|
104
|
+
axis do |axis| # Modifies the axes
|
105
|
+
axis.line(Theme::BlankElement.new) # Hides the lines for the axes
|
106
|
+
axis.text_x(size: 3) # X axis labels
|
107
|
+
end.
|
108
|
+
panel do |panel|
|
109
|
+
panel.grid_major(colour: 'grey70', size: 0.2) # Sets the major ticks grid
|
110
|
+
panel.grid_minor(Theme::BlankElement.new) # Hides the minor ticks grid
|
111
|
+
end.
|
112
|
+
build
|
113
|
+
).build
|
114
|
+
plot.show
|
115
|
+
EOS
|
116
|
+
)
|