charty 0.1.5.dev → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,7 +1,8 @@
1
1
  require_relative "charty/version"
2
2
 
3
+ require_relative "charty/backends"
3
4
  require_relative "charty/plotter"
4
- require_relative "charty/plotter_adapter"
5
5
  require_relative "charty/layout"
6
6
  require_relative "charty/linspace"
7
+ require_relative "charty/table_adapters"
7
8
  require_relative "charty/table"
@@ -0,0 +1,55 @@
1
+ module Charty
2
+ class BackendError < RuntimeError; end
3
+ class BackendNotFoundError < BackendError; end
4
+ class BackendLoadError < BackendError; end
5
+
6
+ module Backends
7
+ @backends = {}
8
+
9
+ def self.names
10
+ @backends.keys
11
+ end
12
+
13
+ def self.register(name, backend_class)
14
+ @backends[normalize_name(name)] = {
15
+ class: backend_class,
16
+ prepared: false,
17
+ }
18
+ end
19
+
20
+ def self.find_backend_class(name)
21
+ backend = @backends[normalize_name(name)]
22
+ unless backend
23
+ raise BackendNotFoundError, "Backend is not found: #{name.inspect}"
24
+ end
25
+ backend_class = backend[:class]
26
+ unless backend[:prepared]
27
+ if backend_class.respond_to?(:prepare)
28
+ begin
29
+ backend_class.prepare
30
+ rescue LoadError
31
+ raise BackendLoadError, "Backend load error: #{name.inspect}"
32
+ end
33
+ end
34
+ backend[:prepared] = true
35
+ end
36
+ backend_class
37
+ end
38
+
39
+ private_class_method def self.normalize_name(name)
40
+ case name
41
+ when Symbol
42
+ name.to_s
43
+ else
44
+ name.to_str
45
+ end
46
+ end
47
+ end
48
+ end
49
+
50
+ require "charty/backends/bokeh"
51
+ require "charty/backends/google_chart"
52
+ require "charty/backends/gruff"
53
+ require "charty/backends/plotly"
54
+ require "charty/backends/pyplot"
55
+ require "charty/backends/rubyplot"
@@ -1,74 +1,80 @@
1
- require 'pycall'
2
-
3
1
  module Charty
4
- class Bokeh < PlotterAdapter
5
- Name = "bokeh"
2
+ module Backends
3
+ class Bokeh
4
+ Backends.register(:bokeh, self)
6
5
 
7
- def initialize
8
- @plot = PyCall.import_module('bokeh.plotting')
9
- end
6
+ class << self
7
+ def prepare
8
+ require 'pycall'
9
+ end
10
+ end
10
11
 
11
- def series=(series)
12
- @series = series
13
- end
12
+ def initialize
13
+ @plot = PyCall.import_module('bokeh.plotting')
14
+ end
14
15
 
15
- def render(context, filename)
16
- plot = plot(context)
17
- save(plot, context, filename)
18
- PyCall.import_module('bokeh.io').show(plot)
19
- end
16
+ def series=(series)
17
+ @series = series
18
+ end
20
19
 
21
- def save(plot, context, filename)
22
- if filename
23
- PyCall.import_module('bokeh.io').save(plot, filename)
20
+ def render(context, filename)
21
+ plot = plot(context)
22
+ save(plot, context, filename)
23
+ PyCall.import_module('bokeh.io').show(plot)
24
24
  end
25
- end
26
25
 
27
- def plot(context)
28
- #TODO To implement boxplot, bublle, error_bar, hist.
26
+ def save(plot, context, filename)
27
+ if filename
28
+ PyCall.import_module('bokeh.io').save(plot, filename)
29
+ end
30
+ end
29
31
 
30
- plot = @plot.figure(title: context&.title)
31
- plot.xaxis[0].axis_label = context&.xlabel
32
- plot.yaxis[0].axis_label = context&.ylabel
32
+ def plot(context)
33
+ #TODO To implement boxplot, bublle, error_bar, hist.
33
34
 
