charty 0.2.3 → 0.2.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +56 -23
  3. data/.github/workflows/nmatrix.yml +67 -0
  4. data/.github/workflows/pycall.yml +86 -0
  5. data/Gemfile +18 -0
  6. data/README.md +123 -4
  7. data/Rakefile +4 -5
  8. data/charty.gemspec +1 -3
  9. data/examples/sample_images/hist_gruff.png +0 -0
  10. data/images/penguins_body_mass_g_flipper_length_mm_scatter_plot.png +0 -0
  11. data/images/penguins_body_mass_g_flipper_length_mm_species_scatter_plot.png +0 -0
  12. data/images/penguins_body_mass_g_flipper_length_mm_species_sex_scatter_plot.png +0 -0
  13. data/images/penguins_species_body_mass_g_bar_plot_h.png +0 -0
  14. data/images/penguins_species_body_mass_g_bar_plot_v.png +0 -0
  15. data/images/penguins_species_body_mass_g_box_plot_h.png +0 -0
  16. data/images/penguins_species_body_mass_g_box_plot_v.png +0 -0
  17. data/images/penguins_species_body_mass_g_sex_bar_plot_v.png +0 -0
  18. data/images/penguins_species_body_mass_g_sex_box_plot_v.png +0 -0
  19. data/lib/charty.rb +4 -0
  20. data/lib/charty/backends/gruff.rb +13 -2
  21. data/lib/charty/backends/plotly.rb +322 -20
  22. data/lib/charty/backends/pyplot.rb +416 -64
  23. data/lib/charty/index.rb +213 -0
  24. data/lib/charty/linspace.rb +1 -1
  25. data/lib/charty/missing_value_support.rb +14 -0
  26. data/lib/charty/plot_methods.rb +173 -8
  27. data/lib/charty/plotters.rb +7 -0
  28. data/lib/charty/plotters/abstract_plotter.rb +87 -12
  29. data/lib/charty/plotters/bar_plotter.rb +200 -3
  30. data/lib/charty/plotters/box_plotter.rb +75 -7
  31. data/lib/charty/plotters/categorical_plotter.rb +272 -40
  32. data/lib/charty/plotters/count_plotter.rb +7 -0
  33. data/lib/charty/plotters/estimation_support.rb +84 -0
  34. data/lib/charty/plotters/random_support.rb +25 -0
  35. data/lib/charty/plotters/relational_plotter.rb +518 -0
  36. data/lib/charty/plotters/scatter_plotter.rb +115 -0
  37. data/lib/charty/plotters/vector_plotter.rb +6 -0
  38. data/lib/charty/statistics.rb +87 -2
  39. data/lib/charty/table.rb +50 -15
  40. data/lib/charty/table_adapters.rb +2 -0
  41. data/lib/charty/table_adapters/active_record_adapter.rb +17 -9
  42. data/lib/charty/table_adapters/base_adapter.rb +69 -0
  43. data/lib/charty/table_adapters/daru_adapter.rb +37 -3
  44. data/lib/charty/table_adapters/datasets_adapter.rb +6 -2
  45. data/lib/charty/table_adapters/hash_adapter.rb +130 -16
  46. data/lib/charty/table_adapters/narray_adapter.rb +25 -6
  47. data/lib/charty/table_adapters/nmatrix_adapter.rb +15 -5
  48. data/lib/charty/table_adapters/pandas_adapter.rb +81 -0
  49. data/lib/charty/vector.rb +69 -0
  50. data/lib/charty/vector_adapters.rb +183 -0
  51. data/lib/charty/vector_adapters/array_adapter.rb +109 -0
  52. data/lib/charty/vector_adapters/daru_adapter.rb +171 -0
  53. data/lib/charty/vector_adapters/narray_adapter.rb +187 -0
  54. data/lib/charty/vector_adapters/nmatrix_adapter.rb +37 -0
  55. data/lib/charty/vector_adapters/numpy_adapter.rb +168 -0
  56. data/lib/charty/vector_adapters/pandas_adapter.rb +200 -0
  57. data/lib/charty/version.rb +1 -1
  58. metadata +33 -45
