charty 0.1.4.dev → 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 (91) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +71 -0
  3. data/.github/workflows/nmatrix.yml +67 -0
  4. data/.github/workflows/pycall.yml +86 -0
  5. data/Dockerfile.dev +9 -1
  6. data/Gemfile +18 -0
  7. data/README.md +128 -9
  8. data/Rakefile +4 -5
  9. data/charty.gemspec +7 -2
  10. data/examples/Gemfile +1 -0
  11. data/examples/active_record.ipynb +34 -34
  12. data/examples/daru.ipynb +71 -29
  13. data/examples/iris_dataset.ipynb +12 -5
  14. data/examples/nmatrix.ipynb +30 -30
  15. data/examples/numo_narray.ipynb +245 -0
  16. data/examples/palette.rb +71 -0
  17. data/examples/sample.png +0 -0
  18. data/examples/sample_bokeh.ipynb +156 -0
  19. data/examples/sample_google_chart.ipynb +229 -68
  20. data/examples/sample_gruff.ipynb +148 -133
  21. data/examples/sample_images/bar_bokeh.html +85 -0
  22. data/examples/sample_images/barh_bokeh.html +85 -0
  23. data/examples/sample_images/barh_gruff.png +0 -0
  24. data/examples/sample_images/box_plot_bokeh.html +85 -0
  25. data/examples/sample_images/{boxplot_pyplot.png → box_plot_pyplot.png} +0 -0
  26. data/examples/sample_images/curve_bokeh.html +85 -0
  27. data/examples/sample_images/curve_with_function_bokeh.html +85 -0
  28. data/examples/sample_images/{errorbar_pyplot.png → error_bar_pyplot.png} +0 -0
  29. data/examples/sample_images/hist_gruff.png +0 -0
  30. data/examples/sample_images/scatter_bokeh.html +85 -0
  31. data/examples/sample_pyplot.ipynb +37 -35
  32. data/images/penguins_body_mass_g_flipper_length_mm_scatter_plot.png +0 -0
  33. data/images/penguins_body_mass_g_flipper_length_mm_species_scatter_plot.png +0 -0
  34. data/images/penguins_body_mass_g_flipper_length_mm_species_sex_scatter_plot.png +0 -0
  35. data/images/penguins_species_body_mass_g_bar_plot_h.png +0 -0
  36. data/images/penguins_species_body_mass_g_bar_plot_v.png +0 -0
  37. data/images/penguins_species_body_mass_g_box_plot_h.png +0 -0
  38. data/images/penguins_species_body_mass_g_box_plot_v.png +0 -0
  39. data/images/penguins_species_body_mass_g_sex_bar_plot_v.png +0 -0
  40. data/images/penguins_species_body_mass_g_sex_box_plot_v.png +0 -0
  41. data/lib/charty.rb +13 -7
  42. data/lib/charty/backend_methods.rb +8 -0
  43. data/lib/charty/backends.rb +80 -0
  44. data/lib/charty/backends/bokeh.rb +80 -0
  45. data/lib/charty/backends/google_charts.rb +267 -0
  46. data/lib/charty/backends/gruff.rb +104 -67
  47. data/lib/charty/backends/plotly.rb +549 -0
  48. data/lib/charty/backends/pyplot.rb +584 -86
  49. data/lib/charty/backends/rubyplot.rb +82 -74
  50. data/lib/charty/backends/unicode_plot.rb +79 -0
  51. data/lib/charty/index.rb +213 -0
  52. data/lib/charty/linspace.rb +1 -1
  53. data/lib/charty/missing_value_support.rb +14 -0
  54. data/lib/charty/plot_methods.rb +184 -0
  55. data/lib/charty/plotter.rb +57 -41
  56. data/lib/charty/plotters.rb +11 -0
  57. data/lib/charty/plotters/abstract_plotter.rb +156 -0
  58. data/lib/charty/plotters/bar_plotter.rb +216 -0
  59. data/lib/charty/plotters/box_plotter.rb +94 -0
  60. data/lib/charty/plotters/categorical_plotter.rb +380 -0
  61. data/lib/charty/plotters/count_plotter.rb +7 -0
  62. data/lib/charty/plotters/estimation_support.rb +84 -0
  63. data/lib/charty/plotters/random_support.rb +25 -0
  64. data/lib/charty/plotters/relational_plotter.rb +518 -0
  65. data/lib/charty/plotters/scatter_plotter.rb +115 -0
  66. data/lib/charty/plotters/vector_plotter.rb +6 -0
  67. data/lib/charty/statistics.rb +114 -0
  68. data/lib/charty/table.rb +82 -3
  69. data/lib/charty/table_adapters.rb +25 -0
  70. data/lib/charty/table_adapters/active_record_adapter.rb +63 -0
  71. data/lib/charty/table_adapters/base_adapter.rb +69 -0
  72. data/lib/charty/table_adapters/daru_adapter.rb +70 -0
  73. data/lib/charty/table_adapters/datasets_adapter.rb +49 -0
  74. data/lib/charty/table_adapters/hash_adapter.rb +224 -0
  75. data/lib/charty/table_adapters/narray_adapter.rb +76 -0
  76. data/lib/charty/table_adapters/nmatrix_adapter.rb +67 -0
  77. data/lib/charty/table_adapters/pandas_adapter.rb +81 -0
  78. data/lib/charty/vector.rb +69 -0
  79. data/lib/charty/vector_adapters.rb +183 -0
  80. data/lib/charty/vector_adapters/array_adapter.rb +109 -0
  81. data/lib/charty/vector_adapters/daru_adapter.rb +171 -0
  82. data/lib/charty/vector_adapters/narray_adapter.rb +187 -0
  83. data/lib/charty/vector_adapters/nmatrix_adapter.rb +37 -0
  84. data/lib/charty/vector_adapters/numpy_adapter.rb +168 -0
  85. data/lib/charty/vector_adapters/pandas_adapter.rb +200 -0
  86. data/lib/charty/version.rb +1 -1
  87. metadata +127 -13
  88. data/.travis.yml +0 -11
  89. data/examples/numo-narray.ipynb +0 -234
  90. data/lib/charty/backends/google_chart.rb +0 -167
  91. data/lib/charty/plotter_adapter.rb +0 -17
