charty 0.2.4 → 0.2.9

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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +64 -15
  3. data/charty.gemspec +10 -3
  4. data/lib/charty.rb +5 -2
  5. data/lib/charty/backends/bokeh.rb +2 -2
  6. data/lib/charty/backends/google_charts.rb +1 -1
  7. data/lib/charty/backends/gruff.rb +1 -1
  8. data/lib/charty/backends/plotly.rb +434 -32
  9. data/lib/charty/backends/plotly_helpers/html_renderer.rb +203 -0
  10. data/lib/charty/backends/plotly_helpers/notebook_renderer.rb +87 -0
  11. data/lib/charty/backends/plotly_helpers/plotly_renderer.rb +121 -0
  12. data/lib/charty/backends/pyplot.rb +187 -48
  13. data/lib/charty/backends/rubyplot.rb +1 -1
  14. data/lib/charty/cache_dir.rb +27 -0
  15. data/lib/charty/dash_pattern_generator.rb +57 -0
  16. data/lib/charty/index.rb +1 -1
  17. data/lib/charty/iruby_helper.rb +18 -0
  18. data/lib/charty/plot_methods.rb +115 -3
  19. data/lib/charty/plotter.rb +2 -2
  20. data/lib/charty/plotters.rb +4 -0
  21. data/lib/charty/plotters/abstract_plotter.rb +106 -11
  22. data/lib/charty/plotters/bar_plotter.rb +1 -16
  23. data/lib/charty/plotters/box_plotter.rb +1 -16
  24. data/lib/charty/plotters/distribution_plotter.rb +150 -0
  25. data/lib/charty/plotters/histogram_plotter.rb +242 -0
  26. data/lib/charty/plotters/line_plotter.rb +300 -0
  27. data/lib/charty/plotters/relational_plotter.rb +213 -96
  28. data/lib/charty/plotters/scatter_plotter.rb +8 -43
  29. data/lib/charty/statistics.rb +11 -2
  30. data/lib/charty/table.rb +124 -14
  31. data/lib/charty/table_adapters/base_adapter.rb +97 -0
  32. data/lib/charty/table_adapters/daru_adapter.rb +2 -0
  33. data/lib/charty/table_adapters/datasets_adapter.rb +7 -0
  34. data/lib/charty/table_adapters/hash_adapter.rb +19 -3
  35. data/lib/charty/table_adapters/pandas_adapter.rb +82 -0
  36. data/lib/charty/util.rb +28 -0
  37. data/lib/charty/vector_adapters.rb +5 -1
  38. data/lib/charty/vector_adapters/array_adapter.rb +2 -10
  39. data/lib/charty/vector_adapters/daru_adapter.rb +3 -11
  40. data/lib/charty/vector_adapters/narray_adapter.rb +1 -6
  41. data/lib/charty/vector_adapters/numpy_adapter.rb +1 -1
  42. data/lib/charty/vector_adapters/pandas_adapter.rb +0 -1
  43. data/lib/charty/version.rb +1 -1
  44. metadata +104 -11
  45. data/lib/charty/missing_value_support.rb +0 -14
