charty 0.2.1 → 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.
Files changed (76) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +56 -23
  3. data/.github/workflows/nmatrix.yml +67 -0
  4. data/.github/workflows/pycall.yml +86 -0
  5. data/Dockerfile.dev +9 -1
  6. data/Gemfile +18 -0
  7. data/README.md +177 -9
  8. data/Rakefile +4 -5
  9. data/charty.gemspec +10 -5
  10. data/examples/palette.rb +1 -1
  11. data/examples/sample_images/hist_gruff.png +0 -0
  12. data/images/penguins_body_mass_g_flipper_length_mm_scatter_plot.png +0 -0
  13. data/images/penguins_body_mass_g_flipper_length_mm_species_scatter_plot.png +0 -0
  14. data/images/penguins_body_mass_g_flipper_length_mm_species_sex_scatter_plot.png +0 -0
  15. data/images/penguins_species_body_mass_g_bar_plot_h.png +0 -0
  16. data/images/penguins_species_body_mass_g_bar_plot_v.png +0 -0
  17. data/images/penguins_species_body_mass_g_box_plot_h.png +0 -0
  18. data/images/penguins_species_body_mass_g_box_plot_v.png +0 -0
  19. data/images/penguins_species_body_mass_g_sex_bar_plot_v.png +0 -0
  20. data/images/penguins_species_body_mass_g_sex_box_plot_v.png +0 -0
  21. data/lib/charty.rb +9 -2
  22. data/lib/charty/backends.rb +1 -0
  23. data/lib/charty/backends/bokeh.rb +2 -2
  24. data/lib/charty/backends/google_charts.rb +1 -1
  25. data/lib/charty/backends/gruff.rb +14 -3
  26. data/lib/charty/backends/plotly.rb +731 -32
  27. data/lib/charty/backends/plotly_helpers/html_renderer.rb +203 -0
  28. data/lib/charty/backends/plotly_helpers/notebook_renderer.rb +86 -0
  29. data/lib/charty/backends/plotly_helpers/plotly_renderer.rb +121 -0
  30. data/lib/charty/backends/pyplot.rb +515 -67
  31. data/lib/charty/backends/rubyplot.rb +1 -1
  32. data/lib/charty/backends/unicode_plot.rb +79 -0
  33. data/lib/charty/cache_dir.rb +27 -0
  34. data/lib/charty/dash_pattern_generator.rb +57 -0
  35. data/lib/charty/index.rb +213 -0
  36. data/lib/charty/iruby_helper.rb +18 -0
  37. data/lib/charty/linspace.rb +1 -1
  38. data/lib/charty/plot_methods.rb +283 -8
  39. data/lib/charty/plotter.rb +2 -2
  40. data/lib/charty/plotters.rb +11 -0
  41. data/lib/charty/plotters/abstract_plotter.rb +188 -18
  42. data/lib/charty/plotters/bar_plotter.rb +189 -7
  43. data/lib/charty/plotters/box_plotter.rb +64 -11
  44. data/lib/charty/plotters/categorical_plotter.rb +272 -40
  45. data/lib/charty/plotters/count_plotter.rb +7 -0
  46. data/lib/charty/plotters/distribution_plotter.rb +143 -0
  47. data/lib/charty/plotters/estimation_support.rb +84 -0
  48. data/lib/charty/plotters/histogram_plotter.rb +182 -0
  49. data/lib/charty/plotters/line_plotter.rb +300 -0
  50. data/lib/charty/plotters/random_support.rb +25 -0
  51. data/lib/charty/plotters/relational_plotter.rb +635 -0
  52. data/lib/charty/plotters/scatter_plotter.rb +80 -0
  53. data/lib/charty/plotters/vector_plotter.rb +6 -0
  54. data/lib/charty/statistics.rb +96 -2
  55. data/lib/charty/table.rb +160 -15
  56. data/lib/charty/table_adapters.rb +2 -0
  57. data/lib/charty/table_adapters/active_record_adapter.rb +17 -9
  58. data/lib/charty/table_adapters/base_adapter.rb +166 -0
  59. data/lib/charty/table_adapters/daru_adapter.rb +39 -3
  60. data/lib/charty/table_adapters/datasets_adapter.rb +13 -2
  61. data/lib/charty/table_adapters/hash_adapter.rb +141 -16
  62. data/lib/charty/table_adapters/narray_adapter.rb +25 -6
  63. data/lib/charty/table_adapters/nmatrix_adapter.rb +15 -5
  64. data/lib/charty/table_adapters/pandas_adapter.rb +163 -0
  65. data/lib/charty/util.rb +28 -0
  66. data/lib/charty/vector.rb +69 -0
  67. data/lib/charty/vector_adapters.rb +187 -0
  68. data/lib/charty/vector_adapters/array_adapter.rb +101 -0
  69. data/lib/charty/vector_adapters/daru_adapter.rb +163 -0
  70. data/lib/charty/vector_adapters/narray_adapter.rb +182 -0
  71. data/lib/charty/vector_adapters/nmatrix_adapter.rb +37 -0
  72. data/lib/charty/vector_adapters/numpy_adapter.rb +168 -0
  73. data/lib/charty/vector_adapters/pandas_adapter.rb +199 -0
  74. data/lib/charty/version.rb +1 -1
  75. metadata +105 -24
  76. data/lib/charty/palette.rb +0 -235