@@ -0,0 +1,213 @@
1
+ require "forwardable"
2
+
3
+ module Charty
4
+ class Index
5
+ extend Forwardable
6
+ include Enumerable
7
+
8
+ def initialize(values, name: nil)
9
+ @values = values
10
+ @name = name
11
+ end
12
+
13
+ attr_reader :values
14
+ attr_accessor :name
15
+
16
+ def_delegators :values, :length, :size, :each, :to_a
17
+
18
+ def ==(other)
19
+ case other
20
+ when DaruIndex, PandasIndex
21
+ return false if length != other.length
22
+ to_a == other.to_a
23
+ when Index
24
+ return false if length != other.length
25
+ return true if values == other.values
26
+ to_a == other.to_a
27
+ else
28
+ super
29
+ end
30
+ end
31
+
32
+ def [](i)
33
+ case i
34
+ when 0 ... length
35
+ values[i]
36
+ else
37
+ raise IndexError, "index out of range"
38
+ end
39
+ end
40
+
41
+ def loc(key)
42
+ values.index(key)
43
+ end
44
+
45
+ def union(other)
46
+ case other
47
+ when PandasIndex
48
+ index = PandasIndex.try_convert(self)
49
+ return index.union(other) if index
50
+ end
51
+
52
+ Index.new(to_a.union(other.to_a))
53
+ end
54
+ end
55
+
56
+ class RangeIndex < Index
57
+ def initialize(values, name: nil)
58
+ if values.is_a?(Range) && values.begin.is_a?(Integer) && values.end.is_a?(Integer)
59
+ super
60
+ else
61
+ raise ArgumentError, "values must be an integer range"
62
+ end
63
+ end
64
+
65
+ def length
66
+ size
67
+ end
68
+
69
+ def [](i)
70
+ case i
71
+ when 0 ... length
72
+ values.begin + i
73
+ else
74
+ raise IndexError, "index out of range (#{i} for 0 ... #{length})"
75
+ end
76
+ end
77
+
78
+ def loc(key)
79
+ case key
80
+ when Integer
81
+ if values.cover?(key)
82
+ return key - values.begin
83
+ end
84
+ end
85
+ end
86
+
87
+ def union(other)
88
+ case other
89
+ when RangeIndex
90
+ return union(other.values)
91
+ when Range
92
+ if disjoint_range?(values, other)
93
+ return Index.new(values.to_a.union(other.to_a))
94
+ end
95
+ new_beg = [values.begin, other.begin].min
96
+ new_end = [values.end, other.end ].max
97
+ new_range = if values.end < new_end
98
+ if other.exclude_end?
99
+ new_beg ... new_end
100
+ else
101
+ new_beg .. new_end
102
+ end
103
+ elsif other.end < new_end
104
+ if values.exclude_end?
105
+ new_beg ... new_end
106
+ else
107
+ new_beg .. new_end
108
+ end
109
+ else
110
+ if values.exclude_end? && other.exclude_end?
111
+ new_beg ... new_end
112
+ else
113
+ new_beg .. new_end
114
+ end
115
+ end
116
+ RangeIndex.new(new_range)
117
+ else
118
+ super
119
+ end
120
+ end
121
+
122
+ private def disjoint_range?(r1, r2)
123
+ r1.end < r2.begin || r2.end < r1.begin
124
+ end
125
+ end
126
+
127
+ class DaruIndex < Index
128
+ def_delegators :values, :name, :name=
129
+
130
+ def length
131
+ size
132
+ end
133
+
134
+ def ==(other)
135
+ case other
136
+ when DaruIndex
137
+ values == other.values
138
+ else
139
+ super
140
+ end
141
+ end
142
+ end
143
+
144
+ class PandasIndex < Index
145
+ def self.try_convert(obj)
146
+ case obj
147
+ when PandasIndex
148
+ obj
149
+ when ->(x) { defined?(Pandas) && x.is_a?(Pandas::Index) }
150
+ PandasIndex.new(obj)
151
+ when RangeIndex, Range
152
+ obj = obj.values if obj.is_a?(RangeIndex)
153
+ stop = if obj.exclude_end?
154
+ obj.end
155
+ else
156
+ obj.end + 1
157
+ end
158
+ PandasIndex.new(Pandas.RangeIndex.new(obj.begin, stop))
159
+ when ->(x) { defined?(Enumerator::ArithmeticSequence) && x.is_a?(Enumerator::ArithmeticSequence) }
160
+ stop = if obj.exclude_end?
161
+ obj.end
162
+ else
163
+ obj.end + 1
164
+ end
165
+ PandasIndex.new(Pandas::RangeIndex.new(obj.begin, stop, obj.step))
166
+ when Index, Array, DaruIndex, ->(x) { defined?(Daru) && x.is_a?(Daru::Index) }
167
+ obj = obj.values if obj.is_a?(Index)
168
+ PandasIndex.new(Pandas::Index.new(obj.to_a))
169
+ else
170
+ nil
171
+ end
172
+ end
173
+
174
+ def_delegators :values, :name, :name=
175
+
176
+ def length
177
+ size
178
+ end
179
+
180
+ def ==(other)
181
+ case other
182
+ when PandasIndex
183
+ Numpy.all(values == other.values)
184
+ when Index
185
+ return false if length != other.length
186
+ Numpy.all(values == other.values.to_a)
187
+ else
188
+ super
189
+ end
190
+ end
191
+
192
+ def each(&block)
193
+ return enum_for(__method__) unless block_given?
194
+
195
+ i, n = 0, length
196
+ while i < n
197
+ yield self[i]
198
+ i += 1
199
+ end
200
+ end
201
+
202
+ def union(other)
203
+ other = PandasIndex.try_convert(other)
204
+ # NOTE: Using `sort=False` in pandas.Index#union does not produce pandas.RangeIndex.
205
+ # TODO: Reconsider to use `sort=True` here.
206
+ PandasIndex.new(values.union(other.values, sort: false))
207
+ end
208
+
209
+ private def arithmetic_sequence?(x)
210
+ defined?(Enumerator::ArithmeticSequence) && x.is_a?(Enumerator::ArithmeticSequence)
211
+ end
212
+ end
213
+ end
@@ -8,7 +8,7 @@ module Charty
8
8
  end
