charty 0.1.5.dev → 0.2.5

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 (87) 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 +176 -9
  8. data/Rakefile +4 -5
  9. data/charty.gemspec +10 -1
  10. data/examples/Gemfile +1 -0
  11. data/examples/active_record.ipynb +1 -1
  12. data/examples/daru.ipynb +1 -1
  13. data/examples/iris_dataset.ipynb +1 -1
  14. data/examples/nmatrix.ipynb +1 -1
  15. data/examples/{numo-narray.ipynb → numo_narray.ipynb} +1 -1
  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_images/bar_bokeh.html +85 -0
  21. data/examples/sample_images/barh_bokeh.html +85 -0
  22. data/examples/sample_images/box_plot_bokeh.html +85 -0
  23. data/examples/sample_images/curve_bokeh.html +85 -0
  24. data/examples/sample_images/curve_with_function_bokeh.html +85 -0
  25. data/examples/sample_images/hist_gruff.png +0 -0
  26. data/examples/sample_images/scatter_bokeh.html +85 -0
  27. data/examples/sample_pyplot.ipynb +40 -38
  28. data/images/penguins_body_mass_g_flipper_length_mm_scatter_plot.png +0 -0
  29. data/images/penguins_body_mass_g_flipper_length_mm_species_scatter_plot.png +0 -0
  30. data/images/penguins_body_mass_g_flipper_length_mm_species_sex_scatter_plot.png +0 -0
  31. data/images/penguins_species_body_mass_g_bar_plot_h.png +0 -0
  32. data/images/penguins_species_body_mass_g_bar_plot_v.png +0 -0
  33. data/images/penguins_species_body_mass_g_box_plot_h.png +0 -0
  34. data/images/penguins_species_body_mass_g_box_plot_v.png +0 -0
  35. data/images/penguins_species_body_mass_g_sex_bar_plot_v.png +0 -0
  36. data/images/penguins_species_body_mass_g_sex_box_plot_v.png +0 -0
  37. data/lib/charty.rb +14 -1
  38. data/lib/charty/backend_methods.rb +8 -0
  39. data/lib/charty/backends.rb +80 -0
  40. data/lib/charty/backends/bokeh.rb +32 -26
  41. data/lib/charty/backends/google_charts.rb +267 -0
  42. data/lib/charty/backends/gruff.rb +102 -83
  43. data/lib/charty/backends/plotly.rb +685 -0
  44. data/lib/charty/backends/pyplot.rb +586 -92
  45. data/lib/charty/backends/rubyplot.rb +82 -74
  46. data/lib/charty/backends/unicode_plot.rb +79 -0
  47. data/lib/charty/index.rb +213 -0
  48. data/lib/charty/linspace.rb +1 -1
  49. data/lib/charty/missing_value_support.rb +14 -0
  50. data/lib/charty/plot_methods.rb +184 -0
  51. data/lib/charty/plotter.rb +48 -40
  52. data/lib/charty/plotters.rb +11 -0
  53. data/lib/charty/plotters/abstract_plotter.rb +183 -0
  54. data/lib/charty/plotters/bar_plotter.rb +201 -0
  55. data/lib/charty/plotters/box_plotter.rb +79 -0
  56. data/lib/charty/plotters/categorical_plotter.rb +380 -0
  57. data/lib/charty/plotters/count_plotter.rb +7 -0
  58. data/lib/charty/plotters/estimation_support.rb +84 -0
  59. data/lib/charty/plotters/random_support.rb +25 -0
  60. data/lib/charty/plotters/relational_plotter.rb +518 -0
  61. data/lib/charty/plotters/scatter_plotter.rb +104 -0
  62. data/lib/charty/plotters/vector_plotter.rb +6 -0
  63. data/lib/charty/statistics.rb +114 -0
  64. data/lib/charty/table.rb +80 -3
  65. data/lib/charty/table_adapters.rb +25 -0
  66. data/lib/charty/table_adapters/active_record_adapter.rb +63 -0
  67. data/lib/charty/table_adapters/base_adapter.rb +69 -0
  68. data/lib/charty/table_adapters/daru_adapter.rb +70 -0
  69. data/lib/charty/table_adapters/datasets_adapter.rb +49 -0
  70. data/lib/charty/table_adapters/hash_adapter.rb +224 -0
  71. data/lib/charty/table_adapters/narray_adapter.rb +76 -0
  72. data/lib/charty/table_adapters/nmatrix_adapter.rb +67 -0
  73. data/lib/charty/table_adapters/pandas_adapter.rb +81 -0
  74. data/lib/charty/util.rb +20 -0
  75. data/lib/charty/vector.rb +69 -0
  76. data/lib/charty/vector_adapters.rb +183 -0
  77. data/lib/charty/vector_adapters/array_adapter.rb +109 -0
  78. data/lib/charty/vector_adapters/daru_adapter.rb +171 -0
  79. data/lib/charty/vector_adapters/narray_adapter.rb +187 -0
  80. data/lib/charty/vector_adapters/nmatrix_adapter.rb +37 -0
  81. data/lib/charty/vector_adapters/numpy_adapter.rb +168 -0
  82. data/lib/charty/vector_adapters/pandas_adapter.rb +200 -0
  83. data/lib/charty/version.rb +1 -1
  84. metadata +179 -10
  85. data/.travis.yml +0 -11
  86. data/lib/charty/backends/google_chart.rb +0 -167
  87. data/lib/charty/plotter_adapter.rb +0 -17
