charty 0.2.6 → 0.2.7

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: 63e13663e8213e077993e52b906b630c35a1f7f9a224abb99fd206bb700659c2
4
- data.tar.gz: d7fd53056c32c18bf5af6b1e1a4b2a29bfe652a427b338087d3cab538a0797ba
3
+ metadata.gz: 6e39148cc6e6fd7916cdfbe5dacda3bab8f95068cbe3ceb4dff673d9688487d0
4
+ data.tar.gz: 5743d17f30055707fe39004f99bb30fb2cb10b164dbf9b222f000068cc800708
5
5
  SHA512:
6
- metadata.gz: cc4da146432f688eb52dd382ea516e02ab84ef7143453c80fa5bc14fef55851728550ee58fe98c1c3a5e9a1eac0f33dcfb702356bc7c0f344ceaf85f87400435
7
- data.tar.gz: f519610073317c3fafd42b97ad5e96d124265f6a7d79a54023995cf82d1fe84624d4f914c26e3eef644ad280110400fe1cdac275ec5e32145d7e4b652ef47891
6
+ metadata.gz: 562b312fb4ca59995d19930f404d6162f432f7731752e5a9016132b6b2696a507f6d7523d5eb6e5bc0141273726d2608b1351b497f2c200cec0fda5204e4fc19
7
+ data.tar.gz: 213f83930bc1bd809ae98af41d0608793c356b234064e9f2ec79b96e76b1c301a331fc44c9c04fc0f3f188f65b780b59af449dca206f68ec310cb121b6bd0e41
data/charty.gemspec CHANGED
@@ -27,6 +27,7 @@ Gem::Specification.new do |spec|
27
27
  spec.require_paths = ["lib"]
28
28
 
29
29
  spec.add_dependency "red-colors", ">= 0.3.0"
30
+ spec.add_dependency "red-datasets", ">= 0.1.2"
30
31
  spec.add_dependency "red-palette", ">= 0.5.0"
31
32
 
32
33
  spec.add_dependency "matplotlib", ">= 1.2.0"
@@ -36,7 +37,6 @@ Gem::Specification.new do |spec|
36
37
  spec.add_development_dependency "bundler", ">= 1.16"
37
38
  spec.add_development_dependency "rake"
38
39
  spec.add_development_dependency "test-unit"
39
- spec.add_development_dependency "red-datasets", ">= 0.1.2"
40
40
  spec.add_development_dependency "daru"
41
41
  spec.add_development_dependency "matrix" # need for daru on Ruby > 3.0
42
42
  spec.add_development_dependency "activerecord"
data/lib/charty.rb CHANGED
@@ -3,7 +3,9 @@ require_relative "charty/version"
3
3
  require "colors"
4
4
  require "palette"
5
5
 
6
+ require_relative "charty/cache_dir"
6
7
  require_relative "charty/util"
8
+ require_relative "charty/iruby_helper"
7
9
  require_relative "charty/dash_pattern_generator"
8
10
  require_relative "charty/backends"
9
11
  require_relative "charty/backend_methods"
@@ -2,6 +2,10 @@ require "json"
2
2
  require "securerandom"
3
3
  require "tmpdir"
4
4
 
5
+ require_relative "plotly_helpers/html_renderer"
6
+ require_relative "plotly_helpers/notebook_renderer"
7
+ require_relative "plotly_helpers/plotly_renderer"
8
+
5
9
  module Charty
6
10
  module Backends
7
11
  class Plotly
@@ -558,6 +562,52 @@ module Charty
558
562
  end
559
563
  end
560
564
 
