charty 0.2.5 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4cf18b5e31bf29099d3d9386b8b022e38013bba339c129a0292af39983e2bccb
4
- data.tar.gz: 4eb934400a4fc7c60354bf7f7f31e33d4be8cdbdeafb1d47762264d94c8c5a2a
3
+ metadata.gz: 63e13663e8213e077993e52b906b630c35a1f7f9a224abb99fd206bb700659c2
4
+ data.tar.gz: d7fd53056c32c18bf5af6b1e1a4b2a29bfe652a427b338087d3cab538a0797ba
5
5
  SHA512:
6
- metadata.gz: 33f5c4ea51ea77a66538e62e43222aaa4f77942b526461ef881cd0ff092eb17a16830937eab032995361a1e7b9e0063e79f682b75fa1c0011644410f02c23b11
7
- data.tar.gz: cd0c31ae974c4efc6192dc0fa74c93db41cdacac6e5a82cfded9ca1b4285e706c925641df483020d620f22e3aed5fae7263d2364ac0cfb5df35bbf4fc72ad034
6
+ metadata.gz: cc4da146432f688eb52dd382ea516e02ab84ef7143453c80fa5bc14fef55851728550ee58fe98c1c3a5e9a1eac0f33dcfb702356bc7c0f344ceaf85f87400435
7
+ data.tar.gz: f519610073317c3fafd42b97ad5e96d124265f6a7d79a54023995cf82d1fe84624d4f914c26e3eef644ad280110400fe1cdac275ec5e32145d7e4b652ef47891
data/README.md CHANGED
@@ -100,7 +100,8 @@ Charty::Backends.use(:plotly) # select plotly backend
100
100
  plot.save("scatter.html") # save the plot as an HTML file
101
101
  ```
102
102
 
103
- If you want to save the plotter into a PNG file, you can do it by specifying a output filename with `.png` extension.
103
+ When you already have prepared [playwright-ruby-client](https://github.com/YusukeIwaki/playwright-ruby-client),
104
+ you can render a plot into a PNG file by plotly backend by specifying a filename with `.png` extension.
104
105
 
105
106
  ```ruby
106
107
  plot.save("scatter.png")
data/charty.gemspec CHANGED
@@ -26,17 +26,20 @@ Gem::Specification.new do |spec|
26
26
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
27
27
  spec.require_paths = ["lib"]
28
28
 
29
- spec.add_dependency "red-colors"
30
- spec.add_dependency "red-palette", ">= 0.2.0"
29
+ spec.add_dependency "red-colors", ">= 0.3.0"
30
+ spec.add_dependency "red-palette", ">= 0.5.0"
31
+
32
+ spec.add_dependency "matplotlib", ">= 1.2.0"
33
+ spec.add_dependency "pandas", ">= 0.3.5"
34
+ spec.add_dependency "playwright-ruby-client"
31
35
 
32
36
  spec.add_development_dependency "bundler", ">= 1.16"
33
37
  spec.add_development_dependency "rake"
34
38
  spec.add_development_dependency "test-unit"
35
- spec.add_development_dependency "red-datasets", ">= 0.0.9"
39
+ spec.add_development_dependency "red-datasets", ">= 0.1.2"
36
40
  spec.add_development_dependency "daru"
37
41
  spec.add_development_dependency "matrix" # need for daru on Ruby > 3.0
38
42
  spec.add_development_dependency "activerecord"
39
43
  spec.add_development_dependency "sqlite3"
40
- spec.add_development_dependency "playwright-ruby-client"
41
- spec.add_development_dependency "iruby"
44
+ spec.add_development_dependency "iruby", ">= 0.7.0"
42
45
  end
data/lib/charty.rb CHANGED
@@ -4,17 +4,17 @@ require "colors"
4
4
  require "palette"
5
5
 
6
6
  require_relative "charty/util"
7
+ require_relative "charty/dash_pattern_generator"
7
8
  require_relative "charty/backends"
8
9
  require_relative "charty/backend_methods"
9
10
  require_relative "charty/plotter"
10
11
  require_relative "charty/index"
11
12
  require_relative "charty/layout"
12
13
  require_relative "charty/linspace"
13
- require_relative "charty/missing_value_support"
14
14
  require_relative "charty/plotters"
15
15
  require_relative "charty/plot_methods"
16
- require_relative "charty/table_adapters"
17
16
  require_relative "charty/table"
17
+ require_relative "charty/table_adapters"
18
18
  require_relative "charty/statistics"
