charty 0.2.3 → 0.2.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +56 -23
  3. data/.github/workflows/nmatrix.yml +67 -0
  4. data/.github/workflows/pycall.yml +86 -0
  5. data/Gemfile +18 -0
  6. data/README.md +123 -4
  7. data/Rakefile +4 -5
  8. data/charty.gemspec +1 -3
  9. data/examples/sample_images/hist_gruff.png +0 -0
  10. data/images/penguins_body_mass_g_flipper_length_mm_scatter_plot.png +0 -0
  11. data/images/penguins_body_mass_g_flipper_length_mm_species_scatter_plot.png +0 -0
  12. data/images/penguins_body_mass_g_flipper_length_mm_species_sex_scatter_plot.png +0 -0
  13. data/images/penguins_species_body_mass_g_bar_plot_h.png +0 -0
  14. data/images/penguins_species_body_mass_g_bar_plot_v.png +0 -0
  15. data/images/penguins_species_body_mass_g_box_plot_h.png +0 -0
  16. data/images/penguins_species_body_mass_g_box_plot_v.png +0 -0
  17. data/images/penguins_species_body_mass_g_sex_bar_plot_v.png +0 -0
  18. data/images/penguins_species_body_mass_g_sex_box_plot_v.png +0 -0
  19. data/lib/charty.rb +4 -0
  20. data/lib/charty/backends/gruff.rb +13 -2
  21. data/lib/charty/backends/plotly.rb +322 -20
  22. data/lib/charty/backends/pyplot.rb +416 -64
  23. data/lib/charty/index.rb +213 -0
  24. data/lib/charty/linspace.rb +1 -1
  25. data/lib/charty/missing_value_support.rb +14 -0
  26. data/lib/charty/plot_methods.rb +173 -8
  27. data/lib/charty/plotters.rb +7 -0
  28. data/lib/charty/plotters/abstract_plotter.rb +87 -12
  29. data/lib/charty/plotters/bar_plotter.rb +200 -3
  30. data/lib/charty/plotters/box_plotter.rb +75 -7
  31. data/lib/charty/plotters/categorical_plotter.rb +272 -40
  32. data/lib/charty/plotters/count_plotter.rb +7 -0
  33. data/lib/charty/plotters/estimation_support.rb +84 -0
  34. data/lib/charty/plotters/random_support.rb +25 -0
  35. data/lib/charty/plotters/relational_plotter.rb +518 -0
  36. data/lib/charty/plotters/scatter_plotter.rb +115 -0
  37. data/lib/charty/plotters/vector_plotter.rb +6 -0
  38. data/lib/charty/statistics.rb +87 -2
  39. data/lib/charty/table.rb +50 -15
  40. data/lib/charty/table_adapters.rb +2 -0
  41. data/lib/charty/table_adapters/active_record_adapter.rb +17 -9
  42. data/lib/charty/table_adapters/base_adapter.rb +69 -0
  43. data/lib/charty/table_adapters/daru_adapter.rb +37 -3
  44. data/lib/charty/table_adapters/datasets_adapter.rb +6 -2
  45. data/lib/charty/table_adapters/hash_adapter.rb +130 -16
  46. data/lib/charty/table_adapters/narray_adapter.rb +25 -6
  47. data/lib/charty/table_adapters/nmatrix_adapter.rb +15 -5
  48. data/lib/charty/table_adapters/pandas_adapter.rb +81 -0
  49. data/lib/charty/vector.rb +69 -0
  50. data/lib/charty/vector_adapters.rb +183 -0
  51. data/lib/charty/vector_adapters/array_adapter.rb +109 -0
  52. data/lib/charty/vector_adapters/daru_adapter.rb +171 -0
  53. data/lib/charty/vector_adapters/narray_adapter.rb +187 -0
  54. data/lib/charty/vector_adapters/nmatrix_adapter.rb +37 -0
  55. data/lib/charty/vector_adapters/numpy_adapter.rb +168 -0
  56. data/lib/charty/vector_adapters/pandas_adapter.rb +200 -0
  57. data/lib/charty/version.rb +1 -1
  58. metadata +33 -45
@@ -8,11 +8,14 @@ module Charty
8
8
  class << self
9
9
  def prepare
10
10
  require 'matplotlib/pyplot'
11
+ require 'numpy'
11
12
  end
12
13
  end
13
14
 
14
15
  def initialize