565
+ PLOTLY_HISTNORM = {
566
+ count: "".freeze,
567
+ frequency: "density".freeze,
568
+ density: "probability density".freeze,
569
+ probability: "probability".freeze
570
+ }.freeze
571
+
572
+ def univariate_histogram(data, name, variable_name, stat,
573
+ bin_start, bin_end, bin_size, alpha,
574
+ color, color_mapper)
575
+ orientation = case variable_name
576
+ when :x
577
+ :v
578
+ else
579
+ :h
580
+ end
581
+ trace = {
582
+ type: "histogram",
583
+ name: name.to_s,
584
+ variable_name => data.to_a,
585
+ orientation: orientation,
586
+ histnorm: PLOTLY_HISTNORM[stat],
587
+ "#{variable_name}bins": {
588
+ start: bin_start,
589
+ end: bin_end,
590
+ size: bin_size
591
+ },
592
+ opacity: alpha
593
+ }
594
+
595
+ if color
596
+ trace[:marker] = {
597
+ color: color_mapper[color].to_rgb.to_hex_string
598
+ }
599
+ end
600
+
601
+ @traces << trace
602
+
603
+ @layout[:bargap] = 0.05
604
+
605
+ if @traces.length > 1
606
+ @layout[:barmode] = "overlay"
607
+ @layout[:showlegend] = true
608
+ end
609
+ end
610
+
561
611
  def set_xlabel(label)
562
612
  @layout[:xaxis] ||= {}
563
613
  @layout[:xaxis][:title] = label
@@ -699,7 +749,7 @@ module Charty
699
749
 
700
750
  def render(element_id: nil, format: nil, notebook: false)
701
751
  case format
702
- when :html, "html"
752
+ when :html, "html", nil
703
753
  format = "text/html"
704
754
  when :png, "png"
705
755
  format = "image/png"
@@ -708,7 +758,7 @@ module Charty
708
758
  end
709
759
 
710
760
  case format
711
- when "text/html", nil
761
+ when "text/html"
712
762
  # render html after this case cause
713
763
  when "image/png", "image/jpeg"
714
764
  image_data = render_image(format, element_id: element_id, notebook: false)
@@ -722,32 +772,54 @@ module Charty
722
772
  "Unsupported mime type to render: %p" % format
723
773
  end
724
774
 
725
- # TODO: size should be customizable
726
- html = <<~HTML
727
- <div id="%{id}" style="width: 100%%; height:525px;"></div>
728
- <script type="text/javascript">
729
- requirejs(["plotly"], function (Plotly) {
730
- Plotly.newPlot("%{id}", %{data}, %{layout});
731
- });
732
- </script>
733
- HTML
734
-
735
775
  element_id = SecureRandom.uuid if element_id.nil?
736
776
 
737
- html %= {
738
- id: element_id,
739
- data: JSON.dump(@traces),
740
- layout: JSON.dump(@layout)
741
- }
742
-
777
+ renderer = PlotlyHelpers::HtmlRenderer.new(full_html: !notebook)
778
+ html = renderer.render({data: @traces, layout: @layout}, element_id: element_id)
743
779
  if notebook
744
- IRubyOutput.prepare
745
- ["text/html", html]
780
+ [format, html]
746
781
  else
747
782
  html
748
783
  end
749
784
  end
750
785
 
786
+ def render_mimebundle(include: [], exclude: [])
787
+ types = case
788
+ when IRubyHelper.vscode?,
789
+ IRubyHelper.nteract?
790
+ [:plotly_mimetype]
791
+ else
792
+ [:plotly_mimetype, :notebook]
793
+ end
794
+ bundle = Util.filter_map(types) { |type|
795
+ case type
796
+ when :plotly_mimetype
797
+ render_plotly_mimetype_bundle
798
+ when :notebook
799
+ render_notebook_bundle
800
+ end
801
+ }.to_h
802
+ bundle
803
+ end
804
+
805
+ private def render_plotly_mimetype_bundle
806
+ renderer = PlotlyHelpers::PlotlyRenderer.new
807
+ obj = renderer.render({data: @traces, layout: @layout})
808
+ [ "application/vnd.plotly.v1+json", obj ]
809
+ end
810
+
811
+ private def render_notebook_bundle
812
+ renderer = self.class.notebook_renderer
813
+ renderer.activate
814
+ html = renderer.render({data: @traces, layout: @layout})
815
+ [ "text/html", html ]
816
+ end
817
+
818
+ # for new APIs
819
+ def self.notebook_renderer
820
+ @notebook_renderer ||= PlotlyHelpers::NotebookRenderer.new
821
+ end
822
+
751
823
  private def render_image(format=nil, filename: nil, element_id: nil, notebook: false,
752
824
  title: nil, width: nil, height: nil)
753
825
  format = "image/png" if format.nil?
