charty 0.2.6 → 0.2.10

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