34
- case context.method
35
- when :bar
36
- context.series.each do |data|
37
- diffs = data.xs.to_a.each_cons(2).map {|n, m| (n - m).abs }
38
- width = diffs.min * 0.8
39
- plot.vbar(data.xs.to_a, width, data.ys.to_a)
40
- end
35
+ plot = @plot.figure(title: context&.title)
36
+ plot.xaxis[0].axis_label = context&.xlabel
37
+ plot.yaxis[0].axis_label = context&.ylabel
41
38
 
42
- when :barh
43
- context.series.each do |data|
44
- diffs = data.xs.to_a.each_cons(2).map {|n, m| (n - m).abs }
45
- height = diffs.min * 0.8
46
- plot.hbar(data.xs.to_a, height, data.ys.to_a)
47
- end
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
48
46
 
49
- when :boxplot
50
- raise NotImplementedError
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
51
53
 
52
- when :bubble
53
- raise NotImplementedError
54
+ when :boxplot
55
+ raise NotImplementedError
54
56
 
55
- when :curve
56
- context.series.each do |data|
57
- plot.line(data.xs.to_a, data.ys.to_a)
58
- end
57
+ when :bubble
58
+ raise NotImplementedError
59
59
 
60
- when :scatter
61
- context.series.each do |data|
62
- plot.scatter(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 :error_bar
66
- raise NotImplementedError
65
+ when :scatter
66
+ context.series.each do |data|
67
+ plot.scatter(data.xs.to_a, data.ys.to_a)
68
+ end
67
69
 
68
- when :hist
69
- raise NotImplementedError
70
- end
71
- plot
70
+ when :error_bar
71
+ raise NotImplementedError
72
+
73
+ when :hist
74
+ raise NotImplementedError
75
+ end
76
+ plot
77
+ end
72
78
  end
73
79
  end
74
80
  end
@@ -1,167 +1,225 @@
1
1
  module Charty
2
- class GoogleChart < PlotterAdapter
3
- Name = "google_chart"
4
- attr_reader :context
2
+ module Backends
3
+ class GoogleChart
4
+ Backends.register(:google_chart, self)
5
5
 
6
- def self.chart_id=(chart_id)
7
- @chart_id = chart_id
8
- end
6
+ attr_reader :context
9
7
 
10
- def self.chart_id
11
- @chart_id ||= 0
12
- end
8
+ class << self
9
+ attr_writer :chart_id, :google_charts_src, :with_api_load_tag
13
10
 
14
- def initilize
15
- end
11
+ def chart_id
12
+ @chart_id ||= 0
13
+ end
16
14
 
17
- def label(x, y)
18
- end
15
+ def with_api_load_tag
16
+ return @with_api_load_tag unless @with_api_load_tag.nil?
19
17
 
20
- def series=(series)
21
- @series = series
22
- end
18
+ @with_api_load_tag = true
19
+ end
23
20
 
24
- def render(context, filename)
25
- plot(nil, context)
26
- end
21
+ def google_charts_src
22
+ @google_charts_src ||= 'https://www.gstatic.com/charts/loader.js'
23
+ end
24
+ end
27
25
 
28
- def plot(plot, context)
29
- @context = context
30
- self.class.chart_id = self.class.chart_id + 1
31
-
32
- case context.method
33
- when :bar
34
- generate_render_js("BarChart")
35
- when :scatter
36
- generate_render_js("ScatterChart")
37
- when :bubble
38
- generate_render_js("BubbleChart")
39
- else
40
- raise NotImplementedError
26
+ def initilize
27
+ end
28
+
29
+ def label(x, y)
41
30
  end
42
- end
43
31
 
44
- private
32
+ def series=(series)
33
+ @series = series
34
+ end
45
35
 
46
- def google_chart_load_tag
47
- "<script type='text/javascript' src='https://www.gstatic.com/charts/loader.js'></script>"
36
+ def render(context, filename)
37
+ plot(nil, context)
48
38
  end
49
39
 
50
- def headers
51
- [].tap do |header|
52
- header << context.xlabel
53
- context.series.to_a.each_with_index do |series_data, index|
54
- header << series_data.label || index
55
- end
40
+ def plot(plot, context)
41
+ @context = context
42
+ self.class.chart_id = self.class.chart_id + 1
43
+
44
+ case context.method
45
+ when :bar
46
+ generate_render_js("ColumnChart")
47
+ when :barh
48
+ generate_render_js("BarChart")
49
+ when :scatter
50
+ generate_render_js("ScatterChart")
51
+ when :bubble
52
+ generate_render_js("BubbleChart")
53
+ when :curve
54
+ generate_render_js("LineChart")
55
+ else
56
+ raise NotImplementedError
56
57
  end
57
58
  end
58
59
 
59
- def x_labels
60
- [].tap do |label|
61
- context.series.each do |series|
62
- series.xs.each do |xs_data|
63
- label << xs_data unless label.any? { |label| label == xs_data }
64
- end
60
+ private
61
+
62
+ def google_charts_load_tag
63
+ if self.class.with_api_load_tag
64
+ "<script type='text/javascript' src='#{self.class.google_charts_src}'></script>"
65
+ else
66
+ nil
65
67
  end
66
68
  end
67
- end
68
69
 
69
- def data_hash
70
- {}.tap do |hash|
71
- context.series.to_a.each_with_index do |series_data, series_index|
72
- x_labels.sort.each do |x_label|
73
- unless hash[x_label]
74
- hash[x_label] = []
75
- end
70
+ def data_column_js
71
+ case context.method
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
80
+ when :curve
81
+ column_js = "data.addColumn('number', '#{context.xlabel}');"
82
+ context.series.to_a.each_with_index do |series_data, index|
83
+ column_js << "data.addColumn('number', '#{series_data.label || index}');"
84
+ end
85
+ else
86
+ column_js = "data.addColumn('string', '#{context.xlabel}');"
87
+ context.series.to_a.each_with_index do |series_data, index|
88
+ column_js << "data.addColumn('number', '#{series_data.label || index}');"
89
+ end
90
+ end
91
+
92
+ column_js
93
+ end
76
94
 
77
- if data_index = series_data.xs.to_a.index(x_label)
78
- hash[x_label] << series_data.ys.to_a[data_index]
79
- else
80
- hash[x_label] << "null"
95
+ 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 }
81
105
  end