@@ -237,11 +237,11 @@ module Charty
237
237
  end
238
238
 
239
239
  def render(filename=nil)
240
- @backend.render(self, filename)
240
+ @backend.old_style_render(self, filename)
241
241
  end
242
242
 
243
243
  def save(filename=nil, **kw)
244
- @backend.save(self, filename, **kw)
244
+ @backend.old_style_save(self, filename, **kw)
245
245
  end
246
246
 
247
247
  def apply(backend)
@@ -1,4 +1,15 @@
1
1
  require_relative "plotters/abstract_plotter"
2
+ require_relative "plotters/random_support"
3
+ require_relative "plotters/estimation_support"
2
4
  require_relative "plotters/categorical_plotter"
3
5
  require_relative "plotters/bar_plotter"
4
6
  require_relative "plotters/box_plotter"
7
+ require_relative "plotters/count_plotter"
8
+
9
+ require_relative "plotters/vector_plotter"
10
+ require_relative "plotters/relational_plotter"
11
+ require_relative "plotters/scatter_plotter"
12
+ require_relative "plotters/line_plotter"
13
+
14
+ require_relative "plotters/distribution_plotter"
15
+ require_relative "plotters/histogram_plotter"
@@ -8,44 +8,76 @@ module Charty
8
8
  self.data = data
9
9
  self.palette = palette
10
10
  substitute_options(options)
11
- yield self if block_given?
12
- end
13
11
 
14
- attr_reader :x, :y, :color, :data, :palette
12
+ @var_levels = {}
13
+ @var_ordered = {x: false, y: false}
15
14
 
16
- def x=(x)
17
- @x = check_dimension(x, :x)
15
+ yield self if block_given?
18
16
  end
19
17
 
20
- def y=(y)
21
- @y = check_dimension(y, :y)
22
- end
18
+ attr_reader :data, :x, :y, :color
19
+ attr_reader :color_order, :key_color, :palette
23
20
 
24
- def color=(color)
25
- # @color = check_dimension(color, :color)
26
- unless color.nil?
27
- raise NotImplementedError,
28
- "Specifying color variable is not supported yet"
21
+ def var_levels
22
+ variables.each_key do |var|
23
+ # TODO: Move mappers from RelationalPlotter to here,
24
+ # and remove the use of instance_variable_get
25
+ if instance_variable_defined?(:"@#{var}_mapper")
26
+ mapper = instance_variable_get(:"@#{var}_mapper")
27
+ @var_levels[var] = mapper.levels
28
+ end
29
29
  end
30
+ @var_levels
31
+ end
32
+
33
+ def inspect
34
+ "#<#{self.class}:0x%016x>" % self.object_id
30
35
  end
31
36
 
32
37
  def data=(data)
33
38
  @data = case data
34
39
  when nil, Charty::Table
35
40
  data
41
+ when method(:array?)
42
+ Charty::Vector.new(data)
36
43
  else
37
44
  Charty::Table.new(data)
38
45
  end
39
46
  end
40
47
 
