charty 0.2.6 → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 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