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
@@ -1,8 +1,15 @@
1
1
  require_relative "charty/version"
2
2
 
3
+ require "colors"
4
+
3
5
  require_relative "charty/backends"
6
+ require_relative "charty/backend_methods"
4
7
  require_relative "charty/plotter"
5
8
  require_relative "charty/layout"
6
9
  require_relative "charty/linspace"
10
+ require_relative "charty/plotters"
11
+ require_relative "charty/plot_methods"
12
+ require_relative "charty/palette"
7
13
  require_relative "charty/table_adapters"
8
14
  require_relative "charty/table"
15
+ require_relative "charty/statistics"
@@ -0,0 +1,8 @@
1
+ module Charty
2
+ module BackendMethods
3
+ def use_backend(backend)
4
+ end
5
+ end
6
+
7
+ extend BackendMethods
8
+ end
@@ -6,6 +6,30 @@ module Charty
6
6
  module Backends
7
7
  @backends = {}
8
8
 
9
+ @current = nil
10
+
11
+ def self.current
12
+ @current
13
+ end
14
+
15
+ def self.current=(backend_name)
16
+ backend_class = Backends.find_backend_class(backend_name)
17
+ @current = backend_class.new
18
+ end
19
+
20
+ def self.use(backend)
21
+ if block_given?
22
+ begin
23
+ saved, self.current = self.current, backend
24
+ yield
25
+ ensure
26
+ self.current = saved
27
+ end
28
+ else
29
+ self.current = backend
30
+ end
31
+ end
32
+
9
33
  def self.names
10
34
  @backends.keys
11
35
  end
@@ -48,7 +72,7 @@ module Charty
48
72
  end
49
73
 
50
74
  require "charty/backends/bokeh"
51
- require "charty/backends/google_chart"
75
+ require "charty/backends/google_charts"
52
76
  require "charty/backends/gruff"
53
77
  require "charty/backends/plotly"
54
78
  require "charty/backends/pyplot"
@@ -37,42 +37,42 @@ module Charty
37
37
  plot.yaxis[0].axis_label = context&.ylabel
38
38
 
39
39
  case context.method
40
- when :bar
41
- context.series.each do |data|
42
- diffs = data.xs.to_a.each_cons(2).map {|n, m| (n - m).abs }
43
- width = diffs.min * 0.8
44
- plot.vbar(data.xs.to_a, width, data.ys.to_a)
45
- end
40
+ when :bar
41
+ context.series.each do |data|
42
+ diffs = data.xs.to_a.each_cons(2).map {|n, m| (n - m).abs }
43
+ width = diffs.min * 0.8
44
+ plot.vbar(data.xs.to_a, width, data.ys.to_a)
45
+ end
46
46
 
47
- when :barh
48
- context.series.each do |data|
49
- diffs = data.xs.to_a.each_cons(2).map {|n, m| (n - m).abs }
50
- height = diffs.min * 0.8
51
- plot.hbar(data.xs.to_a, height, data.ys.to_a)
52
- end
47
+ when :barh
48
+ context.series.each do |data|
49
+ diffs = data.xs.to_a.each_cons(2).map {|n, m| (n - m).abs }
50
+ height = diffs.min * 0.8
51
+ plot.hbar(data.xs.to_a, height, data.ys.to_a)
52
+ end
53
53
 
54
- when :boxplot
55
- raise NotImplementedError
54
+ when :boxplot
55
+ raise NotImplementedError
56
56
 
57
- when :bubble
58
- raise NotImplementedError
57
+ when :bubble
58
+ raise NotImplementedError
59
59
 
60
- when :curve
61
- context.series.each do |data|
62
- plot.line(data.xs.to_a, data.ys.to_a)
63
- end
60
+ when :curve
61
+ context.series.each do |data|
62
+ plot.line(data.xs.to_a, data.ys.to_a)
63
+ end
64
64
 
65
- when :scatter
66
- context.series.each do |data|
67
- plot.scatter(data.xs.to_a, data.ys.to_a)
68
- end
65
+ when :scatter
66
+ context.series.each do |data|
67
+ plot.scatter(data.xs.to_a, data.ys.to_a)
68
+ end
69
69
 
