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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +56 -23
- 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 -5
- data/examples/palette.rb +1 -1
- data/examples/sample_images/hist_gruff.png +0 -0
- 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 +9 -2
- data/lib/charty/backends.rb +1 -0
- data/lib/charty/backends/bokeh.rb +2 -2
- data/lib/charty/backends/google_charts.rb +1 -1
- data/lib/charty/backends/gruff.rb +14 -3
- data/lib/charty/backends/plotly.rb +731 -32
- data/lib/charty/backends/plotly_helpers/html_renderer.rb +203 -0
- data/lib/charty/backends/plotly_helpers/notebook_renderer.rb +86 -0
- data/lib/charty/backends/plotly_helpers/plotly_renderer.rb +121 -0
- data/lib/charty/backends/pyplot.rb +515 -67
- data/lib/charty/backends/rubyplot.rb +1 -1
- data/lib/charty/backends/unicode_plot.rb +79 -0
- data/lib/charty/cache_dir.rb +27 -0
- data/lib/charty/dash_pattern_generator.rb +57 -0
- data/lib/charty/index.rb +213 -0
- data/lib/charty/iruby_helper.rb +18 -0
- data/lib/charty/linspace.rb +1 -1
- data/lib/charty/plot_methods.rb +283 -8
- data/lib/charty/plotter.rb +2 -2
- data/lib/charty/plotters.rb +11 -0
- data/lib/charty/plotters/abstract_plotter.rb +188 -18
- data/lib/charty/plotters/bar_plotter.rb +189 -7
- data/lib/charty/plotters/box_plotter.rb +64 -11
- data/lib/charty/plotters/categorical_plotter.rb +272 -40
- data/lib/charty/plotters/count_plotter.rb +7 -0
- data/lib/charty/plotters/distribution_plotter.rb +143 -0
- data/lib/charty/plotters/estimation_support.rb +84 -0
- data/lib/charty/plotters/histogram_plotter.rb +182 -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 +96 -2
- data/lib/charty/table.rb +160 -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 +39 -3
- data/lib/charty/table_adapters/datasets_adapter.rb +13 -2
- data/lib/charty/table_adapters/hash_adapter.rb +141 -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 +105 -24
- data/lib/charty/palette.rb +0 -235
@@ -1,25 +1,78 @@
|
|
1
1
|
module Charty
|
2
2
|
module Plotters
|
3
3
|
class BoxPlotter < CategoricalPlotter
|
4
|
-
|
5
|
-
|
6
|
-
|
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, **)
|
7
31
|
draw_box_plot(backend)
|
8
32
|
annotate_axes(backend)
|
9
|
-
backend.
|
33
|
+
backend.invert_yaxis if orient == :h
|
10
34
|
end
|
11
35
|
|
12
36
|
private def draw_box_plot(backend)
|
13
|
-
|
14
|
-
|
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
|
15
44
|
|
16
|
-
|
17
|
-
|
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
|
18
64
|
|
19
|
-
|
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)
|
20
75
|
end
|
21
|
-
backend.box_plot(plot_data, (0 ... @plot_data.length).to_a,
|
22
|
-
color: @colors, gray: @gray)
|
23
76
|
end
|
24
77
|
end
|
25
78
|
end
|
@@ -1,7 +1,33 @@
|
|
1
1
|
module Charty
|
2
2
|
module Plotters
|
3
3
|
class CategoricalPlotter < AbstractPlotter
|
4
|
-
|
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)
|
5
31
|
super
|
6
32
|
|
7
33
|
setup_variables
|
@@ -11,9 +37,60 @@ module Charty
|
|
11
37
|
attr_reader :order
|
12
38
|
|
13
39
|
def order=(order)
|
14
|
-
@order = Array(order).map(&method(:normalize_name))
|
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)
|
15
65
|
end
|
16
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
|
+
|
17
94
|
private def normalize_name(value)
|
18
95
|
case value
|
19
96
|
when String, Symbol
|
@@ -23,12 +100,16 @@ module Charty
|
|
23
100
|
end
|
24
101
|
end
|
25
102
|
|
26
|
-
attr_reader :group_names, :plot_data, :group_label
|
103
|
+
attr_reader :group_names, :plot_data, :group_label
|
104
|
+
|
105
|
+
attr_accessor :value_label
|
27
106
|
|
28
107
|
private def setup_variables
|
29
108
|
if x.nil? && y.nil?
|
109
|
+
@input_format = :wide
|
30
110
|
setup_variables_with_wide_form_dataset
|
31
111
|
else
|
112
|
+
@input_format = :long
|
32
113
|
setup_variables_with_long_form_dataset
|
33
114
|
end
|
34
115
|
end
|
@@ -62,36 +143,58 @@ module Charty
|
|
62
143
|
end
|
63
144
|
|
64
145
|
private def setup_variables_with_long_form_dataset
|
65
|
-
x
|
146
|
+
x = self.x
|
147
|
+
y = self.y
|
148
|
+
color = self.color
|
66
149
|
if @data
|
67
|
-
x
|
68
|
-
y
|
150
|
+
x &&= @data[x] || x
|
151
|
+
y &&= @data[y] || y
|
152
|
+
color &&= @data[color] || color
|
69
153
|
end
|
70
154
|
|
71
155
|
# Validate inputs
|
72
|
-
[x, y].each do |input|
|
73
|
-
next if array?(input)
|
156
|
+
[x, y, color].each do |input|
|
157
|
+
next if input.nil? || array?(input)
|
74
158
|
raise RuntimeError,
|
75
|
-
"Could not interpret
|
159
|
+
"Could not interpret input `#{input.inspect}`"
|
76
160
|
end
|
77
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
|
+
|
78
168
|
if x.nil? || y.nil?
|
79
169
|
setup_single_data
|
80
170
|
else
|
81
|
-
|
82
|
-
|
171
|
+
if orient == :v
|
172
|
+
groups, vals = x, y
|
173
|
+
else
|
174
|
+
groups, vals = y, x
|
175
|
+
end
|
83
176
|
|
84
177
|
if groups.respond_to?(:name)
|
85
178
|
@group_label = groups.name
|
86
179
|
end
|
87
180
|
|
88
|
-
|
89
|
-
|
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)
|
90
195
|
end
|
91
196
|
|
92
|
-
#
|
93
|
-
@group_names = groups
|
94
|
-
@plot_data = vals.map {|v| [v] }
|
197
|
+
# TODO: Handle units
|
95
198
|
end
|
96
199
|
end
|
97
200
|
|
@@ -100,48 +203,177 @@ module Charty
|
|
100
203
|
"Single data plot is not supported yet"
|
101
204
|
end
|
102
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
|
+
|
103
275
|
private def setup_colors
|
104
|
-
|
105
|
-
|
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
|
106
284
|
current_palette = Palette.default
|
107
285
|
if n_colors <= current_palette.n_colors
|
108
|
-
|
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 }
|
109
294
|
else
|
110
|
-
|
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
|
111
305
|
end
|
112
306
|
else
|
113
|
-
case
|
307
|
+
case self.palette
|
114
308
|
when Hash
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
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] }
|
120
315
|
end
|
121
|
-
|
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
|
122
321
|
end
|
123
322
|
|
124
|
-
@colors =
|
323
|
+
@colors = colors.map {|c| c.to_rgb }
|
125
324
|
lightness_values = @colors.map {|c| c.to_hsl.l }
|
126
325
|
lum = lightness_values.min * 0.6r
|
127
326
|
@gray = Colors::RGB.new(lum, lum, lum) # TODO: Use Charty::Gray
|
128
327
|
end
|
129
328
|
|
130
|
-
private def
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
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
|
137
339
|
end
|
138
340
|
|
139
|
-
private def
|
140
|
-
|
141
|
-
|
142
|
-
|
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)
|
143
376
|
end
|
144
|
-
ary
|
145
377
|
end
|
146
378
|
end
|
147
379
|
end
|