@@ -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,86 @@
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
+ end
53
+
54
+ def render(figure, element_id: nil, post_script: nil)
55
+ ary = Array.try_convert(post_script)
56
+ post_script = ary || [post_script]
57
+ post_script.unshift(<<~END_POST_SCRIPT)
58
+ var gd = document.getElementById('%{plot_id}');
59
+ var x = new MutationObserver(function (mutations, observer) {
60
+ var display = window.getComputedStyle(gd).display;
61
+ if (!display || display === 'none') {
62
+ console.log([gd, 'removed']);
63
+ Plotly.purge(gd);
64
+ observer.disconnect();
65
+ }
66
+ });
67
+
68
+ // Listen for the removal of the full notebook cell
69
+ var notebookContainer = gd.closest('#notebook-container');
70
+ if (notebookContainer) {
71
+ x.observe(notebookContainer, {childList: true});
72
+ }
73
+
74
+ // Listen for the clearing of the current output cell
75
+ var outputEl = gd.closest('.output');
76
+ if (outputEl) {
77
+ x.observe(outputEl, {childList: true});
78
+ }
79
+ END_POST_SCRIPT
80
+
81
+ super(figure, element_id: element_id, post_script: post_script)
82
+ end
83
+ end
84
+ end
85
+ end
86
+ 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
@@ -670,6 +670,7 @@ module Charty
670
670
 
671
671
  def render(notebook: false)
672
672
  show
673
+ nil
673
674
  end
674
675
 
675
676
  SAVEFIG_OPTIONAL_PARAMS = [
@@ -0,0 +1,27 @@
1
+ require "pathname"
2
+
3
+ module Charty
4
+ module CacheDir
5
+ module_function
6
+
7
+ def cache_dir_path
8
+ platform_cache_dir_path + "charty"
9
+ end
10
+
11
+ def platform_cache_dir_path
12
+ base_dir = case RUBY_PLATFORM
13
+ when /mswin/, /mingw/
14
+ ENV.fetch("LOCALAPPDATA", "~/AppData/Local")
15
+ when /darwin/
16
+ "~/Library/Caches"
17
+ else
18
+ ENV.fetch("XDG_CACHE_HOME", "~/.cache")
19
+ end
20
+ Pathname(base_dir).expand_path
21
+ end
22
+
23
+ def path(*path_components)
24
+ cache_dir_path.join(*path_components)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,18 @@
1
+ module Charty
2
+ module IRubyHelper
3
+ module_function
4
+
5
+ def iruby_notebook?
6
+ # TODO: This cannot distinguish notebook and console.
7
+ defined?(IRuby)
8
+ end
9
+
10
+ def vscode?
11
+ ENV.key?("VSCODE_PID")
12
+ end
13
+
14
+ def nteract?
15
+ ENV.key?("NTERACT_EXE")
16
+ end
17
+ end
18
+ end
@@ -248,6 +248,46 @@ module Charty
248
248
  &block
249
249
  )
250
250
  end
251
+
252
+ def hist_plot(data: nil, x: nil, y: nil, color: nil,
253
+ stat: :count, bins: :auto,
254
+ key_color: nil, palette: nil, color_order: nil, color_norm: nil,
255
+ legend: true, **options, &block)
256
+ # TODO: support following arguments
257
+ # - wiehgts
258
+ # - binwidth
259
+ # - binrange
260
+ # - discrete
261
+ # - cumulative
262
+ # - common_bins
263
+ # - common_norm
264
+ # - multiple
265
+ # - element
266
+ # - fill
267
+ # - shrink
268
+ # - kde
269
+ # - kde_params
270
+ # - line_params
271
+ # - thresh
272
+ # - pthresh
273
+ # - pmax
274
+ # - cbar
275
+ # - cbar_params
276
+ # - x_log_scale
277
+ # - y_log_scale
278
+ Plotters::HistogramPlotter.new(
279
+ data: data,
280
+ variables: { x: x, y: y, color: color },
281
+ stat: stat,
282
+ bins: bins,
283
+ key_color: key_color,
284
+ palette: palette,
285
+ color_order: color_order,
286
+ color_norm: color_norm,
287
+ legend: legend,
288
+ **options,
289
+ &block)
290
+ end
251
291
  end
252
292
 
253
293
  extend PlotMethods
@@ -10,3 +10,6 @@ require_relative "plotters/vector_plotter"
10
10
  require_relative "plotters/relational_plotter"
