charty 0.2.3 → 0.2.4

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.
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