@@ -1,101 +1,599 @@
1
- require 'matplotlib/pyplot'
1
+ require 'fileutils'
2
2
 
3
3
  module Charty
4
- class PyPlot < PlotterAdapter
5
- Name = "pyplot"
4
+ module Backends
5
+ class Pyplot
6
+ Backends.register(:pyplot, self)
6
7
 
7
- def initialize
8
- @plot = Matplotlib::Pyplot
9
- end
8
+ class << self
9
+ def prepare
10
+ require 'matplotlib/pyplot'
11
+ require 'numpy'
12
+ end
13
+ end
10
14
 
11
- def self.activate_iruby_integration
12
- require 'matplotlib/iruby'
13
- Matplotlib::IRuby.activate
14
- end
15
+ def initialize
16
+ @pyplot = ::Matplotlib::Pyplot
17
+ @default_line_width = ::Matplotlib.rcParams["lines.linewidth"]
18
+ @default_marker_size = ::Matplotlib.rcParams["lines.markersize"]
19
+ end
15
20
 
16
- def label(x, y)
17
- end
21
+ def self.activate_iruby_integration
22
+ require 'matplotlib/iruby'
23
+ ::Matplotlib::IRuby.activate
24
+ end
18
25
 
19
- def series=(series)
20
- @series = series
21
- end
26
+ def label(x, y)
27
+ end
22
28
 
23
- def render_layout(layout)
24
- (fig, axes) = *@plot.subplots(nrows: layout.num_rows, ncols: layout.num_cols)
25
- layout.rows.each_with_index do |row, y|
26
- row.each_with_index do |cel, x|
27
- plot = layout.num_rows > 1 ? axes[y][x] : axes[x]
28
- plot(plot, cel, subplot: true)
29
+ def series=(series)
30
+ @series = series
31
+ end
32
+
33
+ def render_layout(layout)
34
+ _fig, axes = @pyplot.subplots(nrows: layout.num_rows, ncols: layout.num_cols)
35
+ layout.rows.each_with_index do |row, y|
36
+ row.each_with_index do |cel, x|
37
+ ax = layout.num_rows > 1 ? axes[y][x] : axes[x]
38
+ plot(ax, cel, subplot: true)
39
+ end
29
40
  end