9
9
 
10
10
  def each(&block)
11
- step = (@range.end - @range.begin).to_f / @num_step
11
+ step = (@range.end - @range.begin).to_r / (@num_step - 1)
12
12
  (@num_step - 1).times do |i|
13
13
  block.call(@range.begin + i * step)
14
14
  end
@@ -0,0 +1,14 @@
1
+ module Charty
2
+ module MissingValueSupport
3
+ def missing_value?(val)
4
+ case
5
+ when val.nil?
6
+ true
7
+ when val.respond_to?(:nan?) && val.nan?
8
+ true
9
+ else
10
+ false
11
+ end
12
+ end
13
+ end
14
+ end
@@ -2,16 +2,181 @@ module Charty
2
2
  module PlotMethods
3
3
  # Show the given data as rectangular bars.
4
4
  #
5
- # @param x
6
- # @param y
7
- # @param color
8
- # @param data
9
- def bar_plot(x=nil, y=nil, color=nil, **options, &block)
10
- Plotters::BarPlotter.new(x, y, color, **options, &block)
5
+ # @param x x-dimension input for plotting long-form data.
6
+ # @param y y-dimension input for plotting long-form data.
7
+ # @param color color-dimension input for plotting long-form data.
8
+ # @param data Dataset for plotting.
9
+ # @param order Order of the categorical dimension to plot the categorical levels in.
10
+ # @param color_order Order of the color dimension to plot the categorical levels in.
11
+ # @param estimator Statistical function to estimate withint each categorical bin.
12
+ # @param ci Size of confidence intervals to draw around estimated values.
13
+ # @param n_boot The size of bootstrap sample to use when computing confidence intervals.
14
+ # @param units Identifier of sampling unit.
15
+ # @param random Random seed or random number generator for reproducible bootstrapping.
16
+ # @param orient Orientation of the plot (:v for vertical, or :h for
17
+ # horizontal).
18
+ # @param key_color Color for all of the elements, or seed for a gradient palette.
19
+ # @param palette Colors to use for the different levels of the color-dimension variable.
20
+ # @param saturation Propotion of the original saturation to draw colors.
21
+ # @param error_color Color for the lines that represent the confidence intervals.
22
+ # @param error_width Thickness of error bar lines (and caps).
23
+ # @param cap_size Width of the caps on error bars.
24
+ # @param dodge [true,false] If true, bar position is shifted along the
25
+ # categorical axis for avoid overlapping when the color-dimension is used.
26
+ def bar_plot(x: nil, y: nil, color: nil, data: nil,
27
+ order: nil, color_order: nil,
28
+ estimator: :mean, ci: 95, n_boot: 1000, units: nil, random: nil,
29
+ orient: nil, key_color: nil, palette: nil, saturation: 1r,
30
+ error_color: [0.26, 0.26, 0.26], error_width: nil, cap_size: nil,
31
+ dodge: true, **options, &block)
32
+ Plotters::BarPlotter.new(
33
+ data: data, variables: { x: x, y: y, color: color },
34
+ order: order, orient: orient,
35
+ estimator: estimator, ci: ci, n_boot: n_boot, units: units, random: random,
36
+ color_order: color_order, key_color: key_color, palette: palette, saturation: saturation,
37
+ error_color: error_color, error_width: error_width, cap_size: cap_size,
38
+ dodge: dodge,
39
+ **options, &block
40
+ )
11
41
  end
