charty 0.2.0 → 0.2.1

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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +38 -0
  3. data/charty.gemspec +1 -0
  4. data/examples/Gemfile +1 -0
  5. data/examples/active_record.ipynb +1 -1
  6. data/examples/daru.ipynb +1 -1
  7. data/examples/iris_dataset.ipynb +1 -1
  8. data/examples/nmatrix.ipynb +1 -1
  9. data/examples/{numo-narray.ipynb → numo_narray.ipynb} +1 -1
  10. data/examples/palette.rb +71 -0
  11. data/examples/sample.png +0 -0
  12. data/examples/sample_pyplot.ipynb +40 -38
  13. data/lib/charty.rb +7 -0
  14. data/lib/charty/backend_methods.rb +8 -0
  15. data/lib/charty/backends.rb +25 -1
  16. data/lib/charty/backends/bokeh.rb +29 -29
  17. data/lib/charty/backends/{google_chart.rb → google_charts.rb} +74 -32
  18. data/lib/charty/backends/plotly.rb +145 -7
  19. data/lib/charty/backends/pyplot.rb +163 -33
  20. data/lib/charty/backends/rubyplot.rb +1 -1
  21. data/lib/charty/palette.rb +235 -0
  22. data/lib/charty/plot_methods.rb +19 -0
  23. data/lib/charty/plotter.rb +9 -9
  24. data/lib/charty/plotters.rb +4 -0
  25. data/lib/charty/plotters/abstract_plotter.rb +81 -0
  26. data/lib/charty/plotters/bar_plotter.rb +19 -0
  27. data/lib/charty/plotters/box_plotter.rb +26 -0
  28. data/lib/charty/plotters/categorical_plotter.rb +148 -0
  29. data/lib/charty/statistics.rb +29 -0
  30. data/lib/charty/table.rb +1 -0
  31. data/lib/charty/table_adapters/active_record_adapter.rb +1 -1
  32. data/lib/charty/table_adapters/daru_adapter.rb +2 -0
  33. data/lib/charty/table_adapters/datasets_adapter.rb +4 -0
  34. data/lib/charty/table_adapters/hash_adapter.rb +2 -0
  35. data/lib/charty/table_adapters/narray_adapter.rb +1 -1
  36. data/lib/charty/table_adapters/nmatrix_adapter.rb +1 -1
  37. data/lib/charty/version.rb +1 -1
  38. metadata +30 -5
  39. data/.travis.yml +0 -10
@@ -12,7 +12,7 @@ module Charty
12
12
  end
13
13
 
14
14
  def initialize
15
- @plot = ::Matplotlib::Pyplot
15
+ @pyplot = ::Matplotlib::Pyplot
16
16
  end
17
17
 
18
18
  def self.activate_iruby_integration
@@ -28,91 +28,221 @@ module Charty
28
28
  end
29
29
 
30
30
  def render_layout(layout)
31
- _fig, axes = @plot.subplots(nrows: layout.num_rows, ncols: layout.num_cols)
31
+ _fig, axes = @pyplot.subplots(nrows: layout.num_rows, ncols: layout.num_cols)
32
32
  layout.rows.each_with_index do |row, y|
33
33
  row.each_with_index do |cel, x|
34
- plot = layout.num_rows > 1 ? axes[y][x] : axes[x]
35
- plot(plot, cel, subplot: true)
34
+ ax = layout.num_rows > 1 ? axes[y][x] : axes[x]
35
+ plot(ax, cel, subplot: true)
36
36
  end
37
37
  end
38
- @plot.show
38
+ @pyplot.show
39
39
  end
40
40
 
41
41
  def render(context, filename)
42
- plot(@plot, context)
42
+ plot(@pyplot, context)
43
43
  if filename
44
44
  FileUtils.mkdir_p(File.dirname(filename))
45
- @plot.savefig(filename)
45
+ @pyplot.savefig(filename)
46
46
  end
47
- @plot.show
47
+ @pyplot.show
48
48
  end
49
49
 