70
- when :error_bar
71
- raise NotImplementedError
70
+ when :error_bar
71
+ raise NotImplementedError
72
72
 
73
- when :hist
74
- raise NotImplementedError
75
- end
73
+ when :hist
74
+ raise NotImplementedError
75
+ end
76
76
  plot
77
77
  end
78
78
  end
@@ -1,7 +1,7 @@
1
1
  module Charty
2
2
  module Backends
3
- class GoogleChart
4
- Backends.register(:google_chart, self)
3
+ class GoogleCharts
4
+ Backends.register(:google_charts, self)
5
5
 
6
6
  attr_reader :context
7
7
 
@@ -70,47 +70,65 @@ module Charty
70
70
  def data_column_js
71
71
  case context.method
72
72
  when :bubble
73
- column_js = <<-COLUMN_JS
74
- data.addColumn('string', 'ID');
75
- data.addColumn('number', 'X');
76
- data.addColumn('number', 'Y');
77
- data.addColumn('string', 'GROUP');
78
- data.addColumn('number', 'SIZE');
79
- COLUMN_JS
73
+ schema = [
74
+ ["string", "ID"],
75
+ ["number", "X"],
76
+ ["number", "Y"],
77
+ ["string", "GROUP"],
78
+ ["number", "SIZE"],
79
+ ]
80
80
  when :curve
81
- column_js = "data.addColumn('number', '#{context.xlabel}');"
81
+ schema = []
82
+ schema << [detect_type(context.series.first.xs), context.xlabel]
82
83
  context.series.to_a.each_with_index do |series_data, index|
83
- column_js << "data.addColumn('number', '#{series_data.label || index}');"
84
+ schema << ["number", series_data.label || index]
84
85
  end
85
86
  else
86
- column_js = "data.addColumn('string', '#{context.xlabel}');"
87
+ schema = ["string", context.xlabel]
87
88
  context.series.to_a.each_with_index do |series_data, index|
88
- column_js << "data.addColumn('number', '#{series_data.label || index}');"
89
+ schema << ["number", series_data.label || index]
89
90
  end
90
91
  end
91
92
 
92
- column_js
93
+ columns = schema.collect do |type, label|
94
+ "data.addColumn(#{type.to_json}, #{label.to_s.to_json});"
95
+ end
96
+ columns.join
97
+ end
98
+
99
+ def detect_type(values)
100
+ case values.first
101
+ when Time
102
+ "date"
103
+ when String
104
+ "string"
105
+ else
106
+ "number"
107
+ end
93
108
  end
94
109
 
95
110
  def x_labels
96
- [].tap do |label|
97
- context.series.each do |series|
98
- xs_series = if series.xs.detect { |xs_data| xs_data.is_a?(String) }
99
- series.xs.sort_by {|x| format('%10s', "#{x}")}
100
- else
101
- series.xs.sort
102
- end
103
- xs_series.each do |xs_data|
104
- label << xs_data unless label.any? { |label| label == xs_data }
105
- end
111
+ labels = {}
112
+ have_string = false
113
+ context.series.each do |series|
114
+ series.xs.each do |x|
115
+ next if labels.key?(x)
116
+ have_string = true if x.is_a?(String)
117
+ labels[x] = true
106
118
  end
107
119
  end
120
+ if have_string
121
+ labels.keys.sort_by {|label| "%10s" % x.to_s}
122
+ else
123
+ labels.keys.sort
124
+ end
108
125
  end
109
126
 
110
127
  def data_hash
111
128
  {}.tap do |hash|
129
+ _x_labels = x_labels
112
130
  context.series.to_a.each_with_index do |series_data, series_index|
113
- x_labels.each do |x_label|
131
+ _x_labels.each do |x_label|
114
132
  unless hash[x_label]
115
133
  hash[x_label] = []
116
134
  end
@@ -118,14 +136,14 @@ module Charty
118
136
  if data_index = series_data.xs.to_a.index(x_label)
119
137
  hash[x_label] << series_data.ys.to_a[data_index]
120
138
  else
