charty 0.2.0 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
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