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.
@@ -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::Line.
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
- # Convenience constructor for GChart::Scatter.
42
- def scatter(*args, &block); Scatter.new(*args, &block) end
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]
@@ -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
@@ -0,0 +1,10 @@
1
+ module GChart
2
+ class BottomAxis < HorizontalAxis
3
+ # Returns 'x'.
4
+ def axis_type_label
5
+ 'x'
6
+ end
7
+
8
+ class << self ; public :new ; end
9
+ end
10
+ end
@@ -0,0 +1,11 @@
1
+ module GChart
2
+ class HorizontalAxis < Axis
3
+ # Returns 'R' to indicate a vertical +range_marker+ (stemming
4
+ # from a horizontal axis).
5
+ def range_marker_type_label
6
+ 'R'
7
+ end
8
+
9
+ class << self ; private :new ; end
10
+ end
11
+ end
@@ -0,0 +1,10 @@
1
+ module GChart
2
+ class LeftAxis < VerticalAxis
3
+ # Returns 'y'.
4
+ def axis_type_label
5
+ 'y'
6
+ end
7
+
8
+ class << self ; public :new ; end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ module GChart
2
+ class RightAxis < VerticalAxis
3
+ # Returns 'r'.
4
+ def axis_type_label
5
+ 'r'
6
+ end
7
+
8
+ class << self ; public :new ; end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ module GChart
2
+ class TopAxis < HorizontalAxis
3
+ # Returns 't'.
4
+ def axis_type_label
5
+ 't'
6
+ end
7
+
8
+ class << self ; public :new ; end
9
+ end
10
+ end
@@ -0,0 +1,11 @@
1
+ module GChart
2
+ class VerticalAxis < Axis
3
+ # Returns 'r' to indicate a horizontal +range_marker+ (stemming
4
+ # from a vertical axis).
5
+ def range_marker_type_label
6
+ 'r'
7
+ end
8
+
9
+ class << self ; private :new ; end
10
+ end
11
+ end
@@ -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
- max = self.max || raw.collect { |s| s.max }.max
113
-
114
- sets = raw.collect do |set|
115
- set.collect { |n| GChart.encode(:text, n, max) }.join(',')
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
- params["chco"] = colors.join(",") if colors
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("|") if legend
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