121
- hash[x_label] << "null"
139
+ hash[x_label] << nil
122
140
  end
123
141
  end
124
142
  end
125
143
  end
126
144
  end
127
145
 
128
- def formatted_data_array
146
+ def rows
129
147
  case context.method
130
148
  when :bubble
131
149
  [].tap do |data_array|
@@ -133,10 +151,10 @@ module Charty
133
151
  series_data.xs.to_a.each_with_index do |data, data_index|
134
152
  data_array << [
135
153
  "",
136
- series_data.xs.to_a[data_index] || "null",
137
- series_data.ys.to_a[data_index] || "null",
154
+ series_data.xs.to_a[data_index],
155
+ series_data.ys.to_a[data_index],
138
156
  series_data[:label] || series_index.to_s,
139
- series_data.zs.to_a[data_index] || "null",
157
+ series_data.zs.to_a[data_index],
140
158
  ]
141
159
  end
142
160
  end
@@ -150,12 +168,36 @@ module Charty
150
168
  else
151
169
  [].tap do |data_array|
152
170
  data_hash.each do |k, v|
153
- data_array << [k.to_s, v].flatten
171
+ data_array << [k, v].flatten
154
172
  end
155
173
  end
156
174
  end
157
175
  end
158
176
 
177
+ def convert_to_javascript(data)
178
+ case data
179
+ when Array
180
+ converted_data = data.collect do |element|
181
+ convert_to_javascript(element)
182
+ end
183
+ "[#{converted_data.join(", ")}]"
184
+ when Time
185
+ time = data.dup.utc
186
+ args = [
187
+ time.year,
188
+ time.month - 1,
189
+ time.day,
190
+ time.hour,
191
+ time.min,
192
+ time.sec,
193
+ time.nsec / 1000 / 1000,
194
+ ]
195
+ "new Date(Date.UTC(#{args.join(", ")}))"
196
+ else
197
+ data.to_json
198
+ end
199
+ end
200
+
159
201
  def x_range_option
160
202
  x_range = if context.method != :barh
161
203
  context&.range&.fetch(:x, nil)