15
16
  @pyplot = ::Matplotlib::Pyplot
17
+ @default_line_width = ::Matplotlib.rcParams["lines.linewidth"]
18
+ @default_marker_size = ::Matplotlib.rcParams["lines.markersize"]
16
19
  end
17
20
 
18
21
  def self.activate_iruby_integration
@@ -90,7 +93,16 @@ module Charty
90
93
  min_l = palette.colors.map {|c| c.to_rgb.to_hsl.l }.min
91
94
  lum = min_l*0.6
92
95
  gray = Colors::RGB.new(lum, lum, lum).to_hex_string
93
- draw_box_plot(context, subplot, colors, gray)
96
+ Array(context.data).each_with_index do |group_data, i|
97
+ next if group_data.empty?
98
+
99
+ box_data = group_data.compact
100
+ next if box_data.empty?
101
+
102
+ color = colors.next
103
+ draw_box_plot(box_data, vert: "v", position: i, color: color,
104
+ gray: gray, width: 0.8, whisker: 1.5, flier_size: 5)
105
+ end
94
106
  when :bubble
95
107
  context.series.each do |data|
96
108
  ax.scatter(data.xs.to_a, data.ys.to_a, s: data.zs.to_a, alpha: 0.5,
@@ -125,93 +137,409 @@ module Charty
125
137
  end
126
138
  end
127
139
 
128
- private def draw_box_plot(context, subplot, colors, gray)
129
- Array(context.data).each_with_index do |group_data, i|
130
- next if group_data.empty?
131
-
132
- box_data = group_data.compact
133
- next if box_data.empty?
134
-
135
- artist_dict = @pyplot.boxplot(box_data, vert: "v", patch_artist: true,
136
- positions: [i], widths: 0.8)
137
-
138
- color = colors.next
139
- artist_dict["boxes"].each do |box|
140
- box.update({facecolor: color, zorder: 0.9, edgecolor: gray}, {})
141
- end
142
- artist_dict["whiskers"].each do |whisker|
143
- whisker.update({color: gray, linestyle: "-"}, {})
144
- end
145
- artist_dict["caps"].each do |cap|
146
- cap.update({color: gray}, {})
147
- end
148
- artist_dict["medians"].each do |median|
149
- median.update({color: gray}, {})
150
- end
151
- artist_dict["fliers"].each do |flier|
152
- flier.update({
153
- markerfacecolor: gray,
154
- marker: "d",
155
- markeredgecolor: gray,
156
- markersize: 5
157
- }, {})
158
- end
159
- end
160
- end
161
-
162
140
  # ==== NEW PLOTTING API ====
163
141
 
164
142
  def begin_figure
165
- # do nothing
143
+ @legend_keys = []
144
+ @legend_labels = []
166
145
  end
167
146
 
168
- def bar(bar_pos, values, color: nil, width: 0.8r, align: :center, orient: :v)
147
+ def bar(bar_pos, _group_names, values, colors, orient, label: nil, width: 0.8r,
148
+ align: :center, conf_int: nil, error_colors: nil, error_width: nil, cap_size: nil)
169
149
  bar_pos = Array(bar_pos)
170
150
  values = Array(values)
171
- color = Array(color).map(&:to_hex_string)
151
+ colors = Array(colors).map(&:to_hex_string)
172
152
  width = Float(width)
153
+
154
+ ax = @pyplot.gca
155
+ kw = {color: colors, align: align}
156
+ kw[:label] = label unless label.nil?
157
+
173
158
  if orient == :v
174
- @pyplot.bar(bar_pos, values, width: width, color: color, align: align)
159
+ ax.bar(bar_pos, values, width, **kw)
175
160
  else
176
- @pyplot.barh(bar_pos, values, width: width, color: color, align: align)
161
+ ax.barh(bar_pos, values, width, **kw)
162
+ end
163
+
164
+ if conf_int
165
+ error_colors = Array(error_colors).map(&:to_hex_string)
166
+ confidence_intervals(ax, bar_pos, conf_int, orient, error_colors, error_width, cap_size)
167
+ end
168
+ end
169
+
170
+ private def confidence_intervals(ax, at_group, conf_int, orient, colors, error_width=nil, cap_size=nil, **options)
171
+ options[:lw] = error_width || @default_line_width * 1.8
172
+
173
+ at_group.each_index do |i|
174
+ at = at_group[i]
175
+ ci_low, ci_high = conf_int[i]
176
+ color = colors[i]
177
+
178
+ if orient == :v
179
+ ax.plot([at, at], [ci_low, ci_high], color: color, **options)
180
+ unless cap_size.nil?
181
+ ax.plot([at - cap_size / 2.0, at + cap_size / 2.0], [ci_low, ci_low], color: color, **options)
182
+ ax.plot([at - cap_size / 2.0, at + cap_size / 2.0], [ci_high, ci_high], color: color, **options)
183
+ end
184
+ else
185
+ ax.plot([ci_low, ci_high], [at, at], color: color, **options)
186
+ unless cap_size.nil?
187
+ ax.plot([ci_low, ci_low], [at - cap_size / 2.0, at + cap_size / 2.0], color: color, **options)
188
+ ax.plot([ci_high, ci_high], [at - cap_size / 2.0, at + cap_size / 2.0], color: color, **options)
189
+ end
190
+ end
177
191
  end
178
192
  end
179
193
 
180
- def box_plot(plot_data, positions, color:, gray:,
181
- width: 0.8r, flier_size: 5, whisker: 1.5, notch: false)
182
- color = Array(color).map(&:to_hex_string)
194
+ def box_plot(plot_data, group_names,
195
+ orient:, colors:, gray:, dodge:, width: 0.8r,
196
+ flier_size: 5, whisker: 1.5, notch: false)
197
+ colors = Array(colors).map(&:to_hex_string)
183
198
  gray = gray.to_hex_string
184
199
  width = Float(width)
185
200
  flier_size = Float(flier_size)
186
201
  whisker = Float(whisker)
202
+
187
203
  plot_data.each_with_index do |group_data, i|
188
- next if group_data.nil? || group_data.empty?
204
+ unless group_data.nil?
205
+ draw_box_plot(group_data,
206
+ vert: (orient == :v),
207
+ position: i,
208
+ color: colors[i],
209
+ gray: gray,
210
+ width: width,
211
+ whisker: whisker,
212
+ flier_size: flier_size)
213
+ end
214
+ end
215
+ end
189
216
 
190
- artist_dict = @pyplot.boxplot(group_data, vert: :v,
191
- patch_artist: true,
192
- positions: [i],
193
- widths: width,
194
- whis: whisker, )
217
+ def grouped_box_plot(plot_data, group_names, color_names,
218
+ orient:, colors:, gray:, dodge:, width: 0.8r,
219
+ flier_size: 5, whisker: 1.5, notch: false)
220
+ colors = Array(colors).map(&:to_hex_string)
221
+ gray = gray.to_hex_string
222
+ width = Float(width)
223
+ flier_size = Float(flier_size)
224
+ whisker = Float(whisker)
195
225
 
196
- artist_dict["boxes"].each do |box|
197
- box.update({facecolor: color[i], zorder: 0.9, edgecolor: gray}, {})
226
+ offsets = color_offsets(color_names, dodge, width)
227
+ orig_width = width
228
+ width = Float(nested_width(color_names, dodge, width))
229
+
230
+ color_names.each_with_index do |color_name, i|
231
+ add_box_plot_legend(gray, colors[i], color_names[i])
232
+
233
+ plot_data[i].each_with_index do |group_data, j|
234
+ next if group_data.empty?
235
+
236
+ position = j + offsets[i]
237
+ draw_box_plot(group_data,
238
+ vert: (orient == :v),
239
+ position: position,
240
+ color: colors[i],
241
+ gray: gray,
242
+ width: width,
243
+ whisker: whisker,
244
+ flier_size: flier_size)
198
245
  end
199
- artist_dict["whiskers"].each do |whisker|
200
- whisker.update({color: gray, linestyle: "-"}, {})
246
+ end
247
+ end
248
+
249
+ private def add_box_plot_legend(gray, color, name)
250
+ patch = @pyplot.Rectangle.new([0, 0], 0, 0, edgecolor: gray, facecolor: color, label: name)
251
+ @pyplot.gca.add_patch(patch)
252
+ end
253
+
254
+ private def draw_box_plot(group_data, vert:, position:, color:, gray:, width:, whisker:, flier_size:)
255
+ # TODO: Do not convert to Array when group_data is Pandas::Series or Numpy::NDArray,
256
+ # and use MemoryView if available when group_data is Numo::NArray
257
+ artist_dict = @pyplot.boxplot(Array(group_data),
258
+ vert: vert,
259
+ patch_artist: true,
260
+ positions: [position],
261
+ widths: width,
262
+ whis: whisker)
263
+
264
+ artist_dict["boxes"].each do |box|
265
+ box.update({facecolor: color, zorder: 0.9, edgecolor: gray}, {})
266
+ end
267
+ artist_dict["whiskers"].each do |whisker|
268
+ whisker.update({color: gray, linestyle: "-"}, {})
269
+ end
270
+ artist_dict["caps"].each do |cap|
271
+ cap.update({color: gray}, {})
272
+ end
273
+ artist_dict["medians"].each do |median|
274
+ median.update({color: gray}, {})
275
+ end
276
+ artist_dict["fliers"].each do |flier|
277
+ flier.update({
278
+ markerfacecolor: gray,
279
+ marker: "d",
280
+ markeredgecolor: gray,
281
+ markersize: flier_size
282
+ }, {})
283
+ end
284
+ end
285
+
286
+ private def color_offsets(color_names, dodge, width)
287
+ n_names = color_names.length
288
+ if dodge
289
+ each_width = width / n_names
290
+ offsets = Charty::Linspace.new(0 .. (width - each_width), n_names).to_a
291
+ mean = Statistics.mean(offsets)
292
+ offsets.map {|x| x - mean }
293
+ else
294
+ Array.new(n_names, 0)
295
+ end
296
+ end
297
+
298
+ private def nested_width(color_names, dodge, width)
299
+ if dodge
300
+ width.to_r / color_names.length * 0.98r
301
+ else
302
+ width
303
+ end
304
+ end
305
+
306
+ def scatter(x, y, variables, legend:, color:, color_mapper:,
307
+ style:, style_mapper:, size:, size_mapper:)
308
+ kwd = {}
309
+ kwd[:edgecolor] = "w"
310
+
311
+ ax = @pyplot.gca
312
+ points = ax.scatter(x.to_a, y.to_a, **kwd)
313
+
314
+ unless color.nil?
315
+ color = color_mapper[color].map(&:to_hex_string)
316
+ points.set_facecolors(color)
317
+ end
318
+
319
+ unless size.nil?
320
+ size = size_mapper[size].map(&method(:scale_scatter_point_size))
321
+ points.set_sizes(size)
322
+ end
323
+
324
+ unless style.nil?
325
+ paths = style_mapper[style, :marker].map(&method(:marker_to_path))
326
+ points.set_paths(paths)
327
+ end
328
+
329
+ sizes = points.get_sizes
330
+ points.set_linewidths(0.08 * Numpy.sqrt(Numpy.percentile(sizes, 10)))
331
+
332
+ if legend
333
+ add_relational_plot_legend(
334
+ ax, legend, variables, color_mapper, size_mapper, style_mapper,
335
+ [:color, :s, :marker]
336
+ ) do |label, kwargs|
337
+ ax.scatter([], [], label: label, **kwargs)
201
338
  end
202
- artist_dict["caps"].each do |cap|
203
- cap.update({color: gray}, {})
339
+ end
340
+ end
341
+
342
+ PYPLOT_MARKERS = {
343
+ circle: "o",
344
+ x: "X",
345
+ cross: "P",
346
+ triangle_up: "^",
347
+ triangle_down: "v",
348
+ square: [4, 0, 45].freeze,
349
+ diamond: [4, 0, 0].freeze,
350
+ star: [5, 1, 0].freeze,
351
+ star_diamond: [4, 1, 0].freeze,
352
+ star_square: [4, 1, 45].freeze,
353
+ pentagon: [5, 0, 0].freeze,
354
+ hexagon: [6, 0, 0].freeze,
355
+ }.freeze
356
+
357
+ private def marker_to_path(marker)
358
+ @path_cache ||= {}
359
+ if @path_cache.key?(marker)
360
+ @path_cache[marker]
361
+ elsif PYPLOT_MARKERS.key?(marker)
362
+ val = PYPLOT_MARKERS[marker]
363
+ ms = Matplotlib.markers.MarkerStyle.new(val)
364
+ @path_cache[marker] = ms.get_path().transformed(ms.get_transform())
365
+ else
366
+ raise ArgumentError, "Unknown marker name: %p" % marker
367
+ end
368
+ end
369
+
370
+ RELATIONAL_PLOT_LEGEND_BRIEF_TICKS = 6
371
+
372
+ private def add_relational_plot_legend(ax, verbosity, variables, color_mapper, size_mapper, style_mapper,
373
+ legend_attributes, &func)
374
+ brief_ticks = RELATIONAL_PLOT_LEGEND_BRIEF_TICKS
375
+ verbosity = :auto if verbosity == true
376
+
377
+ legend_titles = [:color, :size, :style].filter_map {|v| variables[v] }
378
+ legend_title = legend_titles.pop if legend_titles.length == 1
379
+
380
+ legend_kwargs = {}
381
+ update_legend = ->(var_name, val_name, **kw) do
382
+ key = [var_name, val_name]
383
+ if legend_kwargs.key?(key)
384
+ legend_kwargs[key].update(kw)
385
+ else
386
+ legend_kwargs[key] = kw
204
387
  end
205
- artist_dict["medians"].each do |median|
206
- median.update({color: gray}, {})
388
+ end
389
+
390
+ title_kwargs = {visible: false, color: "w", s: 0, linewidth: 0, marker: "", dashes: ""}
391
+
392
+ # color legend
393
+
394
+ brief_color = case verbosity
395
+ when :brief
396
+ color_mapper.map_type == :numeric
397
+ when :auto
398
+ if color_mapper.levels.nil?
399
+ false
400
+ else
401
+ color_mapper.levels.length > brief_ticks
402
+ end
403
+ else
404
+ false
405
+ end
406
+ case
407
+ when brief_color
408
+ # TODO: Also support LogLocator
409
+ # locator = Matplotlib.ticker.LogLocator.new(numticks: brief_ticks)
410
+ locator = Matplotlib.ticker.MaxNLocator.new(nbins: brief_ticks)
411
+ limits = color_map.levels.minmax
412
+ color_levels, color_formatted_levels = locator_to_legend_entries(locator, limits)
413
+ when color_mapper.levels.nil?
414
+ color_levels = color_formatted_levels = []
415
+ else
416
+ color_levels = color_formatted_levels = color_mapper.levels
417
+ end
418
+
419
+ if legend_title.nil? && variables.key?(:color)
420
+ update_legend.([variables[:color], :title], variables[:color], **title_kwargs)
421
+ end
422
+
423
+ color_levels.length.times do |i|
424
+ next if color_levels[i].nil?
425
+ color_value = color_mapper[color_levels[i]].to_hex_string
426
+ update_legend.(variables[:color], color_formatted_levels[i], color: color_value)
427
+ end
428
+
429
+ brief_size = case verbosity
430
+ when :brief
431
+ size_mapper.map_type == :numeric
432
+ when :auto
433
+ if size_mapper.levels.nil?
434
+ false
435
+ else
436
+ size_mapper.levels.length > brief_ticks
437
+ end
438
+ else
439
+ false
440
+ end
441
+ case
442
+ when brief_size
443
+ # TODO: Also support LogLocator
444
+ # locator = Matplotlib.ticker.LogLocator(numticks: brief_ticks)
445
+ locator = Matplotlib.ticker.MaxNLocator.new(nbins: brief_ticks)
446
+ limits = size_mapper.levels.minmax
447
+ size_levels, size_formatted_levels = locator_to_legend_entries(locator, limits)
448
+ when size_mapper.levels.nil?
449
+ size_levels = size_formatted_levels = []
450
+ else
451
+ size_levels = size_formatted_levels = size_mapper.levels
452
+ end
453
+
454
+ if legend_title.nil? && variables.key?(:size)
455
+ update_legend.([variables[:size], :title], variables[:size], **title_kwargs)
456
+ end
457
+
458
+ size_levels.length.times do |i|
459
+ next if size_levels[i].nil?
460
+ size_value = scale_scatter_point_size(size_mapper[size_levels[i]])
461
+ update_legend.(variables[:size], size_formatted_levels[i], linewidth: size_value, s: size_value)
462
+ end
463
+
464
+ if legend_title.nil? && variables.key?(:style)
465
+ update_legend.([variables[:style], :title], variables[:style], **title_kwargs)
466
+ end
467
+
468
+ unless style_mapper.levels.nil?
469
+ style_mapper.levels.each do |level|
470
+ next if level.nil?
471
+ attrs = style_mapper[level]
472
+ marker = if attrs.key?(:marker)
473
+ PYPLOT_MARKERS[attrs[:marker]]
474
+ else
475
+ ""
476
+ end
477
+ # TODO: support dashes
478
+ update_legend.(variables[:style], level,
479
+ marker: marker,
480
+ dashes: attrs.fetch(:dashes, ""))
207
481
  end
208
- artist_dict["fliers"].each do |flier|
209
- flier.update({
210
- markerfacecolor: gray,
211
- marker: "d",
212
- markeredgecolor: gray,
213
- markersize: flier_size
214
- }, {})
482
+ end
483
+
484
+ legend_kwargs.each do |key, kw|
485
+ _, label = key
486
+ kw[:color] ||= ".2"
487
+ use_kw = legend_attributes.filter_map {|attr|
488
+ [attr, kw[attr]] if kw.key?(attr)
489
+ }.to_h
490
+ use_kw[:visible] = kw[:visible] if kw.key?(:visible)
491
+ func.(label, use_kw)
492
+ end
493
+
494
+ handles = ax.get_legend_handles_labels()[0].to_a
495
+ unless handles.empty?
496
+ legend = ax.legend(title: legend_title || "")
497
+ adjust_legend_subtitles(legend)
498
+ end
499
+ end
500
+
501
+ private def scale_scatter_point_size(x)
502
+ min = 0.5 * @default_marker_size**2
503
+ max = 2.0 * @default_marker_size**2
504
+
505
+ min + x * (max - min)
506
+ end
507
+
508
+ private def locator_to_legend_entries(locator, limits)
509
+ vmin, vmax = limits
510
+ raw_levels = locator.tick_values(vmin, vmax).to_a
511
+ raw_levels.reject! {|v| v < limits[0] || limits[1] < v }
512
+
513
+ formatter = case locator
514
+ when Matplotlib.ticker.LogLocator
515
+ Matplotlib.ticker.LogFormatter.new
516
+ else
517
+ Matplotlib.ticker.ScalarFormatter.new
518
+ end
519
+
520
+ dummy_axis = Object.new
521
+ dummy_axis.define_singleton_method(:get_view_interval) { limits }
522
+ formatter.axis = dummy_axis
523
+
524
+ formatter.set_locs(raw_levels)
525
+ formatted_levels = raw_levels.map {|x| formatter.(x) }
526
+
527
+ return raw_levels, formatted_levels
528
+ end
529
+
530
+ private def adjust_legend_subtitles(legend)
531
+ font_size = Matplotlib.rcParams.get("legend.title_fontsize", nil)
532
+ hpackers = legend.findobj(Matplotlib.offsetbox.VPacker)[0].get_children()
533
+ hpackers.each do |hpack|
534
+ draw_area, text_area = hpack.get_children()
535
+ handles = draw_area.get_children()
536
+ unless handles.all? {|a| a.get_visible() }
537
+ draw_area.set_width(0)
538
+ unless font_size.nil?
539
+ text_area.get_children().each do |text|
540
+ text.set_size(font_size)
541
+ end
542
+ end
215
543
  end
216
544
  end
217
545
  end
@@ -228,18 +556,42 @@ module Charty
228
556
  @pyplot.gca.set_xticks(Array(values))
229
557
  end
230
558
 
559
+ def set_yticks(values)
560
+ @pyplot.gca.set_yticks(Array(values))
561
+ end
562
+
231
563
  def set_xtick_labels(labels)
232
564
  @pyplot.gca.set_xticklabels(Array(labels).map(&method(:String)))
233
565
  end
234
566
 
567
+ def set_ytick_labels(labels)
568
+ @pyplot.gca.set_yticklabels(Array(labels).map(&method(:String)))
569
+ end
570
+
235
571
  def set_xlim(min, max)
236
572
  @pyplot.gca.set_xlim(Float(min), Float(max))
237
573
  end
238
574
 
575
+ def set_ylim(min, max)
576
+ @pyplot.gca.set_ylim(Float(min), Float(max))
577
+ end
578
+
239
579
  def disable_xaxis_grid
240
580
  @pyplot.gca.xaxis.grid(false)
241
581
  end
242
582
 
583
+ def disable_yaxis_grid
584
+ @pyplot.gca.xaxis.grid(false)
585
+ end
586
+
587
+ def invert_yaxis
588
+ @pyplot.gca.invert_yaxis
589
+ end
590
+
591
+ def legend(loc:, title:)
592
+ @pyplot.gca.legend(loc: loc, title: title)
593
+ end
594
+
243
595
  def show
244
596
  @pyplot.show
245
597
  end