charty 0.2.3 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +56 -23
- data/.github/workflows/nmatrix.yml +67 -0
- data/.github/workflows/pycall.yml +86 -0
- data/Gemfile +18 -0
- data/README.md +123 -4
- data/Rakefile +4 -5
- data/charty.gemspec +1 -3
- data/examples/sample_images/hist_gruff.png +0 -0
- data/images/penguins_body_mass_g_flipper_length_mm_scatter_plot.png +0 -0
- data/images/penguins_body_mass_g_flipper_length_mm_species_scatter_plot.png +0 -0
- data/images/penguins_body_mass_g_flipper_length_mm_species_sex_scatter_plot.png +0 -0
- data/images/penguins_species_body_mass_g_bar_plot_h.png +0 -0
- data/images/penguins_species_body_mass_g_bar_plot_v.png +0 -0
- data/images/penguins_species_body_mass_g_box_plot_h.png +0 -0
- data/images/penguins_species_body_mass_g_box_plot_v.png +0 -0
- data/images/penguins_species_body_mass_g_sex_bar_plot_v.png +0 -0
- data/images/penguins_species_body_mass_g_sex_box_plot_v.png +0 -0
- data/lib/charty.rb +4 -0
- data/lib/charty/backends/gruff.rb +13 -2
- data/lib/charty/backends/plotly.rb +322 -20
- data/lib/charty/backends/pyplot.rb +416 -64
- data/lib/charty/index.rb +213 -0
- data/lib/charty/linspace.rb +1 -1
- data/lib/charty/missing_value_support.rb +14 -0
- data/lib/charty/plot_methods.rb +173 -8
- data/lib/charty/plotters.rb +7 -0
- data/lib/charty/plotters/abstract_plotter.rb +87 -12
- data/lib/charty/plotters/bar_plotter.rb +200 -3
- data/lib/charty/plotters/box_plotter.rb +75 -7
- data/lib/charty/plotters/categorical_plotter.rb +272 -40
- data/lib/charty/plotters/count_plotter.rb +7 -0
- data/lib/charty/plotters/estimation_support.rb +84 -0
- data/lib/charty/plotters/random_support.rb +25 -0
- data/lib/charty/plotters/relational_plotter.rb +518 -0
- data/lib/charty/plotters/scatter_plotter.rb +115 -0
- data/lib/charty/plotters/vector_plotter.rb +6 -0
- data/lib/charty/statistics.rb +87 -2
- data/lib/charty/table.rb +50 -15
- data/lib/charty/table_adapters.rb +2 -0
- data/lib/charty/table_adapters/active_record_adapter.rb +17 -9
- data/lib/charty/table_adapters/base_adapter.rb +69 -0
- data/lib/charty/table_adapters/daru_adapter.rb +37 -3
- data/lib/charty/table_adapters/datasets_adapter.rb +6 -2
- data/lib/charty/table_adapters/hash_adapter.rb +130 -16
- data/lib/charty/table_adapters/narray_adapter.rb +25 -6
- data/lib/charty/table_adapters/nmatrix_adapter.rb +15 -5
- data/lib/charty/table_adapters/pandas_adapter.rb +81 -0
- data/lib/charty/vector.rb +69 -0
- data/lib/charty/vector_adapters.rb +183 -0
- data/lib/charty/vector_adapters/array_adapter.rb +109 -0
- data/lib/charty/vector_adapters/daru_adapter.rb +171 -0
- data/lib/charty/vector_adapters/narray_adapter.rb +187 -0
- data/lib/charty/vector_adapters/nmatrix_adapter.rb +37 -0
- data/lib/charty/vector_adapters/numpy_adapter.rb +168 -0
- data/lib/charty/vector_adapters/pandas_adapter.rb +200 -0
- data/lib/charty/version.rb +1 -1
- metadata +33 -45
@@ -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
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
|
@@ -90,7 +93,16 @@ module Charty
|
|
90
93
|
min_l = palette.colors.map {|c| c.to_rgb.to_hsl.l }.min
|
91
94
|
lum = min_l*0.6
|
92
95
|
gray = Colors::RGB.new(lum, lum, lum).to_hex_string
|
93
|
-
|
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
|
94
106
|
when :bubble
|
95
107
|
context.series.each do |data|
|
96
108
|
ax.scatter(data.xs.to_a, data.ys.to_a, s: data.zs.to_a, alpha: 0.5,
|
@@ -125,93 +137,409 @@ module Charty
|
|
125
137
|
end
|
126
138
|
end
|
127
139
|
|
128
|
-
private def draw_box_plot(context, subplot, colors, gray)
|
129
|
-
Array(context.data).each_with_index do |group_data, i|
|
130
|
-
next if group_data.empty?
|
131
|
-
|
132
|
-
box_data = group_data.compact
|
133
|
-
next if box_data.empty?
|
134
|
-
|
135
|
-
artist_dict = @pyplot.boxplot(box_data, vert: "v", patch_artist: true,
|
136
|
-
positions: [i], widths: 0.8)
|
137
|
-
|
138
|
-
color = colors.next
|
139
|
-
artist_dict["boxes"].each do |box|
|
140
|
-
box.update({facecolor: color, zorder: 0.9, edgecolor: gray}, {})
|
141
|
-
end
|
142
|
-
artist_dict["whiskers"].each do |whisker|
|
143
|
-
whisker.update({color: gray, linestyle: "-"}, {})
|
144
|
-
end
|
145
|
-
artist_dict["caps"].each do |cap|
|
146
|
-
cap.update({color: gray}, {})
|
147
|
-
end
|
148
|
-
artist_dict["medians"].each do |median|
|
149
|
-
median.update({color: gray}, {})
|
150
|
-
end
|
151
|
-
artist_dict["fliers"].each do |flier|
|
152
|
-
flier.update({
|
153
|
-
markerfacecolor: gray,
|
154
|
-
marker: "d",
|
155
|
-
markeredgecolor: gray,
|
156
|
-
markersize: 5
|
157
|
-
}, {})
|
158
|
-
end
|
159
|
-
end
|
160
|
-
end
|
161
|
-
|
162
140
|
# ==== NEW PLOTTING API ====
|
163
141
|
|
164
142
|
def begin_figure
|
165
|
-
|
143
|
+
@legend_keys = []
|
144
|
+
@legend_labels = []
|
166
145
|
end
|
167
146
|
|
168
|
-
def bar(bar_pos, values,
|
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)
|
169
149
|
bar_pos = Array(bar_pos)
|
170
150
|
values = Array(values)
|
171
|
-
|
151
|
+
colors = Array(colors).map(&:to_hex_string)
|
172
152
|
width = Float(width)
|
153
|
+
|
154
|
+
ax = @pyplot.gca
|
155
|
+
kw = {color: colors, align: align}
|
156
|
+
kw[:label] = label unless label.nil?
|
157
|
+
|
173
158
|
if orient == :v
|
174
|
-
|
159
|
+
ax.bar(bar_pos, values, width, **kw)
|
175
160
|
else
|
176
|
-
|
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
|
177
191
|
end
|
178
192
|
end
|
179
193
|
|
180
|
-
def box_plot(plot_data,
|
181
|
-
|
182
|
-
|
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)
|
183
198
|
gray = gray.to_hex_string
|
184
199
|
width = Float(width)
|
185
200
|
flier_size = Float(flier_size)
|
186
201
|
whisker = Float(whisker)
|
202
|
+
|
187
203
|
plot_data.each_with_index do |group_data, i|
|
188
|
-
|
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
|
189
216
|
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
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)
|
195
225
|
|
196
|
-
|
197
|
-
|
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)
|
198
245
|
end
|
199
|
-
|
200
|
-
|
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)
|
201
338
|
end
|
202
|
-
|
203
|
-
|
339
|
+
end
|
340
|
+
end
|
341
|
+
|
342
|
+
PYPLOT_MARKERS = {
|
343
|
+
circle: "o",
|
344
|
+
x: "X",
|
345
|
+
cross: "P",
|
346
|
+
triangle_up: "^",
|
347
|
+
triangle_down: "v",
|
348
|
+
square: [4, 0, 45].freeze,
|
349
|
+
diamond: [4, 0, 0].freeze,
|
350
|
+
star: [5, 1, 0].freeze,
|
351
|
+
star_diamond: [4, 1, 0].freeze,
|
352
|
+
star_square: [4, 1, 45].freeze,
|
353
|
+
pentagon: [5, 0, 0].freeze,
|
354
|
+
hexagon: [6, 0, 0].freeze,
|
355
|
+
}.freeze
|
356
|
+
|
357
|
+
private def marker_to_path(marker)
|
358
|
+
@path_cache ||= {}
|
359
|
+
if @path_cache.key?(marker)
|
360
|
+
@path_cache[marker]
|
361
|
+
elsif PYPLOT_MARKERS.key?(marker)
|
362
|
+
val = PYPLOT_MARKERS[marker]
|
363
|
+
ms = Matplotlib.markers.MarkerStyle.new(val)
|
364
|
+
@path_cache[marker] = ms.get_path().transformed(ms.get_transform())
|
365
|
+
else
|
366
|
+
raise ArgumentError, "Unknown marker name: %p" % marker
|
367
|
+
end
|
368
|
+
end
|
369
|
+
|
370
|
+
RELATIONAL_PLOT_LEGEND_BRIEF_TICKS = 6
|
371
|
+
|
372
|
+
private def add_relational_plot_legend(ax, verbosity, variables, color_mapper, size_mapper, style_mapper,
|
373
|
+
legend_attributes, &func)
|
374
|
+
brief_ticks = RELATIONAL_PLOT_LEGEND_BRIEF_TICKS
|
375
|
+
verbosity = :auto if verbosity == true
|
376
|
+
|
377
|
+
legend_titles = [:color, :size, :style].filter_map {|v| variables[v] }
|
378
|
+
legend_title = legend_titles.pop if legend_titles.length == 1
|
379
|
+
|
380
|
+
legend_kwargs = {}
|
381
|
+
update_legend = ->(var_name, val_name, **kw) do
|
382
|
+
key = [var_name, val_name]
|
383
|
+
if legend_kwargs.key?(key)
|
384
|
+
legend_kwargs[key].update(kw)
|
385
|
+
else
|
386
|
+
legend_kwargs[key] = kw
|
204
387
|
end
|
205
|
-
|
206
|
-
|
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, ""))
|
207
481
|
end
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
482
|
+
end
|
483
|
+
|
484
|
+
legend_kwargs.each do |key, kw|
|
485
|
+
_, label = key
|
486
|
+
kw[:color] ||= ".2"
|
487
|
+
use_kw = legend_attributes.filter_map {|attr|
|
488
|
+
[attr, kw[attr]] if kw.key?(attr)
|
489
|
+
}.to_h
|
490
|
+
use_kw[:visible] = kw[:visible] if kw.key?(:visible)
|
491
|
+
func.(label, use_kw)
|
492
|
+
end
|
493
|
+
|
494
|
+
handles = ax.get_legend_handles_labels()[0].to_a
|
495
|
+
unless handles.empty?
|
496
|
+
legend = ax.legend(title: legend_title || "")
|
497
|
+
adjust_legend_subtitles(legend)
|
498
|
+
end
|
499
|
+
end
|
500
|
+
|
501
|
+
private def scale_scatter_point_size(x)
|
502
|
+
min = 0.5 * @default_marker_size**2
|
503
|
+
max = 2.0 * @default_marker_size**2
|
504
|
+
|
505
|
+
min + x * (max - min)
|
506
|
+
end
|
507
|
+
|
508
|
+
private def locator_to_legend_entries(locator, limits)
|
509
|
+
vmin, vmax = limits
|
510
|
+
raw_levels = locator.tick_values(vmin, vmax).to_a
|
511
|
+
raw_levels.reject! {|v| v < limits[0] || limits[1] < v }
|
512
|
+
|
513
|
+
formatter = case locator
|
514
|
+
when Matplotlib.ticker.LogLocator
|
515
|
+
Matplotlib.ticker.LogFormatter.new
|
516
|
+
else
|
517
|
+
Matplotlib.ticker.ScalarFormatter.new
|
518
|
+
end
|
519
|
+
|
520
|
+
dummy_axis = Object.new
|
521
|
+
dummy_axis.define_singleton_method(:get_view_interval) { limits }
|
522
|
+
formatter.axis = dummy_axis
|
523
|
+
|
524
|
+
formatter.set_locs(raw_levels)
|
525
|
+
formatted_levels = raw_levels.map {|x| formatter.(x) }
|
526
|
+
|
527
|
+
return raw_levels, formatted_levels
|
528
|
+
end
|
529
|
+
|
530
|
+
private def adjust_legend_subtitles(legend)
|
531
|
+
font_size = Matplotlib.rcParams.get("legend.title_fontsize", nil)
|
532
|
+
hpackers = legend.findobj(Matplotlib.offsetbox.VPacker)[0].get_children()
|
533
|
+
hpackers.each do |hpack|
|
534
|
+
draw_area, text_area = hpack.get_children()
|
535
|
+
handles = draw_area.get_children()
|
536
|
+
unless handles.all? {|a| a.get_visible() }
|
537
|
+
draw_area.set_width(0)
|
538
|
+
unless font_size.nil?
|
539
|
+
text_area.get_children().each do |text|
|
540
|
+
text.set_size(font_size)
|
541
|
+
end
|
542
|
+
end
|
215
543
|
end
|
216
544
|
end
|
217
545
|
end
|
@@ -228,18 +556,42 @@ module Charty
|
|
228
556
|
@pyplot.gca.set_xticks(Array(values))
|
229
557
|
end
|
230
558
|
|
559
|
+
def set_yticks(values)
|
560
|
+
@pyplot.gca.set_yticks(Array(values))
|
561
|
+
end
|
562
|
+
|
231
563
|
def set_xtick_labels(labels)
|
232
564
|
@pyplot.gca.set_xticklabels(Array(labels).map(&method(:String)))
|
233
565
|
end
|
234
566
|
|
567
|
+
def set_ytick_labels(labels)
|
568
|
+
@pyplot.gca.set_yticklabels(Array(labels).map(&method(:String)))
|
569
|
+
end
|
570
|
+
|
235
571
|
def set_xlim(min, max)
|
236
572
|
@pyplot.gca.set_xlim(Float(min), Float(max))
|
237
573
|
end
|
238
574
|
|
575
|
+
def set_ylim(min, max)
|
576
|
+
@pyplot.gca.set_ylim(Float(min), Float(max))
|
577
|
+
end
|
578
|
+
|
239
579
|
def disable_xaxis_grid
|
240
580
|
@pyplot.gca.xaxis.grid(false)
|
241
581
|
end
|
242
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
|
+
|
243
595
|
def show
|
244
596
|
@pyplot.show
|
245
597
|
end
|