82
106
  end
83
107
  end
84
108
  end
85
- end
86
109
 
87
- def formatted_data_array
88
- case context.method
89
- when :bubble
90
- [["ID", "X", "Y", "GROUP", "SIZE"]].tap do |data_array|
110
+ def data_hash
111
+ {}.tap do |hash|
91
112
  context.series.to_a.each_with_index do |series_data, series_index|
92
- series_data.xs.to_a.each_with_index do |data, data_index|
93
- data_array << [
94
- "",
95
- series_data.xs.to_a[data_index] || "null",
96
- series_data.ys.to_a[data_index] || "null",
97
- series_data[:label] || series_index,
98
- series_data.zs.to_a[data_index] || "null",
99
- ]
113
+ x_labels.each do |x_label|
114
+ unless hash[x_label]
115
+ hash[x_label] = []
116
+ end
117
+
118
+ if data_index = series_data.xs.to_a.index(x_label)
119
+ hash[x_label] << series_data.ys.to_a[data_index]
120
+ else
121
+ hash[x_label] << "null"
122
+ end
100
123
  end
101
124
  end
102
125
  end
103
- else
104
- [headers.map(&:to_s)].tap do |data_array|
105
- data_hash.each do |k, v|
106
- data_array << [k.to_s, v].flatten
126
+ end
127
+
128
+ def formatted_data_array
129
+ case context.method
130
+ when :bubble
131
+ [].tap do |data_array|
132
+ context.series.to_a.each_with_index do |series_data, series_index|
133
+ series_data.xs.to_a.each_with_index do |data, data_index|
134
+ data_array << [
135
+ "",
136
+ series_data.xs.to_a[data_index] || "null",
137
+ series_data.ys.to_a[data_index] || "null",
138
+ series_data[:label] || series_index.to_s,
139
+ series_data.zs.to_a[data_index] || "null",
140
+ ]
141
+ end
142
+ end
143
+ end
144
+ when :curve
145
+ [].tap do |data_array|
146
+ data_hash.each do |k, v|
147
+ data_array << [k, v].flatten
148
+ end
149
+ end
150
+ else
151
+ [].tap do |data_array|
152
+ data_hash.each do |k, v|
153
+ data_array << [k.to_s, v].flatten
154
+ end
107
155
  end
108
156
  end
109
157
  end
110
- end
111
158
 
