charty 0.2.3 → 0.2.4

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.
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