charty 0.2.9 → 0.2.10

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/examples/bar_plot.rb +19 -0
  3. data/examples/box_plot.rb +17 -0
  4. data/examples/scatter_plot.rb +17 -0
  5. data/images/penguins_body_mass_g_flipper_length_mm_species_scatter_plot.png +0 -0
  6. data/images/penguins_body_mass_g_flipper_length_mm_species_sex_scatter_plot.png +0 -0
  7. data/images/penguins_species_body_mass_g_bar_plot_h.png +0 -0
  8. data/images/penguins_species_body_mass_g_bar_plot_v.png +0 -0
  9. data/images/penguins_species_body_mass_g_box_plot_h.png +0 -0
  10. data/images/penguins_species_body_mass_g_box_plot_v.png +0 -0
  11. data/images/penguins_species_body_mass_g_sex_bar_plot_v.png +0 -0
  12. data/images/penguins_species_body_mass_g_sex_box_plot_v.png +0 -0
  13. data/lib/charty/backends/plotly.rb +30 -4
  14. data/lib/charty/backends/plotly_helpers/notebook_renderer.rb +3 -1
  15. data/lib/charty/backends/pyplot.rb +30 -0
  16. data/lib/charty/backends/unicode_plot.rb +9 -9
  17. data/lib/charty/plot_methods.rb +41 -7
  18. data/lib/charty/plotters/abstract_plotter.rb +41 -9
  19. data/lib/charty/plotters/bar_plotter.rb +39 -0
  20. data/lib/charty/plotters/categorical_plotter.rb +9 -1
  21. data/lib/charty/plotters/distribution_plotter.rb +36 -6
  22. data/lib/charty/plotters/histogram_plotter.rb +6 -4
  23. data/lib/charty/plotters/line_plotter.rb +38 -5
  24. data/lib/charty/plotters/scatter_plotter.rb +4 -2
  25. data/lib/charty/table.rb +30 -23
  26. data/lib/charty/table_adapters/base_adapter.rb +88 -0
  27. data/lib/charty/table_adapters/daru_adapter.rb +41 -1
  28. data/lib/charty/table_adapters/hash_adapter.rb +54 -1
  29. data/lib/charty/table_adapters/pandas_adapter.rb +49 -1
  30. data/lib/charty/vector.rb +29 -1
  31. data/lib/charty/vector_adapters.rb +16 -0
  32. data/lib/charty/vector_adapters/pandas_adapter.rb +10 -1
  33. data/lib/charty/version.rb +1 -1
  34. metadata +6 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 37ead1e86203bd6580125f4ed97039c6b512e9ae3037a0bc386743ee436f184c
4
- data.tar.gz: a94e66292a866acde67c0cfc51bd8eebd5840af68e2d4112caafce80bdd37ed2
3
+ metadata.gz: d885d476eadfadd3f76f138707ffaf6166eec006b4528fb19c3acce0b97b1c85
4
+ data.tar.gz: 0630f23f19b65177f3b6dc238717a81a019f9dfa40a40c143d71dcc5c5f760f0
5
5
  SHA512:
6
- metadata.gz: b518be17e0ae85405285fe22022bc698d0c080e08a0682f055ae70c97655ed313038e6e2899b7c961bd73c0afe1b6b0c25bdb2085bd3a66dddd57d3e6245df55
7
- data.tar.gz: 0bc460c78d178b098c65ab8737efddac0a8484abd920282d185533b72396f0abad62ea247be9e8af4286a70b7a4dc2069e88ff1675ba446af65b5c67edfb59d8
6
+ metadata.gz: 535a69b8d794ccbd30b4328f3188c8a8b50c850cfd84285f79b31162296a9d981313e561ea1089a68be3368b7eb1c83ccc3cb2ff511b8f79aef77e544e261bbe
7
+ data.tar.gz: 804de2468433db38b5381bcbccab4c28e84c7592bce5c5a98a539ce99c589449759e65ffe7f2e808bd3c5cb5e002ee2219219111561f66f590dbb0ac2c0757bc
@@ -0,0 +1,19 @@
1
+ # This example generates box_plot results in README.md
2
+
3
+ require "charty"
4
+ require "datasets"
5
+ require "matplotlib"
6
+
7
+ Charty::Backends.use(:pyplot)
8
+ Matplotlib.use(:agg)
9
+
10
+ penguins = Datasets::Penguins.new
11
+
12
+ Charty.bar_plot(data: penguins, x: :species, y: :body_mass_g)
13
+ .save("penguins_species_body_mass_g_bar_plot_v.png")
14
+
15
+ Charty.bar_plot(data: penguins, x: :body_mass_g, y: :species)
16
+ .save("penguins_species_body_mass_g_bar_plot_h.png")
17
+
18
+ Charty.bar_plot(data: penguins, x: :species, y: :body_mass_g, color: :sex)
19
+ .save("penguins_species_body_mass_g_sex_bar_plot_v.png")
@@ -0,0 +1,17 @@
1
+ require "charty"
2
+ require "datasets"
3
+ require "matplotlib"
4
+
5
+ Charty::Backends.use(:pyplot)
6
+ Matplotlib.use(:agg)
7
+
8
+ penguins = Datasets::Penguins.new
9
+
10
+ Charty.box_plot(data: penguins, x: :species, y: :body_mass_g)
11
+ .save("penguins_species_body_mass_g_box_plot_v.png")
12
+
13
+ Charty.box_plot(data: penguins, x: :body_mass_g, y: :species)
14
+ .save("penguins_species_body_mass_g_box_plot_h.png")
15
+
16
+ Charty.box_plot(data: penguins, x: :species, y: :body_mass_g, color: :sex)
17
+ .save("penguins_species_body_mass_g_sex_box_plot_v.png")
@@ -0,0 +1,17 @@
1
+ require "charty"
2
+ require "datasets"
3
+ require "matplotlib"
4
+
5
+ Charty::Backends.use(:pyplot)
6
+ Matplotlib.use(:agg)
7
+
8
+ penguins = Datasets::Penguins.new
9
+
10
+ Charty.scatter_plot(data: penguins, x: :body_mass_g, y: :flipper_length_mm)
11
+ .save("penguins_body_mass_g_flipper_length_mm_scatter_plot.png")
12
+
13
+ Charty.scatter_plot(data: penguins, x: :body_mass_g, y: :flipper_length_mm, color: :species)
14
+ .save("penguins_body_mass_g_flipper_length_mm_species_scatter_plot.png")
15
+
16
+ Charty.scatter_plot(data: penguins, x: :body_mass_g, y: :flipper_length_mm, color: :species, style: :sex)
17
+ .save("penguins_body_mass_g_flipper_length_mm_species_sex_scatter_plot.png")
@@ -134,10 +134,8 @@ module Charty
134
134
 
