charty 0.2.6 → 0.2.10

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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/charty.gemspec +2 -1
  3. data/examples/bar_plot.rb +19 -0
  4. data/examples/box_plot.rb +17 -0
  5. data/examples/scatter_plot.rb +17 -0
  6. data/images/penguins_body_mass_g_flipper_length_mm_species_scatter_plot.png +0 -0
  7. data/images/penguins_body_mass_g_flipper_length_mm_species_sex_scatter_plot.png +0 -0
  8. data/images/penguins_species_body_mass_g_bar_plot_h.png +0 -0
  9. data/images/penguins_species_body_mass_g_bar_plot_v.png +0 -0
  10. data/images/penguins_species_body_mass_g_box_plot_h.png +0 -0
  11. data/images/penguins_species_body_mass_g_box_plot_v.png +0 -0
  12. data/images/penguins_species_body_mass_g_sex_bar_plot_v.png +0 -0
  13. data/images/penguins_species_body_mass_g_sex_box_plot_v.png +0 -0
  14. data/lib/charty.rb +2 -0
  15. data/lib/charty/backends/plotly.rb +127 -24
  16. data/lib/charty/backends/plotly_helpers/html_renderer.rb +203 -0
  17. data/lib/charty/backends/plotly_helpers/notebook_renderer.rb +89 -0
  18. data/lib/charty/backends/plotly_helpers/plotly_renderer.rb +121 -0
  19. data/lib/charty/backends/pyplot.rb +74 -0
  20. data/lib/charty/backends/unicode_plot.rb +9 -9
  21. data/lib/charty/cache_dir.rb +27 -0
  22. data/lib/charty/iruby_helper.rb +18 -0
  23. data/lib/charty/plot_methods.rb +82 -6
  24. data/lib/charty/plotters.rb +3 -0
  25. data/lib/charty/plotters/abstract_plotter.rb +56 -16
  26. data/lib/charty/plotters/bar_plotter.rb +39 -0
  27. data/lib/charty/plotters/categorical_plotter.rb +9 -1
  28. data/lib/charty/plotters/distribution_plotter.rb +180 -0
  29. data/lib/charty/plotters/histogram_plotter.rb +244 -0
  30. data/lib/charty/plotters/line_plotter.rb +38 -5
  31. data/lib/charty/plotters/scatter_plotter.rb +4 -2
  32. data/lib/charty/statistics.rb +9 -0
  33. data/lib/charty/table.rb +30 -23
  34. data/lib/charty/table_adapters/base_adapter.rb +88 -0
  35. data/lib/charty/table_adapters/daru_adapter.rb +41 -1
  36. data/lib/charty/table_adapters/hash_adapter.rb +59 -1
  37. data/lib/charty/table_adapters/pandas_adapter.rb +49 -1
  38. data/lib/charty/vector.rb +29 -1
  39. data/lib/charty/vector_adapters.rb +16 -0
  40. data/lib/charty/vector_adapters/pandas_adapter.rb +10 -1
  41. data/lib/charty/version.rb +1 -1
  42. metadata +39 -15
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 63e13663e8213e077993e52b906b630c35a1f7f9a224abb99fd206bb700659c2
4
- data.tar.gz: d7fd53056c32c18bf5af6b1e1a4b2a29bfe652a427b338087d3cab538a0797ba
3
+ metadata.gz: d885d476eadfadd3f76f138707ffaf6166eec006b4528fb19c3acce0b97b1c85
4
+ data.tar.gz: 0630f23f19b65177f3b6dc238717a81a019f9dfa40a40c143d71dcc5c5f760f0
5
5
  SHA512:
6
- metadata.gz: cc4da146432f688eb52dd382ea516e02ab84ef7143453c80fa5bc14fef55851728550ee58fe98c1c3a5e9a1eac0f33dcfb702356bc7c0f344ceaf85f87400435
7
- data.tar.gz: f519610073317c3fafd42b97ad5e96d124265f6a7d79a54023995cf82d1fe84624d4f914c26e3eef644ad280110400fe1cdac275ec5e32145d7e4b652ef47891
6
+ metadata.gz: 535a69b8d794ccbd30b4328f3188c8a8b50c850cfd84285f79b31162296a9d981313e561ea1089a68be3368b7eb1c83ccc3cb2ff511b8f79aef77e544e261bbe
7
+ data.tar.gz: 804de2468433db38b5381bcbccab4c28e84c7592bce5c5a98a539ce99c589449759e65ffe7f2e808bd3c5cb5e002ee2219219111561f66f590dbb0ac2c0757bc
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,10 +37,10 @@ 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"
43
43
  spec.add_development_dependency "sqlite3"
44
44
  spec.add_development_dependency "iruby", ">= 0.7.0"
