charty 0.1.5.dev → 0.2.0

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