135
135
  if orient == :v
136
136
  x, y = bar_pos, values
137
- x = group_names unless group_names.nil?
138
137
  else
139
138
  x, y = values, bar_pos
140
- y = group_names unless group_names.nil?
141
139
  end
142
140
 
143
141
  trace = {
@@ -241,9 +239,9 @@ module Charty
241
239
  }
242
240
 
243
241
  if orient == :v
244
- trace.update(y: values, x: group_keys)
242
+ trace.update(y: values, x: group_keys.map(&:to_s))
245
243
  else
246
- trace.update(x: values, y: group_keys)
244
+ trace.update(x: values, y: group_keys.map(&:to_s))
247
245
  end
248
246
 
249
247
  trace
@@ -613,6 +611,11 @@ module Charty
613
611
  end
614
612
  end
615
613
 
614
+ def set_title(title)
615
+ @layout[:title] ||= {}
616
+ @layout[:title][:text] = title
617
+ end
618
+
616
619
  def set_xlabel(label)
617
620
  @layout[:xaxis] ||= {}
618
621
  @layout[:xaxis][:title] = label
@@ -657,6 +660,29 @@ module Charty
657
660
  @layout[:yaxis][:range] = [min, max]
658
661
  end
659
662
 
663
+ def set_xscale(scale)
664
+ scale = check_scale_type(scale, :xscale)
665
+ @layout[:xaxis] ||= {}
666
+ @layout[:xaxis][:type] = scale
667
+ end
668
+
669
+ def set_yscale(scale)
670
+ scale = check_scale_type(scale, :yscale)
671
+ @layout[:yaxis] ||= {}
672
+ @layout[:yaxis][:type] = scale
673
+ end
674
+
675
+ private def check_scale_type(val, name)
676
+ case
677
+ when :linear, :log
678
+ val
679
+ else
680
+ raise ArgumentError,
681
+ "Invalid #{name} type: %p" % val,
682
+ caller
683
+ end
684
+ end
685
+
660
686
  def disable_xaxis_grid
661
687
  # do nothing
662
688
  end
@@ -7,7 +7,7 @@ module Charty
7
7
  @initialized = false
8
8
  end
9
9
 
10
- def activate()
10
+ def activate
11
11
  return if @initialized
12
12
 
13
13
  unless IRubyHelper.iruby_notebook?
@@ -50,6 +50,8 @@ module Charty
50
50
  END_SCRIPT
51
51
  end
52
52
  IRuby.display(script, mime: "text/html")
53
+ @initialized = true
54
+ nil
53
55
  end
54
56
 
55
57
  def render(figure, element_id: nil, post_script: nil)
@@ -662,6 +662,10 @@ module Charty
662
662
  end
663
663
  end
664
664
 
665
+ def set_title(title)
666
+ @pyplot.gca.set_title(String(title))
667
+ end
668
+
665
669
  def set_xlabel(label)
666
670
  @pyplot.gca.set_xlabel(String(label))
667
671
  end
@@ -694,6 +698,27 @@ module Charty
694
698
  @pyplot.gca.set_ylim(Float(min), Float(max))
695
699
  end
696
700
 
701
+ def set_xscale(scale)
702
+ scale = check_scale_type(scale, :xscale)
703
+ @pyplot.gca.set_xscale(scale)
704
+ end
705
+
706
+ def set_yscale(scale)
707
+ scale = check_scale_type(scale, :yscale)
708
+ @pyplot.gca.set_yscale(scale)
709
+ end
710
+
711
+ private def check_scale_type(val, name)
712
+ case
713
+ when :linear, :log
714
+ val
715
+ else
716
+ raise ArgumentError,
717
+ "Invalid #{name} type: %p" % val,
718
+ caller
719
+ end
720
+ end
721
+
697
722
  def disable_xaxis_grid
698
723
  @pyplot.gca.xaxis.grid(false)
699
724
  end
@@ -715,6 +740,11 @@ module Charty
715
740
  nil
716
741
  end
717
742
 
