charty 0.2.0 → 0.2.6
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 +177 -9
- data/Rakefile +4 -5
- data/charty.gemspec +10 -4
- data/examples/Gemfile +1 -0
- data/examples/active_record.ipynb +1 -1
- data/examples/daru.ipynb +1 -1
- data/examples/iris_dataset.ipynb +1 -1
- data/examples/nmatrix.ipynb +1 -1
- data/examples/{numo-narray.ipynb → numo_narray.ipynb} +1 -1
- data/examples/palette.rb +71 -0
- data/examples/sample.png +0 -0
- data/examples/sample_images/hist_gruff.png +0 -0
- data/examples/sample_pyplot.ipynb +40 -38
- 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 -1
- data/lib/charty/backend_methods.rb +8 -0
- data/lib/charty/backends.rb +26 -1
- data/lib/charty/backends/bokeh.rb +31 -31
- data/lib/charty/backends/{google_chart.rb → google_charts.rb} +75 -33
- data/lib/charty/backends/gruff.rb +14 -3
- data/lib/charty/backends/plotly.rb +774 -9
- data/lib/charty/backends/pyplot.rb +611 -34
- data/lib/charty/backends/rubyplot.rb +2 -2
- data/lib/charty/backends/unicode_plot.rb +79 -0
- data/lib/charty/dash_pattern_generator.rb +57 -0
- data/lib/charty/index.rb +213 -0
- data/lib/charty/linspace.rb +1 -1
- data/lib/charty/plot_methods.rb +254 -0
- data/lib/charty/plotter.rb +10 -10
- data/lib/charty/plotters.rb +12 -0
- data/lib/charty/plotters/abstract_plotter.rb +243 -0
- data/lib/charty/plotters/bar_plotter.rb +201 -0
- data/lib/charty/plotters/box_plotter.rb +79 -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/line_plotter.rb +300 -0
- data/lib/charty/plotters/random_support.rb +25 -0
- data/lib/charty/plotters/relational_plotter.rb +635 -0
- data/lib/charty/plotters/scatter_plotter.rb +80 -0
- data/lib/charty/plotters/vector_plotter.rb +6 -0
- data/lib/charty/statistics.rb +114 -0
- data/lib/charty/table.rb +161 -15
- data/lib/charty/table_adapters.rb +2 -0
- data/lib/charty/table_adapters/active_record_adapter.rb +17 -9
- data/lib/charty/table_adapters/base_adapter.rb +166 -0
- data/lib/charty/table_adapters/daru_adapter.rb +41 -3
- data/lib/charty/table_adapters/datasets_adapter.rb +17 -2
- data/lib/charty/table_adapters/hash_adapter.rb +143 -16
- data/lib/charty/table_adapters/narray_adapter.rb +25 -6
- data/lib/charty/table_adapters/nmatrix_adapter.rb +15 -5
- data/lib/charty/table_adapters/pandas_adapter.rb +163 -0
- data/lib/charty/util.rb +28 -0
- data/lib/charty/vector.rb +69 -0
- data/lib/charty/vector_adapters.rb +187 -0
- data/lib/charty/vector_adapters/array_adapter.rb +101 -0
- data/lib/charty/vector_adapters/daru_adapter.rb +163 -0
- data/lib/charty/vector_adapters/narray_adapter.rb +182 -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 +199 -0
- data/lib/charty/version.rb +1 -1
- metadata +121 -22
- data/.travis.yml +0 -10
@@ -0,0 +1,79 @@
|
|
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
|
+
private def render_plot(backend, **)
|
31
|
+
draw_box_plot(backend)
|
32
|
+
annotate_axes(backend)
|
33
|
+
backend.invert_yaxis if orient == :h
|
34
|
+
end
|
35
|
+
|
36
|
+
private def draw_box_plot(backend)
|
37
|
+
if @plot_colors.nil?
|
38
|
+
plot_data = @plot_data.map do |group_data|
|
39
|
+
unless group_data.empty?
|
40
|
+
group_data = group_data.drop_na
|
41
|
+
group_data unless group_data.empty?
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
backend.box_plot(plot_data,
|
46
|
+
@group_names,
|
47
|
+
orient: orient,
|
48
|
+
colors: @colors,
|
49
|
+
gray: @gray,
|
50
|
+
dodge: dodge,
|
51
|
+
width: @width,
|
52
|
+
flier_size: flier_size,
|
53
|
+
whisker: whisker)
|
54
|
+
else
|
55
|
+
grouped_box_data = @color_names.map.with_index do |color_name, i|
|
56
|
+
@plot_data.map.with_index do |group_data, j|
|
57
|
+
unless group_data.empty?
|
58
|
+
color_mask = @plot_colors[j].eq(color_name)
|
59
|
+
group_data = group_data[color_mask].drop_na
|
60
|
+
group_data unless group_data.empty?
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
backend.grouped_box_plot(grouped_box_data,
|
66
|
+
@group_names,
|
67
|
+
@color_names,
|
68
|
+
orient: orient,
|
69
|
+
colors: @colors,
|
70
|
+
gray: @gray,
|
71
|
+
dodge: dodge,
|
72
|
+
width: @width,
|
73
|
+
flier_size: flier_size,
|
74
|
+
whisker: whisker)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -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
|