@@ -1,109 +1,603 @@
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 old_style_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
39
51
  end
40
- @plot.show
41
- end
42
52
 
43
- def save(context, filename)
44
- plot(@plot, context)
45
- if filename
46
- FileUtils.mkdir_p(File.dirname(filename))
47
- @plot.savefig(filename)
53
+ def old_style_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 = Util.filter_map([:color, :size, :style]) {|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 = Util.filter_map(legend_attributes) {|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)
593
+ end
594
+
595
+ def render(notebook: false)
596
+ show
48
597
  end
49
- end
50
598
 
51
- def plot(plot, context, subplot: false)
52
- # TODO: Since it is not required, research and change conditions.
53
- # case
54
- # when plot.respond_to?(:xlim)
55
- # plot.xlim(context.range_x.begin, context.range_x.end)
56
- # plot.ylim(context.range_y.begin, context.range_y.end)
57
- # when plot.respond_to?(:set_xlim)
58
- # plot.set_xlim(context.range_x.begin, context.range_x.end)
59
- # plot.set_ylim(context.range_y.begin, context.range_y.end)
60
- # end
61
-
62
- plot.title(context.title) if context.title
63
- if !subplot
64
- plot.xlabel(context.xlabel) if context.xlabel
65
- plot.ylabel(context.ylabel) if context.ylabel
66
- end
67
-
68
- case context.method
69
- when :bar
70
- context.series.each do |data|
71
- plot.bar(data.xs.to_a.map(&:to_s), data.ys.to_a, label: data.label)
72
- end
73
- plot.legend()
74
- when :barh
75
- context.series.each do |data|
76
- plot.barh(data.xs.to_a.map(&:to_s), data.ys.to_a)
77
- end
78
- when :box_plot
79
- plot.boxplot(context.data.to_a, labels: context.labels)
80
- when :bubble
81
- context.series.each do |data|
82
- plot.scatter(data.xs.to_a, data.ys.to_a, s: data.zs.to_a, alpha: 0.5, label: data.label)
83
- end
84
- plot.legend()
85
- when :curve
86
- context.series.each do |data|
87
- plot.plot(data.xs.to_a, data.ys.to_a)
88
- end
89
- when :scatter
90
- context.series.each do |data|
91
- plot.scatter(data.xs.to_a, data.ys.to_a, label: data.label)
92
- end
93
- plot.legend()
94
- when :error_bar
95
- context.series.each do |data|
96
- plot.errorbar(
97
- data.xs.to_a,
98
- data.ys.to_a,
99
- data.xerr,
100
- data.yerr,
101
- label: data.label,
102
- )
103
- end
104
- plot.legend()
105
- when :hist
106
- plot.hist(context.data.to_a)
599
+ def show
600
+ @pyplot.show
107
601
  end
108
602
  end
109
603
  end