743
+ def render_mimebundle(include: [], exclude: [])
744
+ show
745
+ {}
746
+ end
747
+
718
748
  SAVEFIG_OPTIONAL_PARAMS = [
719
749
  :dpi, :quality, :optimize, :progressive, :facecolor, :edgecolor,
720
750
  :orientation, :papertype, :transparent, :bbox_inches, :pad_inches,
@@ -16,7 +16,7 @@ module Charty
16
16
  @layout = {}
17
17
  end
18
18
 
19
- def bar(bar_pos, values, color: nil)
19
+ def bar(bar_pos, _group_names, values, colors, _orient, **kwargs)
20
20
  @figure = {
21
21
  type: :bar,
22
22
  bar_pos: bar_pos,
@@ -24,7 +24,7 @@ module Charty
24
24
  }
25
25
  end
26
26
 
27
- def box_plot(plot_data, positions, color:, gray:)
27
+ def box_plot(plot_data, positions, **kwargs)
28
28
  @figure = { type: :box, data: plot_data }
29
29
  end
30
30
 
@@ -52,13 +52,13 @@ module Charty
52
52
  # do nothing
53
53
  end
54
54
 
55
- def show
56
- case @figure[:type]
57
- when :bar
58
- plot = ::UnicodePlot.barplot(@layout[:xtick_labels], @figure[:values], xlabel: @layout[:xlabel])
59
- when :box
60
- plot = ::UnicodePlot.boxplot(@layout[:xtick_labels], @figure[:data], xlabel: @layout[:xlabel])
61
- end
55
+ def render(**kwargs)
56
+ plot = case @figure[:type]
57
+ when :bar
58
+ ::UnicodePlot.barplot(@layout[:xtick_labels], @figure[:values], xlabel: @layout[:xlabel])
59
+ when :box
60
+ ::UnicodePlot.boxplot(@layout[:xtick_labels], @figure[:data], xlabel: @layout[:xlabel])
61
+ end
62
62
  sio = StringIO.new
63
63
  class << sio
64
64
  def tty?; true; end
@@ -23,19 +23,24 @@ module Charty
23
23
  # @param cap_size Width of the caps on error bars.
24
24
  # @param dodge [true,false] If true, bar position is shifted along the
25
25
  # categorical axis for avoid overlapping when the color-dimension is used.
26
+ # @param log [true,false] Set the value-axis (e.g. Y-axis if orient is :v) to be log scale.
27
+ # @param x_label [String,Symbol,#to_str,nil] X-axis label.
28
+ # @param y_label [String,Symbol,#to_str,nil] Y-axis label.
29
+ # @param title [String,Symbol,#to_str,nil] Title text.
26
30
  def bar_plot(x: nil, y: nil, color: nil, data: nil,
27
31
  order: nil, color_order: nil,
28
32
  estimator: :mean, ci: 95, n_boot: 1000, units: nil, random: nil,
29
33
  orient: nil, key_color: nil, palette: nil, saturation: 1r,
30
34
  error_color: [0.26, 0.26, 0.26], error_width: nil, cap_size: nil,
31
- dodge: true, **options, &block)
35
+ dodge: true, log: false, x_label: nil, y_label: nil, title: nil,
36
+ **options, &block)
32
37
  Plotters::BarPlotter.new(
33
38
  data: data, variables: { x: x, y: y, color: color },
34
39
  order: order, orient: orient,
35
40
  estimator: estimator, ci: ci, n_boot: n_boot, units: units, random: random,
36
41
  color_order: color_order, key_color: key_color, palette: palette, saturation: saturation,
37
42
  error_color: error_color, error_width: error_width, cap_size: cap_size,
38
- dodge: dodge,
43
+ dodge: dodge, log: log, x_label: x_label, y_label: y_label, title: title,
39
44
  **options, &block
40
45
  )
41
46
  end
@@ -43,7 +48,8 @@ module Charty
43
48
  def count_plot(x: nil, y: nil, color: nil, data: nil,
44
49
  order: nil, color_order: nil,
45
50
  orient: nil, key_color: nil, palette: nil, saturation: 1r,
46
- dodge: true, **options, &block)
51
+ dodge: true, log: false, x_label: nil, y_label: nil, title: nil,
52
+ **options, &block)
47
53
  case
48
54
  when x.nil? && !y.nil?
49
55
  x = y
@@ -70,6 +76,9 @@ module Charty
70
76
  palette: palette,
71
77
  saturation: saturation,
72
78
  dodge: dodge,
79
+ x_label: x_label,
80
+ y_label: y_label,
81
+ title: title,
73
82
  **options
74
83
  ) do |plotter|
75
84
  plotter.value_label = "count"
@@ -106,11 +115,15 @@ module Charty
106
115
  # @param whisker Propotion of the IQR past the low and high quartiles to
107
116
  # extend the plot whiskers. Points outside of this range will be
108
117
  # treated as outliers.
118
+ # @param x_label [String,Symbol,#to_str,nil] X-axis label.
119
+ # @param y_label [String,Symbol,#to_str,nil] Y-axis label.
120
+ # @param title [String,Symbol,#to_str,nil] Title text.
109
121
  def box_plot(x: nil, y: nil, color: nil, data: nil,
110
122
  order: nil, color_order: nil,
111
123
  orient: nil, key_color: nil, palette: nil, saturation: 1r,
112
124
  width: 0.8r, dodge: true, flier_size: 5, line_width: nil,
113
- whisker: 1.5, **options, &block)
125
+ whisker: 1.5, x_label: nil, y_label: nil, title: nil,
126
+ **options, &block)
114
127
  Plotters::BoxPlotter.new(
115
128
  data: data,
116
129
  variables: { x: x, y: y, color: color },
@@ -125,6 +138,9 @@ module Charty
125
138
  flier_size: flier_size,
126
139
  line_width: line_width,
127
140
  whisker: whisker,
141
+ x_label: x_label,
142
+ y_label: y_label,
143
+ title: title,
128
144
  **options,
129
145
  &block
130
146
  )
@@ -164,13 +180,18 @@ module Charty
164
180
  # :full, every group will get an entry in the legend. If :auto,
165
181
  # choose between brief or full representation based on number of
166
182
  # levels. If false, no legend data is added and no legend is drawn.
183
+ # @param x_label [String,Symbol,#to_str,nil] X-axis label.
184
+ # @param y_label [String,Symbol,#to_str,nil] Y-axis label.
185
+ # @param title [String,Symbol,#to_str,nil] Title text.
167
186
  def line_plot(x: nil, y: nil, color: nil, style: nil, size: nil,
168
187
  data: nil, key_color: nil, palette: nil, color_order: nil,
169
188
  color_norm: nil, sizes: nil, size_order: nil, size_norm: nil,
170
189
  markers: nil, dashes: true, style_order: nil,
171
190
  units: nil, estimator: :mean, n_boot: 1000, random: nil,
172
191
  sort: true, err_style: :band, err_params: nil, error_bar: [:ci, 95],
173
- x_scale: :linear, y_scale: :linear, legend: :auto, **options, &block)
192
+ x_scale: :linear, y_scale: :linear, legend: :auto,
193
+ x_label: nil, y_label: nil, title: nil,
194
+ **options, &block)
174
195
  Plotters::LinePlotter.new(
175
196
  data: data,
176
197
  variables: { x: x, y: y, color: color, style: style, size: size },
@@ -195,6 +216,9 @@ module Charty
195
216
  x_scale: x_scale,
196
217
  y_scale: y_scale,
197
218
  legend: legend,
219
+ x_label: x_label,
220
+ y_label: y_label,
221
+ title: title,
198
222
  **options,
199
223
  &block
200
224
  )
@@ -225,11 +249,14 @@ module Charty
225
249
  # :full, every group will get an entry in the legend. If :auto,
226
250
  # choose between brief or full representation based on number of
227
251
  # levels. If false, no legend data is added and no legend is drawn.