@@ -189,7 +231,7 @@ module Charty
189
231
  function drawChart() {
190
232
  const data = new google.visualization.DataTable();
191
233
  #{data_column_js}
192
- data.addRows(#{formatted_data_array})
234
+ data.addRows(#{convert_to_javascript(rows)})
193
235
 
194
236
  const view = new google.visualization.DataView(data);
195
237
 
@@ -55,24 +55,22 @@ module Charty
55
55
  end
56
56
  end
57
57
 
58
- private
59
-
60
- def plotly_load_tag
58
+ private def plotly_load_tag
61
59
  if self.class.with_api_load_tag
62
60
  "<script type='text/javascript' src='#{self.class.plotly_src}'></script>"
63
61
  else
64
62
  end
65
63
  end
66
64
 
67
- def div_id
65
+ private def div_id
68
66
  "charty-plotly-#{self.class.chart_id}"
69
67
  end
70
68
 
71
- def div_style
69
+ private def div_style
72
70
  "width: 100%;height: 100%;"
73
71
  end
74
72
 
75
- def render_graph(context, type, options: {})
73
+ private def render_graph(context, type, options: {})
76
74
  data = context.series.map do |series|
77
75
  {
78
76
  type: type,
@@ -95,7 +93,7 @@ module Charty
95
93
  render_html(data, layout)
96
94
  end
97
95
 
98
- def render_html(data, layout)
96
+ private def render_html(data, layout)
99
97
  <<~FRAGMENT
100
98
  #{plotly_load_tag unless self.class.chart_id > 1}
101
99
  <div id="#{div_id}" style="#{div_style}"></div>
@@ -104,6 +102,146 @@ module Charty
104
102
  </script>
105
103
  FRAGMENT
106
104
  end
105
+
106
+ # ==== NEW PLOTTING API ====
107
+
108
+ class HTML
109
+ def initialize(html)
110
+ @html = html
111
+ end
112
+
113
+ def to_iruby
114
+ ["text/html", @html]
115
+ end
116
+ end
117
+
118
+ def begin_figure
119
+ @traces = []
120
+ @layout = {}
121
+ end
122
+
123
+ def bar(bar_pos, values, color: nil, width: 0.8r, align: :center, orient: :v)
124
+ color = Array(color).map(&:to_hex_string)
125
+ @traces << {
126
+ type: :bar,
127
+ x: bar_pos,
128
+ y: values,
129
+ marker: {color: color}
130
+ }
131
+ @layout[:showlegend] = false
132
+ end
133
+
134
+ def box_plot(plot_data, positions, color:, gray:,
135
+ width: 0.8r, flier_size: 5, whisker: 1.5, notch: false)
136
+ color = Array(color).map(&:to_hex_string)
137
+ plot_data.each_with_index do |group_data, i|
138
+ data = if group_data.empty?
139
+ {type: :box, y: [] }
140
+ else
141
+ {type: :box, y: group_data, marker: {color: color[i]}}
142
+ end
143
+ @traces << data
144
+ end
145
+ @layout[:showlegend] = false
146
+ end
147
+
148
+ def set_xlabel(label)
149
+ @layout[:xaxis] ||= {}
150
+ @layout[:xaxis][:title] = label
151
+ end
152
+
153
+ def set_ylabel(label)
154
+ @layout[:yaxis] ||= {}
155
+ @layout[:yaxis][:title] = label
156
+ end
157
+
158
+ def set_xticks(values)
159
+ @layout[:xaxis] ||= {}
160
+ @layout[:xaxis][:tickmode] = "array"
161
+ @layout[:xaxis][:tickvals] = values
162
+ end
163
+
164
+ def set_xtick_labels(labels)
165
+ @layout[:xaxis] ||= {}
166
+ @layout[:xaxis][:tickmode] = "array"
167
+ @layout[:xaxis][:ticktext] = labels
168
+ end
169
+
170
+ def set_xlim(min, max)
171
+ @layout[:xaxis] ||= {}
172
+ @layout[:xaxis][:range] = [min, max]
173
+ end
174
+
175
+ def disable_xaxis_grid
176
+ # do nothing
177
+ end
178
+
179
+ def show
180
+ unless defined?(IRuby)
181
+ raise NotImplementedError,
182
+ "Plotly backend outside of IRuby is not supported"
183
+ end
184
+
185
+ IRubyOutput.prepare
186
+
187
+ html = <<~HTML
188
+ <div id="%{id}" style="width: 100%%; height:100%%;"></div>
189
+ <script type="text/javascript">
190
+ requirejs(["plotly"], function (Plotly) {
191
+ Plotly.newPlot("%{id}", %{data}, %{layout});
192
+ });
193
+ </script>
194
+ HTML
195
+
196
+ html %= {
197
+ id: SecureRandom.uuid,
198
+ data: JSON.dump(@traces),
199
+ layout: JSON.dump(@layout)
200
+ }
201
+ IRuby.display(html, mime: "text/html")
202
+ nil
203
+ end
204
+
205
+ module IRubyOutput
206
+ @prepared = false
207
+
208
+ def self.prepare
209
+ return if @prepared
210
+
211
+ html = <<~HTML
212
+ <script type="text/javascript">
213
+ %{win_config}
214
+ %{mathjax_config}
215
+ require.config({
216
+ paths: {
217
+ plotly: "https://cdn.plot.ly/plotly-latest.min"
218
+ }
219
+ });
220
+ </script>
221
+ HTML
222
+
223
+ html %= {
224
+ win_config: window_plotly_config,
225
+ mathjax_config: mathjax_config
226
+ }
227
+
228
+ IRuby.display(html, mime: "text/html")
229
+ @prepared = true
230
+ end
231
+
232
+ def self.window_plotly_config
233
+ <<~END
234
+ window.PlotlyConfig = {MathJaxConfig: 'local'};
235
+ END
236
+ end
237
+
238
+
239
+ def self.mathjax_config
240
+ <<~END
241
+ if (window.MathJax) {MathJax.Hub.Config({SVG: {font: "STIX-Web"}});}
242
+ END
243
+ end
244
+ end
107
245
  end
108
246
  end
109
247
  end