48
+ def x=(x)
49
+ @x = check_dimension(x, :x)
50
+ end
51
+
52
+ def y=(y)
53
+ @y = check_dimension(y, :y)
54
+ end
55
+
56
+ def color=(color)
57
+ @color = check_dimension(color, :color)
58
+ end
59
+
60
+ def color_order=(color_order)
61
+ @color_order = color_order
62
+ end
63
+
64
+ # TODO: move to categorical_plotter
65
+ def key_color=(key_color)
66
+ #@key_color = XXX
67
+ unless key_color.nil?
68
+ raise NotImplementedError,
69
+ "Specifying key_color is not supported yet"
70
+ end
71
+ end
72
+
41
73
  def palette=(palette)
42
74
  @palette = case palette
43
- when nil, Charty::Palette, Symbol, String
75
+ when nil, Palette, Symbol, String
44
76
  palette
45
77
  else
46
78
  raise ArgumentError,
47
79
  "invalid type for palette (given #{palette.class}, " +
48
- "expected Charty::Palette, Symbol, or String)"
80
+ "expected Palette, Symbol, or String)"
49
81
  end
50
82
  end
51
83
 
@@ -57,10 +89,12 @@ module Charty
57
89
 
58
90
  private def check_dimension(value, name)
59
91
  case value
60
- when nil, Symbol, String, method(:array?)
92
+ when nil, Symbol, String
61
93
  value
62
94
  when ->(x) { x.respond_to?(:to_str) }
63
95
  value.to_str
96
+ when method(:array?)
97
+ Charty::Vector.new(value)
64
98
  else
65
99
  raise ArgumentError,
66
100
  "invalid type of dimension for #{name} (given #{value.inspect})",
@@ -68,13 +102,149 @@ module Charty
68
102
  end
69
103
  end
70
104
 
105
+ private def check_number(value, name, allow_nil: false)
106
+ case value
107
+ when Numeric
108
+ value
109
+ else
110
+ if allow_nil && value.nil?
111
+ nil
112
+ else
113
+ expected = if allow_nil
114
+ "number or nil"
115
+ else
116
+ "number"
117
+ end
118
+ raise ArgumentError,
119
+ "invalid value for #{name} (%p for #{expected})" % value,
120
+ caller
121
+ end
122
+ end
123
+ end
124
+
125
+ private def check_boolean(value, name, allow_nil: false)
126
+ case value
127
+ when true, false
128
+ value
129
+ else
130
+ expected = if allow_nil
131
+ "true, false, or nil"
132
+ else
133
+ "true or false"
134
+ end
135
+ raise ArgumentError,
136
+ "invalid value for #{name} (%p for #{expected})" % value,
137
+ caller
138
+ end
139
+ end
140
+
141
+ private def variable_type(vector, boolean_type=:numeric)
142
+ if vector.numeric?
143
+ :numeric
144
+ elsif vector.categorical?
145
+ :categorical
146
+ else
147
+ case vector[0]
148
+ when true, false
149
+ boolean_type
150
+ else
151
+ :categorical
152
+ end
153
+ end
154
+ end
155
+
71
156
  private def array?(value)
72
157
  TableAdapters::HashAdapter.array?(value)
73
158
  end
74
159
 
