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.
- checksums.yaml +4 -4
- data/.travis.yml +4 -5
- data/README.md +1 -1
- data/charty.gemspec +6 -0
- data/examples/sample_bokeh.ipynb +156 -0
- data/examples/sample_google_chart.ipynb +229 -68
- data/examples/sample_images/bar_bokeh.html +85 -0
- data/examples/sample_images/barh_bokeh.html +85 -0
- data/examples/sample_images/box_plot_bokeh.html +85 -0
- data/examples/sample_images/curve_bokeh.html +85 -0
- data/examples/sample_images/curve_with_function_bokeh.html +85 -0
- data/examples/sample_images/scatter_bokeh.html +85 -0
- data/lib/charty.rb +2 -1
- data/lib/charty/backends.rb +55 -0
- data/lib/charty/backends/bokeh.rb +61 -55
- data/lib/charty/backends/google_chart.rb +187 -129
- data/lib/charty/backends/gruff.rb +91 -83
- data/lib/charty/backends/plotly.rb +109 -0
- data/lib/charty/backends/pyplot.rb +96 -88
- data/lib/charty/backends/rubyplot.rb +82 -74
- data/lib/charty/plotter.rb +41 -33
- data/lib/charty/table.rb +44 -3
- data/lib/charty/table_adapters.rb +23 -0
- data/lib/charty/table_adapters/active_record_adapter.rb +55 -0
- data/lib/charty/table_adapters/daru_adapter.rb +34 -0
- data/lib/charty/table_adapters/datasets_adapter.rb +41 -0
- data/lib/charty/table_adapters/hash_adapter.rb +108 -0
- data/lib/charty/table_adapters/narray_adapter.rb +57 -0
- data/lib/charty/table_adapters/nmatrix_adapter.rb +57 -0
- data/lib/charty/version.rb +1 -1
- metadata +104 -5
- data/lib/charty/plotter_adapter.rb +0 -17
data/lib/charty.rb
CHANGED
@@ -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
|
-
|
5
|
-
|
2
|
+
module Backends
|
3
|
+
class Bokeh
|
4
|
+
Backends.register(:bokeh, self)
|
6
5
|
|
7
|
-
|
8
|
-
|
9
|
-
|
6
|
+
class << self
|
7
|
+
def prepare
|
8
|
+
require 'pycall'
|
9
|
+
end
|
10
|
+
end
|
10
11
|
|
11
|
-
|
12
|
-
|
13
|
-
|
12
|
+
def initialize
|
13
|
+
@plot = PyCall.import_module('bokeh.plotting')
|
14
|
+
end
|
14
15
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
PyCall.import_module('bokeh.io').show(plot)
|
19
|
-
end
|
16
|
+
def series=(series)
|
17
|
+
@series = series
|
18
|
+
end
|
20
19
|
|
21
|
-
|
22
|
-
|
23
|
-
|
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
|
-
|
28
|
-
|
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
|
-
|
31
|
-
|
32
|
-
plot.yaxis[0].axis_label = context&.ylabel
|
32
|
+
def plot(context)
|
33
|
+
#TODO To implement boxplot, bublle, error_bar, hist.
|
33
34
|
|
34
|
-
|
35
|
-
|
36
|
-
|
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
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
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
|
-
|
50
|
-
|
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
|
-
|
53
|
-
|
54
|
+
when :boxplot
|
55
|
+
raise NotImplementedError
|
54
56
|
|
55
|
-
|
56
|
-
|
57
|
-
plot.line(data.xs.to_a, data.ys.to_a)
|
58
|
-
end
|
57
|
+
when :bubble
|
58
|
+
raise NotImplementedError
|
59
59
|
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
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
|
-
|
66
|
-
|
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
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
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
|
-
|
3
|
-
|
4
|
-
|
2
|
+
module Backends
|
3
|
+
class GoogleChart
|
4
|
+
Backends.register(:google_chart, self)
|
5
5
|
|
6
|
-
|
7
|
-
@chart_id = chart_id
|
8
|
-
end
|
6
|
+
attr_reader :context
|
9
7
|
|
10
|
-
|
11
|
-
|
12
|
-
end
|
8
|
+
class << self
|
9
|
+
attr_writer :chart_id, :google_charts_src, :with_api_load_tag
|
13
10
|
|
14
|
-
|
15
|
-
|
11
|
+
def chart_id
|
12
|
+
@chart_id ||= 0
|
13
|
+
end
|
16
14
|
|
17
|
-
|
18
|
-
|
15
|
+
def with_api_load_tag
|
16
|
+
return @with_api_load_tag unless @with_api_load_tag.nil?
|
19
17
|
|
20
|
-
|
21
|
-
|
22
|
-
end
|
18
|
+
@with_api_load_tag = true
|
19
|
+
end
|
23
20
|
|
24
|
-
|
25
|
-
|
26
|
-
|
21
|
+
def google_charts_src
|
22
|
+
@google_charts_src ||= 'https://www.gstatic.com/charts/loader.js'
|
23
|
+
end
|
24
|
+
end
|
27
25
|
|
28
|
-
|
29
|
-
|
30
|
-
|
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
|
-
|
32
|
+
def series=(series)
|
33
|
+
@series = series
|
34
|
+
end
|
45
35
|
|
46
|
-
def
|
47
|
-
|
36
|
+
def render(context, filename)
|
37
|
+
plot(nil, context)
|
48
38
|
end
|
49
39
|
|
50
|
-
def
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
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
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
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
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
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
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
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
|
-
|
88
|
-
|
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
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
series_data.
|
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
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
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
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
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
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
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
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
#{
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
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
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
}
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
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
|