41
+ @pyplot.show
30
42
  end
31
- @plot.show
32
- end
33
43
 
34
- def render(context, filename)
35
- plot(@plot, context)
36
- if filename
37
- FileUtils.mkdir_p(File.dirname(filename))
38
- @plot.savefig(filename)
44
+ def render(context, filename)
45
+ plot(@pyplot, context)
46
+ if filename
47
+ FileUtils.mkdir_p(File.dirname(filename))
48
+ @pyplot.savefig(filename)
49
+ end
50
+ @pyplot.show
51
+ end
52
+
53
+ def save(context, filename, finish: true)
54
+ plot(context)
55
+ if filename
56
+ FileUtils.mkdir_p(File.dirname(filename))
57
+ @pyplot.savefig(filename)
58
+ end
59
+ @pyplot.clf if finish
60
+ end
61
+
62
+ def plot(ax, context, subplot: false)
63
+ # TODO: Since it is not required, research and change conditions.
64
+ # case
65
+ # when @pyplot.respond_to?(:xlim)
66
+ # @pyplot.xlim(context.range_x.begin, context.range_x.end)
67
+ # @pyplot.ylim(context.range_y.begin, context.range_y.end)
68
+ # when @pyplot.respond_to?(:set_xlim)
69
+ # @pyplot.set_xlim(context.range_x.begin, context.range_x.end)
70
+ # @pyplot.set_ylim(context.range_y.begin, context.range_y.end)
71
+ # end
72
+
73
+ ax.title(context.title) if context.title
74
+ if !subplot
75
+ ax.xlabel(context.xlabel) if context.xlabel
76
+ ax.ylabel(context.ylabel) if context.ylabel
77
+ end
78
+
79
+ palette = Palette.default
80
+ colors = palette.colors.map {|c| c.to_rgb.to_hex_string }.cycle
81
+ case context.method
82
+ when :bar
83
+ context.series.each do |data|
84
+ ax.bar(data.xs.to_a.map(&:to_s), data.ys.to_a, label: data.label,
85
+ color: colors.next)
86
+ end
87
+ ax.legend()
88
+ when :barh
89
+ context.series.each do |data|
90
+ ax.barh(data.xs.to_a.map(&:to_s), data.ys.to_a, color: colors.next)
91
+ end
92
+ when :box_plot
93
+ min_l = palette.colors.map {|c| c.to_rgb.to_hsl.l }.min
94
+ lum = min_l*0.6
95
+ gray = Colors::RGB.new(lum, lum, lum).to_hex_string
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
106
+ when :bubble
107
+ context.series.each do |data|
108
+ ax.scatter(data.xs.to_a, data.ys.to_a, s: data.zs.to_a, alpha: 0.5,
109
+ color: colors.next, label: data.label)
110
+ end
111
+ ax.legend()
112
+ when :curve
113
+ context.series.each do |data|
114
+ ax.plot(data.xs.to_a, data.ys.to_a, color: colors.next)
115
+ end
116
+ when :scatter
117
+ context.series.each do |data|
118
+ ax.scatter(data.xs.to_a, data.ys.to_a, label: data.label,
119
+ color: colors.next)
120
+ end
121
+ ax.legend()
122
+ when :error_bar
123
+ context.series.each do |data|
124
+ ax.errorbar(
125
+ data.xs.to_a,
126
+ data.ys.to_a,
127
+ data.xerr,
128
+ data.yerr,
129
+ label: data.label,
130
+ color: colors.next
131
+ )
132
+ end
133
+ ax.legend()
134
+ when :hist
135
+ data = Array(context.data)
136
+ ax.hist(data, color: colors.take(data.length), alpha: 0.4)
137
+ end
138
+ end
139
+
140
+ # ==== NEW PLOTTING API ====
141
+
142
+ def begin_figure
143
+ @legend_keys = []
144
+ @legend_labels = []
145
+ end
146
+
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)
149
+ bar_pos = Array(bar_pos)
150
+ values = Array(values)
151
+ colors = Array(colors).map(&:to_hex_string)
152
+ width = Float(width)
153
+
154
+ ax = @pyplot.gca
155
+ kw = {color: colors, align: align}
156
+ kw[:label] = label unless label.nil?
157
+
158
+ if orient == :v
159
+ ax.bar(bar_pos, values, width, **kw)
160
+ else
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
191
+ end
192
+ end
193
+
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)
198
+ gray = gray.to_hex_string
199
+ width = Float(width)
200
+ flier_size = Float(flier_size)
201
+ whisker = Float(whisker)
202
+
203
+ plot_data.each_with_index do |group_data, i|
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
216
+
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)
225
+
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)
245
+ end
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)
338
+ end
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
387
+ end
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, ""))
481
+ end
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
543
+ end
544
+ end
545
+ end
546
+
547
+ def set_xlabel(label)
548
+ @pyplot.gca.set_xlabel(String(label))
549
+ end
550
+
551
+ def set_ylabel(label)
552
+ @pyplot.gca.set_ylabel(String(label))
553
+ end
554
+
555
+ def set_xticks(values)
556
+ @pyplot.gca.set_xticks(Array(values))
557
+ end
558
+
559
+ def set_yticks(values)
560
+ @pyplot.gca.set_yticks(Array(values))
561
+ end
562
+
563
+ def set_xtick_labels(labels)
564
+ @pyplot.gca.set_xticklabels(Array(labels).map(&method(:String)))
565
+ end
566
+
567
+ def set_ytick_labels(labels)
568
+ @pyplot.gca.set_yticklabels(Array(labels).map(&method(:String)))
569
+ end
570
+
571
+ def set_xlim(min, max)
572
+ @pyplot.gca.set_xlim(Float(min), Float(max))
573
+ end
574
+
575
+ def set_ylim(min, max)
576
+ @pyplot.gca.set_ylim(Float(min), Float(max))
577
+ end
578
+
579
+ def disable_xaxis_grid
580
+ @pyplot.gca.xaxis.grid(false)
581
+ end
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)
39
593
  end
