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,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
|
@@ -0,0 +1,300 @@
|
|
1
|
+
module Charty
|
2
|
+
module Plotters
|
3
|
+
class EstimateAggregator
|
4
|
+
def initialize(estimator, error_bar, n_boot, random)
|
5
|
+
@estimator = estimator
|
6
|
+
@method, @level = error_bar
|
7
|
+
@n_boot = n_boot
|
8
|
+
@random = random
|
9
|
+
end
|
10
|
+
|
11
|
+
# Perform aggregation
|
12
|
+
#
|
13
|
+
# @param data [Hash<Any, Charty::Table>]
|
14
|
+
# @param var_name [Symbol, String] A column name to be aggregated
|
15
|
+
def aggregate(data, var_name)
|
16
|
+
values = data[var_name]
|
17
|
+
estimation = case @estimator
|
18
|
+
when :count
|
19
|
+
values.length
|
20
|
+
when :mean
|
21
|
+
values.mean
|
22
|
+
end
|
23
|
+
|
24
|
+
n = values.length
|
25
|
+
case
|
26
|
+
# No error bars
|
27
|
+
when @method.nil?
|
28
|
+
err_min = err_max = Float::NAN
|
29
|
+
when n <= 1
|
30
|
+
err_min = err_max = Float::NAN
|
31
|
+
|
32
|
+
# User-defined method
|
33
|
+
when @method.respond_to?(:call)
|
34
|
+
err_min, err_max = @method.call(values)
|
35
|
+
|
36
|
+
# Parametric
|
37
|
+
when @method == :sd
|
38
|
+
err_radius = values.stdev * @level
|
39
|
+
err_min = estimation - err_radius
|
40
|
+
err_max = estimation + err_radius
|
41
|
+
when @method == :se
|
42
|
+
err_radius = values.stdev / Math.sqrt(n)
|
43
|
+
err_min = estimation - err_radius
|
44
|
+
err_max = estimation + err_radius
|
45
|
+
|
46
|
+
# Nonparametric
|
47
|
+
when @method == :pi
|
48
|
+
err_min, err_max = percentile_interval(values, @level)
|
49
|
+
when @method == :ci
|
50
|
+
# TODO: Support units
|
51
|
+
err_min, err_max =
|
52
|
+
Statistics.bootstrap_ci(values, @level, units: nil, func: @estimator,
|
53
|
+
n_boot: @n_boot, random: @random)
|
54
|
+
end
|
55
|
+
|
56
|
+
{
|
57
|
+
var_name => estimation,
|
58
|
+
"#{var_name}_min" => err_min,
|
59
|
+
"#{var_name}_max" => err_max
|
60
|
+
}
|
61
|
+
end
|
62
|
+
|
63
|
+
def percentile_interval(values, width)
|
64
|
+
q = [50 - width / 2, 50 + width / 2]
|
65
|
+
Statistics.percentile(values, q)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
class LinePlotter < RelationalPlotter
|
70
|
+
def initialize(data: nil, variables: {}, **options, &block)
|
71
|
+
x, y, color, style, size = variables.values_at(:x, :y, :color, :style, :size)
|
72
|
+
super(x, y, color, style, size, data: data, **options, &block)
|
73
|
+
|
74
|
+
@comp_data = nil
|
75
|
+
end
|
76
|
+
|
77
|
+
attr_reader :estimator
|
78
|
+
|
79
|
+
def estimator=(estimator)
|
80
|
+
@estimator = check_estimator(estimator)
|
81
|
+
end
|
82
|
+
|
83
|
+
private def check_estimator(value)
|
84
|
+
case value
|
85
|
+
when nil, false
|
86
|
+
nil
|
87
|
+
when :count, "count"
|
88
|
+
:count
|
89
|
+
when :mean, "mean"
|
90
|
+
:mean
|
91
|
+
when :median
|
92
|
+
raise NotImplementedError,
|
93
|
+
"median estimator has not been supported yet"
|
94
|
+
when Proc
|
95
|
+
raise NotImplementedError,
|
96
|
+
"a callable estimator has not been supported yet"
|
97
|
+
else
|
98
|
+
raise ArgumentError,
|
99
|
+
"invalid value for estimator (%p for :mean)" % value
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
attr_reader :n_boot
|
104
|
+
|
105
|
+
def n_boot=(n_boot)
|
106
|
+
@n_boot = check_n_boot(n_boot)
|
107
|
+
end
|
108
|
+
|
109
|
+
private def check_n_boot(value)
|
110
|
+
case value
|
111
|
+
when Integer
|
112
|
+
if value <= 0
|
113
|
+
raise ArgumentError,
|
114
|
+
"n_boot must be larger than zero, but %p is given" % value
|
115
|
+
end
|
116
|
+
value
|
117
|
+
else
|
118
|
+
raise ArgumentError,
|
119
|
+
"invalid value for n_boot (%p for an integer > 0)" % value
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
include RandomSupport
|
124
|
+
|
125
|
+
attr_reader :sort, :err_style, :err_kws, :error_bar, :x_scale, :y_scale
|
126
|
+
|
127
|
+
def sort=(val)
|
128
|
+
@sort = check_boolean(val, :sort)
|
129
|
+
end
|
130
|
+
|
131
|
+
def err_style=(val)
|
132
|
+
@err_style = check_err_style(val)
|
133
|
+
end
|
134
|
+
|
135
|
+
private def check_err_style(val)
|
136
|
+
case val
|
137
|
+
when :bars, "bars", :band, "band"
|
138
|
+
val.to_sym
|
139
|
+
else
|
140
|
+
raise ArgumentError,
|
141
|
+
"Invalid value for err_style (%p for :band or :bars)" % val
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
# parameters to draw error bars/bands
|
146
|
+
def err_params=(val)
|
147
|
+
unless val.nil?
|
148
|
+
raise NotImplementedError,
|
149
|
+
"Specifying `err_params` is not supported"
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
# The method and level to calculate error bars/bands
|
154
|
+
def error_bar=(val)
|
155
|
+
@error_bar = check_error_bar(val)
|
156
|
+
end
|
157
|
+
|
158
|
+
DEFAULT_ERROR_BAR_LEVELS = {
|
159
|
+
ci: 95,
|
160
|
+
pi: 95,
|
161
|
+
se: 1,
|
162
|
+
sd: 1
|
163
|
+
}.freeze
|
164
|
+
|
165
|
+
VALID_ERROR_BAR_METHODS = DEFAULT_ERROR_BAR_LEVELS.keys
|
166
|
+
VALID_ERROR_BAR_METHODS.concat(VALID_ERROR_BAR_METHODS.map(&:to_s))
|
167
|
+
VALID_ERROR_BAR_METHODS.freeze
|
168
|
+
|
169
|
+
private def check_error_bar(val)
|
170
|
+
case val
|
171
|
+
when nil
|
172
|
+
return [nil, nil]
|
173
|
+
when ->(x) { x.respond_to?(:call) }
|
174
|
+
return [val, nil]
|
175
|
+
when *VALID_ERROR_BAR_METHODS
|
176
|
+
method = val.to_sym
|
177
|
+
level = nil
|
178
|
+
when Array
|
179
|
+
if val.length != 2
|
180
|
+
raise ArgumentError,
|
181
|
+
"The `error_bar` array has the wrong number of items " +
|
182
|
+
"(%d for 2)" % val.length
|
183
|
+
end
|
184
|
+
method, level = *val
|
185
|
+
else
|
186
|
+
raise ArgumentError,
|
187
|
+
"Unable to recognize the value for `error_bar`: %p" % val
|
188
|
+
end
|
189
|
+
|
190
|
+
case method
|
191
|
+
when *VALID_ERROR_BAR_METHODS
|
192
|
+
method = method.to_sym
|
193
|
+
else
|
194
|
+
error_message = "The value for method in `error_bar` array must be in %p, but %p was passed" % [
|
195
|
+
DEFAULT_ERROR_BAR_LEVELS.keys,
|
196
|
+
method
|
197
|
+
]
|
198
|
+
raise ArgumentError, error_message
|
199
|
+
end
|
200
|
+
|
201
|
+
case level
|
202
|
+
when Numeric
|
203
|
+
# nothing to do
|
204
|
+
when nil
|
205
|
+
level = DEFAULT_ERROR_BAR_LEVELS[method]
|
206
|
+
else
|
207
|
+
raise ArgumentError,
|
208
|
+
"The value of level in `error_bar` array must be a number "
|
209
|
+
end
|
210
|
+
|
211
|
+
[method, level]
|
212
|
+
end
|
213
|
+
|
214
|
+
def x_scale=(val)
|
215
|
+
@x_scale = check_axis_scale(val, :x)
|
216
|
+
end
|
217
|
+
|
218
|
+
def y_scale=(val)
|
219
|
+
@y_scale = check_axis_scale(val, :y)
|
220
|
+
end
|
221
|
+
|
222
|
+
private def check_axis_scale(val, axis)
|
223
|
+
case val
|
224
|
+
when :linear, "linear", :log10, "log10"
|
225
|
+
val.to_sym
|
226
|
+
else
|
227
|
+
raise ArgumentError,
|
228
|
+
"The value of `#{axis}_scale` is worng: %p" % val,
|
229
|
+
caller
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
private def render_plot(backend, **)
|
234
|
+
draw_lines(backend)
|
235
|
+
annotate_axes(backend)
|
236
|
+
end
|
237
|
+
|
238
|
+
private def draw_lines(backend)
|
239
|
+
map_color(palette: palette, order: color_order, norm: color_norm)
|
240
|
+
map_size(sizes: sizes, order: size_order, norm: size_norm)
|
241
|
+
map_style(markers: markers, dashes: dashes, order: style_order)
|
242
|
+
|
243
|
+
aggregator = EstimateAggregator.new(estimator, error_bar, n_boot, random)
|
244
|
+
|
245
|
+
agg_var = :y
|
246
|
+
grouper = :x
|
247
|
+
grouping_vars = [:color, :size, :style]
|
248
|
+
|
249
|
+
each_subset(grouping_vars, processed: true) do |sub_vars, sub_data|
|
250
|
+
if self.sort
|
251
|
+
sort_cols = [:units, :x, :y] & self.variables.keys
|
252
|
+
sub_data = sub_data.sort_values(sort_cols)
|
253
|
+
end
|
254
|
+
|
255
|
+
unless estimator.nil?
|
256
|
+
if self.variables.include?(:units)
|
257
|
+
raise "`estimator` is must be nil when specifying `units`"
|
258
|
+
end
|
259
|
+
|
260
|
+
grouped = sub_data.group_by(grouper, sort: self.sort)
|
261
|
+
sub_data = grouped.apply(agg_var, &aggregator.method(:aggregate)).reset_index
|
262
|
+
end
|
263
|
+
|
264
|
+
# TODO: perform inverse conversion of axis scaling before plot
|
265
|
+
|
266
|
+
unit_grouping = if self.variables.include?(:units)
|
267
|
+
sub_data.group_by(:units).each_group
|
268
|
+
else
|
269
|
+
{ nil => sub_data }
|
270
|
+
end
|
271
|
+
unit_grouping.each do |_unit_value, unit_data|
|
272
|
+
ci_params = unless self.estimator.nil? || self.error_bar.nil?
|
273
|
+
{
|
274
|
+
style: self.err_style,
|
275
|
+
y_min: sub_data[:y_min],
|
276
|
+
y_max: sub_data[:y_max]
|
277
|
+
}
|
278
|
+
end
|
279
|
+
backend.line(unit_data[:x], unit_data[:y], self.variables,
|
280
|
+
color: sub_vars[:color], color_mapper: @color_mapper,
|
281
|
+
size: sub_vars[:size], size_mapper: @size_mapper,
|
282
|
+
style: sub_vars[:style], style_mapper: @style_mapper,
|
283
|
+
ci_params: ci_params)
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
if legend
|
288
|
+
backend.add_line_plot_legend(@variables, @color_mapper, @size_mapper, @style_mapper, legend)
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
private def annotate_axes(backend)
|
293
|
+
xlabel = self.variables[:x]
|
294
|
+
ylabel = self.variables[:y]
|
295
|
+
backend.set_xlabel(xlabel) unless xlabel.nil?
|
296
|
+
backend.set_ylabel(ylabel) unless ylabel.nil?
|
297
|
+
end
|
298
|
+
end
|
299
|
+
end
|
300
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Charty
|
2
|
+
module Plotters
|
3
|
+
module RandomSupport
|
4
|
+
attr_reader :random
|
5
|
+
|
6
|
+
def random=(random)
|
7
|
+
@random = check_random(random)
|
8
|
+
end
|
9
|
+
|
10
|
+
module_function def check_random(random)
|
11
|
+
case random
|
12
|
+
when nil
|
13
|
+
Random.new
|
14
|
+
when Integer
|
15
|
+
Random.new(random)
|
16
|
+
when Random
|
17
|
+
random
|
18
|
+
else
|
19
|
+
raise ArgumentError,
|
20
|
+
"invalid value for random (%p for a generator or a seed value)" % value
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,635 @@
|
|
1
|
+
module Charty
|
2
|
+
module Plotters
|
3
|
+
class BaseMapper
|
4
|
+
def initialize(plotter, *params)
|
5
|
+
@plotter = plotter
|
6
|
+
initialize_mapping(*params)
|
7
|
+
end
|
8
|
+
|
9
|
+
attr_reader :plotter
|
10
|
+
|
11
|
+
def [](key, *args)
|
12
|
+
case key
|
13
|
+
when Array, Charty::Vector
|
14
|
+
key.map {|k| lookup_single_value(k, *args) }
|
15
|
+
else
|
16
|
+
lookup_single_value(key, *args)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
private def categorical_lut_from_hash(levels, pairs, name)
|
21
|
+
missing_keys = levels - pairs.keys
|
22
|
+
unless missing_keys.empty?
|
23
|
+
raise ArgumentError,
|
24
|
+
"The `#{name}` hash is missing keys: %p" % missing_keys
|
25
|
+
end
|
26
|
+
pairs.dup
|
27
|
+
end
|
28
|
+
|
29
|
+
private def categorical_lut_from_array(levels, values, name)
|
30
|
+
if levels.length != values.length
|
31
|
+
raise ArgumentError,
|
32
|
+
"The `#{name}` array has the wrong number of values " +
|
33
|
+
"(%d for %d)." % [values.length, levels.length]
|
34
|
+
end
|
35
|
+
levels.zip(values).to_h
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# TODO: This should be replaced with red-colors's Normalize feature
|
40
|
+
class SimpleNormalizer
|
41
|
+
def initialize(vmin=nil, vmax=nil)
|
42
|
+
@vmin = vmin
|
43
|
+
@vmax = vmax
|
44
|
+
end
|
45
|
+
|
46
|
+
attr_accessor :vmin, :vmax
|
47
|
+
|
48
|
+
def call(value, clip=nil)
|
49
|
+
scalar_p = false
|
50
|
+
vector_p = false
|
51
|
+
case value
|
52
|
+
when Charty::Vector
|
53
|
+
vector_p = true
|
54
|
+
value = value.to_a
|
55
|
+
when Array
|
56
|
+
# do nothing
|
57
|
+
else
|
58
|
+
scalar_p = true
|
59
|
+
value = [value]
|
60
|
+
end
|
61
|
+
|
62
|
+
@vmin = value.min if vmin.nil?
|
63
|
+
@vmax = value.max if vmax.nil?
|
64
|
+
|
65
|
+
result = value.map {|x| (x - vmin) / (vmax - vmin).to_f }
|
66
|
+
|
67
|
+
case
|
68
|
+
when scalar_p
|
69
|
+
result[0]
|
70
|
+
when vector_p
|
71
|
+
Charty::Vector.new(result, index: value.index)
|
72
|
+
else
|
73
|
+
result
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
class ColorMapper < BaseMapper
|
79
|
+
private def initialize_mapping(palette, order, norm)
|
80
|
+
@palette = palette
|
81
|
+
@order = order
|
82
|
+
|
83
|
+
if plotter.variables.key?(:color)
|
84
|
+
data = plotter.plot_data[:color]
|
85
|
+
end
|
86
|
+
|
87
|
+
if data && data.notnull.any?
|
88
|
+
@map_type = infer_map_type(palette, norm, @plotter.input_format, @plotter.var_types[:color])
|
89
|
+
|
90
|
+
case @map_type
|
91
|
+
when :numeric
|
92
|
+
@levels, @lookup_table, @norm, @cmap = numeric_mapping(data, palette, norm)
|
93
|
+
when :categorical
|
94
|
+
@cmap = nil
|
95
|
+
@norm = nil
|
96
|
+
@levels, @lookup_table = categorical_mapping(data, palette, order)
|
97
|
+
else
|
98
|
+
raise NotImplementedError,
|
99
|
+
"datetime color mapping is not supported"
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
private def numeric_mapping(data, palette, norm)
|
105
|
+
case palette
|
106
|
+
when Hash
|
107
|
+
levels = palette.keys.sort
|
108
|
+
colors = palette.values_at(*levels)
|
109
|
+
cmap = Colors::ListedColormap.new(colors)
|
110
|
+
lookup_table = palette.dup
|
111
|
+
else
|
112
|
+
levels = data.drop_na.unique_values
|
113
|
+
levels.sort!
|
114
|
+
|
115
|
+
palette ||= "ch:"
|
116
|
+
cmap = case palette
|
117
|
+
when Colors::Colormap
|
118
|
+
palette
|
119
|
+
else
|
120
|
+
Palette.new(palette, 256).to_colormap
|
121
|
+
end
|
122
|
+
|
123
|
+
case norm
|
124
|
+
when nil
|
125
|
+
norm = SimpleNormalizer.new
|
126
|
+
when Array
|
127
|
+
norm = SimpleNormalizer.new(*norm)
|
128
|
+
#when Colors::Normalizer
|
129
|
+
# TODO: Must support red-color's Normalize feature
|
130
|
+
else
|
131
|
+
raise ArgumentError,
|
132
|
+
"`color_norm` must be nil, Array, or Normalizer object"
|
133
|
+
end
|
134
|
+
|
135
|
+
# initialize norm
|
136
|
+
norm.(data.drop_na.to_a)
|
137
|
+
|
138
|
+
lookup_table = levels.map { |level|
|
139
|
+
[
|
140
|
+
level,
|
141
|
+
cmap[norm.(level)]
|
142
|
+
]
|
143
|
+
}.to_h
|
144
|
+
end
|
145
|
+
|
146
|
+
return levels, lookup_table, norm, cmap
|
147
|
+
end
|
148
|
+
|
149
|
+
private def categorical_mapping(data, palette, order)
|
150
|
+
levels = data.categorical_order(order)
|
151
|
+
n_colors = levels.length
|
152
|
+
|
153
|
+
case palette
|
154
|
+
when Hash
|
155
|
+
lookup_table = categorical_lut_from_hash(levels, palette, :palette)
|
156
|
+
return levels, lookup_table
|
157
|
+
|
158
|
+
when nil
|
159
|
+
current_palette = Palette.default
|
160
|
+
if n_colors <= current_palette.n_colors
|
161
|
+
colors = Palette.new(current_palette.colors, n_colors).colors
|
162
|
+
else
|
163
|
+
colors = Palette.husl_colors(n_colors)
|
164
|
+
end
|
165
|
+
|
166
|
+
when Array
|
167
|
+
colors = palette
|
168
|
+
|
169
|
+
else
|
170
|
+
colors = Palette.new(palette, n_colors).colors
|
171
|
+
end
|
172
|
+
|
173
|
+
lookup_table = categorical_lut_from_array(levels, colors, :palette)
|
174
|
+
return levels, lookup_table
|
175
|
+
end
|
176
|
+
|
177
|
+
private def infer_map_type(palette, norm, input_format, var_type)
|
178
|
+
case
|
179
|
+
when false # palette is qualitative_palette
|
180
|
+
:categorical
|
181
|
+
when ! norm.nil?
|
182
|
+
:numeric
|
183
|
+
when palette.is_a?(Array),
|
184
|
+
palette.is_a?(Hash)
|
185
|
+
:categorical
|
186
|
+
when input_format == :wide
|
187
|
+
:categorical
|
188
|
+
else
|
189
|
+
var_type
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
attr_reader :palette, :norm, :levels, :lookup_table, :map_type
|
194
|
+
|
195
|
+
def inverse_lookup_table
|
196
|
+
lookup_table.invert
|
197
|
+
end
|
198
|
+
|
199
|
+
def lookup_single_value(key)
|
200
|
+
if @lookup_table.key?(key)
|
201
|
+
@lookup_table[key]
|
202
|
+
elsif @norm
|
203
|
+
# Use the colormap to interpolate between existing datapoints
|
204
|
+
begin
|
205
|
+
normed = @norm.(key)
|
206
|
+
@cmap[normed]
|
207
|
+
rescue ArgumentError, TypeError => err
|
208
|
+
if Util.nan?(key)
|
209
|
+
return "#000000"
|
210
|
+
else
|
211
|
+
raise err
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
class SizeMapper < BaseMapper
|
219
|
+
private def initialize_mapping(sizes, order, norm)
|
220
|
+
@sizes = sizes
|
221
|
+
@order = order
|
222
|
+
@norm = norm
|
223
|
+
|
224
|
+
return unless plotter.variables.key?(:size)
|
225
|
+
|
226
|
+
data = plotter.plot_data[:size]
|
227
|
+
return unless data.notnull.any?
|
228
|
+
|
229
|
+
@map_type = infer_map_type(sizes, norm, @plotter.var_types[:size])
|
230
|
+
case @map_type
|
231
|
+
when :numeric
|
232
|
+
@levels, @lookup_table, @norm = numeric_mapping(data, sizes, norm)
|
233
|
+
when :categorical
|
234
|
+
@levels, @lookup_table = categorical_mapping(data, sizes, order)
|
235
|
+
else
|
236
|
+
raise NotImplementedError,
|
237
|
+
"datetime color mapping is not supported"
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
private def infer_map_type(sizes, norm, var_type)
|
242
|
+
case
|
243
|
+
when ! norm.nil?
|
244
|
+
:numeric
|
245
|
+
when sizes.is_a?(Hash),
|
246
|
+
sizes.is_a?(Array)
|
247
|
+
:categorical
|
248
|
+
else
|
249
|
+
var_type
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
private def numeric_mapping(data, sizes, norm)
|
254
|
+
case sizes
|
255
|
+
when Hash
|
256
|
+
# The presence of a norm object overrides a dictionary of sizes
|
257
|
+
# in specifying a numeric mapping, so we need to process the
|
258
|
+
# dictionary here
|
259
|
+
levels = sizes.keys.sort
|
260
|
+
size_values = sizes.values
|
261
|
+
size_range = [size_values.min, size_values.max]
|
262
|
+
else
|
263
|
+
levels = Charty::Vector.new(data.unique_values).drop_na.to_a
|
264
|
+
levels.sort!
|
265
|
+
|
266
|
+
case sizes
|
267
|
+
when Range
|
268
|
+
size_range = [sizes.begin, sizes.end]
|
269
|
+
when nil
|
270
|
+
size_range = [0r, 1r]
|
271
|
+
else
|
272
|
+
raise ArgumentError,
|
273
|
+
"Unable to recognize the value for `sizes`: %p" % sizes
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
# Now we have the minimum and the maximum values of sizes
|
278
|
+
case norm
|
279
|
+
when nil
|
280
|
+
norm = SimpleNormalizer.new
|
281
|
+
sizes_scaled = norm.(levels)
|
282
|
+
# when Colors::Normalize
|
283
|
+
# TODO: Must support red-color's Normalize feature
|
284
|
+
else
|
285
|
+
raise ArgumentError,
|
286
|
+
"Unable to recognize the value for `norm`: %p" % norm
|
287
|
+
end
|
288
|
+
|
289
|
+
case sizes
|
290
|
+
when Hash
|
291
|
+
# do nothing
|
292
|
+
else
|
293
|
+
lo, hi = size_range
|
294
|
+
sizes = sizes_scaled.map {|x| lo + x * (hi - lo) }
|
295
|
+
lookup_table = levels.zip(sizes).to_h
|
296
|
+
end
|
297
|
+
|
298
|
+
return levels, lookup_table, norm
|
299
|
+
end
|
300
|
+
|
301
|
+
private def categorical_mapping(data, sizes, order)
|
302
|
+
levels = data.categorical_order(order)
|
303
|
+
|
304
|
+
case sizes
|
305
|
+
when Hash
|
306
|
+
lookup_table = categorical_lut_from_hash(levels, sizes, :sizes)
|
307
|
+
return levels, lookup_table
|
308
|
+
|
309
|
+
when Array
|
310
|
+
# nothing to do
|
311
|
+
|
312
|
+
when Range, nil
|
313
|
+
# Reverse values to use the largest size for the first category
|
314
|
+
size_range = sizes || (0r .. 1r)
|
315
|
+
sizes = Linspace.new(size_range, levels.length).reverse_each.to_a
|
316
|
+
else
|
317
|
+
raise ArgumentError,
|
318
|
+
"Unable to recognize the value for `sizes`: %p" % sizes
|
319
|
+
end
|
320
|
+
|
321
|
+
lookup_table = categorical_lut_from_array(levels, sizes, :sizes)
|
322
|
+
return levels, lookup_table
|
323
|
+
end
|
324
|
+
|
325
|
+
attr_reader :palette, :order, :norm, :levels, :lookup_table, :map_type
|
326
|
+
|
327
|
+
def lookup_single_value(key)
|
328
|
+
if @lookup_table.key?(key)
|
329
|
+
@lookup_table[key]
|
330
|
+
else
|
331
|
+
normed = @norm.(key) || Float::NAN
|
332
|
+
size_values = @lookup_table.values
|
333
|
+
min, max = size_values.min, size_values.max
|
334
|
+
min + normed * (max - min)
|
335
|
+
end
|
336
|
+
end
|
337
|
+
end
|
338
|
+
|
339
|
+
class StyleMapper < BaseMapper
|
340
|
+
private def initialize_mapping(markers, dashes, order)
|
341
|
+
@markers = markers
|
342
|
+
@dashes = dashes
|
343
|
+
@order = order
|
344
|
+
|
345
|
+
return unless plotter.variables.key?(:style)
|
346
|
+
|
347
|
+
data = plotter.plot_data[:style]
|
348
|
+
return unless data.notnull.any?
|
349
|
+
|
350
|
+
@levels = data.categorical_order(order)
|
351
|
+
|
352
|
+
markers = map_attributes(markers, @levels, unique_markers(@levels.length), :markers)
|
353
|
+
dashes = map_attributes(dashes, @levels, unique_dashes(@levels.length), :dashes)
|
354
|
+
|
355
|
+
@lookup_table = @levels.map {|key|
|
356
|
+
record = {
|
357
|
+
marker: markers[key],
|
358
|
+
dashes: dashes[key]
|
359
|
+
}
|
360
|
+
record.compact!
|
361
|
+
[key, record]
|
362
|
+
}.to_h
|
363
|
+
end
|
364
|
+
|
365
|
+
MARKER_NAMES = [
|
366
|
+
:circle, :x, :square, :cross, :diamond, :star_diamond,
|
367
|
+
:triangle_up, :star_square, :triangle_down, :hexagon, :star, :pentagon,
|
368
|
+
].freeze
|
369
|
+
|
370
|
+
private def unique_markers(n)
|
371
|
+
if n > MARKER_NAMES.length
|
372
|
+
raise ArgumentError,
|
373
|
+
"Too many markers are required (%p for %p)" % [n, MARKER_NAMES.length]
|
374
|
+
end
|
375
|
+
MARKER_NAMES[0, n]
|
376
|
+
end
|
377
|
+
|
378
|
+
private def unique_dashes(n)
|
379
|
+
DashPatternGenerator.take(n)
|
380
|
+
end
|
381
|
+
|
382
|
+
private def map_attributes(vals, levels, defaults, attr)
|
383
|
+
case vals
|
384
|
+
when true
|
385
|
+
return levels.zip(defaults).to_h
|
386
|
+
when Hash
|
387
|
+
missing_keys = lavels - vals.keys
|
388
|
+
unless missing_keys.empty?
|
389
|
+
raise ArgumentError,
|
390
|
+
"The `%s` levels are missing values: %p" % [attr, missing_keys]
|
391
|
+
end
|
392
|
+
return vals
|
393
|
+
when Array, Enumerable
|
394
|
+
if levels.length != vals.length
|
395
|
+
raise ArgumentError,
|
396
|
+
"%he `%s` argument has the wrong number of values" % attr
|
397
|
+
end
|
398
|
+
return levels.zip(vals).to_h
|
399
|
+
when nil, false
|
400
|
+
return {}
|
401
|
+
else
|
402
|
+
raise ArgumentError,
|
403
|
+
"Unable to recognize the value for `%s`: %p" % [attr, vals]
|
404
|
+
end
|
405
|
+
end
|
406
|
+
|
407
|
+
attr_reader :palette, :order, :norm, :lookup_table, :levels
|
408
|
+
|
409
|
+
def inverse_lookup_table(attr)
|
410
|
+
lookup_table.map { |k, v| [v[attr], k] }.to_h
|
411
|
+
end
|
412
|
+
|
413
|
+
def lookup_single_value(key, attr=nil)
|
414
|
+
case attr
|
415
|
+
when nil
|
416
|
+
@lookup_table[key]
|
417
|
+
else
|
418
|
+
@lookup_table[key][attr]
|
419
|
+
end
|
420
|
+
end
|
421
|
+
end
|
422
|
+
|
423
|
+
class RelationalPlotter < AbstractPlotter
|
424
|
+
def flat_structure
|
425
|
+
{
|
426
|
+
x: :index,
|
427
|
+
y: :values
|
428
|
+
}
|
429
|
+
end
|
430
|
+
|
431
|
+
def initialize(x, y, color, style, size, data: nil, **options, &block)
|
432
|
+
super(x, y, color, data: data, **options, &block)
|
433
|
+
|
434
|
+
self.style = style
|
435
|
+
self.size = size
|
436
|
+
|
437
|
+
setup_variables
|
438
|
+
end
|
439
|
+
|
440
|
+
attr_reader :style, :size, :units
|
441
|
+
|
442
|
+
attr_reader :color_norm
|
443
|
+
|
444
|
+
attr_reader :sizes, :size_order, :size_norm
|
445
|
+
|
446
|
+
attr_reader :markers, :dashes, :style_order
|
447
|
+
|
448
|
+
attr_reader :legend
|
449
|
+
|
450
|
+
def units=(units)
|
451
|
+
@units = check_dimension(units, :units)
|
452
|
+
end
|
453
|
+
|
454
|
+
def style=(val)
|
455
|
+
@style = check_dimension(val, :style)
|
456
|
+
end
|
457
|
+
|
458
|
+
def size=(val)
|
459
|
+
@size = check_dimension(val, :size)
|
460
|
+
end
|
461
|
+
|
462
|
+
def color_norm=(val)
|
463
|
+
unless val.nil?
|
464
|
+
raise NotImplementedError,
|
465
|
+
"Specifying color_norm is not supported yet"
|
466
|
+
end
|
467
|
+
end
|
468
|
+
|
469
|
+
def sizes=(val)
|
470
|
+
# NOTE: the value check will be perfomed in SizeMapper
|
471
|
+
@sizes = val
|
472
|
+
end
|
473
|
+
|
474
|
+
def size_order=(val)
|
475
|
+
unless val.nil?
|
476
|
+
raise NotImplementedError,
|
477
|
+
"Specifying size_order is not supported yet"
|
478
|
+
end
|
479
|
+
end
|
480
|
+
|
481
|
+
def size_norm=(val)
|
482
|
+
unless val.nil?
|
483
|
+
raise NotImplementedError,
|
484
|
+
"Specifying size_order is not supported yet"
|
485
|
+
end
|
486
|
+
end
|
487
|
+
|
488
|
+
def markers=(val)
|
489
|
+
@markers = check_markers(val)
|
490
|
+
end
|
491
|
+
|
492
|
+
private def check_markers(val)
|
493
|
+
# TODO
|
494
|
+
val
|
495
|
+
end
|
496
|
+
|
497
|
+
def dashes=(val)
|
498
|
+
@dashes = check_dashes(val)
|
499
|
+
end
|
500
|
+
|
501
|
+
private def check_dashes(val)
|
502
|
+
# TODO
|
503
|
+
val
|
504
|
+
end
|
505
|
+
|
506
|
+
def style_order=(val)
|
507
|
+
unless val.nil?
|
508
|
+
raise NotImplementedError,
|
509
|
+
"Specifying style_order is not supported yet"
|
510
|
+
end
|
511
|
+
end
|
512
|
+
|
513
|
+
def legend=(val)
|
514
|
+
case val
|
515
|
+
when :auto, :brief, :full, false
|
516
|
+
@legend = val
|
517
|
+
when "auto", "brief", "full"
|
518
|
+
@legend = val.to_sym
|
519
|
+
else
|
520
|
+
raise ArgumentError,
|
521
|
+
"invalid value of legend (%p for :auto, :brief, :full, or false)" % val
|
522
|
+
end
|
523
|
+
end
|
524
|
+
|
525
|
+
attr_reader :input_format, :plot_data, :variables, :var_types
|
526
|
+
|
527
|
+
private def setup_variables
|
528
|
+
if x.nil? && y.nil?
|
529
|
+
@input_format = :wide
|
530
|
+
setup_variables_with_wide_form_dataset
|
531
|
+
else
|
532
|
+
@input_format = :long
|
533
|
+
setup_variables_with_long_form_dataset
|
534
|
+
end
|
535
|
+
|
536
|
+
@var_types = @plot_data.columns.map { |k|
|
537
|
+
[k, variable_type(@plot_data[k], :categorical)]
|
538
|
+
}.to_h
|
539
|
+
end
|
540
|
+
|
541
|
+
private def setup_variables_with_wide_form_dataset
|
542
|
+
unless color.nil? && style.nil? && size.nil?
|
543
|
+
vars = []
|
544
|
+
vars << "color" unless color.nil?
|
545
|
+
vars << "style" unless style.nil?
|
546
|
+
vars << "size" unless size.nil?
|
547
|
+
raise ArgumentError,
|
548
|
+
"Unable to assign the following variables in wide-form data: " +
|
549
|
+
vars.join(", ")
|
550
|
+
end
|
551
|
+
|
552
|
+
if data.nil? || data.empty?
|
553
|
+
@plot_data = Charty::Table.new({})
|
554
|
+
@variables = {}
|
555
|
+
return
|
556
|
+
end
|
557
|
+
|
558
|
+
flat = data.is_a?(Charty::Vector)
|
559
|
+
if flat
|
560
|
+
@plot_data = {}
|
561
|
+
@variables = {}
|
562
|
+
|
563
|
+
[:x, :y].each do |var|
|
564
|
+
case self.flat_structure[var]
|
565
|
+
when :index
|
566
|
+
@plot_data[var] = data.index.to_a
|
567
|
+
@variables[var] = data.index.name
|
568
|
+
when :values
|
569
|
+
@plot_data[var] = data.to_a
|
570
|
+
@variables[var] = data.name
|
571
|
+
end
|
572
|
+
end
|
573
|
+
|
574
|
+
@plot_data = Charty::Table.new(@plot_data)
|
575
|
+
else
|
576
|
+
raise NotImplementedError,
|
577
|
+
"wide-form input is not supported"
|
578
|
+
end
|
579
|
+
end
|
580
|
+
|
581
|
+
private def setup_variables_with_long_form_dataset
|
582
|
+
self.data = {} if data.nil?
|
583
|
+
|
584
|
+
plot_data = {}
|
585
|
+
variables = {}
|
586
|
+
|
587
|
+
{
|
588
|
+
x: self.x,
|
589
|
+
y: self.y,
|
590
|
+
color: self.color,
|
591
|
+
style: self.style,
|
592
|
+
size: self.size,
|
593
|
+
units: self.units
|
594
|
+
}.each do |key, val|
|
595
|
+
next if val.nil?
|
596
|
+
|
597
|
+
if data.column?(val)
|
598
|
+
plot_data[key] = data[val]
|
599
|
+
variables[key] = val
|
600
|
+
else
|
601
|
+
case val
|
602
|
+
when Charty::Vector
|
603
|
+
plot_data[key] = val
|
604
|
+
variables[key] = val.name
|
605
|
+
else
|
606
|
+
raise ArgumentError,
|
607
|
+
"Could not interpret value %p for parameter %p" % [val, key]
|
608
|
+
end
|
609
|
+
end
|
610
|
+
end
|
611
|
+
|
612
|
+
@plot_data = Charty::Table.new(plot_data)
|
613
|
+
@variables = variables.select do |var, name|
|
614
|
+
@plot_data[var].notnull.any?
|
615
|
+
end
|
616
|
+
end
|
617
|
+
|
618
|
+
private def annotate_axes(backend)
|
619
|
+
# TODO
|
620
|
+
end
|
621
|
+
|
622
|
+
private def map_color(palette: nil, order: nil, norm: nil)
|
623
|
+
@color_mapper = ColorMapper.new(self, palette, order, norm)
|
624
|
+
end
|
625
|
+
|
626
|
+
private def map_size(sizes: nil, order: nil, norm: nil)
|
627
|
+
@size_mapper = SizeMapper.new(self, sizes, order, norm)
|
628
|
+
end
|
629
|
+
|
630
|
+
private def map_style(markers: nil, dashes: nil, order: nil)
|
631
|
+
@style_mapper = StyleMapper.new(self, markers, dashes, order)
|
632
|
+
end
|
633
|
+
end
|
634
|
+
end
|
635
|
+
end
|