charty 0.2.0 → 0.2.6

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 (78) 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 +177 -9
  8. data/Rakefile +4 -5
  9. data/charty.gemspec +10 -4
  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_images/hist_gruff.png +0 -0
  19. data/examples/sample_pyplot.ipynb +40 -38
  20. data/images/penguins_body_mass_g_flipper_length_mm_scatter_plot.png +0 -0
  21. data/images/penguins_body_mass_g_flipper_length_mm_species_scatter_plot.png +0 -0
  22. data/images/penguins_body_mass_g_flipper_length_mm_species_sex_scatter_plot.png +0 -0
  23. data/images/penguins_species_body_mass_g_bar_plot_h.png +0 -0
  24. data/images/penguins_species_body_mass_g_bar_plot_v.png +0 -0
  25. data/images/penguins_species_body_mass_g_box_plot_h.png +0 -0
  26. data/images/penguins_species_body_mass_g_box_plot_v.png +0 -0
  27. data/images/penguins_species_body_mass_g_sex_bar_plot_v.png +0 -0
  28. data/images/penguins_species_body_mass_g_sex_box_plot_v.png +0 -0
  29. data/lib/charty.rb +13 -1
  30. data/lib/charty/backend_methods.rb +8 -0
  31. data/lib/charty/backends.rb +26 -1
  32. data/lib/charty/backends/bokeh.rb +31 -31
  33. data/lib/charty/backends/{google_chart.rb → google_charts.rb} +75 -33
  34. data/lib/charty/backends/gruff.rb +14 -3
  35. data/lib/charty/backends/plotly.rb +774 -9
  36. data/lib/charty/backends/pyplot.rb +611 -34
  37. data/lib/charty/backends/rubyplot.rb +2 -2
  38. data/lib/charty/backends/unicode_plot.rb +79 -0
  39. data/lib/charty/dash_pattern_generator.rb +57 -0
  40. data/lib/charty/index.rb +213 -0
  41. data/lib/charty/linspace.rb +1 -1
  42. data/lib/charty/plot_methods.rb +254 -0
  43. data/lib/charty/plotter.rb +10 -10
  44. data/lib/charty/plotters.rb +12 -0
  45. data/lib/charty/plotters/abstract_plotter.rb +243 -0
  46. data/lib/charty/plotters/bar_plotter.rb +201 -0
  47. data/lib/charty/plotters/box_plotter.rb +79 -0
  48. data/lib/charty/plotters/categorical_plotter.rb +380 -0
  49. data/lib/charty/plotters/count_plotter.rb +7 -0
  50. data/lib/charty/plotters/estimation_support.rb +84 -0
  51. data/lib/charty/plotters/line_plotter.rb +300 -0
  52. data/lib/charty/plotters/random_support.rb +25 -0
  53. data/lib/charty/plotters/relational_plotter.rb +635 -0
  54. data/lib/charty/plotters/scatter_plotter.rb +80 -0
  55. data/lib/charty/plotters/vector_plotter.rb +6 -0
  56. data/lib/charty/statistics.rb +114 -0
  57. data/lib/charty/table.rb +161 -15
  58. data/lib/charty/table_adapters.rb +2 -0
  59. data/lib/charty/table_adapters/active_record_adapter.rb +17 -9
  60. data/lib/charty/table_adapters/base_adapter.rb +166 -0
  61. data/lib/charty/table_adapters/daru_adapter.rb +41 -3
  62. data/lib/charty/table_adapters/datasets_adapter.rb +17 -2
  63. data/lib/charty/table_adapters/hash_adapter.rb +143 -16
  64. data/lib/charty/table_adapters/narray_adapter.rb +25 -6
  65. data/lib/charty/table_adapters/nmatrix_adapter.rb +15 -5
  66. data/lib/charty/table_adapters/pandas_adapter.rb +163 -0
  67. data/lib/charty/util.rb +28 -0
  68. data/lib/charty/vector.rb +69 -0
  69. data/lib/charty/vector_adapters.rb +187 -0
  70. data/lib/charty/vector_adapters/array_adapter.rb +101 -0
  71. data/lib/charty/vector_adapters/daru_adapter.rb +163 -0
  72. data/lib/charty/vector_adapters/narray_adapter.rb +182 -0
  73. data/lib/charty/vector_adapters/nmatrix_adapter.rb +37 -0
  74. data/lib/charty/vector_adapters/numpy_adapter.rb +168 -0
  75. data/lib/charty/vector_adapters/pandas_adapter.rb +199 -0
  76. data/lib/charty/version.rb +1 -1
  77. metadata +121 -22
  78. data/.travis.yml +0 -10
