charty 0.1.5.dev → 0.2.5

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