252
+ # @param x_label [String,Symbol,#to_str,nil] X-axis label.
253
+ # @param y_label [String,Symbol,#to_str,nil] Y-axis label.
254
+ # @param title [String,Symbol,#to_str,nil] Title text.
228
255
  def scatter_plot(x: nil, y: nil, color: nil, style: nil, size: nil,
229
256
  data: nil, key_color: nil, palette: nil, color_order: nil,
230
257
  color_norm: nil, sizes: nil, size_order: nil, size_norm: nil,
231
258
  markers: true, style_order: nil, alpha: nil, legend: :auto,
232
- **options, &block)
259
+ x_label: nil, y_label: nil, title: nil, **options, &block)
233
260
  Plotters::ScatterPlotter.new(
234
261
  data: data,
235
262
  variables: { x: x, y: y, color: color, style: style, size: size },
@@ -244,6 +271,9 @@ module Charty
244
271
  style_order: style_order,
245
272
  alpha: alpha,
246
273
  legend: legend,
274
+ x_label: x_label,
275
+ y_label: y_label,
276
+ title: title,
247
277
  **options,
248
278
  &block
249
279
  )
@@ -253,7 +283,8 @@ module Charty
253
283
  stat: :count, bins: :auto,
254
284
  bin_range: nil, common_bins: true,
255
285
  key_color: nil, palette: nil, color_order: nil, color_norm: nil,
256
- legend: true, **options, &block)
286
+ legend: true, x_label: nil, y_label: nil, title: nil,
287
+ **options, &block)
257
288
  # TODO: support following arguments
258
289
  # - wiehgts
259
290
  # - binwidth
@@ -287,6 +318,9 @@ module Charty
287
318
  color_order: color_order,
288
319
  color_norm: color_norm,
289
320
  legend: legend,
321
+ x_label: x_label,
322
+ y_label: y_label,
323
+ title: title,
290
324
  **options,
291
325
  &block)
292
326
  end
@@ -35,6 +35,8 @@ module Charty
35
35
  end
36
36
 
37
37
  def data=(data)
38
+ # TODO: Convert a Charty::Vector to a Charty::Table so that
39
+ # the Charty::Vector is handled as a wide form data
38
40
  @data = case data
39
41
  when nil, Charty::Table
40
42
  data
@@ -81,6 +83,24 @@ module Charty
81
83
  end
82
84
  end
83
85
 
86
+ attr_reader :x_label
87
+
88
+ def x_label=(val)
89
+ @x_label = check_string(val, :x_label, allow_nil: true)
90
+ end
91
+
92
+ attr_reader :y_label
93
+
94
+ def y_label=(val)
95
+ @y_label = check_string(val, :y_label, allow_nil: true)
96
+ end
97
+
98
+ attr_reader :title
99
+
100
+ def title=(val)
101
+ @title = check_string(val, :title, allow_nil: true)
102
+ end
103
+
84
104
  private def substitute_options(options)
85
105
  options.each do |key, val|
86
106
  send("#{key}=", val)
@@ -138,6 +158,27 @@ module Charty
138
158
  end
139
159
  end
140
160
 
161
+ private def check_string(value, name, allow_nil: false)
162
+ case value
163
+ when Symbol
164
+ value.to_s
165
+ else
166
+ if allow_nil && value.nil?
167
+ nil
168
+ else
169
+ orig_value = value
170
+ value = String.try_convert(value)
171
+ if value.nil?
172
+ raise ArgumentError,
173
+ "`#{name}` must be convertible to String: %p" % orig_value,
174
+ caller
175
+ else
176
+ value
177
+ end
178
+ end
179
+ end
180
+ end
181
+
141
182
  private def variable_type(vector, boolean_type=:numeric)
142
183
  if vector.numeric?
143
184
  :numeric
@@ -181,15 +222,6 @@ module Charty
181
222
  data = processed ? processed_data : plot_data
182
223
  data = data.drop_na if drop_na
183
224
 
184
- levels = var_levels.dup
185
-
186
- ([:x, :y] & grouping_vars).each do |axis|
187
- levels[axis] = plot_data[axis].categorical_order()
188
- if processed
189
- # TODO: perform inverse conversion of axis scaling here
190
- end
191
- end
192
-
193
225
  if not grouping_vars.empty?
194
226
  grouped = data.group_by(grouping_vars, sort: false)
195
227
  grouped.each_group do |group_key, group_data|
@@ -42,6 +42,12 @@ module Charty
42
42
  @cap_size = check_number(cap_size, :cap_size, allow_nil: true)
43
43
  end
44
44
 
45
+ attr_reader :log
46
+
47
+ def log=(val)
48
+ @log = check_boolean(val, :log)
49
+ end
50
+
45
51
  private def render_plot(backend, **)
46
52
  draw_bars(backend)
47
53
  annotate_axes(backend)
@@ -81,6 +87,39 @@ module Charty
81
87
  end
82
88
  end
83
89
 
90
+ private def annotate_axes(backend)
91
+ super
92
+
93
+ if self.log
94
+ min_value, max_value = @estimations.minmax
95
+ if @plot_colors
96
+ unless @conf_int.empty?
97
+ min_value = [min_value, @conf_int[0]].min
98
+ max_value = [max_value, @conf_int[1]].max
99
+ end
100
+ else
101
+ ci_min = Util.filter_map(@conf_int) { |ci| ci[0] unless ci.empty? }
102
+ ci_max = Util.filter_map(@conf_int) { |ci| ci[1] unless ci.empty? }
103
+ min_value = [min_value, ci_min.min].min unless ci_min.empty?
104
+ max_value = [max_value, ci_max.max].max unless ci_max.empty?
105
+ end
106
+ if min_value > 1
107
+ min_value = 0
108
+ else
109
+ min_value = Math.log10(min_value).floor
110
+ end
111
+ max_value = Math.log10(max_value).ceil
112
+ case self.orient
113
+ when :v
114
+ backend.set_yscale(:log)
115
+ backend.set_ylim(min_value, max_value)
116
+ else
117
+ backend.set_xscale(:log)
118
+ backend.set_xlim(min_value, max_value)
119
+ end
120
+ end
121
+ end
122
+
84
123
  private def setup_estimations
85
124
  if @color_names.nil?
86
125
  setup_estimations_with_single_color_group