12
42
 
13
- def box_plot(x=nil, y=nil, color=nil, **options, &block)
14
- Plotters::BoxPlotter.new(x, y, color, **options, &block)
43
+ def count_plot(x: nil, y: nil, color: nil, data: nil,
44
+ order: nil, color_order: nil,
45
+ orient: nil, key_color: nil, palette: nil, saturation: 1r,
46
+ dodge: true, **options, &block)
47
+ case
48
+ when x.nil? && !y.nil?
49
+ x = y
50
+ orient = :h
51
+ when y.nil? && !x.nil?
52
+ y = x
53
+ orient = :v
54
+ when !x.nil? && !y.nil?
55
+ raise ArgumentError,
56
+ "Unable to pass both x and y to count_plot"
57
+ end
58
+
59
+ Plotters::CountPlotter.new(
60
+ data: data,
61
+ variables: { x: x, y: y, color: color },
62
+ order: order,
63
+ orient: orient,
64
+ estimator: :count,
65
+ ci: nil,
66
+ units: nil,
67
+ random: nil,
68
+ color_order: color_order,
69
+ key_color: key_color,
70
+ palette: palette,
71
+ saturation: saturation,
72
+ dodge: dodge,
73
+ **options
74
+ ) do |plotter|
75
+ plotter.value_label = "count"
76
+ block.(plotter) unless block.nil?
77
+ end
78
+ end
79
+
80
+ # Show the distributions of the given data by boxes and whiskers.
81
+ #
82
+ # @param x X-dimension input for plotting long-Form data.
83
+ # @param y Y-dimension input for plotting long-form data.
84
+ # @param color Color-dimension input for plotting long-form data.
85
+ # @param data Dataset for plotting.
86
+ # @param order Order of the categorical dimension to plot the categorical
87
+ # levels in.
88
+ # @param color_order Order of the color dimension to plot the categorical
89
+ # levels in.
90
+ # @param orient Orientation of the plot (:v for vertical, or :h for
91
+ # horizontal).
92
+ # @param key_color Color for all of the elements, or seed for a gradient
93
+ # palette.
94
+ # @param palette Colors to use for the different levels of the
95
+ # color-dimension variable.
96
+ # @param saturation Propotion of the original saturation to draw colors.
97
+ # @param width Width of a full element when not using the color-dimension,
98
+ # or width of all the elements for one level of the major grouping
99
+ # variable.
100
+ # @param dodge [true,false] If true, bar position is shifted along the
101
+ # categorical axis for avoid overlapping when the color-dimension
102
+ # is used.
103
+ # @param flier_size Size of the markers used to indicate outlier
104
+ # observations.
105
+ # @param line_width Width of the gray lines that frame the plot elements.
106
+ # @param whisker Propotion of the IQR past the low and high quartiles to
107
+ # extend the plot whiskers. Points outside of this range will be
108
+ # treated as outliers.
109
+ def box_plot(x: nil, y: nil, color: nil, data: nil,
110
+ order: nil, color_order: nil,
111
+ orient: nil, key_color: nil, palette: nil, saturation: 1r,
112
+ width: 0.8r, dodge: true, flier_size: 5, line_width: nil,
113
+ whisker: 1.5, **options, &block)
114
+ Plotters::BoxPlotter.new(
115
+ data: data,
116
+ variables: { x: x, y: y, color: color },
117
+ order: order,
118
+ color_order: color_order,
119
+ orient: orient,
120
+ key_color: key_color,
121
+ palette: palette,
122
+ saturation: saturation,
123
+ width: width,
124
+ dodge: dodge,
125
+ flier_size: flier_size,
126
+ line_width: line_width,
127
+ whisker: whisker,
128
+ **options,
129
+ &block
130
+ )
131
+ end
132
+
133
+ # Scatter plot
134
+ #
135
+ # @param x [vector-like object, key in data]
136
+ # @param y [vector-like object, key in data]
137
+ # @param color [vector-like object, key in data]
138
+ # @param style [vector-like object, key in data]
139
+ # @param size [vector-like object, key in data]
140
+ # @param data [table-like object]
141
+ # @param key_color [color object]
142
+ # @param palette [String,Array<Numeric>,Palette]
143
+ # @param color_order [Array<String>,Array<Symbol>]
144
+ # @param color_norm
145
+ # @param sizes [Array, Hash]
146
+ # @param size_order [Array]
147
+ # @param size_norm
148
+ # @param markers [true, false, Array, Hash]
149
+ # @param marker_order [Array]
150
+ # @param alpha [scalar number]
151
+ # Propotional opacity of the points.
152
+ # @param legend [:auto, :brief, :full, false]
153
+ # How to draw legend. If :brief, numeric color and size variables
154
+ # will be represented with a sample of evenly spaced values. If
155
+ # :full, every group will get an entry in the legend. If :auto,
156
+ # choose between brief or full representation based on number of
157
+ # levels. If false, no legend data is added and no legend is drawn.
158
+ def scatter_plot(x: nil, y: nil, color: nil, style: nil, size: nil,
159
+ data: nil, key_color: nil, palette: nil, color_order: nil,
160
+ color_norm: nil, sizes: nil, size_order: nil, size_norm: nil,
161
+ markers: true, marker_order: nil, alpha: nil, legend: :auto,
162
+ **options, &block)
163
+ Plotters::ScatterPlotter.new(
164
+ data: data,
165
+ variables: { x: x, y: y, color: color, style: style, size: size },
166
+ key_color: key_color,
167
+ palette: palette,
168
+ color_order: color_order,
169
+ color_norm: color_norm,
170
+ sizes: sizes,
171
+ size_order: size_order,
172
+ size_norm: size_norm,
173
+ markers: markers,
174
+ marker_order: marker_order,
175
+ alpha: alpha,
176
+ legend: legend,
177
+ **options,
178
+ &block
179
+ )
15
180
  end