11
11
  require_relative "plotters/scatter_plotter"
12
12
  require_relative "plotters/line_plotter"
13
+
14
+ require_relative "plotters/distribution_plotter"
15
+ require_relative "plotters/histogram_plotter"
@@ -183,7 +183,7 @@ module Charty
183
183
 
184
184
  levels = var_levels.dup
185
185
 
186
- [:x, :y].each do |axis|
186
+ ([:x, :y] & grouping_vars).each do |axis|
187
187
  levels[axis] = plot_data[axis].categorical_order()
188
188
  if processed
189
189
  # TODO: perform inverse conversion of axis scaling here
@@ -213,16 +213,19 @@ module Charty
213
213
 
214
214
  def save(filename, **kwargs)
215
215
  backend = Backends.current
216
- backend.begin_figure
217
- render_plot(backend, **kwargs)
216
+ call_render_plot(backend, notebook: false, **kwargs)
218
217
  backend.save(filename, **kwargs)
219
218
  end
220
219
 
221
220
  def render(notebook: false, **kwargs)
222
221
  backend = Backends.current
222
+ call_render_plot(backend, notebook: notebook, **kwargs)
223
+ backend.render(notebook: notebook, **kwargs)
224
+ end
225
+
226
+ private def call_render_plot(backend, notebook: false, **kwargs)
223
227
  backend.begin_figure
224
228
  render_plot(backend, notebook: notebook, **kwargs)
225
- backend.render(notebook: notebook, **kwargs)
226
229
  end
227
230
 
228
231
  private def render_plot(*, **)
@@ -231,12 +234,17 @@ module Charty
231
234
  end
232
235
 
233
236
  def to_iruby
234
- render(notebook: iruby_notebook?)
237
+ render(notebook: IRubyHelper.iruby_notebook?)
235
238
  end
236
239
 
237
- private def iruby_notebook?
238
- return false unless defined?(IRuby)
239
- true # TODO: Check the server is notebook or not
240
+ def to_iruby_mimebundle(include: [], exclude: [])
241
+ backend = Backends.current
242
+ if backend.respond_to?(:render_mimebundle)
243
+ call_render_plot(backend, notebook: true)
244
+ backend.render_mimebundle(include: include, exclude: exclude)
245
+ else
246
+ {}
247
+ end
240
248
  end
241
249
  end
242
250
  end