40
- @plot.show
41
- end
42
594
 
43
- def plot(plot, context, subplot: false)
44
- # TODO: Since it is not required, research and change conditions.
45
- # case
46
- # when plot.respond_to?(:xlim)
47
- # plot.xlim(context.range_x.begin, context.range_x.end)
48
- # plot.ylim(context.range_y.begin, context.range_y.end)
49
- # when plot.respond_to?(:set_xlim)
50
- # plot.set_xlim(context.range_x.begin, context.range_x.end)
51
- # plot.set_ylim(context.range_y.begin, context.range_y.end)
52
- # end
53
-
54
- plot.title(context.title) if context.title
55
- if !subplot
56
- plot.xlabel(context.xlabel) if context.xlabel
57
- plot.ylabel(context.ylabel) if context.ylabel
58
- end
59
-
60
- case context.method
61
- when :bar
62
- context.series.each do |data|
63
- plot.bar(data.xs.to_a.map(&:to_s), data.ys.to_a, label: data.label)
64
- end
65
- plot.legend()
66
- when :barh
67
- context.series.each do |data|
68
- plot.barh(data.xs.to_a.map(&:to_s), data.ys.to_a)
69
- end
70
- when :box_plot
71
- plot.boxplot(context.data.to_a)
72
- when :bubble
73
- context.series.each do |data|
74
- plot.scatter(data.xs.to_a, data.ys.to_a, s: data.zs.to_a, alpha: 0.5, label: data.label)
75
- end
76
- plot.legend()
77
- when :curve
78
- context.series.each do |data|
79
- plot.plot(data.xs.to_a, data.ys.to_a)
80
- end
81
- when :scatter
82
- context.series.each do |data|
83
- plot.scatter(data.xs.to_a, data.ys.to_a, label: data.label)
84
- end
85
- plot.legend()
86
- when :error_bar
87
- context.series.each do |data|
88
- plot.errorbar(
89
- data.xs.to_a,
90
- data.ys.to_a,
91
- data.xerr,
92
- data.yerr,
93
- label: data.label,
94
- )
95
- end
96
- plot.legend()
97
- when :hist
98
- plot.hist(context.data.to_a)
595
+ def show
596
+ @pyplot.show
99
597
  end
100
598
  end
101
599
  end