112
- def x_range_option
113
- x_range = context&.range&.fetch(:x, nil)
114
- {
115
- max: x_range&.max,
116
- min: x_range&.min,
117
- }.reject { |_k, v| v.nil? }
118
- end
159
+ def x_range_option
160
+ x_range = if context.method != :barh
161
+ context&.range&.fetch(:x, nil)
162
+ else
163
+ context&.range&.fetch(:y, nil)
164
+ end
165
+ {
166
+ max: x_range&.max,
167
+ min: x_range&.min,
168
+ }.reject { |_k, v| v.nil? }
169
+ end
119
170
 
120
- def y_range_option
121
- y_range = context&.range&.fetch(:y, nil)
122
- {
123
- max: y_range&.max,
124
- min: y_range&.min,
125
- }.reject { |_k, v| v.nil? }
126
- end
171
+ def y_range_option
172
+ y_range = if context.method != :barh
173
+ context&.range&.fetch(:y, nil)
174
+ else
175
+ context&.range&.fetch(:x, nil)
176
+ end
177
+ {
178
+ max: y_range&.max,
179
+ min: y_range&.min,
180
+ }.reject { |_k, v| v.nil? }
181
+ end
127
182
 
128
- def generate_render_js(chart_type)
129
- js = <<-JS
130
- #{google_chart_load_tag}
131
- <script type="text/javascript">
132
- google.charts.load("current", {packages:["corechart"]});
133
- google.charts.setOnLoadCallback(drawChart);
134
- function drawChart() {
135
- const data = google.visualization.arrayToDataTable(
136
- #{formatted_data_array}
137
- );
138
-
139
- const view = new google.visualization.DataView(data);
140
-
141
- const options = {
142
- title: "#{context.title}",
143
- vAxis: {
144
- title: "#{context.ylabel}",
145
- viewWindow: {
146
- max: #{y_range_option[:max] || "null"},
147
- min: #{y_range_option[:min] || "null"},
183
+ def generate_render_js(chart_type)
184
+ js = <<-JS
185
+ #{google_charts_load_tag unless self.class.chart_id > 1}
186
+ <script type="text/javascript">
187
+ google.charts.load("current", {packages:["corechart"]});
188
+ google.charts.setOnLoadCallback(drawChart);
189
+ function drawChart() {
190
+ const data = new google.visualization.DataTable();
191
+ #{data_column_js}
192
+ data.addRows(#{formatted_data_array})
193
+
194
+ const view = new google.visualization.DataView(data);
195
+
196
+ const options = {
197
+ title: "#{context.title}",
198
+ vAxis: {
199
+ title: "#{context.ylabel}",
200
+ viewWindow: {
201
+ max: #{y_range_option[:max] || "null"},
202
+ min: #{y_range_option[:min] || "null"},
203
+ },
148
204
  },
149
- },
150
- hAxis: {
151
- title: "#{context.xlabel}",
152
- viewWindow: {
153
- max: #{x_range_option[:max] || "null"},
154
- min: #{x_range_option[:min] || "null"},
155
- }
156
- },
157
- legend: { position: "none" },
158
- };
159
- const chart = new google.visualization.#{chart_type}(document.getElementById("#{chart_type}-#{self.class.chart_id}"));
160
- chart.draw(view, options);
161
- }
162
- </script>
163
- <div id="#{chart_type}-#{self.class.chart_id}" style="width: 900px; height: 300px;"></div>
164
- JS
165
- end
205
+ hAxis: {
206
+ title: "#{context.xlabel}",
207
+ viewWindow: {
208
+ max: #{x_range_option[:max] || "null"},
209
+ min: #{x_range_option[:min] || "null"},
210
+ }
211
+ },
212
+ legend: { position: "none" },
213
+ };
214
+ const chart = new google.visualization.#{chart_type}(document.getElementById("#{chart_type}-#{self.class.chart_id}"));
215
+ chart.draw(view, options);
216
+ }
217
+ </script>
218
+ <div id="#{chart_type}-#{self.class.chart_id}" style="width: 900px; height: 300px;"></div>
219
+ JS
220
+ js.gsub!(/\"null\"/, 'null')
221
+ js
222
+ end
223
+ end
166
224
  end
167
225
  end