@@ -0,0 +1,143 @@
1
+ module Charty
2
+ module Plotters
3
+ class DistributionPlotter < AbstractPlotter
4
+ def flat_structure
5
+ {
6
+ x: :values
7
+ }
8
+ end
9
+
10
+ def initialize(data:, variables:, **options, &block)
11
+ x, y, color = variables.values_at(:x, :y, :color)
12
+ super(x, y, color, data: data, **options, &block)
13
+
14
+ setup_variables
15
+ end
16
+
17
+ attr_reader :variables
18
+
19
+ attr_reader :color_norm
20
+
21
+ def color_norm=(val)
22
+ unless val.nil?
23
+ raise NotImplementedError,
24
+ "Specifying color_norm is not supported yet"
25
+ end
26
+ end
27
+
28
+ attr_reader :legend
29
+
30
+ def legend=(val)
31
+ @legend = check_legend(val)
32
+ end
33
+
34
+ private def check_legend(val)
35
+ check_boolean(val, :legend)
36
+ end
37
+
38
+ attr_reader :input_format, :plot_data, :variables, :var_types
39
+
40
+ # This should be the same as one in RelationalPlotter
41
+ # TODO: move this to AbstractPlotter and refactor with CategoricalPlotter
42
+ private def setup_variables
43
+ if x.nil? && y.nil?
44
+ @input_format = :wide
45
+ setup_variables_with_wide_form_dataset
46
+ else
47
+ @input_format = :long
48
+ setup_variables_with_long_form_dataset
49
+ end
50
+
51
+ @var_types = @plot_data.columns.map { |k|
52
+ [k, variable_type(@plot_data[k], :categorical)]
53
+ }.to_h
54
+ end
55
+
56
+ private def setup_variables_with_wide_form_dataset
57
+ unless color.nil?
58
+ raise ArgumentError,
59
+ "Unable to assign the following variables in wide-form data: color"
60
+ end
61
+
62
+ if data.nil? || data.empty?
63
+ @plot_data = Charty::Table.new({})
64
+ @variables = {}
65
+ return
66
+ end
67
+
68
+ # TODO: detect flat data
69
+ flat = data.is_a?(Charty::Vector)
70
+ if flat
71
+ @plot_data = {}
72
+ @variables = {}
73
+
74
+ [:x, :y].each do |var|
75
+ case self.flat_structure[var]
76
+ when :index
77
+ @plot_data[var] = data.index.to_a
78
+ @variables[var] = data.index.name
79
+ when :values
80
+ @plot_data[var] = data.to_a
81
+ @variables[var] = data.name
82
+ end
83
+ end
84
+
85
+ @plot_data = Charty::Table.new(@plot_data)
86
+ else
87
+ raise NotImplementedError,
88
+ "wide-form input is not supported"
89
+ end
90
+ end
91
+
92
+ private def setup_variables_with_long_form_dataset
93
+ if data.nil? || data.empty?
94
+ @plot_data = Charty::Table.new({})
95
+ @variables = {}
96
+ return
97
+ end
98
+
99
+ plot_data = {}
100
+ variables = {}
101
+
102
+ {
103
+ x: self.x,
104
+ y: self.y,
105
+ color: self.color,
106
+ }.each do |key, val|
107
+ next if val.nil?
108
+
109
+ if data.column_names.include?(val)
110
+ plot_data[key] = data[val]
111
+ variables[key] = val
112
+ else
113
+ case val
114
+ when Charty::Vector
115
+ plot_data[key] = val
116
+ variables[key] = val.name
117
+ else
118
+ raise ArgumentError,
119
+ "Could not interpret value %p for parameter %p" % [val, key]
120
+ end
121
+ end
122
+ end
123
+
124
+ @plot_data = Charty::Table.new(plot_data)
125
+ @variables = variables.select do |var, name|
126
+ @plot_data[var].notnull.any?
127
+ end
128
+ end
129
+
130
+ private def map_color(palette: nil, order: nil, norm: nil)
131
+ @color_mapper = ColorMapper.new(self, palette, order, norm)
132
+ end
133
+
134
+ private def map_size(sizes: nil, order: nil, norm: nil)
135
+ @size_mapper = SizeMapper.new(self, sizes, order, norm)
136
+ end
137
+
138
+ private def map_style(markers: nil, dashes: nil, order: nil)
139
+ @style_mapper = StyleMapper.new(self, markers, dashes, order)
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,182 @@
1
+ module Charty
2
+ module Plotters
3
+ class HistogramPlotter < DistributionPlotter
4
+ def univariate?
5
+ self.variables.key?(:x) != self.variables.key?(:y)
6
+ end
7
+
8
+ def univariate_variable
9
+ unless univariate?
10
+ raise TypeError, "This is not a univariate plot"
11
+ end
12
+ ([:x, :y] & self.variables.keys)[0]
13
+ end
14
+
15
+ attr_reader :weights
16
+
17
+ def weights=(val)
18
+ @weights = check_weights(val)
19
+ end
20
+
21
+ private def check_weights(val)
22
+ raise NotImplementedError, "weights is not supported yet"
23
+ end
24
+
25
+ attr_reader :stat
26
+
27
+ def stat=(val)
28
+ @stat = check_stat(val)
29
+ end
30
+
31
+ private def check_stat(val)
32
+ case val
33
+ when :count, "count"
34
+ val.to_sym
35
+ when :frequency, "frequency",
36
+ :density, "density",
37
+ :probability, "probability"
38
+ raise ArgumentError,
39
+ "%p for `stat` is not supported yet" % val,
40
+ caller
41
+ else
42
+ raise ArgumentError,
43
+ "Invalid value for `stat` (%p)" % val,
44
+ caller
45
+ end
46
+ end
47
+
48
+ attr_reader :bins
49
+
50
+ def bins=(val)
51
+ @bins = check_bins(val)
52
+ end
53
+
54
+ private def check_bins(val)
55
+ case val
56
+ when :auto, "auto"
57
+ val.to_sym
58
+ when Integer
59
+ val
60
+ else
61
+ raise ArgumentError,
62
+ "Invalid value for `bins` (%p)" % val,
63
+ caller
64
+ end
65
+ end
66
+
67
+ # TODO: bin_width
68
+ # TODO: bin_range
69
+ # TODO: discrete
70
+ # TODO: cumulative
71
+ # TODO: common_bins
72
+ # TODO: common_norm
73
+
74
+ attr_reader :multiple
75
+
76
+ def multiple=(val)
77
+ @multiple = check_multiple(val)
78
+ end
79
+
80
+ private def check_multiple(val)
81
+ case val
82
+ when :layer, "layer"
83
+ val.to_sym
84
+ when :dodge, "dodge",
85
+ :stack, "stack",
86
+ :fill, "fill"
87
+ val = val.to_sym
88
+ raise NotImplementedError,
89
+ "%p for `multiple` is not supported yet" % val,
90
+ caller
91
+ else
92
+ raise ArgumentError,
93
+ "Invalid value for `multiple` (%p)" % val,
94
+ caller
95
+ end
96
+ end
97
+
98
+ # TODO: element
99
+ # TODO: fill
100
+ # TODO: shrink
101
+
102
+ attr_reader :kde
103
+
104
+ def kde=(val)
105
+ raise NotImplementedError, "kde is not supported yet"
106
+ end
107
+
108
+ attr_reader :kde_params
109
+
110
+ def kde_params=(val)
111
+ raise NotImplementedError, "kde_params is not supported yet"
112
+ end
113
+
114
+ # TODO: thresh
115
+ # TODO: pthresh
116
+ # TODO: pmax
117
+ # TODO: cbar
118
+ # TODO: cbar_params
119
+ # TODO: x_log_scale
120
+ # TODO: y_log_scale
121
+
122
+ private def render_plot(backend, **)
123
+ draw_univariate_histogram(backend)
124
+ annotate_axes(backend)
125
+ end
126
+
127
+ private def draw_univariate_histogram(backend)
128
+ map_color(palette: palette, order: color_order, norm: color_norm)
129
+
130
+ # TODO: calculate histogram here and use bar plot to visualize
131
+ data_variable = self.univariate_variable
132
+
133
+ histograms = {}
134
+ each_subset([:color], processed: true) do |sub_vars, sub_data|
135
+ key = sub_vars.to_a
136
+ observations = sub_data[data_variable].drop_na.to_a
137
+ hist = Statistics.histogram(observations)
138
+ histograms[key] = hist
139
+ end
140
+
141
+ bin_start, bin_end, bin_size = nil
142
+ histograms.each do |_, hist|
143
+ s, e = hist.edge.minmax
144
+ z = (e - s).to_f / (hist.edge.length - 1)
145
+ bin_start = [bin_start, s].compact.min
146
+ bin_end = [bin_end, e].compact.max
147
+ bin_size = [bin_size, z].compact.min
148
+ end
149
+
150
+ if self.variables.key?(:color)
151
+ alpha = 0.5
152
+ else
153
+ alpha = 0.75
154
+ end
155
+
156
+ each_subset([:color], processed: true) do |sub_vars, sub_data|
157
+ name = sub_vars[:color]
158
+ observations = sub_data[data_variable].drop_na.to_a
159
+
160
+ backend.univariate_histogram(observations, name, data_variable, stat,
161
+ bin_start, bin_end, bin_size, alpha,
162
+ name, @color_mapper)
163
+ end
164
+ end
165
+
166
+ private def annotate_axes(backend)
167
+ if univariate?
168
+ xlabel = self.variables[:x]
169
+ ylabel = self.variables[:y]
170
+ case self.univariate_variable
171
+ when :x
172
+ ylabel = self.stat.to_s.capitalize
173
+ else
174
+ xlabel = self.stat.to_s.capitalize
175
+ end
176
+ backend.set_ylabel(ylabel) if ylabel
177
+ backend.set_xlabel(xlabel) if xlabel
178
+ end
179
+ end
180
+ end
181
+ end
182
+ end
@@ -10,6 +10,10 @@ module Charty
10
10
  def self.stdev(enum, population: false)