160
+ private def remove_na!(ary)
161
+ ary.reject! {|x| Util.missing?(x) }
162
+ ary
163
+ end
164
+
165
+ private def each_subset(grouping_vars, reverse: false, processed: false, by_facet: true, allow_empty: false, drop_na: true)
166
+ case grouping_vars
167
+ when nil
168
+ grouping_vars = []
169
+ when String, Symbol
170
+ grouping_vars = [grouping_vars.to_sym]
171
+ end
172
+
173
+ if by_facet
174
+ [:col, :row].each do |facet_var|
175
+ grouping_vars << facet_var if variables.key?(facet_var)
176
+ end
177
+ end
178
+
179
+ grouping_vars = grouping_vars.select {|var| variables.key?(var) }
180
+
181
+ data = processed ? processed_data : plot_data
182
+ data = data.drop_na if drop_na
183
+
184
+ levels = var_levels.dup
185
+
186
+ ([:x, :y] & grouping_vars).each do |axis|
187
+ levels[axis] = plot_data[axis].categorical_order()
188
+ if processed
189
+ # TODO: perform inverse conversion of axis scaling here
190
+ end
191
+ end
192
+
193
+ if not grouping_vars.empty?
194
+ grouped = data.group_by(grouping_vars, sort: false)
195
+ grouped.each_group do |group_key, group_data|
196
+ next if group_data.empty? && !allow_empty
197
+
198
+ yield(grouping_vars.zip(group_key).to_h, group_data)
199
+ end
200
+ else
201
+ yield({}, data.dup)
202
+ end
203
+ end
204
+
205
+ def processed_data
206
+ @processed_data ||= calculate_processed_data
207
+ end
208
+
209
+ private def calculate_processed_data
210
+ # TODO: axis scaling support
211
+ plot_data
212
+ end
213
+
214
+ def save(filename, **kwargs)
215
+ backend = Backends.current
216
+ call_render_plot(backend, notebook: false, **kwargs)
217
+ backend.save(filename, **kwargs)
218
+ end
219
+
220
+ def render(notebook: false, **kwargs)
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)
227
+ backend.begin_figure
228
+ render_plot(backend, notebook: notebook, **kwargs)
229
+ end
230
+
231
+ private def render_plot(*, **)
232
+ raise NotImplementedError,
233
+ "subclass must implement #{__method__}"
234
+ end
235
+
75
236
  def to_iruby
76
- result = render
77
- ["text/html", result] if result
237
+ render(notebook: IRubyHelper.iruby_notebook?)
238
+ end
239
+
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
78
248
  end
79
249
  end
80
250
  end
@@ -1,18 +1,200 @@
1
1
  module Charty
2
2
  module Plotters
3
3
  class BarPlotter < CategoricalPlotter
4
- def render
5
- backend = Backends.current
6
- backend.begin_figure
4
+ self.default_palette = :light
5
+ self.require_numeric = true
6
+
7
+ def initialize(data: nil, variables: {}, **options, &block)
8
+ x, y, color = variables.values_at(:x, :y, :color)
9
+ super(x, y, color, data: data, **options, &block)
10
+ end
11
+
12
+ attr_reader :error_color
13
+
14
+ def error_color=(error_color)
15
+ @error_color = check_error_color(error_color)
16
+ end
17
+
18
+ private def check_error_color(value)
19
+ case value
20
+ when Colors::AbstractColor
21
+ value
22
+ when Array
23
+ Colors::RGB.new(*value)
24
+ when String
25
+ # TODO: Use Colors.parse when it'll be available
26
+ Colors::RGB.parse(value)
27
+ else
28
+ raise ArgumentError,
29
+ "invalid value for error_color (%p for a color, a RGB tripret, or a RGB hex string)" % value
30
+ end
31
+ end
32
+
33
+ attr_reader :error_width
34
+
35
+ def error_width=(error_width)
36
+ @error_width = check_number(error_width, :error_width, allow_nil: true)
37
+ end
38
+
39
+ attr_reader :cap_size
40
+
41
+ def cap_size=(cap_size)
42
+ @cap_size = check_number(cap_size, :cap_size, allow_nil: true)
43
+ end
44
+
45
+ private def render_plot(backend, **)
7
46
  draw_bars(backend)
8
47
  annotate_axes(backend)
9
- backend.show
48
+ backend.invert_yaxis if orient == :h
10
49
  end
11
50
 
12
51
  private def draw_bars(backend)
