charty 0.1.4.dev → 0.2.4

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 (91) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +71 -0
  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 +128 -9
  8. data/Rakefile +4 -5
  9. data/charty.gemspec +7 -2
  10. data/examples/Gemfile +1 -0
  11. data/examples/active_record.ipynb +34 -34
  12. data/examples/daru.ipynb +71 -29
  13. data/examples/iris_dataset.ipynb +12 -5
  14. data/examples/nmatrix.ipynb +30 -30
  15. data/examples/numo_narray.ipynb +245 -0
  16. data/examples/palette.rb +71 -0
  17. data/examples/sample.png +0 -0
  18. data/examples/sample_bokeh.ipynb +156 -0
  19. data/examples/sample_google_chart.ipynb +229 -68
  20. data/examples/sample_gruff.ipynb +148 -133
  21. data/examples/sample_images/bar_bokeh.html +85 -0
  22. data/examples/sample_images/barh_bokeh.html +85 -0
  23. data/examples/sample_images/barh_gruff.png +0 -0
  24. data/examples/sample_images/box_plot_bokeh.html +85 -0
  25. data/examples/sample_images/{boxplot_pyplot.png → box_plot_pyplot.png} +0 -0
  26. data/examples/sample_images/curve_bokeh.html +85 -0
  27. data/examples/sample_images/curve_with_function_bokeh.html +85 -0
  28. data/examples/sample_images/{errorbar_pyplot.png → error_bar_pyplot.png} +0 -0
  29. data/examples/sample_images/hist_gruff.png +0 -0
  30. data/examples/sample_images/scatter_bokeh.html +85 -0
  31. data/examples/sample_pyplot.ipynb +37 -35
  32. data/images/penguins_body_mass_g_flipper_length_mm_scatter_plot.png +0 -0
  33. data/images/penguins_body_mass_g_flipper_length_mm_species_scatter_plot.png +0 -0
  34. data/images/penguins_body_mass_g_flipper_length_mm_species_sex_scatter_plot.png +0 -0
  35. data/images/penguins_species_body_mass_g_bar_plot_h.png +0 -0
  36. data/images/penguins_species_body_mass_g_bar_plot_v.png +0 -0
  37. data/images/penguins_species_body_mass_g_box_plot_h.png +0 -0
  38. data/images/penguins_species_body_mass_g_box_plot_v.png +0 -0
  39. data/images/penguins_species_body_mass_g_sex_bar_plot_v.png +0 -0
  40. data/images/penguins_species_body_mass_g_sex_box_plot_v.png +0 -0
  41. data/lib/charty.rb +13 -7
  42. data/lib/charty/backend_methods.rb +8 -0
  43. data/lib/charty/backends.rb +80 -0
  44. data/lib/charty/backends/bokeh.rb +80 -0
  45. data/lib/charty/backends/google_charts.rb +267 -0
  46. data/lib/charty/backends/gruff.rb +104 -67
  47. data/lib/charty/backends/plotly.rb +549 -0
  48. data/lib/charty/backends/pyplot.rb +584 -86
  49. data/lib/charty/backends/rubyplot.rb +82 -74
  50. data/lib/charty/backends/unicode_plot.rb +79 -0
  51. data/lib/charty/index.rb +213 -0
  52. data/lib/charty/linspace.rb +1 -1
  53. data/lib/charty/missing_value_support.rb +14 -0
  54. data/lib/charty/plot_methods.rb +184 -0
  55. data/lib/charty/plotter.rb +57 -41
  56. data/lib/charty/plotters.rb +11 -0
  57. data/lib/charty/plotters/abstract_plotter.rb +156 -0
  58. data/lib/charty/plotters/bar_plotter.rb +216 -0
  59. data/lib/charty/plotters/box_plotter.rb +94 -0
  60. data/lib/charty/plotters/categorical_plotter.rb +380 -0
  61. data/lib/charty/plotters/count_plotter.rb +7 -0
  62. data/lib/charty/plotters/estimation_support.rb +84 -0
  63. data/lib/charty/plotters/random_support.rb +25 -0
  64. data/lib/charty/plotters/relational_plotter.rb +518 -0
  65. data/lib/charty/plotters/scatter_plotter.rb +115 -0
  66. data/lib/charty/plotters/vector_plotter.rb +6 -0
  67. data/lib/charty/statistics.rb +114 -0
  68. data/lib/charty/table.rb +82 -3
  69. data/lib/charty/table_adapters.rb +25 -0
  70. data/lib/charty/table_adapters/active_record_adapter.rb +63 -0
  71. data/lib/charty/table_adapters/base_adapter.rb +69 -0
  72. data/lib/charty/table_adapters/daru_adapter.rb +70 -0
  73. data/lib/charty/table_adapters/datasets_adapter.rb +49 -0
  74. data/lib/charty/table_adapters/hash_adapter.rb +224 -0
  75. data/lib/charty/table_adapters/narray_adapter.rb +76 -0
  76. data/lib/charty/table_adapters/nmatrix_adapter.rb +67 -0
  77. data/lib/charty/table_adapters/pandas_adapter.rb +81 -0
  78. data/lib/charty/vector.rb +69 -0
  79. data/lib/charty/vector_adapters.rb +183 -0
  80. data/lib/charty/vector_adapters/array_adapter.rb +109 -0
  81. data/lib/charty/vector_adapters/daru_adapter.rb +171 -0
  82. data/lib/charty/vector_adapters/narray_adapter.rb +187 -0
  83. data/lib/charty/vector_adapters/nmatrix_adapter.rb +37 -0
  84. data/lib/charty/vector_adapters/numpy_adapter.rb +168 -0
  85. data/lib/charty/vector_adapters/pandas_adapter.rb +200 -0
  86. data/lib/charty/version.rb +1 -1
  87. metadata +127 -13
  88. data/.travis.yml +0 -11
  89. data/examples/numo-narray.ipynb +0 -234
  90. data/lib/charty/backends/google_chart.rb +0 -167
  91. data/lib/charty/plotter_adapter.rb +0 -17