19
19
  require_relative "charty/vector_adapters"
20
20
  require_relative "charty/vector"
@@ -248,12 +248,8 @@ module Charty
248
248
  @traces.concat(traces)
249
249
  end
250
250
 
251
- def scatter(x, y, variables, legend:, color:, color_mapper:,
251
+ def scatter(x, y, variables, color:, color_mapper:,
252
252
  style:, style_mapper:, size:, size_mapper:)
253
- if legend == :full
254
- warn("Plotly backend does not support full verbosity legend")
255
- end
256
-
257
253
  orig_x, orig_y = x, y
258
254
 
259
255
  x = case x
@@ -277,7 +273,7 @@ module Charty
277
273
  end
278
274
 
279
275
  unless color.nil? && style.nil?
280
- grouped_scatter(x, y, variables, legend: legend,
276
+ grouped_scatter(x, y, variables,
281
277
  color: color, color_mapper: color_mapper,
282
278
  style: style, style_mapper: style_mapper,
283
279
  size: size, size_mapper: size_mapper)
@@ -305,7 +301,7 @@ module Charty
305
301
  @traces << trace
306
302
  end
307
303
 
308
- private def grouped_scatter(x, y, variables, legend:, color:, color_mapper:,
304
+ private def grouped_scatter(x, y, variables, color:, color_mapper:,
309
305
  style:, style_mapper:, size:, size_mapper:)
310
306
  @layout[:showlegend] = true
311
307
 
@@ -333,7 +329,9 @@ module Charty
333
329
 
334
330
  unless size.nil?
335
331
  vals = size.values_at(*indices)
336
- trace[:marker][:size] = size_mapper[vals].map(&method(:scale_scatter_point_size))
332
+ trace[:marker][:size] = size_mapper[vals].map do |x|
333
+ scale_scatter_point_size(x).to_f
334
+ end
337
335
  end
338
336
 
339
337
  name = []
@@ -362,6 +360,12 @@ module Charty
362
360
  end
363
361
  end
364
362
 
363
+ def add_scatter_plot_legend(variables, color_mapper, size_mapper, style_mapper, legend)
364
+ if legend == :full
365
+ warn("Plotly backend does not support full verbosity legend")
366
+ end
367
+ end
368
+
365
369
  private def scale_scatter_point_size(x)
366
370
  min = 6
367
371
  max = 12
@@ -369,6 +373,191 @@ module Charty
369
373
  min + x * (max - min)
370
374
  end
371
375
 
376
+ def line(x, y, variables, color:, color_mapper:, size:, size_mapper:, style:, style_mapper:, ci_params:)
377
+ x = case x
378
+ when Charty::Vector
379
+ x.to_a
380
+ else
381
+ orig_x, x = x, Array.try_convert(x)
382
+ if x.nil?
383
+ raise ArgumentError, "Invalid value for x: %p" % orig_x
384
+ end
385
+ end
386
+
387
+ y = case y
388
+ when Charty::Vector
389
+ y.to_a
390
+ else
391
+ orig_y, y = y, Array.try_convert(y)
392
+ if y.nil?
393
+ raise ArgumentError, "Invalid value for y: %p" % orig_y
394
+ end
395
+ end
396
+
397
+ name = []
398
+ legend_title = []
399
+
400
+ if color.nil?
401
+ # TODO: do not hard code this
402
+ line_color = Colors["#1f77b4"] # the first color of D3's category10 palette
403
+ else
404
+ line_color = color_mapper[color].to_rgb
405
+ name << color
406
+ legend_title << variables[:color]
407
+ end
408
+
409
+ unless style.nil?
410
+ marker, dashes = style_mapper[style].values_at(:marker, :dashes)
411
+ name << style
412
+ legend_title << variables[:style]
413
+ end
414
+
415
+ trace = {
416
+ type: :scatter,
417
+ mode: marker.nil? ? "lines" : "lines+markers",
418
+ x: x,
419
+ y: y,
420
+ line: {
421
+ shape: :linear,
422
+ color: line_color.to_hex_string
423
+ }
424
+ }
425
+
426
+ default_line_width = 2.0
427
+ unless size.nil?
428
+ line_width = default_line_width + 2.0 * size_mapper[size]
429
+ trace[:line][:width] = line_width
430
+ end
431
+
432
+ unless dashes.nil?
433
+ trace[:line][:dash] = convert_dash_pattern(dashes, line_width || default_line_width)
434
+ end
435
+
436
+ unless marker.nil?
437
+ trace[:marker] = {
438
+ line: {
439
+ width: 1,
440
+ color: "#fff"
441
+ },
442
+ symbol: marker,
443
+ size: 10
444
+ }
445
+ end
446
+
447
+ unless ci_params.nil?
448
+ case ci_params[:style]
449
+ when :band
450
+ y_min = ci_params[:y_min].to_a
451
+ y_max = ci_params[:y_max].to_a
452
+ @traces << {
453
+ type: :scatter,
454
+ x: x,
455
+ y: y_max,
456
+ mode: :lines,
457
+ line: { shape: :linear, width: 0 },
458
+ showlegend: false
459
+ }
460
+ @traces << {
461
+ type: :scatter,
462
+ x: x,
463
+ y: y_min,
464
+ mode: :lines,
465
+ line: { shape: :linear, width: 0 },
466
+ fill: :tonexty,
467
+ fillcolor: line_color.to_rgba(alpha: 0.2).to_hex_string,
468
+ showlegend: false
469
+ }
470
+ when :bars
471
+ y_min = ci_params[:y_min].map.with_index {|v, i| y[i] - v }
472
+ y_max = ci_params[:y_max].map.with_index {|v, i| v - y[i] }
473
+ trace[:error_y] = {
474
+ visible: true,
475
+ type: :data,
476
+ array: y_max,
477
+ arrayminus: y_min
478
+ }
479
+ unless line_color.nil?
480
+ trace[:error_y][:color] = line_color
481
+ end
482
+ unless line_width.nil?
483
+ trace[:error_y][:thickness] = line_width
484
+ end
485
+ end
486
+ end
487
+
488
+ trace[:name] = name.uniq.join(", ") unless name.empty?
489
+
490
+ @traces << trace
491
+
492
+ unless legend_title.empty?
493
+ @layout[:showlegend] = true
494
+ @layout[:legend] ||= {}
495
+ @layout[:legend][:title] = {text: legend_title.uniq.join(", ")}
496
+ end
497
+ end
498
+
499
+ def add_line_plot_legend(variables, color_mapper, size_mapper, style_mapper, legend)
500
+ if legend == :full
501
+ warn("Plotly backend does not support full verbosity legend")
502
+ end
503
+
504
+ legend_order = if variables.key?(:color)
505
+ if variables.key?(:style)
506
+ # both color and style
507
+ color_mapper.levels.product(style_mapper.levels)
508
+ else
509
+ # only color
510
+ color_mapper.levels
511
+ end
512
+ elsif variables.key?(:style)
513
+ # only style
514
+ style_mapper.levels
515
+ else
516
+ # no legend entries
517
+ nil
518
+ end
519
+
520
+ if legend_order
521
+ # sort traces
522
+ legend_index = legend_order.map.with_index { |name, i|
523
+ [Array(name).uniq.join(", "), i]
524
+ }.to_h
525
+ @traces = @traces.each_with_index.sort_by { |trace, trace_index|
526
+ index = legend_index.fetch(trace[:name], legend_order.length)
527
+ [index, trace_index]
528
+ }.map(&:first)
529
+
530
+ # remove duplicated legend entries
531
+ names = {}
532
+ @traces.each do |trace|
533
+ if trace[:showlegend] != false
534
+ name = trace[:name]
535
+ if name
536
+ if names.key?(name)
537
+ # Hide duplications
538
+ trace[:showlegend] = false
539
+ else
540
+ trace[:showlegend] = true
541
+ names[name] = true
542
+ end
543
+ else
544
+ # Hide no name trace in legend
545
+ trace[:showlegend] = false
546
+ end
547
+ end
548
+ end
549
+ end
550
+ end
551
+
552
+ private def convert_dash_pattern(pattern, line_width)
553
+ case pattern
554
+ when ""
555
+ :solid
556
+ else
557
+ pattern.map {|d| "#{line_width * d}px" }.join(",")
558
+ end
559
+ end
560
+
372
561
  def set_xlabel(label)
373
562
  @layout[:xaxis] ||= {}
374
563
  @layout[:xaxis][:title] = label
@@ -303,7 +303,7 @@ module Charty
303
303
  end
304
304
  end
305
305
 
306
- def scatter(x, y, variables, legend:, color:, color_mapper:,
306
+ def scatter(x, y, variables, color:, color_mapper:,
307
307
  style:, style_mapper:, size:, size_mapper:)
308
308
  kwd = {}
309
309
  kwd[:edgecolor] = "w"
@@ -317,7 +317,7 @@ module Charty
317
317
  end
318
318
 
319
319
  unless size.nil?
320
- size = size_mapper[size].map(&method(:scale_scatter_point_size))
320
+ size = size_mapper[size].map {|x| scale_scatter_point_size(x).to_f }
321
321
  points.set_sizes(size)
322
322
  end
323
323
 
@@ -328,14 +328,15 @@ module Charty
328
328
 
329
329
  sizes = points.get_sizes
330
330
  points.set_linewidths(0.08 * Numpy.sqrt(Numpy.percentile(sizes, 10)))
331
+ end
331
332
 
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
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)
339
340
  end
340
341
  end
341
342
 
@@ -369,8 +370,8 @@ module Charty
369
370
 
370
371
  RELATIONAL_PLOT_LEGEND_BRIEF_TICKS = 6
371
372
 
372
- private def add_relational_plot_legend(ax, verbosity, variables, color_mapper, size_mapper, style_mapper,
373
- legend_attributes, &func)
373
+ private def add_relational_plot_legend(ax, variables, color_mapper, size_mapper, style_mapper,
374
+ verbosity, legend_attributes, &func)
374
375
  brief_ticks = RELATIONAL_PLOT_LEGEND_BRIEF_TICKS
375
376
  verbosity = :auto if verbosity == true
376
377
 
@@ -391,24 +392,17 @@ module Charty
391
392
 
392
393
  # color legend
393
394
 
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
395
+ brief_color = (color_mapper.map_type == :numeric) && (
396
+ (verbosity == :brief) || (
397
+ verbosity == :auto && color_mapper.levels.length > brief_ticks
398
+ )
399
+ )
406
400
  case
407
401
  when brief_color
408
402
  # TODO: Also support LogLocator
409
403
  # locator = Matplotlib.ticker.LogLocator.new(numticks: brief_ticks)
410
404
  locator = Matplotlib.ticker.MaxNLocator.new(nbins: brief_ticks)
411
- limits = color_map.levels.minmax
405
+ limits = color_mapper.levels.minmax
412
406
  color_levels, color_formatted_levels = locator_to_legend_entries(locator, limits)
413
407
  when color_mapper.levels.nil?
414
408
  color_levels = color_formatted_levels = []
@@ -422,22 +416,14 @@ module Charty
422
416
 
423
417
  color_levels.length.times do |i|
424
418
  next if color_levels[i].nil?
425
- color_value = color_mapper[color_levels[i]].to_hex_string
419
+ color_value = color_mapper[color_levels[i]].to_rgb.to_hex_string
426
420
  update_legend.(variables[:color], color_formatted_levels[i], color: color_value)
427
421
  end
428
422
 
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
423
+ brief_size = (size_mapper.map_type == :numeric) && (
424
+ verbosity == :brief ||
425
+ (verbosity == :auto && size_mapper.levels.length > brief_ticks)
426
+ )
441
427
  case
442
428
  when brief_size
443
429
  # TODO: Also support LogLocator
@@ -457,8 +443,9 @@ module Charty
457
443
 
458
444
  size_levels.length.times do |i|
459
445
  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)
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)
462
449
  end
463
450
 
464
451
  if legend_title.nil? && variables.key?(:style)
@@ -474,10 +461,12 @@ module Charty
474
461
  else
475
462
  ""
476
463
  end
477
- # TODO: support dashes
478
- update_legend.(variables[:style], level,
479
- marker: marker,
480
- dashes: attrs.fetch(:dashes, ""))
464
+ dashes = if attrs.key?(:dashes)
465
+ attrs[:dashes]
466
+ else
467
+ ""
468
+ end
469
+ update_legend.(variables[:style], level, marker: marker, dashes: dashes)
481
470
  end
482
471
  end
483
472
 
@@ -505,9 +494,96 @@ module Charty
505
494
  min + x * (max - min)
506
495
  end
507
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
+
508
578
  private def locator_to_legend_entries(locator, limits)
509
579
  vmin, vmax = limits
510
- raw_levels = locator.tick_values(vmin, vmax).to_a
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
511
587
  raw_levels.reject! {|v| v < limits[0] || limits[1] < v }
512
588
 
513
589
  formatter = case locator
@@ -596,6 +672,21 @@ module Charty
596
672
  show
597
673
  end
598
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
+
599
690
  def show
600
691
  @pyplot.show
601
692
  end