16
181
  end
17
182
 
@@ -1,4 +1,11 @@
1
1
  require_relative "plotters/abstract_plotter"
2
+ require_relative "plotters/random_support"
3
+ require_relative "plotters/estimation_support"
2
4
  require_relative "plotters/categorical_plotter"
3
5
  require_relative "plotters/bar_plotter"
4
6
  require_relative "plotters/box_plotter"
7
+ require_relative "plotters/count_plotter"
8
+
9
+ require_relative "plotters/vector_plotter"
10
+ require_relative "plotters/relational_plotter"
11
+ require_relative "plotters/scatter_plotter"
@@ -11,7 +11,17 @@ module Charty
11
11
  yield self if block_given?
12
12
  end
13
13
 
14
- attr_reader :x, :y, :color, :data, :palette
14
+ attr_reader :data, :x, :y, :color
15
+ attr_reader :color_order, :key_color, :palette
16
+
17
+ def data=(data)
18
+ @data = case data
19
+ when nil, Charty::Table
20
+ data
21
+ else
22
+ Charty::Table.new(data)
23
+ end
24
+ end
15
25
 
16
26
  def x=(x)
17
27
  @x = check_dimension(x, :x)
@@ -22,20 +32,24 @@ module Charty
22
32
  end
23
33
 