@@ -134,6 +134,7 @@ module Charty
134
134
  order = @order # TODO: supply order via parameter
135
135
  unless order
136
136
  order = @data.column_names.select do |cn|
137
+ # TODO: Use Charty::Vector#numeric?
137
138
  @data[cn].all? {|x| Float(x, exception: false) }
138
139
  end
139
140
  end
@@ -230,6 +231,7 @@ module Charty
230
231
  end
231
232
  return :h
232
233
  end
234
+
233
235
  case orient
234
236
  when :v
235
237
  if require_numeric && y_type != :numeric
@@ -263,7 +265,9 @@ module Charty
263
265
  private def group_long_form(vals, groups, group_order)
264
266
  grouped_vals = vals.group_by(groups)
265
267
 
266
- plot_data = group_order.map {|g| grouped_vals[g] || [] }
268
+ plot_data = group_order.map do |g|
269
+ grouped_vals[g] || Charty::Vector.new([])
270
+ end
267
271
 
268
272
  if vals.respond_to?(:name)
269
273
  value_label = vals.name
@@ -347,11 +351,15 @@ module Charty
347
351
  end
348
352
 
349
353
  private def annotate_axes(backend)
354
+ backend.set_title(self.title) if self.title
355
+
350
356
  if orient == :v
351
357
  xlabel, ylabel = @group_label, @value_label
352
358
  else
353
359
  xlabel, ylabel = @value_label, @group_label
354
360
  end
361
+ xlabel = self.x_label if self.x_label
362
+ ylabel = self.y_label if self.y_label
355
363
  backend.set_xlabel(xlabel) unless xlabel.nil?
356
364
  backend.set_ylabel(ylabel) unless ylabel.nil?
357
365
 
@@ -3,7 +3,14 @@ module Charty
3
3
  class DistributionPlotter < AbstractPlotter
4
4
  def flat_structure
5
5
  {
6
- x: :values
6
+ x: :@values
7
+ }
8
+ end
9
+
10
+ def wide_structure
11
+ {
12
+ x: :@values,
13
+ color: :@columns
7
14
  }
8
15
  end
9
16
 
@@ -71,7 +78,6 @@ module Charty
71
78
  return
72
79
  end
73
80
 
74
- # TODO: detect flat data
75
81
  flat = data.is_a?(Charty::Vector)
76
82
  if flat
77
83
  @plot_data = {}
@@ -79,10 +85,10 @@ module Charty
79
85
 
80
86
  [:x, :y].each do |var|
81
87
  case self.flat_structure[var]
82
- when :index
88
+ when :@index
83
89
  @plot_data[var] = data.index.to_a
84
90
  @variables[var] = data.index.name
85
- when :values
91
+ when :@values
86
92
  @plot_data[var] = data.to_a
87
93
  @variables[var] = data.name
88
94
  end
@@ -90,8 +96,32 @@ module Charty
90
96
 
91
97
  @plot_data = Charty::Table.new(@plot_data)
92
98
  else
93
- raise NotImplementedError,
94
- "wide-form input is not supported"
99
+ numeric_columns = @data.column_names.select do |cn|
100
+ @data[cn].numeric?
101
+ end
102
+ wide_data = @data[numeric_columns]
103
+
104
+ melt_params = {var_name: :@columns, value_name: :@values }
105
+ if self.wide_structure.include?(:index)
106
+ melt_params[:id_vars] = :@index
107
+ end
108
+
109
+ @plot_data = wide_data.melt(**melt_params)
110
+ @variables = {}
111
+ self.wide_structure.each do |var, attr|
112
+ @plot_data[var] = @plot_data[attr]
113
+
114
+ @variables[var] = case attr
115
+ when :@columns
116
+ wide_data.columns.name
117
+ when :@index
118
+ wide_data.index.name
119
+ else
120
+ nil
121
+ end
122
+ end
123
+
124
+ @plot_data = @plot_data[self.wide_structure.keys]
95
125
  end
96
126
  end
97
127
 
@@ -220,14 +220,16 @@ module Charty
220
220
  end
221
221
 
222
222
  private def annotate_axes(backend)
223
+ backend.set_title(self.title) if self.title
224
+
223
225
  if univariate?
224
- xlabel = self.variables[:x]
225
- ylabel = self.variables[:y]
226
+ xlabel = self.x_label || self.variables[:x]
227
+ ylabel = self.y_label || self.variables[:y]
226
228
  case self.univariate_variable
227
229
  when :x
228
- ylabel = self.stat.to_s.capitalize
230
+ ylabel ||= self.stat.to_s.capitalize
229
231
  else
230
- xlabel = self.stat.to_s.capitalize
232
+ xlabel ||= self.stat.to_s.capitalize
231
233
  end
232
234
  backend.set_ylabel(ylabel) if ylabel
233
235
  backend.set_xlabel(xlabel) if xlabel
@@ -122,7 +122,7 @@ module Charty
122
122
 
123
123
  include RandomSupport
124
124
 
125
- attr_reader :sort, :err_style, :err_kws, :error_bar, :x_scale, :y_scale
125
+ attr_reader :sort, :err_style, :err_kws, :error_bar
126
126
 
127
127
  def sort=(val)
128
128
  @sort = check_boolean(val, :sort)
@@ -211,17 +211,21 @@ module Charty
211
211
  [method, level]
212
212
  end
213
213
 
214
+ attr_reader :x_scale
215
+
214
216
  def x_scale=(val)
215
217
  @x_scale = check_axis_scale(val, :x)
216
218
  end
217
219
 
220
+ attr_reader :y_scale
221
+
218
222
  def y_scale=(val)
219
223
  @y_scale = check_axis_scale(val, :y)
220
224
  end
221
225
 
222
226
  private def check_axis_scale(val, axis)
223
227
  case val
224
- when :linear, "linear", :log10, "log10"
228
+ when :linear, "linear", :log, "log"
225
229
  val.to_sym
226
230
  else
227
231
  raise ArgumentError,
@@ -252,6 +256,15 @@ module Charty
252
256
  sub_data = sub_data.sort_values(sort_cols)
253
257
  end
254
258
 