11
11
  enum.stdev(population: population)
12
12
  end
13
+
14
+ def self.histogram(ary, *args, **kwargs)
15
+ ary.histogram(*args, **kwargs)
16
+ end
13
17
  rescue LoadError
14
18
  def self.mean(enum)
15
19
  xs = enum.to_a
@@ -24,6 +28,11 @@ module Charty
24
28
  var = xs.map {|x| (x - mean)**2 }.sum / (n - ddof)
25
29
  Math.sqrt(var)
26
30
  end
31
+
32
+ def self.histogram(ary, *args, **kwargs)
33
+ raise NotImplementedError,
34
+ "histogram is currently supported only with enumerable-statistics"
35
+ end
27
36
  end
28
37
 
29
38
  def self.bootstrap(vector, n_boot: 2000, func: :mean, units: nil, random: nil)
@@ -1,5 +1,5 @@
1
1
  module Charty
2
- VERSION = "0.2.6"
2
+ VERSION = "0.2.7"
3
3
 
4
4
  module Version
5
5
  numbers, TAG = VERSION.split("-")
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: charty
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.6
4
+ version: 0.2.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - youchan
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: exe
12
12
  cert_chain: []
13
- date: 2021-06-17 00:00:00.000000000 Z
13
+ date: 2021-06-21 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: red-colors
@@ -26,6 +26,20 @@ dependencies:
26
26
  - - ">="