13
- statistic = @plot_data.map {|xs| Statistics.mean(xs) }
14
- bar_pos = (0 ... statistic.length).to_a
15
- backend.bar(bar_pos, statistic, color: @colors)
52
+ setup_estimations
53
+
54
+ if @plot_colors.nil?
55
+ bar_pos = (0 ... @estimations.length).to_a
56
+ error_colors = bar_pos.map { error_color }
57
+ if @conf_int.empty?
58
+ ci_params = {}
59
+ else
60
+ ci_params = {conf_int: @conf_int, error_colors: error_colors,
61
+ error_width: error_width, cap_size: cap_size}
62
+ end
63
+ backend.bar(bar_pos, nil, @estimations, @colors, orient, **ci_params)
64
+ else
65
+ bar_pos = (0 ... @estimations[0].length).to_a
66
+ error_colors = bar_pos.map { error_color }
67
+ offsets = color_offsets
68
+ width = nested_width
69
+ @color_names.each_with_index do |color_name, i|
70
+ pos = bar_pos.map {|x| x + offsets[i] }
71
+ colors = Array.new(@estimations[i].length) { @colors[i] }
72
+ if @conf_int[i].empty?
73
+ ci_params = {}
74
+ else
75
+ ci_params = {conf_int: @conf_int[i], error_colors: error_colors,
76
+ error_width: error_width, cap_size: cap_size}
77
+ end
78
+ backend.bar(pos, @group_names, @estimations[i], colors, orient,
79
+ label: color_name, width: width, **ci_params)
80
+ end
81
+ end
82
+ end
83
+
84
+ private def setup_estimations
85
+ if @color_names.nil?
86
+ setup_estimations_with_single_color_group
87
+ else
88
+ setup_estimations_with_multiple_color_groups
89
+ end
90
+ end
91
+
92
+ private def setup_estimations_with_single_color_group
93
+ estimations = []
94
+ conf_int = []
95
+
96
+ @plot_data.each do |group_data|
97
+ # Single color group
98
+ if @plot_units.nil?
99
+ stat_data = group_data.drop_na
100
+ unit_data = nil
101
+ else
102
+ # TODO: Support units
103
+ end
104
+
105
+ estimation = if stat_data.size == 0
106
+ Float::NAN
107
+ else
108
+ estimate(estimator, stat_data)
109
+ end
110
+ estimations << estimation
111
+
112
+ unless ci.nil?
113
+ if stat_data.size < 2
114
+ conf_int << [Float::NAN, Float::NAN]
115
+ next
116
+ end
117
+
118
+ if ci == :sd
119
+ sd = stat_data.stdev
120
+ conf_int << [estimation - sd, estimation + sd]
121
+ else
122
+ conf_int << Statistics.bootstrap_ci(stat_data, ci, func: estimator, n_boot: n_boot,
123
+ units: unit_data, random: random)
124
+ end
125
+ end
126
+ end
127
+
128
+ @estimations = estimations
129
+ @conf_int = conf_int
130
+ end
131
+
132
+ private def setup_estimations_with_multiple_color_groups
133
+ estimations = Array.new(@color_names.length) { [] }
134
+ conf_int = Array.new(@color_names.length) { [] }
135
+
136
+ @plot_data.each_with_index do |group_data, i|
137
+ @color_names.each_with_index do |color_name, j|
138
+ if @plot_colors[i].length == 0
139
+ estimations[j] << Float::NAN
140
+ unless ci.nil?
141
+ conf_int[j] << [Float::NAN, Float::NAN]
142
+ end
143
+ next
144
+ end
145
+
146
+ color_mask = @plot_colors[i].eq(color_name)
147
+ if @plot_units.nil?
148
+ begin
149
+ stat_data = group_data[color_mask].drop_na
150
+ rescue
151
+ @plot_data.each_with_index {|pd, k| p k => pd }
152
+ @plot_colors.each_with_index {|pc, k| p k => pc }
153
+ raise
154
+ end
155
+ unit_data = nil
156
+ else
157
+ # TODO: Support units
158
+ end
159
+
160
+ estimation = if stat_data.size == 0
161
+ Float::NAN
162
+ else
163
+ estimate(estimator, stat_data)
164
+ end
165
+ estimations[j] << estimation
166
+
167
+ unless ci.nil?
168
+ if stat_data.size < 2
169
+ conf_int[j] << [Float::NAN, Float::NAN]
170
+ next
171
+ end
172
+
173
+ if ci == :sd
174
+ sd = stat_data.stdev
175
+ conf_int[j] << [estimation - sd, estimation + sd]
176
+ else
177
+ conf_int[j] << Statistics.bootstrap_ci(stat_data, ci, func: estimator, n_boot: n_boot,
178
+ units: unit_data, random: random)
179
+ end
180
+ end
181
+ end
182
+ end
183
+
184
+ @estimations = estimations
185
+ @conf_int = conf_int
186
+ end
187
+
188
+ private def estimate(estimator, data)
189
+ case estimator
190
+ when :count
191
+ data.length
192
+ when :mean
193
+ data.mean
194
+ else
195
+ # TODO: Support other estimations
196
+ raise NotImplementedError, "#{estimator} estimator is not supported yet"
197
+ end
16
198
  end
17
199
  end
18
200
  end