@@ -0,0 +1,11 @@
1
+ require_relative "plotters/abstract_plotter"
2
+ require_relative "plotters/random_support"
3
+ require_relative "plotters/estimation_support"
4
+ require_relative "plotters/categorical_plotter"
5
+ require_relative "plotters/bar_plotter"
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"
@@ -0,0 +1,156 @@
1
+ module Charty
2
+ module Plotters
3
+ class AbstractPlotter
4
+ def initialize(x, y, color, **options)
5
+ self.x = x
6
+ self.y = y
7
+ self.color = color
8
+ self.data = data
9
+ self.palette = palette
10
+ substitute_options(options)
11
+ yield self if block_given?
12
+ end
13
+
14
+ attr_reader :data, :x, :y, :color
15
+ attr_reader :color_order, :key_color, :palette
16
+
17
+ def data=(data)
18
+ @data = case data
19
+ when nil, Charty::Table
20
+ data
21
+ else
22
+ Charty::Table.new(data)
23
+ end
24
+ end
25
+
26
+ def x=(x)
27
+ @x = check_dimension(x, :x)
28
+ end
29
+
30
+ def y=(y)
31
+ @y = check_dimension(y, :y)
32
+ end
33
+
34
+ def color=(color)
35
+ @color = check_dimension(color, :color)
36
+ end
37
+
38
+ def color_order=(color_order)
39
+ #@color_order = XXX
40
+ unless color_order.nil?
41
+ raise NotImplementedError,
42
+ "Specifying color_order is not supported yet"
43
+ end
44
+ end
45
+
46
+ # TODO: move to categorical_plotter
47
+ def key_color=(key_color)
48
+ #@key_color = XXX
49
+ unless key_color.nil?
50
+ raise NotImplementedError,
51
+ "Specifying key_color is not supported yet"
52
+ end
53
+ end
54
+
55
+ def palette=(palette)
56
+ @palette = case palette
57
+ when nil, Palette, Symbol, String
58
+ palette
59
+ else
60
+ raise ArgumentError,
61
+ "invalid type for palette (given #{palette.class}, " +
62
+ "expected Palette, Symbol, or String)"
63
+ end
64
+ end
65
+
66
+ private def substitute_options(options)
67
+ options.each do |key, val|
68
+ send("#{key}=", val)
69
+ end
70
+ end
71
+
72
+ private def check_dimension(value, name)
73
+ case value
74
+ when nil, Symbol, String
75
+ value
76
+ when ->(x) { x.respond_to?(:to_str) }
77
+ value.to_str
78
+ when method(:array?)
79
+ Charty::Vector.new(value)
80
+ else
81
+ raise ArgumentError,
82
+ "invalid type of dimension for #{name} (given #{value.inspect})",
83
+ caller
84
+ end
85
+ end
86
+
87
+ private def check_number(value, name, allow_nil: false)
88
+ case value
89
+ when Numeric
90
+ value
91
+ else
92
+ if allow_nil && value.nil?
93
+ nil
94
+ else
95
+ expected = if allow_nil
96
+ "number or nil"
97
+ else
98
+ "number"
99
+ end
100
+ raise ArgumentError,
101
+ "invalid value for #{name} (%p for #{expected})" % value,
102
+ caller
103
+ end
104
+ end
105
+ end
106
+
107
+ private def check_boolean(value, name, allow_nil: false)
108
+ case value
109
+ when true, false
110
+ value
111
+ else
112
+ expected = if allow_nil
113
+ "true, false, or nil"
114
+ else
115
+ "true or false"
116
+ end
117
+ raise ArgumentError,
118
+ "invalid value for #{name} (%p for #{expected})" % value,
119
+ caller
120
+ end
121
+ end
122
+
123
+ private def variable_type(vector, boolean_type=:numeric)
124
+ if vector.numeric?
125
+ :numeric
126
+ elsif vector.categorical?
127
+ :categorical
128
+ else
129
+ case vector[0]
130
+ when true, false
131
+ boolean_type
132
+ else
133
+ :categorical
134
+ end
135
+ end
136
+ end
137
+
138
+ private def array?(value)
139
+ TableAdapters::HashAdapter.array?(value)
140
+ end
141
+
142
+ private def remove_na!(ary)
143
+ ary.reject! do |x|
144
+ next true if x.nil?
145
+ x.respond_to?(:nan?) && x.nan?
146
+ end
147
+ ary
148
+ end
149
+
150
+ def to_iruby
151
+ result = render
152
+ ["text/html", result] if result
153
+ end
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,216 @@
1
+ module Charty
2
+ module Plotters
3
+ class BarPlotter < CategoricalPlotter
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
+ def render
46
+ backend = Backends.current
47
+ backend.begin_figure
48
+ draw_bars(backend)
49
+ annotate_axes(backend)
50
+ backend.invert_yaxis if orient == :h
51
+ backend.show
52
+ end
53
+
54
+ # TODO:
55
+ # - Should infer mime type from file's extname
56
+ # - Should check backend's supported mime type before begin_figure
57
+ def save(filename, **opts)
58
+ backend = Backends.current
59
+ backend.begin_figure
60
+ draw_bars(backend)
61
+ annotate_axes(backend)
62
+ backend.invert_yaxis if orient == :h
63
+ backend.save(filename, **opts)
64
+ end
65
+
66
+ private def draw_bars(backend)
67
+ setup_estimations
68
+
69
+ if @plot_colors.nil?
70
+ bar_pos = (0 ... @estimations.length).to_a
71
+ error_colors = bar_pos.map { error_color }
72
+ if @conf_int.empty?
73
+ ci_params = {}
74
+ else
75
+ ci_params = {conf_int: @conf_int, error_colors: error_colors,
76
+ error_width: error_width, cap_size: cap_size}
77
+ end
78
+ backend.bar(bar_pos, nil, @estimations, @colors, orient, **ci_params)
79
+ else
80
+ bar_pos = (0 ... @estimations[0].length).to_a
81
+ error_colors = bar_pos.map { error_color }
82
+ offsets = color_offsets
83
+ width = nested_width
84
+ @color_names.each_with_index do |color_name, i|
85
+ pos = bar_pos.map {|x| x + offsets[i] }
86
+ colors = Array.new(@estimations[i].length) { @colors[i] }
87
+ if @conf_int[i].empty?
88
+ ci_params = {}
89
+ else
90
+ ci_params = {conf_int: @conf_int[i], error_colors: error_colors,
91
+ error_width: error_width, cap_size: cap_size}
92
+ end
93
+ backend.bar(pos, @group_names, @estimations[i], colors, orient,
94
+ label: color_name, width: width, **ci_params)
95
+ end
96
+ end
97
+ end
98
+
99
+ private def setup_estimations
100
+ if @color_names.nil?
101
+ setup_estimations_with_single_color_group
102
+ else
103
+ setup_estimations_with_multiple_color_groups
104
+ end
105
+ end
106
+
107
+ private def setup_estimations_with_single_color_group
108
+ estimations = []
109
+ conf_int = []
110
+
111
+ @plot_data.each do |group_data|
112
+ # Single color group
113
+ if @plot_units.nil?
114
+ stat_data = group_data.drop_na
115
+ unit_data = nil
116
+ else
117
+ # TODO: Support units
118
+ end
119
+
120
+ estimation = if stat_data.size == 0
121
+ Float::NAN
122
+ else
123
+ estimate(estimator, stat_data)
124
+ end
125
+ estimations << estimation
126
+
127
+ unless ci.nil?
128
+ if stat_data.size < 2
129
+ conf_int << [Float::NAN, Float::NAN]
130
+ next
131
+ end
132
+
133
+ if ci == :sd
134
+ sd = stat_data.stdev
135
+ conf_int << [estimation - sd, estimation + sd]
136
+ else
137
+ conf_int << Statistics.bootstrap_ci(stat_data, ci, func: estimator, n_boot: n_boot,
138
+ units: unit_data, random: random)
139
+ end
140
+ end
141
+ end
142
+
143
+ @estimations = estimations
144
+ @conf_int = conf_int
145
+ end
146
+
147
+ private def setup_estimations_with_multiple_color_groups
148
+ estimations = Array.new(@color_names.length) { [] }
149
+ conf_int = Array.new(@color_names.length) { [] }
150
+
151
+ @plot_data.each_with_index do |group_data, i|
152
+ @color_names.each_with_index do |color_name, j|
153
+ if @plot_colors[i].length == 0
154
+ estimations[j] << Float::NAN
155
+ unless ci.nil?
156
+ conf_int[j] << [Float::NAN, Float::NAN]
157
+ end
158
+ next
159
+ end
160
+
161
+ color_mask = @plot_colors[i].eq(color_name)
162
+ if @plot_units.nil?
163
+ begin
164
+ stat_data = group_data[color_mask].drop_na
165
+ rescue
166
+ @plot_data.each_with_index {|pd, k| p k => pd }
167
+ @plot_colors.each_with_index {|pc, k| p k => pc }
168
+ raise
169
+ end
170
+ unit_data = nil
171
+ else
172
+ # TODO: Support units
173
+ end
174
+
175
+ estimation = if stat_data.size == 0
176
+ Float::NAN
177
+ else
178
+ estimate(estimator, stat_data)
179
+ end
180
+ estimations[j] << estimation
181
+
182
+ unless ci.nil?
183
+ if stat_data.size < 2
184
+ conf_int[j] << [Float::NAN, Float::NAN]
185
+ next
186
+ end
187
+
188
+ if ci == :sd
189
+ sd = stat_data.stdev
190
+ conf_int[j] << [estimation - sd, estimation + sd]
191
+ else
192
+ conf_int[j] << Statistics.bootstrap_ci(stat_data, ci, func: estimator, n_boot: n_boot,
193
+ units: unit_data, random: random)
194
+ end
195
+ end
196
+ end
197
+ end
198
+
199
+ @estimations = estimations
200
+ @conf_int = conf_int
201
+ end
202
+
203
+ private def estimate(estimator, data)
204
+ case estimator
205
+ when :count
206
+ data.length
207
+ when :mean
208
+ data.mean
209
+ else
210
+ # TODO: Support other estimations
211
+ raise NotImplementedError, "#{estimator} estimator is not supported yet"
212
+ end
213
+ end
214
+ end
215
+ end
216
+ end
@@ -0,0 +1,94 @@
1
+ module Charty
2
+ module Plotters
3
+ class BoxPlotter < CategoricalPlotter
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 :flier_size
13
+
14
+ def flier_size=(val)
15
+ @flier_size = check_number(val, :flier_size, allow_nil: true)
16
+ end
17
+
18
+ attr_reader :line_width
19
+
20
+ def line_width=(val)
21
+ @line_width = check_number(val, :line_width, allow_nil: true)
22
+ end
23
+
24
+ attr_reader :whisker
25
+
26
+ def whisker=(val)
27
+ @whisker = check_number(val, :whisker, allow_nil: true)
28
+ end
29
+
30
+ def render
31
+ backend = Backends.current
32
+ backend.begin_figure
33
+ draw_box_plot(backend)
34
+ annotate_axes(backend)
35
+ backend.invert_yaxis if orient == :h
36
+ backend.show
37
+ end
38
+
39
+ # TODO:
40
+ # - Should infer mime type from file's extname
41
+ # - Should check backend's supported mime type before begin_figure
42
+ def save(filename, **opts)
43
+ backend = Backends.current
44
+ backend.begin_figure
45
+ draw_box_plot(backend)
46
+ annotate_axes(backend)
47
+ backend.invert_yaxis if orient == :h
48
+ backend.save(filename, **opts)
49
+ end
50
+
51
+ private def draw_box_plot(backend)
52
+ if @plot_colors.nil?
53
+ plot_data = @plot_data.map do |group_data|
54
+ unless group_data.empty?
55
+ group_data = group_data.drop_na
56
+ group_data unless group_data.empty?
57
+ end
58
+ end
59
+
60
+ backend.box_plot(plot_data,
61
+ @group_names,
62
+ orient: orient,
63
+ colors: @colors,
64
+ gray: @gray,
65
+ dodge: dodge,
66
+ width: @width,
67
+ flier_size: flier_size,
68
+ whisker: whisker)
69
+ else
70
+ grouped_box_data = @color_names.map.with_index do |color_name, i|
71
+ @plot_data.map.with_index do |group_data, j|
72
+ unless group_data.empty?
73
+ color_mask = @plot_colors[j].eq(color_name)
74
+ group_data = group_data[color_mask].drop_na
75
+ group_data unless group_data.empty?
76
+ end
77
+ end
78
+ end
79
+
80
+ backend.grouped_box_plot(grouped_box_data,
81
+ @group_names,
82
+ @color_names,
83
+ orient: orient,
84
+ colors: @colors,
85
+ gray: @gray,
86
+ dodge: dodge,
87
+ width: @width,
88
+ flier_size: flier_size,
89
+ whisker: whisker)
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end