45
+ spec.add_development_dependency "csv"
45
46
  end
@@ -0,0 +1,19 @@
1
+ # This example generates box_plot results in README.md
2
+
3
+ require "charty"
4
+ require "datasets"
5
+ require "matplotlib"
6
+
7
+ Charty::Backends.use(:pyplot)
8
+ Matplotlib.use(:agg)
9
+
10
+ penguins = Datasets::Penguins.new
11
+
12
+ Charty.bar_plot(data: penguins, x: :species, y: :body_mass_g)
13
+ .save("penguins_species_body_mass_g_bar_plot_v.png")
14
+
15
+ Charty.bar_plot(data: penguins, x: :body_mass_g, y: :species)
16
+ .save("penguins_species_body_mass_g_bar_plot_h.png")
17
+
18
+ Charty.bar_plot(data: penguins, x: :species, y: :body_mass_g, color: :sex)
19
+ .save("penguins_species_body_mass_g_sex_bar_plot_v.png")
@@ -0,0 +1,17 @@
1
+ require "charty"
2
+ require "datasets"
3
+ require "matplotlib"
4
+
5
+ Charty::Backends.use(:pyplot)
6
+ Matplotlib.use(:agg)
7
+
8
+ penguins = Datasets::Penguins.new
9
+
10
+ Charty.box_plot(data: penguins, x: :species, y: :body_mass_g)
11
+ .save("penguins_species_body_mass_g_box_plot_v.png")
12
+
13
+ Charty.box_plot(data: penguins, x: :body_mass_g, y: :species)
14
+ .save("penguins_species_body_mass_g_box_plot_h.png")
15
+
16
+ Charty.box_plot(data: penguins, x: :species, y: :body_mass_g, color: :sex)
17
+ .save("penguins_species_body_mass_g_sex_box_plot_v.png")
@@ -0,0 +1,17 @@
1
+ require "charty"
2
+ require "datasets"
3
+ require "matplotlib"
4
+
5
+ Charty::Backends.use(:pyplot)
6
+ Matplotlib.use(:agg)
7
+
8
+ penguins = Datasets::Penguins.new
9
+
10
+ Charty.scatter_plot(data: penguins, x: :body_mass_g, y: :flipper_length_mm)
11
+ .save("penguins_body_mass_g_flipper_length_mm_scatter_plot.png")
12
+
13
+ Charty.scatter_plot(data: penguins, x: :body_mass_g, y: :flipper_length_mm, color: :species)
14
+ .save("penguins_body_mass_g_flipper_length_mm_species_scatter_plot.png")
15
+
16
+ Charty.scatter_plot(data: penguins, x: :body_mass_g, y: :flipper_length_mm, color: :species, style: :sex)
17
+ .save("penguins_body_mass_g_flipper_length_mm_species_sex_scatter_plot.png")
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
@@ -130,10 +134,8 @@ module Charty
130
134
 
131
135
  if orient == :v
132
136
  x, y = bar_pos, values
133
- x = group_names unless group_names.nil?
134
137
  else
135
138
  x, y = values, bar_pos
136
- y = group_names unless group_names.nil?
137
139
  end
138
140
 
139
141
  trace = {
@@ -237,9 +239,9 @@ module Charty
237
239
  }
238
240
 
239
241
  if orient == :v
240
- trace.update(y: values, x: group_keys)
242
+ trace.update(y: values, x: group_keys.map(&:to_s))
241
243
  else
242
- trace.update(x: values, y: group_keys)
244
+ trace.update(x: values, y: group_keys.map(&:to_s))
243
245
  end
244
246
 
245
247
  trace
@@ -558,6 +560,62 @@ module Charty
558
560
  end
559
561
  end
560
562
 
563
+ PLOTLY_HISTNORM = {
564
+ count: "".freeze,
565
+ frequency: "density".freeze,
566
+ density: "probability density".freeze,
567
+ probability: "probability".freeze
568
+ }.freeze
569
+
570
+ def univariate_histogram(hist, name, variable_name, stat,
571
+ alpha, color, key_color, color_mapper,
572
+ _multiple, _element, _fill, _shrink)
573
+ value_axis = variable_name
574
+ case value_axis
575
+ when :x
576
+ weights_axis = :y
577
+ orientation = :v
578
+ else
579
+ weights_axis = :x
580
+ orientation = :h
581
+ end
582
+
583
+ mid_points = hist.edges.each_cons(2).map {|a, b| a + (b - a) / 2 }
584
+
585
+ trace = {
586
+ type: :bar,
587
+ name: name.to_s,
588
+ value_axis => mid_points,
589
+ weights_axis => hist.weights,
590
+ orientation: orientation,
591
+ opacity: alpha
592
+ }
593
+
594
+ if color.nil?
595
+ trace[:marker] = {
596
+ color: key_color.to_rgb.to_hex_string
597
+ }
598
+ else
599
+ trace[:marker] = {
600
+ color: color_mapper[color].to_rgb.to_hex_string
601
+ }
602
+ end
603
+
604
+ @traces << trace
605
+
606
+ @layout[:bargap] = 0.05
607
+
608
+ if @traces.length > 1
609
+ @layout[:barmode] = "overlay"
610
+ @layout[:showlegend] = true
611
+ end
612
+ end
613
+
614
+ def set_title(title)
615
+ @layout[:title] ||= {}
616
+ @layout[:title][:text] = title
617
+ end
618
+
561
619
  def set_xlabel(label)