24
34
  def color=(color)
25
- # @color = check_dimension(color, :color)
26
- unless color.nil?
35
+ @color = check_dimension(color, :color)
36
+ end
37
+
38
+ def color_order=(color_order)
39
+ #@color_order = XXX
40
+ unless color_order.nil?
27
41
  raise NotImplementedError,
28
- "Specifying color variable is not supported yet"
42
+ "Specifying color_order is not supported yet"
29
43
  end
30
44
  end
31
45
 
32
- def data=(data)
33
- @data = case data
34
- when nil, Charty::Table
35
- data
36
- else
37
- Charty::Table.new(data)
38
- end
46
+ # TODO: move to categorical_plotter
47
+ def key_color=(key_color)
48
+ #@key_color = XXX
49
+ unless key_color.nil?
50
+ raise NotImplementedError,
51
+ "Specifying key_color is not supported yet"
52
+ end
39
53
  end
40
54
 
41
55
  def palette=(palette)
@@ -57,10 +71,12 @@ module Charty
57
71
 
58
72
  private def check_dimension(value, name)
59
73
  case value
60
- when nil, Symbol, String, method(:array?)
74
+ when nil, Symbol, String
61
75
  value
62
76
  when ->(x) { x.respond_to?(:to_str) }
63
77
  value.to_str
78
+ when method(:array?)
79
+ Charty::Vector.new(value)
64
80
  else
65
81
  raise ArgumentError,
66
82
  "invalid type of dimension for #{name} (given #{value.inspect})",
@@ -68,10 +84,69 @@ module Charty
68
84
  end
69
85
  end
70
86
 
87
+ private def check_number(value, name, allow_nil: false)
88
+ case value
89
+ when Numeric
90
+ value
91
+ else
92
+ if allow_nil && value.nil?
93
+ nil
94
+ else
95
+ expected = if allow_nil
96
+ "number or nil"
97
+ else
98
+ "number"
99
+ end
100
+ raise ArgumentError,
101
+ "invalid value for #{name} (%p for #{expected})" % value,
102
+ caller
103
+ end
104
+ end
105
+ end
106
+
107
+ private def check_boolean(value, name, allow_nil: false)
108
+ case value
109
+ when true, false
110
+ value
111
+ else
112
+ expected = if allow_nil
113
+ "true, false, or nil"
114
+ else
115
+ "true or false"
116
+ end
117
+ raise ArgumentError,
118
+ "invalid value for #{name} (%p for #{expected})" % value,
119
+ caller
120
+ end
121
+ end
122
+
123
+ private def variable_type(vector, boolean_type=:numeric)
124
+ if vector.numeric?
125
+ :numeric
126
+ elsif vector.categorical?
127
+ :categorical
128
+ else
129
+ case vector[0]
130
+ when true, false
131
+ boolean_type
132
+ else
133
+ :categorical
134
+ end
135
+ end
136
+ end
137
+
71
138
  private def array?(value)
72
139
  TableAdapters::HashAdapter.array?(value)
73
140
  end
74
141
 
142
+ private def remove_na!(ary)
143
+ ary.reject! do |x|
144
+ next true if x.nil?
145
+ x.respond_to?(:nan?) && x.nan?
146
+ end
147
+ ary
148
+ end
149
+
75
150
  def to_iruby
76
151
  result = render
77
152
  ["text/html", result] if result