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.
@@ -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
+ )