@@ -0,0 +1,203 @@
1
+ require "datasets/downloader"
2
+ require "json"
3
+ require "securerandom"
4
+
5
+ module Charty
6
+ module Backends
7
+ module PlotlyHelpers
8
+ class HtmlRenderer
9
+ def initialize(use_cdn: true,
10
+ full_html: false,
11
+ requirejs: true)
12
+ @use_cdn = use_cdn
13
+ @full_html = full_html
14
+ @requirejs = requirejs
15
+ end
16
+
17
+ PLOTLY_URL = "https://plot.ly".freeze
18
+ PLOTLY_LATEST_CDN_URL = "https://cdn.plot.ly/plotly-latest.min.js".freeze
19
+ MATHJAX_CDN_URL = ("https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js").freeze
20
+
21
+ DEFAULT_WIDTH = "100%".freeze
22
+ DEFAULT_HEIGHT = 525
23
+
24
+ def render(figure, element_id: nil, post_script: nil)
25
+ element_id = SecureRandom.uuid if element_id.nil?
26
+ plotly_html_div = build_plotly_html_div(figure, element_id, post_script)
27
+
28
+ if @full_html
29
+ <<~END_HTML % {div: plotly_html_div}
30
+ <!DOCTYPE html>
31
+ <html>
32
+ <head><meta charset="utf-8" /></head>
33
+ <body>
34
+ %{div}
35
+ </body>
36
+ </html>
37
+ END_HTML
38
+ else
39
+ plotly_html_div
40
+ end
41
+ end
42
+
43
+ private def build_plotly_html_div(figure, element_id, post_script)
44
+ layout = figure.fetch(:layout, {})
45
+
46
+ json_data = JSON.dump(figure.fetch(:data, []))
47
+ json_layout = JSON.dump(layout)
48
+ json_frames = JSON.dump(figure[:frames]) if figure.key?(:frames)
49
+
50
+ # TODO: config and responsive support
51
+
52
+ template = layout.fetch(:template, {}).fetch(:layout, {})
53
+ div_width = layout.fetch(:width, template.fetch(:width, DEFAULT_WIDTH))
54
+ div_height = layout.fetch(:height, template.fetch(:height, DEFAULT_HEIGHT))
55
+
56
+ div_width = "#{div_width}px" if Float(div_width, exception: false)
57
+ div_height = "#{div_height}px" if Float(div_height, exception: false)
58
+
59
+ # TODO: showLink and showSendToCloud support
60
+ base_url_line = "window.PLOTLYENV.BASE_URL = '%{url}';" % {url: PLOTLY_URL}
61
+
62
+ ## build script body
63
+
64
+ # TODO: post_script support
65
+ then_post_script = ""
66
+ if post_script
67
+ ary = Array.try_convert(post_script)
68
+ post_script = ary || [post_script]
69
+ post_script.each do |ps|
70
+ next if ps.nil?
71
+ then_post_script << '.then(function(){ %{post_script} })' % {
72
+ post_script: ps % {plot_id: element_id}
73
+ }
74
+ end
75
+ end
76
+
77
+ then_addframes = ""
78
+ then_animate = ""
79
+ if json_frames
80
+ then_addframes = <<~END_ADDFRAMES % {id: element_id, frames: json_frames}
81
+ .then(function(){
82
+ Plotly.addFrames('%{id}', {frames});
83
+ })
84
+ END_ADDFRAMES
85
+
86
+ # TODO: auto_play support
87
+ end
88
+
89
+ json_config = JSON.dump({}) # TODO: config support
90
+
91
+ script = <<~END_SCRIPT
92
+ if (document.getElementById("%{id}")) {
93
+ Plotly.newPlot("%{id}", %{data}, %{layout}, %{config})%{then_addframes}%{then_animate}%{then_post_script};
94
+ }
95
+ END_SCRIPT
96
+ script = script % {
97
+ id: element_id,
98
+ data: json_data,
99
+ layout: json_layout,
100
+ config: json_config,
101
+ then_addframes: then_addframes,
102
+ then_animate: then_animate,
103
+ then_post_script: then_post_script
104
+ }
105
+
106
+ ## Handle loading/initializing plotlyjs
107
+
108
+ case
109
+ when @requirejs
110
+ include_plotlyjs = :require
111
+ include_mathjax = false
112
+ when @use_cdn
113
+ include_plotlyjs = :cdn
114
+ include_mathjax = :cdn
115
+ else
116
+ include_plotlyjs = true
117
+ include_mathjax = :cdn
118
+ end
119
+
120
+ case include_plotlyjs
121
+ when :require
122
+ require_start = 'require(["plotly"], function (Plotly) {'
123
+ require_end = '});'
124
+ when :cdn
125
+ load_plotlyjs = <<~END_LOAD_PLOTLYJS % {win_config: window_plotly_config, url: PLOTLY_LATEST_CDN_URL}
126
+ %{win_config}
127
+ <script src="%{url}"></script>
128
+ END_LOAD_PLOTLYJS
129
+ when true
130
+ load_plotlyjs = <<~END_LOAD_PLOTLYJS % {win_config: window_plotly_config, script: get_plotlyjs}
131
+ %{win_config}
132
+ <script type="text/javascript">%{script}</script>
133
+ END_LOAD_PLOTLYJS
134
+ end
135
+
136
+ ## Handle loading/initializing MathJax
137
+
138
+ mathjax_tmplate = %Q[<script src="%{url}?config=TeX-AMS-MML_SVG"></script>]
139
+ case include_mathjax
140
+ when :cdn
141
+ mathjax_script = mathjax_tmplate % {url: MATHJAX_CDN_URL}
142
+ mathjax_script << <<~END_SCRIPT % {mathjax_config: mathjax_config}
143
+ <script type="text/javascript">%{mathjax_config}</script>
144
+ END_SCRIPT
145
+ else
146
+ mathjax_script = ""
147
+ end
148
+
149
+ div_template = <<~END_DIV
150
+ <div>
151
+ %{mathjax_script}
152
+ %{load_plotlyjs}
153
+ <div id="%{id}" class="plotly-graph-div" style="height: %{height}; width: %{width};"></div>
154
+ <script type="text/javascript">
155
+ %{require_start}
156
+ window.PLOTLYENV = window.PLOTLYENV || {};
157
+ %{base_url_line}
158
+ %{script}
159
+ %{require_end}
160
+ </script>
161
+ </div>
162
+ END_DIV
163
+
164
+ plotly_html_div = div_template % {
165
+ mathjax_script: mathjax_script,
166
+ load_plotlyjs: load_plotlyjs,
167
+ id: element_id,
168
+ height: div_height,
169
+ width: div_width,
170
+ require_start: require_start,
171
+ base_url_line: base_url_line,
172
+ script: script,
173
+ require_end: require_end
174
+ }
175
+ plotly_html_div.strip!
176
+
177
+ plotly_html_div
178
+ end
179
+
180
+ private def window_plotly_config
181
+ %Q(window.PlotlyConfig = {MathJaxConfig: 'local'};)
182
+ end
183
+
184
+ private def mathjax_config
185
+ %Q(if (window.MathJax) { MathJax.Hub.Config({SVG: {font: "STIX-Web"}}); })
186
+ end
187
+
188
+ private def get_plotlyjs
189
+ cache_path = CacheDir.path("plotly.min.js")
190
+ unless cache_path.exist?
191
+ download_plotlyjs(cache_path)
192
+ end
193
+ cache_path.read
194
+ end
195
+
196
+ private def download_plotlyjs(output_path)
197
+ downloader = Datasets::Downloader.new(PLOTLY_LATEST_CDN_URL)
198
+ downloader.download(output_path)
199
+ end
200
+ end
201
+ end
202
+ end
203
+ end
@@ -0,0 +1,87 @@
1
+ module Charty
2
+ module Backends
3
+ module PlotlyHelpers
4
+ class NotebookRenderer < HtmlRenderer
5
+ def initialize(use_cdn: false)
6
+ super(use_cdn: use_cdn, full_html: false, requirejs: true)
7
+ @initialized = false
8
+ end
9
+
10
+ def activate()
11
+ return if @initialized
12
+
13
+ unless IRubyHelper.iruby_notebook?
14
+ raise "IRuby is unavailable"
15
+ end
16
+
17
+ if @use_cdn
18
+ script = <<~END_SCRIPT % {win_config: window_plotly_config, mathjax_config: mathjax_config}
19
+ <script type="text/javascript">
20
+ %{win_config}
21
+ %{mathjax_config}
22
+ if (typeof require !== 'undefined') {
23
+ require.undef("plotly");
24
+ requirejs.config({
25
+ paths: {
26
+ 'plotly': ['https://cdn.plot.ly/plotly-latest.min']
27
+ }
28
+ });
29
+ require(['plotly'], function (Plotly) {
30
+ window._Plotly = Plotly;
31
+ });
32
+ }
33
+ </script>
34
+ END_SCRIPT
35
+ else
36
+ script = <<~END_SCRIPT % {script: get_plotlyjs, win_config: window_plotly_config, mathjax_config: mathjax_config}
37
+ <script type="text/javascript">
38
+ %{win_config}
39
+ %{mathjax_config}
40
+ if (typeof require !== 'undefined') {
41
+ require.undef("plotly");
42
+ define('plotly', function (require, exports, module) {
43
+ %{script}
44
+ });
45
+ require(['plotly'], function (Plotly) {
46
+ window._Plotly = Plotly;
47
+ });
48
+ }
49
+ </script>
50
+ END_SCRIPT
51
+ end
52
+ IRuby.display(script, mime: "text/html")
53
+ end
54
+
55
+ def render(figure, element_id: nil, post_script: nil)
56
+ ary = Array.try_convert(post_script)
57
+ post_script = ary || [post_script]
58
+ post_script.unshift(<<~END_POST_SCRIPT)
59
+ var gd = document.getElementById('%{plot_id}');
60
+ var x = new MutationObserver(function (mutations, observer) {
61
+ var display = window.getComputedStyle(gd).display;
62
+ if (!display || display === 'none') {
63
+ console.log([gd, 'removed']);
64
+ Plotly.purge(gd);
65
+ observer.disconnect();
66
+ }
67
+ });
68
+
69
+ // Listen for the removal of the full notebook cell
70
+ var notebookContainer = gd.closest('#notebook-container');
71
+ if (notebookContainer) {
72
+ x.observe(notebookContainer, {childList: true});
73
+ }
74
+
75
+ // Listen for the clearing of the current output cell
76
+ var outputEl = gd.closest('.output');
77
+ if (outputEl) {
78
+ x.observe(outputEl, {childList: true});
79
+ }
80
+ END_POST_SCRIPT
81
+
82
+ super(figure, element_id: element_id, post_script: post_script)
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,121 @@
1
+ require "date"
2
+ require "json"
3
+ require "time"
4
+
5
+ module Charty
6
+ module Backends
7
+ module PlotlyHelpers
8
+ class PlotlyRenderer
9
+ def render(figure)
10
+ json = JSON.generate(figure, allow_nan: true)
11
+ case json
12
+ when /\b(?:Infinity|NaN)\b/
13
+ visit(figure)
14
+ else
15
+ JSON.load(json)
16
+ end
17
+ end
18
+
19
+ private def visit(obj)
20
+ case obj
21
+ when Integer, String, Symbol, true, false, nil
22
+ obj
23
+
24
+ when Numeric
25
+ visit_float(obj)
26
+
27
+ when Time
28
+ visit_time(obj)
29
+
30
+ when Date
31
+ visit_date(obj)
32
+
33
+ when DateTime
34
+ visit_datetime(obj)
35
+
36
+ when Array
37
+ visit_array(obj)
38
+
39
+ when Hash
40
+ visit_hash(obj)
41
+
42
+ when ->(x) { defined?(Numo::NArray) && obj.is_a?(Numo::NArray) }
43
+ visit_array(obj.to_a)
44
+
45
+ when ->(x) { defined?(NMatrix) && obj.is_a?(NMatrix) }
46
+ visit_array(obj.to_a)
47
+
48
+ when ->(x) { defined?(Numpy::NDArray) && obj.is_a?(Numpy::NDArray) }
49
+ visit_array(obj.to_a)
50
+
51
+ when ->(x) { defined?(PyCall::List) && obj.is_a?(PyCall::List) }
52
+ visit_array(obj.to_a)
53
+
54
+ when ->(x) { defined?(PyCall::Tuple) && obj.is_a?(PyCall::Tuple) }
55
+ visit_array(obj.to_a)
56
+
57
+ when ->(x) { defined?(PyCall::Dict) && obj.is_a?(PyCall::Dict) }
58
+ visit_hash(obj.to_h)
59
+
60
+ when ->(x) { defined?(Pandas::Series) && obj.is_a?(Pandas::Series) }
61
+ visit_array(obj.to_a)
62
+
63
+ else
64
+ str = String.try_convert(obj)
65
+ return str unless str.nil?
66
+
67
+ ary = Array.try_convert(obj)
68
+ return visit_array(ary) unless ary.nil?
69
+
70
+ hsh = Hash.try_convert(obj)
71
+ return visit_hash(hsh) unless hsh.nil?
72
+
73
+ type_error(obj)
74
+ end
75
+ end
76
+
77
+ private def visit_float(obj)
78
+ obj = obj.to_f
79
+ rescue RangeError
80
+ type_error(obj)
81
+ else
82
+ case
83
+ when obj.finite?
84
+ obj
85
+ else
86
+ nil
87
+ end
88
+ end
89
+
90
+ private def visit_time(obj)
91
+ obj.iso8601(6)
92
+ end
93
+
94
+ private def visit_date(obj)
95
+ obj.iso8601(6)
96
+ end
97
+
98
+ private def visit_datetime(obj)
99
+ obj.iso8601(6)
100
+ end
101
+
102
+ private def visit_array(obj)
103
+ obj.map {|x| visit(x) }
104
+ end
105
+
106
+ private def visit_hash(obj)
107
+ obj.map { |key, value|
108
+ [
109
+ key,
110
+ visit(value)
111
+ ]
112
+ }.to_h
113
+ end
114
+
115
+ private def type_error(obj)
116
+ raise TypeError, "Unable to convert to JSON: %p" % obj
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
@@ -14,6 +14,7 @@ module Charty
14
14
 
