charty 0.2.5 → 0.2.6

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