562
620
  @layout[:xaxis] ||= {}
563
621
  @layout[:xaxis][:title] = label
@@ -602,6 +660,29 @@ module Charty
602
660
  @layout[:yaxis][:range] = [min, max]
603
661
  end
604
662
 
663
+ def set_xscale(scale)
664
+ scale = check_scale_type(scale, :xscale)
665
+ @layout[:xaxis] ||= {}
666
+ @layout[:xaxis][:type] = scale
667
+ end
668
+
669
+ def set_yscale(scale)
670
+ scale = check_scale_type(scale, :yscale)
671
+ @layout[:yaxis] ||= {}
672
+ @layout[:yaxis][:type] = scale
673
+ end
674
+
675
+ private def check_scale_type(val, name)
676
+ case
677
+ when :linear, :log
678
+ val
679
+ else
680
+ raise ArgumentError,
681
+ "Invalid #{name} type: %p" % val,
682
+ caller
683
+ end
684
+ end
685
+
605
686
  def disable_xaxis_grid
606
687
  # do nothing
607
688
  end
@@ -699,7 +780,7 @@ module Charty
699
780
 
700
781
  def render(element_id: nil, format: nil, notebook: false)
701
782
  case format
702
- when :html, "html"
783
+ when :html, "html", nil
703
784
  format = "text/html"
704
785
  when :png, "png"
705
786
  format = "image/png"
@@ -708,7 +789,7 @@ module Charty
708
789
  end
709
790
 
710
791
  case format
711
- when "text/html", nil
792
+ when "text/html"
712
793
  # render html after this case cause
713
794
  when "image/png", "image/jpeg"
714
795
  image_data = render_image(format, element_id: element_id, notebook: false)
@@ -722,32 +803,54 @@ module Charty
722
803
  "Unsupported mime type to render: %p" % format
723
804
  end
724
805
 
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
806
  element_id = SecureRandom.uuid if element_id.nil?
736
807
 
737
- html %= {
738
- id: element_id,
739
- data: JSON.dump(@traces),
740
- layout: JSON.dump(@layout)
741
- }
742
-
808
+ renderer = PlotlyHelpers::HtmlRenderer.new(full_html: !notebook)
809
+ html = renderer.render({data: @traces, layout: @layout}, element_id: element_id)
743
810
  if notebook
744
- IRubyOutput.prepare
745
- ["text/html", html]
811
+ [format, html]
746
812
  else
747
813
  html
748
814
  end
749
815
  end
750
816
 
817
+ def render_mimebundle(include: [], exclude: [])
818
+ types = case
819
+ when IRubyHelper.vscode?,
820
+ IRubyHelper.nteract?
821
+ [:plotly_mimetype]
822
+ else
823
+ [:plotly_mimetype, :notebook]
824
+ end
825
+ bundle = Util.filter_map(types) { |type|
826
+ case type
827
+ when :plotly_mimetype
828
+ render_plotly_mimetype_bundle
829
+ when :notebook
830
+ render_notebook_bundle
831
+ end
832
+ }.to_h
833
+ bundle
834
+ end
835
+
836
+ private def render_plotly_mimetype_bundle
837
+ renderer = PlotlyHelpers::PlotlyRenderer.new
838
+ obj = renderer.render({data: @traces, layout: @layout})
839
+ [ "application/vnd.plotly.v1+json", obj ]
840
+ end
841
+
842
+ private def render_notebook_bundle
843
+ renderer = self.class.notebook_renderer
844
+ renderer.activate
845
+ html = renderer.render({data: @traces, layout: @layout})
846
+ [ "text/html", html ]
847
+ end
848
+
849
+ # for new APIs
850
+ def self.notebook_renderer
851
+ @notebook_renderer ||= PlotlyHelpers::NotebookRenderer.new
852
+ end
853
+
751
854
  private def render_image(format=nil, filename: nil, element_id: nil, notebook: false,
752
855
  title: nil, width: nil, height: nil)
753
856
  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