50
- def save(context, filename)
51
- plot(@plot, context)
50
+ def save(context, filename, finish: true)
51
+ plot(context)
52
52
  if filename
53
53
  FileUtils.mkdir_p(File.dirname(filename))
54
- @plot.savefig(filename)
54
+ @pyplot.savefig(filename)
55
55
  end
56
+ @pyplot.clf if finish
56
57
  end
57
58
 
58
- def plot(plot, context, subplot: false)
59
+ def plot(ax, context, subplot: false)
59
60
  # TODO: Since it is not required, research and change conditions.
60
61
  # case
61
- # when plot.respond_to?(:xlim)
62
- # plot.xlim(context.range_x.begin, context.range_x.end)
63
- # plot.ylim(context.range_y.begin, context.range_y.end)
64
- # when plot.respond_to?(:set_xlim)
65
- # plot.set_xlim(context.range_x.begin, context.range_x.end)
66
- # plot.set_ylim(context.range_y.begin, context.range_y.end)
62
+ # when @pyplot.respond_to?(:xlim)
63
+ # @pyplot.xlim(context.range_x.begin, context.range_x.end)
64
+ # @pyplot.ylim(context.range_y.begin, context.range_y.end)
65
+ # when @pyplot.respond_to?(:set_xlim)
66
+ # @pyplot.set_xlim(context.range_x.begin, context.range_x.end)
67
+ # @pyplot.set_ylim(context.range_y.begin, context.range_y.end)
67
68
  # end
68
69
 
69
- plot.title(context.title) if context.title
70
+ ax.title(context.title) if context.title
70
71
  if !subplot
71
- plot.xlabel(context.xlabel) if context.xlabel
72
- plot.ylabel(context.ylabel) if context.ylabel
72
+ ax.xlabel(context.xlabel) if context.xlabel
73
+ ax.ylabel(context.ylabel) if context.ylabel
73
74
  end
74
75
 
76
+ palette = Charty::Palette.default
77
+ colors = palette.colors.map {|c| c.to_rgb.to_hex_string }.cycle
75
78
  case context.method
76
79
  when :bar
77
80
  context.series.each do |data|
78
- plot.bar(data.xs.to_a.map(&:to_s), data.ys.to_a, label: data.label)
81
+ ax.bar(data.xs.to_a.map(&:to_s), data.ys.to_a, label: data.label,
82
+ color: colors.next)
79
83
  end
80
- plot.legend()
84
+ ax.legend()
81
85
  when :barh
82
86
  context.series.each do |data|
83
- plot.barh(data.xs.to_a.map(&:to_s), data.ys.to_a)
87
+ ax.barh(data.xs.to_a.map(&:to_s), data.ys.to_a, color: colors.next)
84
88
  end
85
89
  when :box_plot
86
- plot.boxplot(context.data.to_a, labels: context.labels)
90
+ min_l = palette.colors.map {|c| c.to_rgb.to_hsl.l }.min
91
+ lum = min_l*0.6
92
+ gray = Colors::RGB.new(lum, lum, lum).to_hex_string
93
+ draw_box_plot(context, subplot, colors, gray)
87
94
  when :bubble
88
95
  context.series.each do |data|
89
- plot.scatter(data.xs.to_a, data.ys.to_a, s: data.zs.to_a, alpha: 0.5, label: data.label)
96
+ ax.scatter(data.xs.to_a, data.ys.to_a, s: data.zs.to_a, alpha: 0.5,
97
+ color: colors.next, label: data.label)
90
98
  end
91
- plot.legend()
99
+ ax.legend()
92
100
  when :curve
93
101
  context.series.each do |data|
94
- plot.plot(data.xs.to_a, data.ys.to_a)
102
+ ax.plot(data.xs.to_a, data.ys.to_a, color: colors.next)
95
103
  end
96
104
  when :scatter
97
105
  context.series.each do |data|
98
- plot.scatter(data.xs.to_a, data.ys.to_a, label: data.label)
106
+ ax.scatter(data.xs.to_a, data.ys.to_a, label: data.label,
107
+ color: colors.next)
99
108
  end
