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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +71 -0
- data/.github/workflows/nmatrix.yml +67 -0
- data/.github/workflows/pycall.yml +86 -0
- data/Dockerfile.dev +9 -1
- data/Gemfile +18 -0
- data/README.md +128 -9
- data/Rakefile +4 -5
- data/charty.gemspec +7 -2
- data/examples/Gemfile +1 -0
- data/examples/active_record.ipynb +34 -34
- data/examples/daru.ipynb +71 -29
- data/examples/iris_dataset.ipynb +12 -5
- data/examples/nmatrix.ipynb +30 -30
- data/examples/numo_narray.ipynb +245 -0
- data/examples/palette.rb +71 -0
- data/examples/sample.png +0 -0
- data/examples/sample_bokeh.ipynb +156 -0
- data/examples/sample_google_chart.ipynb +229 -68
- data/examples/sample_gruff.ipynb +148 -133
- data/examples/sample_images/bar_bokeh.html +85 -0
- data/examples/sample_images/barh_bokeh.html +85 -0
- data/examples/sample_images/barh_gruff.png +0 -0
- data/examples/sample_images/box_plot_bokeh.html +85 -0
- data/examples/sample_images/{boxplot_pyplot.png → box_plot_pyplot.png} +0 -0
- data/examples/sample_images/curve_bokeh.html +85 -0
- data/examples/sample_images/curve_with_function_bokeh.html +85 -0
- data/examples/sample_images/{errorbar_pyplot.png → error_bar_pyplot.png} +0 -0
- data/examples/sample_images/hist_gruff.png +0 -0
- data/examples/sample_images/scatter_bokeh.html +85 -0
- data/examples/sample_pyplot.ipynb +37 -35
- data/images/penguins_body_mass_g_flipper_length_mm_scatter_plot.png +0 -0
- data/images/penguins_body_mass_g_flipper_length_mm_species_scatter_plot.png +0 -0
- data/images/penguins_body_mass_g_flipper_length_mm_species_sex_scatter_plot.png +0 -0
- data/images/penguins_species_body_mass_g_bar_plot_h.png +0 -0
- data/images/penguins_species_body_mass_g_bar_plot_v.png +0 -0
- data/images/penguins_species_body_mass_g_box_plot_h.png +0 -0
- data/images/penguins_species_body_mass_g_box_plot_v.png +0 -0
- data/images/penguins_species_body_mass_g_sex_bar_plot_v.png +0 -0
- data/images/penguins_species_body_mass_g_sex_box_plot_v.png +0 -0
- data/lib/charty.rb +13 -7
- data/lib/charty/backend_methods.rb +8 -0
- data/lib/charty/backends.rb +80 -0
- data/lib/charty/backends/bokeh.rb +80 -0
- data/lib/charty/backends/google_charts.rb +267 -0
- data/lib/charty/backends/gruff.rb +104 -67
- data/lib/charty/backends/plotly.rb +549 -0
- data/lib/charty/backends/pyplot.rb +584 -86
- data/lib/charty/backends/rubyplot.rb +82 -74
- data/lib/charty/backends/unicode_plot.rb +79 -0
- data/lib/charty/index.rb +213 -0
- data/lib/charty/linspace.rb +1 -1
- data/lib/charty/missing_value_support.rb +14 -0
- data/lib/charty/plot_methods.rb +184 -0
- data/lib/charty/plotter.rb +57 -41
- data/lib/charty/plotters.rb +11 -0
- data/lib/charty/plotters/abstract_plotter.rb +156 -0
- data/lib/charty/plotters/bar_plotter.rb +216 -0
- data/lib/charty/plotters/box_plotter.rb +94 -0
- data/lib/charty/plotters/categorical_plotter.rb +380 -0
- data/lib/charty/plotters/count_plotter.rb +7 -0
- data/lib/charty/plotters/estimation_support.rb +84 -0
- data/lib/charty/plotters/random_support.rb +25 -0
- data/lib/charty/plotters/relational_plotter.rb +518 -0
- data/lib/charty/plotters/scatter_plotter.rb +115 -0
- data/lib/charty/plotters/vector_plotter.rb +6 -0
- data/lib/charty/statistics.rb +114 -0
- data/lib/charty/table.rb +82 -3
- data/lib/charty/table_adapters.rb +25 -0
- data/lib/charty/table_adapters/active_record_adapter.rb +63 -0
- data/lib/charty/table_adapters/base_adapter.rb +69 -0
- data/lib/charty/table_adapters/daru_adapter.rb +70 -0
- data/lib/charty/table_adapters/datasets_adapter.rb +49 -0
- data/lib/charty/table_adapters/hash_adapter.rb +224 -0
- data/lib/charty/table_adapters/narray_adapter.rb +76 -0
- data/lib/charty/table_adapters/nmatrix_adapter.rb +67 -0
- data/lib/charty/table_adapters/pandas_adapter.rb +81 -0
- data/lib/charty/vector.rb +69 -0
- data/lib/charty/vector_adapters.rb +183 -0
- data/lib/charty/vector_adapters/array_adapter.rb +109 -0
- data/lib/charty/vector_adapters/daru_adapter.rb +171 -0
- data/lib/charty/vector_adapters/narray_adapter.rb +187 -0
- data/lib/charty/vector_adapters/nmatrix_adapter.rb +37 -0
- data/lib/charty/vector_adapters/numpy_adapter.rb +168 -0
- data/lib/charty/vector_adapters/pandas_adapter.rb +200 -0
- data/lib/charty/version.rb +1 -1
- metadata +127 -13
- data/.travis.yml +0 -11
- data/examples/numo-narray.ipynb +0 -234
- data/lib/charty/backends/google_chart.rb +0 -167
- data/lib/charty/plotter_adapter.rb +0 -17
@@ -0,0 +1,380 @@
|
|
1
|
+
module Charty
|
2
|
+
module Plotters
|
3
|
+
class CategoricalPlotter < AbstractPlotter
|
4
|
+
class << self
|
5
|
+
attr_reader :default_palette
|
6
|
+
|
7
|
+
def default_palette=(val)
|
8
|
+
case val
|
9
|
+
when :light, :dark
|
10
|
+
@default_palette = val
|
11
|
+
when "light", "dark"
|
12
|
+
@default_palette = val.to_sym
|
13
|
+
else
|
14
|
+
raise ArgumentError, "default_palette must be :light or :dark"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
attr_reader :require_numeric
|
19
|
+
|
20
|
+
def require_numeric=(val)
|
21
|
+
case val
|
22
|
+
when true, false
|
23
|
+
@require_numeric = val
|
24
|
+
else
|
25
|
+
raise ArgumentError, "require_numeric must be ture or false"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def initialize(x, y, color, order: nil, orient: nil, width: 0.8r, dodge: false, **options, &block)
|
31
|
+
super
|
32
|
+
|
33
|
+
setup_variables
|
34
|
+
setup_colors
|
35
|
+
end
|
36
|
+
|
37
|
+
attr_reader :order
|
38
|
+
|
39
|
+
def order=(order)
|
40
|
+
@order = order && Array(order).map(&method(:normalize_name))
|
41
|
+
end
|
42
|
+
|
43
|
+
attr_reader :orient
|
44
|
+
|
45
|
+
def orient=(orient)
|
46
|
+
@orient = check_orient(orient)
|
47
|
+
end
|
48
|
+
|
49
|
+
private def check_orient(value)
|
50
|
+
case value
|
51
|
+
when nil, :v, :h
|
52
|
+
value
|
53
|
+
when "v", "h"
|
54
|
+
value.to_sym
|
55
|
+
else
|
56
|
+
raise ArgumentError,
|
57
|
+
"invalid value for orient (#{value.inspect} for nil, :v, or :h)"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
attr_reader :width
|
62
|
+
|
63
|
+
def width=(val)
|
64
|
+
@width = check_number(val, :width)
|
65
|
+
end
|
66
|
+
|
67
|
+
attr_reader :dodge
|
68
|
+
|
69
|
+
def dodge=(dodge)
|
70
|
+
@dodge = check_boolean(dodge, :dodge)
|
71
|
+
end
|
72
|
+
|
73
|
+
attr_reader :saturation
|
74
|
+
|
75
|
+
def saturation=(saturation)
|
76
|
+
@saturation = check_saturation(saturation)
|
77
|
+
end
|
78
|
+
|
79
|
+
private def check_saturation(value)
|
80
|
+
case value
|
81
|
+
when 0..1
|
82
|
+
value
|
83
|
+
when Numeric
|
84
|
+
raise ArgumentError,
|
85
|
+
"saturation is out of range (%p for 0..1)" % value
|
86
|
+
else
|
87
|
+
raise ArgumentError,
|
88
|
+
"invalid value for saturation (%p for a value in 0..1)" % value
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
include EstimationSupport
|
93
|
+
|
94
|
+
private def normalize_name(value)
|
95
|
+
case value
|
96
|
+
when String, Symbol
|
97
|
+
value
|
98
|
+
else
|
99
|
+
value.to_str
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
attr_reader :group_names, :plot_data, :group_label
|
104
|
+
|
105
|
+
attr_accessor :value_label
|
106
|
+
|
107
|
+
private def setup_variables
|
108
|
+
if x.nil? && y.nil?
|
109
|
+
@input_format = :wide
|
110
|
+
setup_variables_with_wide_form_dataset
|
111
|
+
else
|
112
|
+
@input_format = :long
|
113
|
+
setup_variables_with_long_form_dataset
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
private def setup_variables_with_wide_form_dataset
|
118
|
+
if @color
|
119
|
+
raise ArgumentError,
|
120
|
+
"Cannot use `color` without `x` or `y`"
|
121
|
+
end
|
122
|
+
|
123
|
+
# No color grouping with wide inputs
|
124
|
+
@plot_colors = nil
|
125
|
+
@color_title = nil
|
126
|
+
@color_names = nil
|
127
|
+
|
128
|
+
# No statistical units with wide inputs
|
129
|
+
@plot_units = nil
|
130
|
+
|
131
|
+
@value_label = nil
|
132
|
+
@group_label = nil
|
133
|
+
|
134
|
+
order = @order # TODO: supply order via parameter
|
135
|
+
unless order
|
136
|
+
order = @data.column_names.select do |cn|
|
137
|
+
@data[cn].all? {|x| Float(x, exception: false) }
|
138
|
+
end
|
139
|
+
end
|
140
|
+
order ||= @data.column_names
|
141
|
+
@plot_data = order.map {|cn| @data[cn] }
|
142
|
+
@group_names = order
|
143
|
+
end
|
144
|
+
|
145
|
+
private def setup_variables_with_long_form_dataset
|
146
|
+
x = self.x
|
147
|
+
y = self.y
|
148
|
+
color = self.color
|
149
|
+
if @data
|
150
|
+
x &&= @data[x] || x
|
151
|
+
y &&= @data[y] || y
|
152
|
+
color &&= @data[color] || color
|
153
|
+
end
|
154
|
+
|
155
|
+
# Validate inputs
|
156
|
+
[x, y, color].each do |input|
|
157
|
+
next if input.nil? || array?(input)
|
158
|
+
raise RuntimeError,
|
159
|
+
"Could not interpret input `#{input.inspect}`"
|
160
|
+
end
|
161
|
+
|
162
|
+
x = Charty::Vector.try_convert(x)
|
163
|
+
y = Charty::Vector.try_convert(y)
|
164
|
+
color = Charty::Vector.try_convert(color)
|
165
|
+
|
166
|
+
self.orient = infer_orient(x, y, orient, self.class.require_numeric)
|
167
|
+
|
168
|
+
if x.nil? || y.nil?
|
169
|
+
setup_single_data
|
170
|
+
else
|
171
|
+
if orient == :v
|
172
|
+
groups, vals = x, y
|
173
|
+
else
|
174
|
+
groups, vals = y, x
|
175
|
+
end
|
176
|
+
|
177
|
+
if groups.respond_to?(:name)
|
178
|
+
@group_label = groups.name
|
179
|
+
end
|
180
|
+
|
181
|
+
@group_names = groups.categorical_order(order)
|
182
|
+
@plot_data, @value_label = group_long_form(vals, groups, @group_names)
|
183
|
+
|
184
|
+
# Handle color variable
|
185
|
+
if color.nil?
|
186
|
+
@plot_colors = nil
|
187
|
+
@color_title = nil
|
188
|
+
@color_names = nil
|
189
|
+
else
|
190
|
+
# Get the order of color levels
|
191
|
+
@color_names = color.categorical_order(color_order)
|
192
|
+
|
193
|
+
# Group the color data
|
194
|
+
@plot_colors, @color_title = group_long_form(color, groups, @group_names)
|
195
|
+
end
|
196
|
+
|
197
|
+
# TODO: Handle units
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
private def setup_single_data
|
202
|
+
raise NotImplementedError,
|
203
|
+
"Single data plot is not supported yet"
|
204
|
+
end
|
205
|
+
|
206
|
+
private def infer_orient(x, y, orient, require_numeric)
|
207
|
+
x_type = x.nil? ? nil : variable_type(x)
|
208
|
+
y_type = y.nil? ? nil : variable_type(y)
|
209
|
+
|
210
|
+
nonnumeric_error = "%{orient} orientation requires numeric `%{dim}` variable"
|
211
|
+
single_variable_warning = "%{orient} orientation ignored with only `%{dim}` specified"
|
212
|
+
|
213
|
+
case
|
214
|
+
when x.nil?
|
215
|
+
case orient
|
216
|
+
when :h
|
217
|
+
warn single_variable_warning % {orient: "Horizontal", dim: "y"}
|
218
|
+
end
|
219
|
+
if require_numeric && y_type != :numeric
|
220
|
+
raise ArgumentError, nonnumeric_error % {orient: "Vertical", dim: "y"}
|
221
|
+
end
|
222
|
+
return :v
|
223
|
+
when y.nil?
|
224
|
+
case orient
|
225
|
+
when :v
|
226
|
+
warn single_variable_warning % {orient: "Vertical", dim: "x"}
|
227
|
+
end
|
228
|
+
if require_numeric && x_type != :numeric
|
229
|
+
raise ArgumentError, nonnumeric_error % {orient: "Horizontal", dim: "x"}
|
230
|
+
end
|
231
|
+
return :h
|
232
|
+
end
|
233
|
+
case orient
|
234
|
+
when :v
|
235
|
+
if require_numeric && y_type != :numeric
|
236
|
+
raise ArgumentError, nonnumeric_error % {orient: "Vertical", dim: "y"}
|
237
|
+
end
|
238
|
+
return :v
|
239
|
+
when :h
|
240
|
+
if require_numeric && x_type != :numeric
|
241
|
+
raise ArgumentError, nonnumeric_error % {orient: "Horizontal", dim: "x"}
|
242
|
+
end
|
243
|
+
return :h
|
244
|
+
when nil
|
245
|
+
case
|
246
|
+
when x_type != :categorical && y_type == :categorical
|
247
|
+
return :h
|
248
|
+
when x_type != :numeric && y_type == :numeric
|
249
|
+
return :v
|
250
|
+
when x_type == :numeric && y_type != :numeric
|
251
|
+
return :h
|
252
|
+
when require_numeric && x_type != :numeric && y_type != :numeric
|
253
|
+
raise ArgumentError, "Neither the `x` nor `y` variable appears to be numeric."
|
254
|
+
else
|
255
|
+
:v
|
256
|
+
end
|
257
|
+
else
|
258
|
+
# must be unreachable
|
259
|
+
raise RuntimeError, "BUG in Charty. Please report the issue."
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
private def group_long_form(vals, groups, group_order)
|
264
|
+
grouped_vals = vals.group_by(groups)
|
265
|
+
|
266
|
+
plot_data = group_order.map {|g| grouped_vals[g] || [] }
|
267
|
+
|
268
|
+
if vals.respond_to?(:name)
|
269
|
+
value_label = vals.name
|
270
|
+
end
|
271
|
+
|
272
|
+
return plot_data, value_label
|
273
|
+
end
|
274
|
+
|
275
|
+
private def setup_colors
|
276
|
+
if @color_names.nil?
|
277
|
+
n_colors = @plot_data.length
|
278
|
+
else
|
279
|
+
n_colors = @color_names.length
|
280
|
+
end
|
281
|
+
|
282
|
+
if key_color.nil? && self.palette.nil?
|
283
|
+
# Check the current palette has enough colors
|
284
|
+
current_palette = Palette.default
|
285
|
+
if n_colors <= current_palette.n_colors
|
286
|
+
colors = Palette.new(current_palette.colors, n_colors).colors
|
287
|
+
else
|
288
|
+
# Use huls palette as default when the default palette is not usable
|
289
|
+
colors = Palette.husl_colors(n_colors, l: 0.7r)
|
290
|
+
end
|
291
|
+
elsif self.palette.nil?
|
292
|
+
if @color_names.nil?
|
293
|
+
colors = Array.new(n_colors) { key_color }
|
294
|
+
else
|
295
|
+
raise NotImplementedError,
|
296
|
+
"Default palette with key_color is not supported"
|
297
|
+
# TODO: Support light_palette and dark_palette in red-palette
|
298
|
+
# if default_palette is light
|
299
|
+
# colors = Palette.light_palette(key_color, n_colors)
|
300
|
+
# elsif default_palette is dark
|
301
|
+
# colors = Palette.dark_palette(key_color, n_colors)
|
302
|
+
# else
|
303
|
+
# raise "No default palette specified"
|
304
|
+
# end
|
305
|
+
end
|
306
|
+
else
|
307
|
+
case self.palette
|
308
|
+
when Hash
|
309
|
+
if @color_names.nil?
|
310
|
+
levels = @group_names
|
311
|
+
else
|
312
|
+
levels = @color_names
|
313
|
+
end
|
314
|
+
colors = levels.map {|gn| self.palette[gn] }
|
315
|
+
end
|
316
|
+
colors = Palette.new(colors, n_colors).colors
|
317
|
+
end
|
318
|
+
|
319
|
+
if saturation < 1
|
320
|
+
colors = Palette.new(colors, n_colors, desaturate_factor: saturation).colors
|
321
|
+
end
|
322
|
+
|
323
|
+
@colors = colors.map {|c| c.to_rgb }
|
324
|
+
lightness_values = @colors.map {|c| c.to_hsl.l }
|
325
|
+
lum = lightness_values.min * 0.6r
|
326
|
+
@gray = Colors::RGB.new(lum, lum, lum) # TODO: Use Charty::Gray
|
327
|
+
end
|
328
|
+
|
329
|
+
private def color_offsets
|
330
|
+
n_names = @color_names.length
|
331
|
+
if self.dodge
|
332
|
+
each_width = @width / n_names
|
333
|
+
offsets = Charty::Linspace.new(0 .. (@width - each_width), n_names).to_a
|
334
|
+
offsets_mean = Statistics.mean(offsets)
|
335
|
+
offsets.map {|x| x - offsets_mean }
|
336
|
+
else
|
337
|
+
Array.new(n_names) { 0 }
|
338
|
+
end
|
339
|
+
end
|
340
|
+
|
341
|
+
private def nested_width
|
342
|
+
if self.dodge
|
343
|
+
@width / @color_names.length * 0.98r
|
344
|
+
else
|
345
|
+
@width
|
346
|
+
end
|
347
|
+
end
|
348
|
+
|
349
|
+
private def annotate_axes(backend)
|
350
|
+
if orient == :v
|
351
|
+
xlabel, ylabel = @group_label, @value_label
|
352
|
+
else
|
353
|
+
xlabel, ylabel = @value_label, @group_label
|
354
|
+
end
|
355
|
+
backend.set_xlabel(xlabel) unless xlabel.nil?
|
356
|
+
backend.set_ylabel(ylabel) unless ylabel.nil?
|
357
|
+
|
358
|
+
if orient == :v
|
359
|
+
backend.set_xticks((0 ... @plot_data.length).to_a)
|
360
|
+
backend.set_xtick_labels(@group_names)
|
361
|
+
else
|
362
|
+
backend.set_yticks((0 ... @plot_data.length).to_a)
|
363
|
+
backend.set_ytick_labels(@group_names)
|
364
|
+
end
|
365
|
+
|
366
|
+
if orient == :v
|
367
|
+
backend.disable_xaxis_grid
|
368
|
+
backend.set_xlim(-0.5, @plot_data.length - 0.5)
|
369
|
+
else
|
370
|
+
backend.disable_yaxis_grid
|
371
|
+
backend.set_ylim(-0.5, @plot_data.length - 0.5)
|
372
|
+
end
|
373
|
+
|
374
|
+
unless @color_names.nil?
|
375
|
+
backend.legend(loc: :best, title: @color_title)
|
376
|
+
end
|
377
|
+
end
|
378
|
+
end
|
379
|
+
end
|
380
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
module Charty
|
2
|
+
module Plotters
|
3
|
+
module EstimationSupport
|
4
|
+
attr_reader :estimator
|
5
|
+
|
6
|
+
def estimator=(estimator)
|
7
|
+
@estimator = check_estimator(estimator)
|
8
|
+
end
|
9
|
+
|
10
|
+
module_function def check_estimator(value)
|
11
|
+
case value
|
12
|
+
when :count, "count"
|
13
|
+
:count
|
14
|
+
when :mean, "mean"
|
15
|
+
:mean
|
16
|
+
when :median
|
17
|
+
raise NotImplementedError,
|
18
|
+
"median estimator has not been supported yet"
|
19
|
+
when Proc
|
20
|
+
raise NotImplementedError,
|
21
|
+
"a callable estimator has not been supported yet"
|
22
|
+
else
|
23
|
+
raise ArgumentError,
|
24
|
+
"invalid value for estimator (%p for :mean)" % value
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
attr_reader :ci
|
29
|
+
|
30
|
+
def ci=(ci)
|
31
|
+
@ci = check_ci(ci)
|
32
|
+
end
|
33
|
+
|
34
|
+
private def check_ci(value)
|
35
|
+
case value
|
36
|
+
when nil
|
37
|
+
nil
|
38
|
+
when :sd, "sd"
|
39
|
+
:sd
|
40
|
+
when 0..100
|
41
|
+
value
|
42
|
+
when Numeric
|
43
|
+
raise ArgumentError,
|
44
|
+
"ci must be in 0..100, but %p is given" % value
|
45
|
+
else
|
46
|
+
raise ArgumentError,
|
47
|
+
"invalid value for ci (%p for nil, :sd, or a number in 0..100)" % value
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
attr_reader :n_boot
|
52
|
+
|
53
|
+
def n_boot=(n_boot)
|
54
|
+
@n_boot = check_n_boot(n_boot)
|
55
|
+
end
|
56
|
+
|
57
|
+
private def check_n_boot(value)
|
58
|
+
case value
|
59
|
+
when Integer
|
60
|
+
if value <= 0
|
61
|
+
raise ArgumentError,
|
62
|
+
"n_boot must be larger than zero, but %p is given" % value
|
63
|
+
end
|
64
|
+
value
|
65
|
+
else
|
66
|
+
raise ArgumentError,
|
67
|
+
"invalid value for n_boot (%p for an integer > 0)" % value
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
attr_reader :units
|
72
|
+
|
73
|
+
def units=(units)
|
74
|
+
@units = check_dimension(units, :units)
|
75
|
+
unless units.nil?
|
76
|
+
raise NotImplementedError,
|
77
|
+
"Specifying units variable is not supported yet"
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
include RandomSupport
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|