27
27
  - !ruby/object:Gem::Version
28
28
  version: 0.3.0
29
+ - !ruby/object:Gem::Dependency
30
+ name: red-datasets
31
+ requirement: !ruby/object:Gem::Requirement
32
+ requirements:
33
+ - - ">="
34
+ - !ruby/object:Gem::Version
35
+ version: 0.1.2
36
+ type: :runtime
37
+ prerelease: false
38
+ version_requirements: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: 0.1.2
29
43
  - !ruby/object:Gem::Dependency
30
44
  name: red-palette
31
45
  requirement: !ruby/object:Gem::Requirement
@@ -124,20 +138,6 @@ dependencies:
124
138
  - - ">="
125
139
  - !ruby/object:Gem::Version
126
140
  version: '0'
127
- - !ruby/object:Gem::Dependency
128
- name: red-datasets
129
- requirement: !ruby/object:Gem::Requirement
130
- requirements:
131
- - - ">="
132
- - !ruby/object:Gem::Version
133
- version: 0.1.2
134
- type: :development
135
- prerelease: false
136
- version_requirements: !ruby/object:Gem::Requirement
137
- requirements:
138
- - - ">="
139
- - !ruby/object:Gem::Version
140
- version: 0.1.2
141
141
  - !ruby/object:Gem::Dependency
142
142
  name: daru
143
143
  requirement: !ruby/object:Gem::Requirement
@@ -287,11 +287,16 @@ files:
287
287
  - lib/charty/backends/google_charts.rb
288
288
  - lib/charty/backends/gruff.rb
289
289
  - lib/charty/backends/plotly.rb
290
+ - lib/charty/backends/plotly_helpers/html_renderer.rb
291
+ - lib/charty/backends/plotly_helpers/notebook_renderer.rb
292
+ - lib/charty/backends/plotly_helpers/plotly_renderer.rb
290
293
  - lib/charty/backends/pyplot.rb
291
294
  - lib/charty/backends/rubyplot.rb
292
295
  - lib/charty/backends/unicode_plot.rb
296
+ - lib/charty/cache_dir.rb
293
297
  - lib/charty/dash_pattern_generator.rb
294
298
  - lib/charty/index.rb
299
+ - lib/charty/iruby_helper.rb
295
300
  - lib/charty/layout.rb
296
301
  - lib/charty/linspace.rb
297
302
  - lib/charty/plot_methods.rb
@@ -302,7 +307,9 @@ files:
302
307
  - lib/charty/plotters/box_plotter.rb
303
308
  - lib/charty/plotters/categorical_plotter.rb
304
309
  - lib/charty/plotters/count_plotter.rb
310
+ - lib/charty/plotters/distribution_plotter.rb
305
311
  - lib/charty/plotters/estimation_support.rb
312
+ - lib/charty/plotters/histogram_plotter.rb
306
313
  - lib/charty/plotters/line_plotter.rb
307
314
  - lib/charty/plotters/random_support.rb
308
315
  - lib/charty/plotters/relational_plotter.rb