259
+ # Perform axis scaling
260
+ if x_scale != :linear
261
+ sub_data[:x] = sub_data[:x].scale(x_scale)
262
+ end
263
+ if y_scale != :linear
264
+ sub_data[:y] = sub_data[:y].scale(x_scale)
265
+ end
266
+
267
+ # Perform estimation and error calculation
255
268
  unless estimator.nil?
256
269
  if self.variables.include?(:units)
257
270
  raise "`estimator` is must be nil when specifying `units`"
@@ -261,7 +274,22 @@ module Charty
261
274
  sub_data = grouped.apply(agg_var, &aggregator.method(:aggregate)).reset_index
262
275
  end
263
276
 
264
- # TODO: perform inverse conversion of axis scaling before plot
277
+ # Perform axis inverse scaling
278
+ if x_scale != :linear
279
+ sub_data.column_names.each do |cn|
280
+ if cn.start_with?("x")
281
+ sub_data[cn] = sub_data[cn].scale_inverse(x_scale)
282
+ end
283
+ end
284
+ end
285
+
286
+ if y_scale != :linear
287
+ sub_data.column_names.each do |cn|
288
+ if cn.start_with?("y")
289
+ sub_data[cn] = sub_data[cn].scale_inverse(x_scale)
290
+ end
291
+ end
292
+ end
265
293
 
266
294
  unit_grouping = if self.variables.include?(:units)
267
295
  sub_data.group_by(:units).each_group
@@ -290,10 +318,15 @@ module Charty
290
318
  end
291
319
 
292
320
  private def annotate_axes(backend)
293
- xlabel = self.variables[:x]
294
- ylabel = self.variables[:y]
321
+ backend.set_title(self.title) if self.title
322
+
323
+ xlabel = self.x_label || self.variables[:x]
324
+ ylabel = self.y_label || self.variables[:y]
295
325
  backend.set_xlabel(xlabel) unless xlabel.nil?
296
326
  backend.set_ylabel(ylabel) unless ylabel.nil?
327
+
328
+ backend.set_xscale(x_scale)
329
+ backend.set_yscale(y_scale)
297
330
  end
298
331
  end
299
332
  end
@@ -70,8 +70,10 @@ module Charty
70
70
  end
71
71
 
72
72
  private def annotate_axes(backend)
73
- xlabel = self.variables[:x]
74
- ylabel = self.variables[:y]
73
+ backend.set_title(self.title) if self.title
74
+
75
+ xlabel = self.x_label || self.variables[:x]
76
+ ylabel = self.y_label || self.variables[:y]
75
77
  backend.set_xlabel(xlabel) unless xlabel.nil?
76
78
  backend.set_ylabel(ylabel) unless ylabel.nil?
77
79
  end
data/lib/charty/table.rb CHANGED
@@ -32,20 +32,7 @@ module Charty
32
32
  def_delegators :adapter, :columns, :columns=
33
33
  def_delegators :adapter, :index, :index=
34
34
 
35
- def_delegator :@adapter, :column_names
36
-
37
- def column?(name)
38
- return true if column_names.include?(name)
39
-
40
- case name
41
- when String
42
- column_names.include?(name.to_sym)
43
- when Symbol
44
- column_names.include?(name.to_s)
45
- else
46
- false
47
- end
48
- end
35
+ def_delegators :@adapter, :column_names, :column?
49
36
 
50
37
  def_delegator :@adapter, :data, :raw_data
51
38
 
@@ -65,17 +52,35 @@ module Charty
65
52
  end
66
53
 
67
54
  def [](key)
68
- key = case key
69
- when Symbol
70
- key
71
- else
72
- String.try_convert(key).to_sym
73
- end
74
- if @column_cache.key?(key)
75
- @column_cache[key]
55
+ case key
56
+ when Array
57
+ @adapter[nil, key]
58
+ else
59
+ key = case key
60
+ when Symbol
61
+ key
62
+ else
63
+ String.try_convert(key).to_sym
64
+ end
65
+ if @column_cache.key?(key)
66
+ @column_cache[key]
67
+ else
68
+ @column_cache[key] = @adapter[nil, key]
69
+ end
70
+ end
71
+ end
72
+
73
+ def []=(key, values)
74
+ case key
75
+ when Array
76
+ raise ArgumentError,
77
+ "Substituting multiple keys is not supported"
78
+ when Symbol
79
+ # do nothing
76
80
  else
77
- @column_cache[key] = @adapter[nil, key]
81
+ key = key.to_str.to_sym
78
82
  end
83
+ @adapter[key] = values
79
84
  end
80
85
 
81
86
  def group_by(grouper, sort: true, drop_na: true)
@@ -123,6 +128,8 @@ module Charty
123
128
 
124
129
  def_delegator :adapter, :reset_index
125
130
 
131
+ def_delegator :adapter, :melt
132
+
126
133
  class GroupByBase
127
134
  end
128
135
 
@@ -16,6 +16,19 @@ module Charty
16
16
  columns.to_a
17
17
  end
18
18
 
19
+ def column?(name)
20
+ return true if column_names.include?(name)
21
+
22
+ case name
23
+ when String
24
+ column_names.include?(name.to_sym)
25
+ when Symbol
26
+ column_names.include?(name.to_s)
27
+ else
28
+ false
29
+ end
30
+ end
31
+
19
32
  attr_reader :index
20
33
 
21
34
  def index=(values)
@@ -126,6 +139,81 @@ module Charty
126
139
  )
127
140
  end
128
141
 
