charty 0.2.0 → 0.2.6

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