15
15
  def initialize
16
16
  @pyplot = ::Matplotlib::Pyplot
17
+ @default_edgecolor = Colors["white"].to_rgb
17
18
  @default_line_width = ::Matplotlib.rcParams["lines.linewidth"]
18
19
  @default_marker_size = ::Matplotlib.rcParams["lines.markersize"]
19
20
  end
@@ -41,7 +42,7 @@ module Charty
41
42
  @pyplot.show
42
43
  end
43
44
 
44
- def render(context, filename)
45
+ def old_style_render(context, filename)
45
46
  plot(@pyplot, context)
46
47
  if filename
47
48
  FileUtils.mkdir_p(File.dirname(filename))
@@ -50,7 +51,7 @@ module Charty
50
51
  @pyplot.show
51
52
  end
52
53
 
53
- def save(context, filename, finish: true)
54
+ def old_style_save(context, filename, finish: true)
54
55
  plot(context)
55
56
  if filename
56
57
  FileUtils.mkdir_p(File.dirname(filename))
@@ -303,7 +304,7 @@ module Charty
303
304
  end
304
305
  end
305
306
 
306
- def scatter(x, y, variables, legend:, color:, color_mapper:,
307
+ def scatter(x, y, variables, color:, color_mapper:,
307
308
  style:, style_mapper:, size:, size_mapper:)
308
309
  kwd = {}
309
310
  kwd[:edgecolor] = "w"
@@ -317,7 +318,7 @@ module Charty
317
318
  end
318
319
 
319
320
  unless size.nil?
320
- size = size_mapper[size].map(&method(:scale_scatter_point_size))
321
+ size = size_mapper[size].map {|x| scale_scatter_point_size(x).to_f }
321
322
  points.set_sizes(size)
322
323
  end
323
324
 
@@ -328,14 +329,15 @@ module Charty
328
329
 
329
330
  sizes = points.get_sizes
330
331
  points.set_linewidths(0.08 * Numpy.sqrt(Numpy.percentile(sizes, 10)))
332
+ end
331
333
 
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
334
+ def add_scatter_plot_legend(variables, color_mapper, size_mapper, style_mapper, legend)
335
+ ax = @pyplot.gca
336
+ add_relational_plot_legend(
337
+ ax, variables, color_mapper, size_mapper, style_mapper,
338
+ legend, [:color, :s, :marker]
339
+ ) do |label, kwargs|
340
+ ax.scatter([], [], label: label, **kwargs)
339
341
  end
340
342
  end
341
343
 
@@ -369,12 +371,12 @@ module Charty
369
371
 
370
372
  RELATIONAL_PLOT_LEGEND_BRIEF_TICKS = 6
371
373
 
372
- private def add_relational_plot_legend(ax, verbosity, variables, color_mapper, size_mapper, style_mapper,
373
- legend_attributes, &func)
374
+ private def add_relational_plot_legend(ax, variables, color_mapper, size_mapper, style_mapper,
375
+ verbosity, legend_attributes, &func)
374
376
  brief_ticks = RELATIONAL_PLOT_LEGEND_BRIEF_TICKS
375
377
  verbosity = :auto if verbosity == true
376
378
 
377
- legend_titles = [:color, :size, :style].filter_map {|v| variables[v] }
379
+ legend_titles = Util.filter_map([:color, :size, :style]) {|v| variables[v] }
378
380
  legend_title = legend_titles.pop if legend_titles.length == 1
379
381
 
380
382
  legend_kwargs = {}
@@ -391,24 +393,17 @@ module Charty
391
393
 
392
394
  # color legend
393
395
 
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
396
+ brief_color = (color_mapper.map_type == :numeric) && (
397
+ (verbosity == :brief) || (
398
+ verbosity == :auto && color_mapper.levels.length > brief_ticks
399
+ )
400
+ )
406
401
  case
407
402
  when brief_color
408
403
  # TODO: Also support LogLocator
409
404
  # locator = Matplotlib.ticker.LogLocator.new(numticks: brief_ticks)
410
405
  locator = Matplotlib.ticker.MaxNLocator.new(nbins: brief_ticks)
411
- limits = color_map.levels.minmax
406
+ limits = color_mapper.levels.minmax
412
407
  color_levels, color_formatted_levels = locator_to_legend_entries(locator, limits)
413
408
  when color_mapper.levels.nil?
414
409
  color_levels = color_formatted_levels = []
@@ -422,22 +417,14 @@ module Charty
422
417
 
423
418
  color_levels.length.times do |i|
424
419
  next if color_levels[i].nil?
425
- color_value = color_mapper[color_levels[i]].to_hex_string
420
+ color_value = color_mapper[color_levels[i]].to_rgb.to_hex_string
426
421
  update_legend.(variables[:color], color_formatted_levels[i], color: color_value)
427
422
  end
428
423
 
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
424
+ brief_size = (size_mapper.map_type == :numeric) && (
425
+ verbosity == :brief ||
426
+ (verbosity == :auto && size_mapper.levels.length > brief_ticks)
427
+ )
441
428
  case
442
429
  when brief_size
443
430
  # TODO: Also support LogLocator
@@ -457,8 +444,9 @@ module Charty
457
444
 
458
445
  size_levels.length.times do |i|
459
446
  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)