142
+ def melt(id_vars: nil, value_vars: nil, var_name: nil, value_name: :value)
143
+ if column?(value_name)
144
+ raise ArgumentError,
145
+ "The value of `value_name` must not be matched to the existing column names."
146
+ end
147
+
148
+ case value_name
149
+ when Symbol
150
+ # do nothing
151
+ else
152
+ value_name = value.to_str.to_sym
153
+ end
154
+
155
+ id_vars = check_melt_vars(id_vars, :id_vars)
156
+ value_vars = check_melt_vars(value_vars, :value_vars) { self.column_names }
157
+ value_vars -= id_vars
158
+
159
+ case var_name
160
+ when nil
161
+ var_name = self.columns.name
162
+ var_name = :variable if var_name.nil?
163
+ when Symbol
164
+ # do nothing
165
+ else
166
+ var_name = var_name.to_str
167
+ end
168
+ var_name = var_name.to_sym
169
+
170
+ n_batch_rows = self.length
171
+ n_target_columns = value_vars.length
172
+ melted_data = id_vars.map { |cn|
173
+ id_values = self[nil, cn].to_a
174
+ [cn.to_sym, id_values * n_target_columns]
175
+ }.to_h
176
+
177
+ melted_data[var_name] = value_vars.map { |cn| Array.new(n_batch_rows, cn) }.flatten
178
+
179
+ melted_data[value_name] = value_vars.map { |cn| self[nil, cn].to_a }.flatten
180
+
181
+ Charty::Table.new(melted_data)
182
+ end
183
+
184
+ private def check_melt_vars(val, name)
185
+ if val.nil?
186
+ val = if block_given?
187
+ yield
188
+ else
189
+ []
190
+ end
191
+ end
192
+ case val
193
+ when nil
194
+ nil
195
+ when Array
196
+ missing = val.reject {|cn| self.column?(cn) }
197
+ if missing.empty?
198
+ val.map do |v|
199
+ case v
200
+ when Symbol
201
+ v.to_s
202
+ else
203
+ v.to_str
204
+ end
205
+ end
206
+ else
207
+ raise ArgumentError,
208
+ "Missing column names in `#{name}` (%s)" % missing.join(", ")
209
+ end
210
+ when Symbol
211
+ [val.to_s]
212
+ else
213
+ [val.to_str]
214
+ end
215
+ end
216
+
129
217
  private def check_na_position(val)
130
218
  case val
131
219
  when :first, "first"
@@ -57,12 +57,52 @@ module Charty
57
57
  column_data = if @data.has_vector?(column)
58
58
  @data[column]
59
59
  else
60
- @data[column.to_s]
60
+ case column
61
+ when String
62
+ @data[column.to_sym]
63
+ else
64
+ @data[column.to_s]
65
+ end
61
66
  end
62
67
  Vector.new(column_data)
63
68
  end
64
69
  end
65
70
 
71
+ def []=(key, values)
72
+ case key
73
+ when Symbol
74
+ sym_key = key
75
+ str_key = key.to_s
76
+ else
77
+ str_key = key.to_str
78
+ sym_key = str_key.to_sym
79
+ end
80
+ case
81
+ when @data.has_vector?(sym_key)
82
+ key = sym_key
83
+ when @data.has_vector?(str_key)
84
+ key = str_key
85
+ end
86
+
87
+ case values
88
+ when Charty::Vector
89
+ case values.adapter
90
+ when Charty::VectorAdapters::DaruVectorAdapter
91
+ @data[key] = values.adapter.data
92
+ else
93
+ @data[key] = values.to_a
94
+ end
95
+ else
96
+ orig_values = values
97
+ values = Array.try_convert(values)
98
+ if values.nil?
99
+ raise ArgumentError, "`values` must be convertible to Array"
100
+ end
101
+ @data[key] = values
102
+ end
103
+ return values
104
+ end
105
+
66
106
  private def check_type(type, data, name)
67
107
  return data if data.is_a?(type)
68
108
  raise TypeError, "#{name} must be a #{type}"
@@ -181,6 +181,11 @@ module Charty
181
181
  @data[column][row]
182
182
  else
183
183
  case column
184
+ when Array
185
+ slice_data = column.map { |cn|
186
+ [cn, self[nil, cn]]
187
+ }.to_h
188
+ return Charty::Table.new(slice_data, index: self.index)
184
189
  when Symbol
185
190
  sym_key = column
186
191
  str_key = column.to_s
@@ -194,10 +199,58 @@ module Charty
194
199
  else
195
200
  @data[str_key]
196
201
  end
197
- Vector.new(column_data, index: index, name: column)
202
+ # FIXME: Here column_data need to be dupped to
203
+ # prevent to overwrite the name of Pandas::Series
204
+ Vector.new(column_data.dup, index: index, name: column)
198
205
  end
199
206
  end
200
207
 
208
+ def []=(key, values)
209
+ case key
210
+ when Symbol
211
+ str_key = key.to_s
212
+ sym_key = key
213
+ else
214
+ str_key = key.to_str
215
+ sym_key = str_key.to_sym
216
+ end
217
+
218
+ orig_values = values
219
+ case values
220
+ when Charty::Vector
221
+ values = values.data
222
+ else
223
+ values = Array.try_convert(values)
224
+ end
225
+ if values.nil?
226
+ raise ArgumentError,
227
+ "`values` must be convertible to Array"
228
+ end
229
+
230
+ if values.length != self.length
231
+ raise ArgumentError,
232
+ "`values` length does not match the length of the table"
233
+ end
234
+
235
+ if @data.key?(sym_key)
236
+ @data[sym_key] = values
237
+ elsif @data.key?(str_key)
238
+ @data[str_key] = values
239
+ elsif key == sym_key
240
+ @data[sym_key] = values
241
+ new_column = sym_key
242
+ else
243
+ @data[str_key] = values
244
+ new_column = sym_key
245
+ end
246
+
247
+ if new_column
248
+ self.columns = Index.new([*self.columns, new_column])
249
+ end
250
+
251
+ values
252
+ end
253
+
201
254
  def each
202
255
  i, n = 0, shape[0]
203
256
  while i < n
@@ -42,6 +42,10 @@ module Charty
42
42
  end
43
43
  end
44
44
 
45
+ def column?(name)
46
+ data.__contains__(name)
47
+ end
48
+
45
49
  def index
46
50
  PandasIndex.new(data.index)
47
51
  end
@@ -76,8 +80,33 @@ module Charty
76
80
  if row
77
81
  @data[column][row]
78
82
  else
79
- Vector.new(@data[column])
83
+ case column
84
+ when Array
85
+ Table.new(@data[column])
86
+ else
87
+ Vector.new(@data[column])
88
+ end
89
+ end
90
+ end
91
+
92
+ def []=(key, values)
93
+ case values
94
+ when Charty::Vector
95
+ case values.adapter
96
+ when Charty::VectorAdapters::PandasSeriesAdapter
97
+ @data[key] = values.adapter.data
98
+ else
99
+ @data[key] = values.to_a
100
+ end
101
+ else
102
+ orig_values = values
103
+ values = Array.try_convert(values)
104
+ if values.nil?
105
+ raise ArgumentError, "`values` must be convertible to Array"
106
+ end
107
+ @data[key] = values
80
108
  end
