charty 0.2.4 → 0.2.9

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