447
+ line_width = scale_line_width(size_mapper[size_levels[i]])
448
+ point_size = scale_scatter_point_size(size_mapper[size_levels[i]])
449
+ update_legend.(variables[:size], size_formatted_levels[i], linewidth: line_width, s: point_size)
462
450
  end
463
451
 
464
452
  if legend_title.nil? && variables.key?(:style)
@@ -474,17 +462,19 @@ module Charty
474
462
  else
475
463
  ""
476
464
  end
477
- # TODO: support dashes
478
- update_legend.(variables[:style], level,
479
- marker: marker,
480
- dashes: attrs.fetch(:dashes, ""))
465
+ dashes = if attrs.key?(:dashes)
466
+ attrs[:dashes]
467
+ else
468
+ ""
469
+ end
470
+ update_legend.(variables[:style], level, marker: marker, dashes: dashes)
481
471
  end
482
472
  end
483
473
 
484
474
  legend_kwargs.each do |key, kw|
485
475
  _, label = key
486
476
  kw[:color] ||= ".2"
487
- use_kw = legend_attributes.filter_map {|attr|
477
+ use_kw = Util.filter_map(legend_attributes) {|attr|
488
478
  [attr, kw[attr]] if kw.key?(attr)
489
479
  }.to_h
490
480
  use_kw[:visible] = kw[:visible] if kw.key?(:visible)
@@ -505,9 +495,137 @@ module Charty
505
495
  min + x * (max - min)
506
496
  end
507
497
 
498
+ def line(x, y, variables, color:, color_mapper:, size:, size_mapper:, style:, style_mapper:, ci_params:)
499
+ kws = {
500
+ markeredgewidth: 0.75,
501
+ markeredgecolor: "w",
502
+ }
503
+ ax = @pyplot.gca
504
+
505
+ x = x.to_a
506
+ y = y.to_a
507
+ lines = ax.plot(x, y, **kws)
508
+
509
+ lines.each do |line|
510
+ unless color.nil?
511
+ line.set_color(color_mapper[color].to_rgb.to_hex_string)
512
+ end
513
+
514
+ unless size.nil?
515
+ scaled_size = scale_line_width(size_mapper[size])
516
+ line.set_linewidth(scaled_size.to_f)
517
+ end
518
+
519
+ unless style.nil?
520
+ attributes = style_mapper[style]
521
+ if attributes.key?(:dashes)
522
+ line.set_dashes(attributes[:dashes])
523
+ end
524
+ if attributes.key?(:marker)
525
+ line.set_marker(PYPLOT_MARKERS[attributes[:marker]])
526
+ end
527
+ end
528
+ end
529
+
530
+ # TODO: support color, size, and style
531
+
532
+ line = lines[0]
533
+ line_color = line.get_color
534
+ line_alpha = line.get_alpha
535
+ line_capstyle = line.get_solid_capstyle
536
+
537
+ unless ci_params.nil?
538
+ y_min = ci_params[:y_min].to_a
539
+ y_max = ci_params[:y_max].to_a
540
+ case ci_params[:style]
541
+ when :band
542
+ # TODO: support to supply `alpha` via `err_kws`
543
+ ax.fill_between(x, y_min, y_max, color: line_color, alpha: 0.2)
544
+ when :bars
545
+ error_deltas = [
546
+ y.zip(y_min).map {|v, v_min| v - v_min },
547
+ y.zip(y_max).map {|v, v_max| v_max - v }
548
+ ]
549
+ ebars = ax.errorbar(x, y, error_deltas,
550
+ linestyle: "", color: line_color, alpha: line_alpha)
551
+ ebars.get_children.each do |bar|
552
+ case bar
553
+ when Matplotlib.collections.LineCollection
554
+ bar.set_capstyle(line_capstyle)
555
+ end
556
+ end
557
+ end
558
+ end
559
+ end
560
+
561
+ def add_line_plot_legend(variables, color_mapper, size_mapper, style_mapper, legend)
562
+ ax = @pyplot.gca
563
+ add_relational_plot_legend(
564
+ ax, variables, color_mapper, size_mapper, style_mapper,
565
+ legend, [:color, :linewidth, :marker, :dashes]
566
+ ) do |label, kwargs|
567
+ ax.plot([], [], label: label, **kwargs)
568
+ end
569
+ end
570
+
571
+
572
+ private def scale_line_width(x)
573
+ min = 0.5 * @default_line_width
574
+ max = 2.0 * @default_line_width
575
+
576
+ min + x * (max - min)
577
+ end
578
+
579
+ def univariate_histogram(hist, name, variable_name, stat,
580
+ alpha, color, key_color, color_mapper,
581
+ multiple, element, fill, shrink)
582
+ mid_points = hist.edges.each_cons(2).map {|a, b| a + (b - a) / 2 }
583
+ orient = variable_name == :x ? :v : :h
584
+ width = shrink * (hist.edges[1] - hist.edges[0])
585
+
586
+ kw = {align: :edge}
587
+
588
+ color = if color.nil?
589
+ key_color.to_rgb
590
+ else
591
+ color_mapper[color].to_rgb
592
+ end
593
+
594
+ alpha = 1r unless fill
595
+
596
+ if fill
597
+ kw[:facecolor] = color.to_rgba(alpha: alpha).to_hex_string
598
+ if multiple == :stack || multiple == :fill || element == :bars
599
+ kw[:edgecolor] = @default_edgecolor.to_hex_string
600
+ else
601
+ kw[:edgecolor] = color.to_hex_string
602
+ end
603
+ elsif element == :bars
604
+ kw.delete(:facecolor)
605
+ kw[:edgecolor] = color.to_rgba(alpha: alpha).to_hex_string
606
+ else
607
+ kw[:color] = color.to_rgba(alpha: alpha).to_hex_string
608
+ end
609
+
610
+ kw[:label] = name unless name.nil?
611
+
612
+ ax = @pyplot.gca
613
+ if orient == :v
614
+ ax.bar(mid_points, hist.weights, width, **kw)
615
+ else
616
+ ax.barh(mid_points, hist.weights, width, **kw)
617
+ end
618
+ end
619
+
508
620
  private def locator_to_legend_entries(locator, limits)
509
621
  vmin, vmax = limits
510
- raw_levels = locator.tick_values(vmin, vmax).to_a
622
+ dtype = case vmin
623
+ when Numeric
624
+ :float64
625
+ else
626
+ :object
627
+ end
628
+ raw_levels = locator.tick_values(vmin, vmax).astype(dtype).to_a
511
629
  raw_levels.reject! {|v| v < limits[0] || limits[1] < v }
512
630
 
513
631
  formatter = case locator
@@ -592,6 +710,27 @@ module Charty
592
710
  @pyplot.gca.legend(loc: loc, title: title)
593
711
  end
594
712
 
713
+ def render(notebook: false)
714
+ show
715
+ nil
716
+ end
717
+
718
+ SAVEFIG_OPTIONAL_PARAMS = [
719
+ :dpi, :quality, :optimize, :progressive, :facecolor, :edgecolor,
720
+ :orientation, :papertype, :transparent, :bbox_inches, :pad_inches,
721
+ :bbox_extra_artists, :backend, :metadata, :pil_kwargs
722
+ ].freeze
723
+
724
+ def save(filename, format: nil, title: nil, width: 700, height: 500, **kwargs)
725
+ params = {}
726
+ params[:format] = format unless format.nil?
727
+ SAVEFIG_OPTIONAL_PARAMS.each do |key|
728
+ params[key] = kwargs[key] if kwargs.key?(key)
729
+ end
730
+ @pyplot.savefig(filename, **params)
731
+ @pyplot.close
732
+ end
733
+
595
734
  def show
596
735
  @pyplot.show
597
736
  end