charty 0.1.4.dev → 0.2.4

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