cyberfox-gchart 0.5.2 → 0.5.4
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.
- data/lib/gchart.rb +34 -6
- data/lib/gchart/axis.rb +139 -0
- data/lib/gchart/axis/bottom_axis.rb +10 -0
- data/lib/gchart/axis/horizontal_axis.rb +11 -0
- data/lib/gchart/axis/left_axis.rb +10 -0
- data/lib/gchart/axis/right_axis.rb +10 -0
- data/lib/gchart/axis/top_axis.rb +10 -0
- data/lib/gchart/axis/vertical_axis.rb +11 -0
- data/lib/gchart/base.rb +153 -24
- data/lib/gchart/colors.rb +690 -0
- data/spec/gchart/axis/bottom_axis_spec.rb +13 -0
- data/spec/gchart/axis/left_axis_spec.rb +13 -0
- data/spec/gchart/axis/right_axis_spec.rb +13 -0
- data/spec/gchart/axis/top_axis_spec.rb +13 -0
- data/spec/gchart/axis_spec.rb +216 -0
- data/spec/gchart/base_spec.rb +107 -0
- data/spec/gchart/colors_spec.rb +79 -0
- metadata +3 -11
data/lib/gchart.rb
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
require File.expand_path(File.dirname(__FILE__) + "/version")
|
|
2
|
+
require File.expand_path(File.dirname(__FILE__) + "/gchart/colors")
|
|
3
|
+
require File.expand_path(File.dirname(__FILE__) + "/gchart/axis")
|
|
2
4
|
|
|
3
5
|
%w(base bar line map meter pie pie_3d scatter sparkline venn xy_line).each do |type|
|
|
4
6
|
require File.expand_path(File.dirname(__FILE__) + "/gchart/#{type}")
|
|
5
7
|
end
|
|
6
8
|
|
|
9
|
+
%w(horizontal vertical top right bottom left).each do |type|
|
|
10
|
+
require File.expand_path(File.dirname(__FILE__) + "/gchart/axis/#{type}_axis")
|
|
11
|
+
end
|
|
12
|
+
|
|
7
13
|
module GChart
|
|
8
14
|
URL = "http://chart.apis.google.com/chart"
|
|
9
15
|
SIMPLE_CHARS = ('A'..'Z').to_a + ('a'..'z').to_a + ('0'..'9').to_a
|
|
@@ -32,25 +38,47 @@ module GChart
|
|
|
32
38
|
# Convenience constructor for GChart::Pie3D.
|
|
33
39
|
def pie3d(*args, &block); Pie3D.new(*args, &block) end
|
|
34
40
|
|
|
35
|
-
# Convenience constructor for GChart::
|
|
41
|
+
# Convenience constructor for GChart::Scatter.
|
|
42
|
+
def scatter(*args, &block); Scatter.new(*args, &block) end
|
|
43
|
+
|
|
44
|
+
# Convenience constructor for GChart::Sparkline.
|
|
36
45
|
def sparkline(*args, &block); Sparkline.new(*args, &block) end
|
|
37
46
|
|
|
38
47
|
# Convenience constructor for GChart::Venn.
|
|
39
48
|
def venn(*args, &block); Venn.new(*args, &block) end
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
49
|
+
|
|
50
|
+
def encoding=(new_encoding)
|
|
51
|
+
@encoding = new_encoding if [:text, :simple, :extended].include? new_encoding
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def encode_datasets(sets, force_max=nil)
|
|
55
|
+
max = force_max || sets.collect { |s| s.max }.max
|
|
56
|
+
|
|
57
|
+
join_character = @encoding == :text ? ',' : ''
|
|
58
|
+
|
|
59
|
+
output = sets.collect do |set|
|
|
60
|
+
set.collect { |n| GChart.encode(@encoding || :extended, n, max) }.join(join_character)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
if @encoding == :text
|
|
64
|
+
"t:#{output.join('|')}"
|
|
65
|
+
else
|
|
66
|
+
"e:#{output.join(',')}"
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
44
70
|
# Encode +n+ as a string. +n+ is normalized based on +max+.
|
|
45
71
|
# +encoding+ can currently only be :extended.
|
|
46
72
|
def encode(encoding, n, max)
|
|
73
|
+
encoding = @encoding if @encoding
|
|
74
|
+
|
|
47
75
|
case encoding
|
|
48
76
|
when :simple
|
|
49
77
|
return "_" if n.nil?
|
|
50
78
|
SIMPLE_CHARS[((n/max.to_f) * (SIMPLE_CHARS.size - 1)).round]
|
|
51
79
|
when :text
|
|
52
80
|
return "-1" if n.nil?
|
|
53
|
-
n.to_s
|
|
81
|
+
n.to_f.to_s
|
|
54
82
|
when :extended
|
|
55
83
|
return "__" if n.nil?
|
|
56
84
|
EXTENDED_PAIRS[max.zero? ? 0 : ((n/max.to_f) * (EXTENDED_PAIRS.size - 1)).round]
|
data/lib/gchart/axis.rb
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
module GChart
|
|
2
|
+
class Axis
|
|
3
|
+
|
|
4
|
+
# Array of axis labels. Can be exactly placed along the axis with
|
|
5
|
+
# +label_positions+, otherwise are evenly spaced.
|
|
6
|
+
attr_accessor :labels
|
|
7
|
+
|
|
8
|
+
# Array of float positions for +labels+. Without +labels+, the
|
|
9
|
+
# +label_positions+ are self-labeling.
|
|
10
|
+
attr_accessor :label_positions
|
|
11
|
+
|
|
12
|
+
# Takes a +Range+ of float values. With +labels+, defines +labels+
|
|
13
|
+
# context, meaning the +labels+ will be spaced at their proper
|
|
14
|
+
# location within the +range+. Without +labels+, smart-labeling
|
|
15
|
+
# occurs for +range+.
|
|
16
|
+
attr_accessor :range
|
|
17
|
+
|
|
18
|
+
# An rrggbb color for axis text.
|
|
19
|
+
attr_accessor :text_color
|
|
20
|
+
|
|
21
|
+
# Size of font in pixels. To set +font_size+, +text_color+ is also
|
|
22
|
+
# required.
|
|
23
|
+
attr_accessor :font_size
|
|
24
|
+
|
|
25
|
+
# +TEXT_ALIGNMENT+ property for axis labeling. To set
|
|
26
|
+
# +text_alignment+, both +text_color+ and +font_size+ must also be
|
|
27
|
+
# set.
|
|
28
|
+
attr_accessor :text_alignment
|
|
29
|
+
|
|
30
|
+
# Array of 2-element sub-arrays such that the 1st element in each
|
|
31
|
+
# sub-array is a +Range+ of float values which describe the start
|
|
32
|
+
# and end points of the range marker, and the 2nd element in each
|
|
33
|
+
# sub-array is an rrggbb color for the range marker. For +:top+
|
|
34
|
+
# and +:bottom+ +AXIS_TYPES+, markers are vertical. For +:right+
|
|
35
|
+
# and +:left+ +AXIS_TYPES+, markers are horizontal.
|
|
36
|
+
attr_accessor :range_markers
|
|
37
|
+
|
|
38
|
+
AXIS_TYPES = [ :top, :right, :bottom, :left ]
|
|
39
|
+
|
|
40
|
+
# Defaults: +:left+ for +RightAxis+, +:center+ for +TopAxis+ and
|
|
41
|
+
# for +BottomAxis+, and +:right+ for +LeftAxis+.
|
|
42
|
+
TEXT_ALIGNMENT = { :left => -1, :center => 0, :right => 1 }
|
|
43
|
+
|
|
44
|
+
class << self
|
|
45
|
+
# Instantiates the proper +GChart::Axis+ subclass based on the
|
|
46
|
+
# +axis_type+.
|
|
47
|
+
def create(axis_type, &block)
|
|
48
|
+
raise ArgumentError.new("Invalid axis type '#{axis_type}'") unless AXIS_TYPES.include?(axis_type)
|
|
49
|
+
|
|
50
|
+
axis = Object.module_eval("GChart::#{axis_type.to_s.capitalize}Axis").new
|
|
51
|
+
|
|
52
|
+
yield(axis) if block_given?
|
|
53
|
+
axis
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private :new
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def initialize
|
|
60
|
+
@labels = []
|
|
61
|
+
@label_positions = []
|
|
62
|
+
@range_markers = {}
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Returns a one-character label of the axis according to its type.
|
|
66
|
+
def axis_type_label
|
|
67
|
+
raise NotImplementedError.new("Method must be overridden in a subclass of this abstract base class.")
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Returns a one-character label to indicate whether
|
|
71
|
+
# +ranger_markers+ are vertical or horizontal.
|
|
72
|
+
def range_marker_type_label
|
|
73
|
+
raise NotImplementedError.new("Method must be overridden in a subclass of this abstract base class.")
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Ensures that all combinations of attributes which have been set
|
|
77
|
+
# will work with each other. Raises +ArgumentError+ otherwise.
|
|
78
|
+
def validate!
|
|
79
|
+
if labels.size > 0 and label_positions.size > 0 and labels.size != label_positions.size
|
|
80
|
+
raise ArgumentError.new(
|
|
81
|
+
"Both labels and label_positions have been specified, but their " +
|
|
82
|
+
"respective counts do not match (labels.size = '#{labels.size}' " +
|
|
83
|
+
"and label_positions.size = '#{label_positions.size}')"
|
|
84
|
+
)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
unless label_positions.all? { |pos| pos.is_a?(Numeric) }
|
|
88
|
+
raise ArgumentError.new(
|
|
89
|
+
"The label_positions attribute requires numeric values for each position specified"
|
|
90
|
+
)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
if range
|
|
94
|
+
unless range.is_a?(Range)
|
|
95
|
+
raise ArgumentError.new("The range attribute has been specified with a non-Range class")
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
unless range.first.is_a?(Numeric)
|
|
99
|
+
raise ArgumentError.new("The range attribute has been specified with non-numeric range values")
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
if font_size and not text_color
|
|
104
|
+
raise ArgumentError.new("To specify a font_size, a text_color must also be specified")
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
if text_alignment and not (text_color and font_size)
|
|
108
|
+
raise ArgumentError.new(
|
|
109
|
+
"To specify a text_alignment, both text_color and font_size must also be specified"
|
|
110
|
+
)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
if text_color and not GChart.valid_color?(text_color)
|
|
114
|
+
raise ArgumentError.new("The text_color attribute has been specified with an invalid color")
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
if font_size and not font_size.is_a?(Numeric)
|
|
118
|
+
raise ArgumentError.new("The font_size must have a numeric value")
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
if text_alignment and not TEXT_ALIGNMENT[text_alignment]
|
|
122
|
+
raise ArgumentError.new(
|
|
123
|
+
"The text_alignment attribute has been specified with a non-TEXT_ALIGNMENT"
|
|
124
|
+
)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
if not range_markers.all? { |array| array.is_a?(Array) and array.size == 2 and
|
|
128
|
+
array[0].is_a?(Range) and array[0].first.is_a?(Numeric) and
|
|
129
|
+
GChart.valid_color?(array[1]) }
|
|
130
|
+
raise ArgumentError.new(
|
|
131
|
+
"The range_markers attribute must be an array of 2-element sub-arrays such that " +
|
|
132
|
+
"the first element in each sub-array is a Range of numeric values and the second " +
|
|
133
|
+
"element in each sub-array is a valid color"
|
|
134
|
+
)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
end
|
|
139
|
+
end
|
data/lib/gchart/base.rb
CHANGED
|
@@ -5,31 +5,44 @@ module GChart
|
|
|
5
5
|
class Base
|
|
6
6
|
# Array of chart data. See subclasses for specific usage.
|
|
7
7
|
attr_accessor :data
|
|
8
|
-
|
|
8
|
+
|
|
9
9
|
# Hash of additional HTTP query params.
|
|
10
10
|
attr_accessor :extras
|
|
11
|
-
|
|
11
|
+
|
|
12
12
|
# Chart title.
|
|
13
13
|
attr_accessor :title
|
|
14
|
-
|
|
14
|
+
|
|
15
15
|
# Array of rrggbb colors, one per data set.
|
|
16
16
|
attr_accessor :colors
|
|
17
|
-
|
|
17
|
+
|
|
18
18
|
# Array of legend text, one per data set.
|
|
19
19
|
attr_accessor :legend
|
|
20
|
-
|
|
20
|
+
|
|
21
21
|
# Max data value for quantization.
|
|
22
22
|
attr_accessor :max
|
|
23
23
|
|
|
24
24
|
# Chart width, in pixels.
|
|
25
|
-
attr_reader :width
|
|
26
|
-
|
|
25
|
+
attr_reader :width
|
|
26
|
+
|
|
27
27
|
# Chart height, in pixels.
|
|
28
28
|
attr_reader :height
|
|
29
29
|
|
|
30
|
+
# Background rrggbb color of entire chart image.
|
|
31
|
+
attr_accessor :entire_background
|
|
32
|
+
|
|
33
|
+
# Background rrggbb color of just chart area of chart image.
|
|
34
|
+
attr_accessor :chart_background
|
|
35
|
+
|
|
36
|
+
# Array of +GChart::Axis+ objects.
|
|
37
|
+
attr_accessor :axes
|
|
38
|
+
|
|
30
39
|
def initialize(options={}, &block)
|
|
31
|
-
@data
|
|
40
|
+
@data = []
|
|
41
|
+
@colors = []
|
|
42
|
+
@legend = []
|
|
43
|
+
@axes = []
|
|
32
44
|
@extras = {}
|
|
45
|
+
|
|
33
46
|
@width = 300
|
|
34
47
|
@height = 200
|
|
35
48
|
|
|
@@ -51,9 +64,9 @@ module GChart
|
|
|
51
64
|
# if +height+ is less than 1 or greater than 1,000.
|
|
52
65
|
def height=(height)
|
|
53
66
|
if height.nil? || height < 1 || height > 1_000
|
|
54
|
-
raise ArgumentError, "Invalid height: #{height.inspect}"
|
|
67
|
+
raise ArgumentError, "Invalid height: #{height.inspect}"
|
|
55
68
|
end
|
|
56
|
-
|
|
69
|
+
|
|
57
70
|
@height = height
|
|
58
71
|
end
|
|
59
72
|
|
|
@@ -66,7 +79,7 @@ module GChart
|
|
|
66
79
|
# if +width+ * +height+ is greater than 300,000 pixels.
|
|
67
80
|
def size=(size)
|
|
68
81
|
self.width, self.height = size.split("x").collect { |n| Integer(n) }
|
|
69
|
-
|
|
82
|
+
|
|
70
83
|
if (width * height) > 300_000
|
|
71
84
|
raise ArgumentError, "Invalid size: #{size.inspect} yields a graph with more than 300,000 pixels"
|
|
72
85
|
end
|
|
@@ -90,6 +103,14 @@ module GChart
|
|
|
90
103
|
open(io_or_file, "w+") { |io| io.write(fetch) }
|
|
91
104
|
end
|
|
92
105
|
|
|
106
|
+
# Adds an +axis_type+ +GChart::Axis+ to the chart's set of
|
|
107
|
+
# +axes+. See +GChart::Axis::AXIS_TYPES+.
|
|
108
|
+
def axis(axis_type, &block)
|
|
109
|
+
axis = GChart::Axis.create(axis_type, &block)
|
|
110
|
+
@axes.push(axis)
|
|
111
|
+
axis
|
|
112
|
+
end
|
|
113
|
+
|
|
93
114
|
protected
|
|
94
115
|
|
|
95
116
|
def query_params(raw_params={}) #:nodoc:
|
|
@@ -99,6 +120,13 @@ module GChart
|
|
|
99
120
|
render_title(params)
|
|
100
121
|
render_colors(params)
|
|
101
122
|
render_legend(params)
|
|
123
|
+
render_backgrounds(params)
|
|
124
|
+
|
|
125
|
+
unless @axes.empty?
|
|
126
|
+
if is_a?(GChart::Line) or is_a?(GChart::Bar) or is_a?(GChart::Scatter) # or is_a?(GChart::Radar)
|
|
127
|
+
render_axes(params)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
102
130
|
|
|
103
131
|
params.merge(extras)
|
|
104
132
|
end
|
|
@@ -106,28 +134,129 @@ module GChart
|
|
|
106
134
|
def render_chart_type #:nodoc:
|
|
107
135
|
raise NotImplementedError, "override in subclasses"
|
|
108
136
|
end
|
|
109
|
-
|
|
137
|
+
|
|
110
138
|
def render_data(params) #:nodoc:
|
|
111
139
|
raw = data && data.first.is_a?(Array) ? data : [data]
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
params["chd"] = "t:#{sets.join("|")}"
|
|
140
|
+
|
|
141
|
+
encoded = GChart.encode_datasets(raw, max)
|
|
142
|
+
|
|
143
|
+
params["chd"] = encoded
|
|
119
144
|
end
|
|
120
145
|
|
|
121
146
|
def render_title(params) #:nodoc:
|
|
122
147
|
params["chtt"] = title.tr("\n ", "|+") if title
|
|
123
148
|
end
|
|
124
|
-
|
|
149
|
+
|
|
125
150
|
def render_colors(params) #:nodoc:
|
|
126
|
-
|
|
151
|
+
unless colors.empty?
|
|
152
|
+
params["chco"] = colors.collect{ |color| GChart.expand_color(color) }.join(",")
|
|
153
|
+
end
|
|
127
154
|
end
|
|
128
|
-
|
|
155
|
+
|
|
129
156
|
def render_legend(params) #:nodoc:
|
|
130
|
-
params["chdl"] = legend.join("|")
|
|
131
|
-
end
|
|
157
|
+
params["chdl"] = legend.join("|") unless legend.empty?
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def render_backgrounds(params) #:nodoc:
|
|
161
|
+
if entire_background || chart_background
|
|
162
|
+
if entire_background and not GChart.valid_color?(entire_background)
|
|
163
|
+
raise ArgumentError.new("The entire_background attribute has an invalid color")
|
|
164
|
+
end
|
|
165
|
+
if chart_background and not GChart.valid_color?(chart_background)
|
|
166
|
+
raise ArgumentError.new("The chart_background attribute has an invalid color")
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
separator = entire_background && chart_background ? "|" : ""
|
|
170
|
+
params["chf"] = entire_background ? "bg,s,#{GChart.expand_color(entire_background)}" : ""
|
|
171
|
+
params["chf"] += "#{separator}c,s,#{GChart.expand_color(chart_background)}" if chart_background
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def render_axes(params) #:nodoc:
|
|
176
|
+
@axes.each do |axis|
|
|
177
|
+
axis.validate!
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
render_axis_type_labels(params)
|
|
181
|
+
render_axis_labels(params)
|
|
182
|
+
render_axis_label_positions(params)
|
|
183
|
+
render_axis_ranges(params)
|
|
184
|
+
render_axis_styles(params)
|
|
185
|
+
render_axis_range_markers(params)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def render_axis_type_labels(params) #:nodoc:
|
|
189
|
+
params["chxt"] = @axes.collect{ |axis| axis.axis_type_label }.join(',')
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def render_axis_labels(params) #:nodoc:
|
|
193
|
+
if @axes.any?{ |axis| axis.labels.size > 0 }
|
|
194
|
+
chxl = []
|
|
195
|
+
|
|
196
|
+
@axes.each_with_index do |axis, index|
|
|
197
|
+
if axis.labels.size > 0
|
|
198
|
+
chxl.push("#{index}:")
|
|
199
|
+
chxl += axis.labels
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
params["chxl"] = chxl.join('|')
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def render_axis_label_positions(params) #:nodoc:
|
|
208
|
+
if @axes.any?{ |axis| axis.label_positions.size > 0 }
|
|
209
|
+
chxp = []
|
|
210
|
+
|
|
211
|
+
@axes.each_with_index do |axis, index|
|
|
212
|
+
chxp.push("#{index}," + axis.label_positions.join(',')) if axis.label_positions.size > 0
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
params["chxp"] = chxp.join('|')
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def render_axis_ranges(params) #:nodoc:
|
|
220
|
+
if @axes.any?{ |axis| axis.range }
|
|
221
|
+
chxr = []
|
|
222
|
+
|
|
223
|
+
@axes.each_with_index do |axis, index|
|
|
224
|
+
chxr.push("#{index},#{axis.range.first},#{axis.range.last}") if axis.range
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
params["chxr"] = chxr.join('|')
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def render_axis_styles(params) #:nodoc:
|
|
232
|
+
if @axes.any?{ |axis| axis.text_color }
|
|
233
|
+
chxs = []
|
|
234
|
+
|
|
235
|
+
@axes.each_with_index do |axis, index|
|
|
236
|
+
if axis.text_color
|
|
237
|
+
chxs.push(
|
|
238
|
+
"#{index}," +
|
|
239
|
+
[GChart.expand_color(axis.text_color), axis.font_size, axis.text_alignment].compact.join(',')
|
|
240
|
+
)
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
params["chxs"] = chxs.join('|')
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def render_axis_range_markers(params) #:nodoc:
|
|
249
|
+
if @axes.any?{ |axis| axis.range_markers.size > 0 }
|
|
250
|
+
chmr = []
|
|
251
|
+
|
|
252
|
+
@axes.each do |axis|
|
|
253
|
+
axis.range_markers.each do |range, color|
|
|
254
|
+
chmr.push("#{axis.range_marker_type_label},#{color},0,#{range.first},#{range.last}")
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
params["chm"] = chmr.join('|')
|
|
259
|
+
end
|
|
260
|
+
end
|
|
132
261
|
end
|
|
133
262
|
end
|