rust 0.11 → 0.13

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+ )