@@ -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
- @plot = ::Matplotlib::Pyplot
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
@@ -28,90 +31,664 @@ module Charty
28
31
  end
29
32
 
30
33
  def render_layout(layout)
31
- _fig, axes = @plot.subplots(nrows: layout.num_rows, ncols: layout.num_cols)
34
+ _fig, axes = @pyplot.subplots(nrows: layout.num_rows, ncols: layout.num_cols)
32
35
  layout.rows.each_with_index do |row, y|
33
36
  row.each_with_index do |cel, x|
34
- plot = layout.num_rows > 1 ? axes[y][x] : axes[x]
35
- plot(plot, cel, subplot: true)
37
+ ax = layout.num_rows > 1 ? axes[y][x] : axes[x]
38
+ plot(ax, cel, subplot: true)
36
39
  end
37
40
  end
38
- @plot.show
41
+ @pyplot.show
39
42
  end
40
43
 
41
- def render(context, filename)
42
- plot(@plot, context)
44
+ def old_style_render(context, filename)
45
+ plot(@pyplot, context)
43
46
  if filename
44
47
  FileUtils.mkdir_p(File.dirname(filename))
45
- @plot.savefig(filename)
48
+ @pyplot.savefig(filename)
46
49
  end
47
- @plot.show
50
+ @pyplot.show
48
51
  end
49
52
 
50
- def save(context, filename)
51
- plot(@plot, context)
53
+ def old_style_save(context, filename, finish: true)
54
+ plot(context)
52
55
  if filename
53
56
  FileUtils.mkdir_p(File.dirname(filename))
54
- @plot.savefig(filename)
57
+ @pyplot.savefig(filename)
55
58
  end
59
+ @pyplot.clf if finish
56
60
  end
57
61
 
58
- def plot(plot, context, subplot: false)
62
+ def plot(ax, context, subplot: false)
59
63
  # TODO: Since it is not required, research and change conditions.
60
64
  # case
61
- # when plot.respond_to?(:xlim)
62
- # plot.xlim(context.range_x.begin, context.range_x.end)
63
- # plot.ylim(context.range_y.begin, context.range_y.end)
64
- # when plot.respond_to?(:set_xlim)
65
- # plot.set_xlim(context.range_x.begin, context.range_x.end)
66
- # plot.set_ylim(context.range_y.begin, context.range_y.end)
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)
67
71
  # end
68
72
 
69
- plot.title(context.title) if context.title
73
+ ax.title(context.title) if context.title
70
74
  if !subplot
71
- plot.xlabel(context.xlabel) if context.xlabel
72
- plot.ylabel(context.ylabel) if context.ylabel
75
+ ax.xlabel(context.xlabel) if context.xlabel
76
+ ax.ylabel(context.ylabel) if context.ylabel
73
77
  end
74
78
 
79
+ palette = Palette.default
80
+ colors = palette.colors.map {|c| c.to_rgb.to_hex_string }.cycle
75
81
  case context.method
76
82
  when :bar
77
83
  context.series.each do |data|
78
- plot.bar(data.xs.to_a.map(&:to_s), data.ys.to_a, label: data.label)
84
+ ax.bar(data.xs.to_a.map(&:to_s), data.ys.to_a, label: data.label,
85
+ color: colors.next)
79
86
  end
80
- plot.legend()
87
+ ax.legend()
81
88
  when :barh
82
89
  context.series.each do |data|