109
+ return values
81
110
  end
82
111
 
83
112
  def drop_na
@@ -101,6 +130,15 @@ module Charty
101
130
  Charty::Table.new(data.reset_index)
102
131
  end
103
132
 
133
+ def melt(id_vars: nil, value_vars: nil, var_name: nil, value_name: :value)
134
+ id_vars = check_melt_vars(id_vars, :id_vars) { nil }
135
+ value_vars = check_melt_vars(value_vars, :value_vars) { nil }
136
+
137
+ Charty::Table.new(data.melt(id_vars: id_vars, value_vars: value_vars,
138
+ var_name: var_name, value_name: value_name,
139
+ ignore_index: true))
140
+ end
141
+
104
142
  class GroupBy < Charty::Table::GroupByBase
105
143
  def initialize(groupby)
106
144
  @groupby = groupby
@@ -116,6 +154,7 @@ module Charty
116
154
  each_group_key.to_a
117
155
  end
118
156
 
157
+ # TODO: test
119
158
  def each_group_key
120
159
  return enum_for(__method__) unless block_given?
121
160
 
@@ -146,6 +185,15 @@ module Charty
146
185
  end
147
186
  end
148
187
 
188
+ # TODO: test
189
+ def each_group
190
+ return enum_for(__method__) unless block_given?
191
+
192
+ each_group_key do |key|
193
+ yield(Array(key), self[key])
194
+ end
195
+ end
196
+
149
197
  def apply(*args, &block)
150
198
  res = @groupby.apply(->(data) {
151
199
  res = block.call(Charty::Table.new(data), *args)
data/lib/charty/vector.rb CHANGED
@@ -51,6 +51,32 @@ module Charty
51
51
 
52
52
  def_delegators :adapter, :mean, :stdev
53
53
 
54
+ def_delegators :adapter, :scale, :scale_inverse
55
+
56
+ def scale(method)
57
+ case method
58
+ when :linear
59
+ self
60
+ when :log
61
+ adapter.log_scale(method)
62
+ else
63
+ raise ArgumentError,
64
+ "Invalid scaling method: %p" % method
65
+ end
66
+ end
67
+
68
+ def scale_inverse(method)
69
+ case method
70
+ when :linear
71
+ self
72
+ when :log
73
+ adapter.inverse_log_scale(method)
74
+ else
75
+ raise ArgumentError,
76
+ "Invalid scaling method: %p" % method
77
+ end
78
+ end
79
+
54
80
  # TODO: write test
55
81
  def categorical_order(order=nil)
56
82
  if order.nil?
@@ -59,7 +85,9 @@ module Charty
59
85
  order = categories
60
86
  else
61
87
  order = unique_values.compact
62
- order.sort! if numeric?
88
+ if numeric?
89
+ order.sort_by! {|x| Util.missing?(x) ? Float::INFINITY : x }
90
+ end
63
91
  end
64
92
  order.compact!
65
93
  end
@@ -99,6 +99,22 @@ module Charty
99
99
  def stdev(population: false)
100
100
  Statistics.stdev(data, population: population)
101
101
  end
102
+
103
+ def log_scale(method)
104
+ Charty::Vector.new(
105
+ self.map {|x| Math.log10(x) },
106
+ index: index,
107
+ name: name
108
+ )
109
+ end
110
+
111
+ def inverse_log_scale(method)
112
+ Charty::Vector.new(
113
+ self.map {|x| 10.0 ** x },
114
+ index: index,
115
+ name: name
116
+ )
117
+ end
102
118
  end
103
119
 
104
120
  module NameSupport
@@ -152,7 +152,8 @@ module Charty
152
152
  group_keys = grouper.unique.to_a
153
153
  groups = data.groupby(grouper)
154
154
  group_keys.map {|g|
155
- [g, Charty::Vector.new(groups.get_group(g))]
155
+ g_vals = groups.get_group(g) rescue []
156
+ [g, Charty::Vector.new(g_vals)]
156
157
  }.to_h
157
158
  when Charty::Vector
158
159
  case grouper.adapter
@@ -194,6 +195,14 @@ module Charty
194
195
  q = q.map {|x| x / 100.0 }
195
196
  data.quantile(q)
196
197
  end
198
+
199
+ def log_scale(method)
200
+ Charty::Vector.new(Numpy.log10(data))
201
+ end
202
+
203
+ def inverse_log_scale(method)
204
+ Charty::Vector.new(Numpy.power(10, data))
205
+ end
197
206
  end
198
207
  end
199
208
  end
@@ -1,5 +1,5 @@
1
1
  module Charty
2
- VERSION = "0.2.9"
2
+ VERSION = "0.2.10"
3
3
 
4
4
  module Version
5
5
  numbers, TAG = VERSION.split("-")
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: charty
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.9
4
+ version: 0.2.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - youchan
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: exe
12
12
  cert_chain: []
13
- date: 2021-06-24 00:00:00.000000000 Z
13
+ date: 2021-08-18 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: red-colors
@@ -246,6 +246,8 @@ files:
246
246
  - charty.gemspec
247
247
  - examples/Gemfile
248
248
  - examples/active_record.ipynb
249
+ - examples/bar_plot.rb
250
+ - examples/box_plot.rb
249
251
  - examples/daru.ipynb
250
252
  - examples/iris_dataset.ipynb
251
253
  - examples/nmatrix.ipynb
@@ -284,6 +286,7 @@ files:
284
286
  - examples/sample_images/subplot_pyplot.png
285
287
  - examples/sample_pyplot.ipynb
286
288
  - examples/sample_rubyplot.ipynb
289
+ - examples/scatter_plot.rb
287
290
  - images/design_concept.png
288
291
  - images/penguins_body_mass_g_flipper_length_mm_scatter_plot.png
289
292
  - images/penguins_body_mass_g_flipper_length_mm_species_scatter_plot.png
@@ -369,7 +372,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
369
372
  - !ruby/object:Gem::Version
370
373
  version: '0'
371
374
  requirements: []
372
- rubygems_version: 3.2.3
375
+ rubygems_version: 3.2.23
373
376
  signing_key:
374
377
  specification_version: 4
375
378
  summary: Visualizing your data in a simple way.