100
- plot.legend()
109
+ ax.legend()
101
110
  when :error_bar
102
111
  context.series.each do |data|
103
- plot.errorbar(
112
+ ax.errorbar(
104
113
  data.xs.to_a,
105
114
  data.ys.to_a,
106
115
  data.xerr,
107
116
  data.yerr,
108
117
  label: data.label,
118
+ color: colors.next
109
119
  )
110
120
  end
111
- plot.legend()
121
+ ax.legend()
112
122
  when :hist
113
- plot.hist(context.data.to_a)
123
+ data = Array(context.data)
124
+ ax.hist(data, color: colors.take(data.length), alpha: 0.4)
114
125
  end
115
126
  end
127
+
128
+ private def draw_box_plot(context, subplot, colors, gray)
129
+ Array(context.data).each_with_index do |group_data, i|
130
+ next if group_data.empty?
131
+
132
+ box_data = group_data.compact
133
+ next if box_data.empty?
134
+
135
+ artist_dict = @pyplot.boxplot(box_data, vert: "v", patch_artist: true,
136
+ positions: [i], widths: 0.8)
137
+
138
+ color = colors.next
139
+ artist_dict["boxes"].each do |box|
140
+ box.update({facecolor: color, zorder: 0.9, edgecolor: gray}, {})
141
+ end
142
+ artist_dict["whiskers"].each do |whisker|
143
+ whisker.update({color: gray, linestyle: "-"}, {})
144
+ end
145
+ artist_dict["caps"].each do |cap|
146
+ cap.update({color: gray}, {})
147
+ end
148
+ artist_dict["medians"].each do |median|
149
+ median.update({color: gray}, {})
150
+ end
151
+ artist_dict["fliers"].each do |flier|
152
+ flier.update({
153
+ markerfacecolor: gray,
154
+ marker: "d",
155
+ markeredgecolor: gray,
156
+ markersize: 5
157
+ }, {})
158
+ end
159
+ end
160
+ end
161
+
162
+ # ==== NEW PLOTTING API ====
163
+
164
+ def begin_figure
165
+ # do nothing
166
+ end
167
+
168
+ def bar(bar_pos, values, color: nil, width: 0.8r, align: :center, orient: :v)
169
+ bar_pos = Array(bar_pos)
170
+ values = Array(values)
171
+ color = Array(color).map(&:to_hex_string)
172
+ width = Float(width)
173
+ if orient == :v
174
+ @pyplot.bar(bar_pos, values, width: width, color: color, align: align)
175
+ else
176
+ @pyplot.barh(bar_pos, values, width: width, color: color, align: align)
177
+ end
178
+ end
179
+
180
+ def box_plot(plot_data, positions, color:, gray:,
181
+ width: 0.8r, flier_size: 5, whisker: 1.5, notch: false)
182
+ color = Array(color).map(&:to_hex_string)
183
+ gray = gray.to_hex_string
184
+ width = Float(width)
185
+ flier_size = Float(flier_size)
186
+ whisker = Float(whisker)
187
+ plot_data.each_with_index do |group_data, i|
188
+ next if group_data.nil? || group_data.empty?
189
+
190
+ artist_dict = @pyplot.boxplot(group_data, vert: :v,
191
+ patch_artist: true,
192
+ positions: [i],
193
+ widths: width,
194
+ whis: whisker, )
195
+
196
+ artist_dict["boxes"].each do |box|
197
+ box.update({facecolor: color[i], zorder: 0.9, edgecolor: gray}, {})
198
+ end
199
+ artist_dict["whiskers"].each do |whisker|
200
+ whisker.update({color: gray, linestyle: "-"}, {})
201
+ end
202
+ artist_dict["caps"].each do |cap|
203
+ cap.update({color: gray}, {})
204
+ end
205
+ artist_dict["medians"].each do |median|
206
+ median.update({color: gray}, {})
207
+ end
208
+ artist_dict["fliers"].each do |flier|
209
+ flier.update({
210
+ markerfacecolor: gray,
211
+ marker: "d",
212
+ markeredgecolor: gray,
213
+ markersize: flier_size
214
+ }, {})
215
+ end
216
+ end
217
+ end
218
+
219
+ def set_xlabel(label)
220
+ @pyplot.gca.set_xlabel(String(label))
221
+ end
222
+
223
+ def set_ylabel(label)
224
+ @pyplot.gca.set_ylabel(String(label))
225
+ end
226
+
227
+ def set_xticks(values)
228
+ @pyplot.gca.set_xticks(Array(values))
229
+ end
230
+
231
+ def set_xtick_labels(labels)
232
+ @pyplot.gca.set_xticklabels(Array(labels).map(&method(:String)))
233
+ end
234
+
235
+ def set_xlim(min, max)
236
+ @pyplot.gca.set_xlim(Float(min), Float(max))
237
+ end
238
+
239
+ def disable_xaxis_grid
240
+ @pyplot.gca.xaxis.grid(false)
241
+ end
242
+
243
+ def show
244
+ @pyplot.show
245
+ end
116
246
  end
117
247
  end
118
248
  end
@@ -23,7 +23,7 @@ module Charty
23
23
  end
24
24
 
25
25
  def render_layout(layout)
26
- (fig, axes) = *@plot.subplots(nrows: layout.num_rows, ncols: layout.num_cols)
26
+ (_fig, axes) = *@plot.subplots(nrows: layout.num_rows, ncols: layout.num_cols)
27
27
  layout.rows.each_with_index do |row, y|
28
28
  row.each_with_index do |cel, x|
29
29
  plot = layout.num_rows > 1 ? axes[y][x] : axes[x]
@@ -0,0 +1,235 @@
1
+ require "numo/narray"
2
+
3
+ module Charty
4
+ class Palette
5
+ SEABORN_PALETTES = {
6
+ "deep" => ["#4C72B0", "#DD8452", "#55A868", "#C44E52", "#8172B3",
7
+ "#937860", "#DA8BC3", "#8C8C8C", "#CCB974", "#64B5CD"].freeze,
8
+ "deep6" => ["#4C72B0", "#55A868", "#C44E52",
9
+ "#8172B3", "#CCB974", "#64B5CD"].freeze,
10
+ "muted" => ["#4878D0", "#EE854A", "#6ACC64", "#D65F5F", "#956CB4",
11
+ "#8C613C", "#DC7EC0", "#797979", "#D5BB67", "#82C6E2"].freeze,
12
+ "muted6" => ["#4878D0", "#6ACC64", "#D65F5F",
13
+ "#956CB4", "#D5BB67", "#82C6E2"].freeze,
14
+ "pastel" => ["#A1C9F4", "#FFB482", "#8DE5A1", "#FF9F9B", "#D0BBFF",
15
+ "#DEBB9B", "#FAB0E4", "#CFCFCF", "#FFFEA3", "#B9F2F0"].freeze,
16
+ "pastel6" => ["#A1C9F4", "#8DE5A1", "#FF9F9B",
17
+ "#D0BBFF", "#FFFEA3", "#B9F2F0"].freeze,
18
+ "bright" => ["#023EFF", "#FF7C00", "#1AC938", "#E8000B", "#8B2BE2",
19
+ "#9F4800", "#F14CC1", "#A3A3A3", "#FFC400", "#00D7FF"].freeze,
20
+ "bright6" => ["#023EFF", "#1AC938", "#E8000B",
21
+ "#8B2BE2", "#FFC400", "#00D7FF"].freeze,
22
+ "dark" => ["#001C7F", "#B1400D", "#12711C", "#8C0800", "#591E71",
23
+ "#592F0D", "#A23582", "#3C3C3C", "#B8850A", "#006374"].freeze,
24
+ "dark6" => ["#001C7F", "#12711C", "#8C0800",
25
+ "#591E71", "#B8850A", "#006374"].freeze,
26
+ "colorblind" => ["#0173B2", "#DE8F05", "#029E73", "#D55E00", "#CC78BC",
27
+ "#CA9161", "#FBAFE4", "#949494", "#ECE133", "#56B4E9"].freeze,
28
+ "colorblind6" => ["#0173B2", "#029E73", "#D55E00",
29
+ "#CC78BC", "#ECE133", "#56B4E9"].freeze
30
+ }.freeze
31
+
32
+ MPL_QUAL_PALS = {
33
+ "tab10" => 10,
34
+ "tab20" => 20,
35
+ "tab20b" => 20,
36
+ "tab20c" => 20,
37
+ "Set1" => 9,
38
+ "Set2" => 8,
39
+ "Set3" => 12,
40
+ "Accent" => 8,
41
+ "Paired" => 12,
42
+ "Pastel1" => 9,
43
+ "Pastel2" => 8,
44
+ "Dark2" => 8,
45
+ }.freeze
46
+
47
+ QUAL_PALETTE_SIZES = MPL_QUAL_PALS.dup
48
+ SEABORN_PALETTES.each do |k, v|
49
+ QUAL_PALETTE_SIZES[k] = v.length
50
+ end
51
+ QUAL_PALETTE_SIZES.freeze
52
+
53
+ def self.seaborn_colors(name)
54
+ SEABORN_PALETTES[name].map do |hex_string|
55
+ Colors::RGB.parse(hex_string)
56
+ end
57
+ end
58
+
59
+ # Get a set of evenly spaced colors in HSL hue space.
60
+ #
61
+ # @param n_colors [Integer]
62
+ # The number of colors in the palette
63
+ # @param h [Numeric]
64
+ # The hue value of the first color in degree
65
+ # @param s [Numeric]
66
+ # The saturation value of the first color (between 0 and 1)
67
+ # @param l [Numeric]
68
+ # The lightness value of the first color (between 0 and 1)
69
+ #
70
+ # @return [Array<Colors::HSL>]
71
+ # The array of colors
72
+ def self.hsl_colors(n_colors=6, h: 3.6r, s: 0.65r, l: 0.6r)
73
+ hues = Numo::DFloat.linspace(0, 1, n_colors + 1)[0...-1]
74
+ hues.inplace + (h/360r).to_f
75
+ hues.inplace % 1
76
+ hues.inplace - Numo::Int32.cast(hues)
77
+ (0...n_colors).map {|i| Colors::HSL.new(hues[i]*360r, s, l) }
78
+ end
79
+
80
+ # Get a set of evenly spaced colors in HUSL hue space.
81
+ #
82
+ # @param n_colors [Integer]
83
+ # The number of colors in the palette
84
+ # @param h [Numeric]
85
+ # The hue value of the first color in degree
86
+ # @param s [Numeric]
87
+ # The saturation value of the first color (between 0 and 1)
88
+ # @param l [Numeric]
89
+ # The lightness value of the first color (between 0 and 1)
90
+ #
91
+ # @return [Array<Colors::HSL>]
92
+ # The array of colors
93
+ def self.husl_colors(n_colors=6, h: 3.6r, s: 0.9r, l: 0.65r)
94
+ hues = Numo::DFloat.linspace(0, 1, n_colors + 1)[0...-1]
95
+ hues.inplace + (h/360r).to_f
96
+ hues.inplace % 1
97
+ hues.inplace * 359
98
+ (0...n_colors).map {|i| Colors::HUSL.new(hues[i], s, l) }
99
+ end
100
+
101
+ def self.cubehelix_colors(n_colors, start=0, rot=0.4r, gamma=1.0r, hue=0.8r,
102
+ light=0.85r, dark=0.15r, reverse=false, as_cmap: false)
103
+ raise NotImplementedError,
104
+ "Cubehelix palette has not been implemented"
105
+ end
106
+
107
+ def self.matplotlib_colors(name, n_colors=6)
108
+ raise NotImplementedError,
109
+ "Matplotlib's colormap emulation has not been implemented"
110
+ end
111
+
112
+ # Return a list of colors defining a color palette
113
+ #
114
+ # @param palette [nil, String, Palette]
115
+ # Name of palette or nil to return current palette.
116
+ # If a Palette is given, input colors are used but
117
+ # possibly cycled and desaturated.
118
+ # @param n_colors [Integer, nil]
119
+ # Number of colors in the palette.
120
+ # If `nil`, the default will depend on how `palette` is specified.
121
+ # Named palettes default to 6 colors, but grabbing the current palette
122
+ # or passing in a list of colors will not change the number of colors
123
+ # unless this is specified. Asking for more colors than exist in the
124
+ # palette cause it to cycle.
125
+ # @param desaturate_factor [Float, nil]
126
+ # Propotion to desaturate each color by.
127
+ #
128
+ # @return [Palette]
129
+ # Color palette. Behaves like a list.
130
+ def initialize(palette=nil, n_colors=nil, desaturate_factor: nil)
131
+ case
132
+ when palette.nil?
133
+ @name = nil
134
+ palette = Colors::ColorDate::DEFAULT_COLOR_CYCLE
135
+ n_colors ||= palette.length
136
+ else
137
+ palette = normalize_palette_name(palette)
138
+ case palette
139
+ when String
140
+ @name = palette
141
+ # Use all colors in a qualitative palette or 6 of another kind
142
+ n_colors ||= QUAL_PALETTE_SIZES.fetch(palette, 6)
143
+ case @name
144
+ when SEABORN_PALETTES.method(:has_key?)
145
+ palette = self.class.seaborn_colors(@name)
146
+ when "hls", "HLS", "hsl", "HSL"
147
+ palette = self.class.hsl_colors(n_colors)
148
+ when "husl", "HUSL"
149
+ palette = self.class.husl_colors(n_colors)
150
+ when /\Ach:/
151
+ # Cubehelix palette with params specified in string
152
+ args, kwargs = parse_cubehelix_args(palette)
153
+ palette = self.class.cubehelix_colors(n_colors, *args, **kwargs)
154
+ else
155
+ begin
156
+ palette = self.class.matplotlib_colors(palette, n_colors)
157
+ rescue ArgumentError
158
+ raise ArgumentError,
159
+ "#{palette} is not a valid palette name"
160
+ end
161
+ end
162
+ else
163
+ n_colors ||= palette.length
164
+ end
165
+ end
166
+ if desaturate_factor
167
+ palette = palette.map {|c| Colors.desaturate(c, desaturate_factor) }
168
+ end
169
+
170
+ # Always return as many colors as we asked for
171
+ @colors = palette.cycle.take(n_colors).freeze
172
+ @desaturate_factor = desaturate_factor
173
+ end
174
+
175
+ attr_reader :name, :colors, :desaturate_factor
176
+
177
+ def n_colors
178
+ @colors.length
179
+ end
180
+
181
+ # Two palettes are equal if they have the same colors, even if they have
182
+ # the different names and different desaturate factors.
183
+ def ==(other)
184
+ case other
185
+ when Palette
186
+ colors == other.colors
187
+ else
188
+ super
189
+ end
190
+ end
191
+
192
+ def [](i)
193
+ @palette[i % n_colors]
194
+ end
195
+
196
+ def to_ary
197
+ @palette.dup
198
+ end
199
+
200
+ private def normalize_palette_name(palette)
201
+ case palette
202
+ when String
203
+ palette
204
+ when Symbol
205
+ palette.to_s
206
+ else
207
+ palette.to_str
208
+ end
209
+ rescue NoMethodError, TypeError
210
+ palette
211
+ end
212
+
213
+ class << self
214
+ attr_reader :default
215
+
216
+ def default=(args)
217
+ @default = case args
218
+ when Palette
219
+ args
220
+ when Array
221
+ case args[0]
222
+ when Array
223
+ Palette.new(*args)
224
+ else
225
+ Palette.new(args)
226
+ end
227
+ else
228
+ Palette.new(args)
229
+ end
230
+ end
231
+ end
232
+
233
+ self.default = Palette.new("deep").freeze
234
+ end
235
+ end