83
- plot.barh(data.xs.to_a.map(&:to_s), data.ys.to_a)
90
+ ax.barh(data.xs.to_a.map(&:to_s), data.ys.to_a, color: colors.next)
84
91
  end
85
92
  when :box_plot
86
- plot.boxplot(context.data.to_a, labels: context.labels)
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
87
106
  when :bubble
88
107
  context.series.each do |data|
89
- plot.scatter(data.xs.to_a, data.ys.to_a, s: data.zs.to_a, alpha: 0.5, label: data.label)
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)
90
110
  end
91
- plot.legend()
111
+ ax.legend()
92
112
  when :curve
93
113
  context.series.each do |data|
94
- plot.plot(data.xs.to_a, data.ys.to_a)
114
+ ax.plot(data.xs.to_a, data.ys.to_a, color: colors.next)
95
115
  end
96
116
  when :scatter
97
117
  context.series.each do |data|
98
- plot.scatter(data.xs.to_a, data.ys.to_a, label: data.label)
118
+ ax.scatter(data.xs.to_a, data.ys.to_a, label: data.label,
119
+ color: colors.next)
99
120
  end
100
- plot.legend()
121
+ ax.legend()
101
122
  when :error_bar
102
123
  context.series.each do |data|
103
- plot.errorbar(
124
+ ax.errorbar(
104
125
  data.xs.to_a,
105
126
  data.ys.to_a,
106
127
  data.xerr,
107
128
  data.yerr,
108
129
  label: data.label,
130
+ color: colors.next
109
131
  )
110
132
  end
111
- plot.legend()
133
+ ax.legend()
112
134
  when :hist
113
- plot.hist(context.data.to_a)
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, 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 {|x| scale_scatter_point_size(x).to_f }
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
+ end
332
+
333
+ def add_scatter_plot_legend(variables, color_mapper, size_mapper, style_mapper, legend)
334
+ ax = @pyplot.gca
335
+ add_relational_plot_legend(
336
+ ax, variables, color_mapper, size_mapper, style_mapper,
337
+ legend, [:color, :s, :marker]
338
+ ) do |label, kwargs|
339
+ ax.scatter([], [], label: label, **kwargs)
340
+ end
341
+ end
342
+
343
+ PYPLOT_MARKERS = {
344
+ circle: "o",
345
+ x: "X",
346
+ cross: "P",
347
+ triangle_up: "^",
348
+ triangle_down: "v",
349
+ square: [4, 0, 45].freeze,
350
+ diamond: [4, 0, 0].freeze,
351
+ star: [5, 1, 0].freeze,
352
+ star_diamond: [4, 1, 0].freeze,
353
+ star_square: [4, 1, 45].freeze,
354
+ pentagon: [5, 0, 0].freeze,
355
+ hexagon: [6, 0, 0].freeze,
356
+ }.freeze
357
+
358
+ private def marker_to_path(marker)
359
+ @path_cache ||= {}
360
+ if @path_cache.key?(marker)
361
+ @path_cache[marker]
362
+ elsif PYPLOT_MARKERS.key?(marker)
363
+ val = PYPLOT_MARKERS[marker]
364
+ ms = Matplotlib.markers.MarkerStyle.new(val)
365
+ @path_cache[marker] = ms.get_path().transformed(ms.get_transform())
366
+ else
367
+ raise ArgumentError, "Unknown marker name: %p" % marker
368
+ end
369
+ end
370
+
371
+ RELATIONAL_PLOT_LEGEND_BRIEF_TICKS = 6
372
+
373
+ private def add_relational_plot_legend(ax, variables, color_mapper, size_mapper, style_mapper,
374
+ verbosity, legend_attributes, &func)
375
+ brief_ticks = RELATIONAL_PLOT_LEGEND_BRIEF_TICKS
376
+ verbosity = :auto if verbosity == true
377
+
378
+ legend_titles = Util.filter_map([:color, :size, :style]) {|v| variables[v] }
379
+ legend_title = legend_titles.pop if legend_titles.length == 1
380
+
381
+ legend_kwargs = {}
382
+ update_legend = ->(var_name, val_name, **kw) do
383
+ key = [var_name, val_name]
384
+ if legend_kwargs.key?(key)
385
+ legend_kwargs[key].update(kw)
386
+ else
387
+ legend_kwargs[key] = kw
388
+ end
389
+ end
390
+
391
+ title_kwargs = {visible: false, color: "w", s: 0, linewidth: 0, marker: "", dashes: ""}
392
+
393
+ # color legend
394
+
395
+ brief_color = (color_mapper.map_type == :numeric) && (
396
+ (verbosity == :brief) || (
397
+ verbosity == :auto && color_mapper.levels.length > brief_ticks
398
+ )
399
+ )
400
+ case
401
+ when brief_color
402
+ # TODO: Also support LogLocator
403
+ # locator = Matplotlib.ticker.LogLocator.new(numticks: brief_ticks)
404
+ locator = Matplotlib.ticker.MaxNLocator.new(nbins: brief_ticks)
405
+ limits = color_mapper.levels.minmax
406
+ color_levels, color_formatted_levels = locator_to_legend_entries(locator, limits)
407
+ when color_mapper.levels.nil?
408
+ color_levels = color_formatted_levels = []
409
+ else
410
+ color_levels = color_formatted_levels = color_mapper.levels
411
+ end
412
+
413
+ if legend_title.nil? && variables.key?(:color)
414
+ update_legend.([variables[:color], :title], variables[:color], **title_kwargs)
415
+ end
416
+
417
+ color_levels.length.times do |i|
418
+ next if color_levels[i].nil?
419
+ color_value = color_mapper[color_levels[i]].to_rgb.to_hex_string
420
+ update_legend.(variables[:color], color_formatted_levels[i], color: color_value)
421
+ end
422
+
423
+ brief_size = (size_mapper.map_type == :numeric) && (
424
+ verbosity == :brief ||
425
+ (verbosity == :auto && size_mapper.levels.length > brief_ticks)
426
+ )
427
+ case
428
+ when brief_size
429
+ # TODO: Also support LogLocator
430
+ # locator = Matplotlib.ticker.LogLocator(numticks: brief_ticks)
431
+ locator = Matplotlib.ticker.MaxNLocator.new(nbins: brief_ticks)
432
+ limits = size_mapper.levels.minmax
433
+ size_levels, size_formatted_levels = locator_to_legend_entries(locator, limits)
434
+ when size_mapper.levels.nil?
435
+ size_levels = size_formatted_levels = []
436
+ else
437
+ size_levels = size_formatted_levels = size_mapper.levels
438
+ end
439
+
440
+ if legend_title.nil? && variables.key?(:size)
441
+ update_legend.([variables[:size], :title], variables[:size], **title_kwargs)
114
442
  end
443
+
444
+ size_levels.length.times do |i|
445
+ next if size_levels[i].nil?
446
+ line_width = scale_line_width(size_mapper[size_levels[i]])
447
+ point_size = scale_scatter_point_size(size_mapper[size_levels[i]])
448
+ update_legend.(variables[:size], size_formatted_levels[i], linewidth: line_width, s: point_size)
449
+ end
450
+
451
+ if legend_title.nil? && variables.key?(:style)
452
+ update_legend.([variables[:style], :title], variables[:style], **title_kwargs)
453
+ end
454
+
455
+ unless style_mapper.levels.nil?
456
+ style_mapper.levels.each do |level|
457
+ next if level.nil?
458
+ attrs = style_mapper[level]
459
+ marker = if attrs.key?(:marker)
460
+ PYPLOT_MARKERS[attrs[:marker]]
461
+ else
462
+ ""
463
+ end
464
+ dashes = if attrs.key?(:dashes)
465
+ attrs[:dashes]
466
+ else
467
+ ""
468
+ end
469
+ update_legend.(variables[:style], level, marker: marker, dashes: dashes)
470
+ end
471
+ end
472
+
473
+ legend_kwargs.each do |key, kw|
474
+ _, label = key
475
+ kw[:color] ||= ".2"
476
+ use_kw = Util.filter_map(legend_attributes) {|attr|
477
+ [attr, kw[attr]] if kw.key?(attr)
478
+ }.to_h
479
+ use_kw[:visible] = kw[:visible] if kw.key?(:visible)
480
+ func.(label, use_kw)
481
+ end
482
+
483
+ handles = ax.get_legend_handles_labels()[0].to_a
484
+ unless handles.empty?
485
+ legend = ax.legend(title: legend_title || "")
486
+ adjust_legend_subtitles(legend)
487
+ end
488
+ end
489
+
490
+ private def scale_scatter_point_size(x)
491
+ min = 0.5 * @default_marker_size**2
492
+ max = 2.0 * @default_marker_size**2
493
+
494
+ min + x * (max - min)
495
+ end
496
+
497
+ def line(x, y, variables, color:, color_mapper:, size:, size_mapper:, style:, style_mapper:, ci_params:)
498
+ kws = {
499
+ markeredgewidth: 0.75,
500
+ markeredgecolor: "w",
501
+ }
502
+ ax = @pyplot.gca
503
+
504
+ x = x.to_a
505
+ y = y.to_a
506
+ lines = ax.plot(x, y, **kws)
507
+
508
+ lines.each do |line|
509
+ unless color.nil?
510
+ line.set_color(color_mapper[color].to_rgb.to_hex_string)
511
+ end
512
+
513
+ unless size.nil?
514
+ scaled_size = scale_line_width(size_mapper[size])
515
+ line.set_linewidth(scaled_size.to_f)
516
+ end
517
+
518
+ unless style.nil?
519
+ attributes = style_mapper[style]
520
+ if attributes.key?(:dashes)
521
+ line.set_dashes(attributes[:dashes])
522
+ end
523
+ if attributes.key?(:marker)
524
+ line.set_marker(PYPLOT_MARKERS[attributes[:marker]])
525
+ end
526
+ end
527
+ end
528
+
529
+ # TODO: support color, size, and style
530
+
531
+ line = lines[0]
532
+ line_color = line.get_color
533
+ line_alpha = line.get_alpha
534
+ line_capstyle = line.get_solid_capstyle
535
+
536
+ unless ci_params.nil?
537
+ y_min = ci_params[:y_min].to_a
538
+ y_max = ci_params[:y_max].to_a
539
+ case ci_params[:style]
540
+ when :band
541
+ # TODO: support to supply `alpha` via `err_kws`
542
+ ax.fill_between(x, y_min, y_max, color: line_color, alpha: 0.2)
543
+ when :bars
544
+ error_deltas = [
545
+ y.zip(y_min).map {|v, v_min| v - v_min },
546
+ y.zip(y_max).map {|v, v_max| v_max - v }
547
+ ]
548
+ ebars = ax.errorbar(x, y, error_deltas,
549
+ linestyle: "", color: line_color, alpha: line_alpha)
550
+ ebars.get_children.each do |bar|
551
+ case bar
552
+ when Matplotlib.collections.LineCollection
553
+ bar.set_capstyle(line_capstyle)
554
+ end
555
+ end
556
+ end
557
+ end
558
+ end
559
+
560
+ def add_line_plot_legend(variables, color_mapper, size_mapper, style_mapper, legend)
561
+ ax = @pyplot.gca
562
+ add_relational_plot_legend(
563
+ ax, variables, color_mapper, size_mapper, style_mapper,
564
+ legend, [:color, :linewidth, :marker, :dashes]
565
+ ) do |label, kwargs|
566
+ ax.plot([], [], label: label, **kwargs)
567
+ end
568
+ end
569
+
570
+
571
+ private def scale_line_width(x)
572
+ min = 0.5 * @default_line_width
573
+ max = 2.0 * @default_line_width
574
+
575
+ min + x * (max - min)
576
+ end
577
+
578
+ private def locator_to_legend_entries(locator, limits)
579
+ vmin, vmax = limits
580
+ dtype = case vmin
581
+ when Numeric
582
+ :float64
583
+ else
584
+ :object
585
+ end
586
+ raw_levels = locator.tick_values(vmin, vmax).astype(dtype).to_a
587
+ raw_levels.reject! {|v| v < limits[0] || limits[1] < v }
588
+
589
+ formatter = case locator
590
+ when Matplotlib.ticker.LogLocator
591
+ Matplotlib.ticker.LogFormatter.new
592
+ else
593
+ Matplotlib.ticker.ScalarFormatter.new
594
+ end
595
+
596
+ dummy_axis = Object.new
597
+ dummy_axis.define_singleton_method(:get_view_interval) { limits }
598
+ formatter.axis = dummy_axis
599
+
600
+ formatter.set_locs(raw_levels)
601
+ formatted_levels = raw_levels.map {|x| formatter.(x) }
602
+
603
+ return raw_levels, formatted_levels
604
+ end
605
+
606
+ private def adjust_legend_subtitles(legend)
607
+ font_size = Matplotlib.rcParams.get("legend.title_fontsize", nil)
608
+ hpackers = legend.findobj(Matplotlib.offsetbox.VPacker)[0].get_children()
609
+ hpackers.each do |hpack|
610
+ draw_area, text_area = hpack.get_children()
611
+ handles = draw_area.get_children()
612
+ unless handles.all? {|a| a.get_visible() }
613
+ draw_area.set_width(0)
614
+ unless font_size.nil?
615
+ text_area.get_children().each do |text|
616
+ text.set_size(font_size)
617
+ end
618
+ end
619
+ end
620
+ end
621
+ end
622
+
623
+ def set_xlabel(label)
624
+ @pyplot.gca.set_xlabel(String(label))
625
+ end
626
+
627
+ def set_ylabel(label)
628
+ @pyplot.gca.set_ylabel(String(label))
629
+ end
630
+
631
+ def set_xticks(values)
632
+ @pyplot.gca.set_xticks(Array(values))
633
+ end
634
+
635
+ def set_yticks(values)
636
+ @pyplot.gca.set_yticks(Array(values))
637
+ end
638
+
639
+ def set_xtick_labels(labels)
640
+ @pyplot.gca.set_xticklabels(Array(labels).map(&method(:String)))
641
+ end
642
+
643
+ def set_ytick_labels(labels)
644
+ @pyplot.gca.set_yticklabels(Array(labels).map(&method(:String)))
645
+ end
646
+
647
+ def set_xlim(min, max)
648
+ @pyplot.gca.set_xlim(Float(min), Float(max))
649
+ end
650
+
651
+ def set_ylim(min, max)
652
+ @pyplot.gca.set_ylim(Float(min), Float(max))
653
+ end
654
+
655
+ def disable_xaxis_grid
656
+ @pyplot.gca.xaxis.grid(false)
657
+ end
658
+
659
+ def disable_yaxis_grid
660
+ @pyplot.gca.xaxis.grid(false)
661
+ end
662
+
663
+ def invert_yaxis
664
+ @pyplot.gca.invert_yaxis
665
+ end
666
+
667
+ def legend(loc:, title:)
668
+ @pyplot.gca.legend(loc: loc, title: title)
669
+ end
670
+
671
+ def render(notebook: false)
672
+ show
673
+ end
674
+
675
+ SAVEFIG_OPTIONAL_PARAMS = [
676
+ :dpi, :quality, :optimize, :progressive, :facecolor, :edgecolor,
677
+ :orientation, :papertype, :transparent, :bbox_inches, :pad_inches,
678
+ :bbox_extra_artists, :backend, :metadata, :pil_kwargs
679
+ ].freeze
680
+
681
+ def save(filename, format: nil, title: nil, width: 700, height: 500, **kwargs)
682
+ params = {}
683
+ params[:format] = format unless format.nil?
684
+ SAVEFIG_OPTIONAL_PARAMS.each do |key|
685
+ params[key] = kwargs[key] if kwargs.key?(key)
686
+ end
687
+ @pyplot.savefig(filename, **params)
688
+ end
689
+
690
+ def show
691
+ @pyplot.show
115
692
  end
116
693
  end
117
694
  end