smart_chart 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ *.sw?
2
+ .DS_Store
3
+ coverage
4
+ rdoc
5
+ pkg
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Alex Reisner
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,187 @@
1
+ = SmartChart
2
+
3
+ SmartChart is an easy way to render charts on web pages. It uses the Google Charts engine so there are no server-side dependencies or performance issues--just install and go.
4
+
5
+ <b>SmartChart is still in the early stages of development. Only maps, barcodes, and line charts (partially) are working at all. However, much of the interface is described below (plus to-do list) and if you'd like to contribute, please do.</b>
6
+
7
+
8
+ == Key Benefits
9
+
10
+ <b>1. Designed as a chart-making interface, not as a Google Charts wrapper.</b> Other APIs effectively just give Google Chart parameters different names, leading you to wonder: why am I learning an API to an API? SmartChart is an intelligent chart-authoring syntax that happens to use Google Charts as a back-end. It may support other charting engines in the future.
11
+
12
+ <b>2. Place chart elements with respect to data points, not chart size.</b> If you want horizontal axis lines on your graph every 10 units (along the Y-axis) you simply specify this. If you've worked much with the native Google Charts interface you know you have to do several calculations to get this to work, and any library that simply "wraps" Google Charts suffers from this same annoyance.
13
+
14
+ <b>3. You get useful feedback when you do something wrong.</b> If you specify more data points than Google can handle, you get an error message. If you specify a bigger chart than Google will serve, you get an error message. Forget to specify a required parameter? That's an error message too. The thing is, with the raw Google Charts interface you get no useful feedback in any of these cases, which can lead to _very_ long and frustrating debugging sessions.
15
+
16
+ <b>4. The best data encoding is selected automatically.</b> SmartChart examines your data and selects the optimal way to encode your data to keep HTTP requests short while preserving granularity. There's no way a chart author should have to think about Google's data encoding methods. Forget I even mentioned it.
17
+
18
+
19
+ == Examples
20
+
21
+ SmartChart::Line.new(
22
+
23
+ # y-axis range
24
+ :y_min => -40,
25
+ :y_max => 80,
26
+
27
+ # data (specify line/bar styles with data)
28
+ :data => [
29
+ {
30
+ :values => [1,2,3,4],
31
+ :label => "Profit",
32
+ :thickness => 2,
33
+ :color => '550055',
34
+ :style => {:solid => 3, :blank => 2}
35
+ },
36
+ {
37
+ :values => [2,4,6,8],
38
+ :label => "Reputation",
39
+ :thickness => 2,
40
+ :color => 'AABBCC',
41
+ :style => :dotted
42
+ }
43
+ ],
44
+
45
+ # axis lines
46
+ :axis => {
47
+ :sides => [:left, :right, :bottom], # empty array for none
48
+ :color => 'DDDDDD',
49
+ :style => :dashed
50
+ }
51
+
52
+ # grid lines
53
+ :grid => {
54
+ :x => {:every => 10, :offset => 2}, # based on number of data points
55
+ :y => {:every => 5}, # based on numeric data range
56
+ :style => :dashed # no :color or :thickness
57
+ },
58
+
59
+ # labels
60
+ :x_labels => {
61
+ 1 => "Jan",
62
+ 4 => "Apr",
63
+ 7 => "Jul",
64
+ 10 => "Oct"
65
+ }
66
+
67
+ # options for HTML tag
68
+ :html => {
69
+ :id => "stock_graph",
70
+ :class => "graph"
71
+ }
72
+ )
73
+
74
+ SmartChart::Pie.new(
75
+ :style => "3d",
76
+ :rotate => 45, # degrees from vertical (start of first slice)
77
+ ...
78
+ )
79
+
80
+ # display
81
+ g = SmartChart::Line.new(...)
82
+ g.to_url
83
+ g.to_html
84
+
85
+ # QR Code
86
+ g = SmartChart::QRCode.new(:data => "some data").to_s
87
+
88
+
89
+ == Specifying Data
90
+
91
+ Data is specified in slightly different ways for different charts. In the simplest case, a QR code (<tt>SmartChart::QRCode</tt>), the data is simply a string:
92
+
93
+ chart.data = "A sentence full of data."
94
+
95
+ Another simple case is a map (<tt>SmartChart::Map</tt>), where data is specified as a hash of region-value pairs:
96
+
97
+ chart.data = {
98
+ :US => 74,
99
+ :CA => 81,
100
+ :MX => 52,
101
+ :RU => 19,
102
+ :AU => 41
103
+ }
104
+
105
+ Data can be passed to pie charts in a similar way. For more complex graphs depicting multiple series, data and other information about each series is given as a hash (in an array if there is more than one), for example for a line graph:
106
+
107
+ chart.data = [
108
+ {
109
+ :values => [23, 26, 46, 52, 51, 78],
110
+ :label => "Stock price",
111
+ :thickness => 2,
112
+ :color => '0099FF'
113
+ },
114
+ {
115
+ :values => [65, 64, 58, 52, 63, 79],
116
+ :label => "Consumer interest",
117
+ :thickness => 1,
118
+ :color => 'FF0099',
119
+ :style => :dotted
120
+ }
121
+ }
122
+ ]
123
+
124
+
125
+ == Axis Lines
126
+
127
+ Actual output is limited by Google's requirements. For example, while Google provides for a line chart with no axis lines ("sparkline"), there is no analogous bar chart type. However, SmartChart will simulate this for you by cropping the chart image using a <div> when you call the chart.to_html method.
128
+
129
+
130
+ == To-do List
131
+
132
+ * query_string_params should be a class variable so feature modules can
133
+ auto-add their parameters
134
+
135
+ * validations
136
+ * margins and legend dimensions are integers
137
+ * grid line attributes
138
+
139
+ * grids
140
+ * easy placement of y-gridline at zero, if exists
141
+ * easy placement of gridlines at label positions
142
+
143
+ * axis lines
144
+ * specify which ones to print
145
+ * hide all ("sparklines")
146
+ * hide bar graph axes by hiding 1px from left and bottom of image when to_html is called
147
+ * chxr parameter?
148
+
149
+ * labels
150
+ * on line, scatter, bar graphs
151
+ * labels on other axes (top and right)
152
+ * multiple rows of labels on same axis
153
+
154
+ * legend
155
+ * size, position
156
+ * inline legends (line up with ends of lines -- see http://code.google.com/p/graphy/wiki/UserGuide)
157
+
158
+ * general
159
+ * support advanced background ("fill") options like gradients
160
+
161
+ * markers
162
+ * note: invisible data series available for marker positioning
163
+ see: http://code.google.com/apis/chart/formats.html#multiple_data_series
164
+
165
+ * SingleDataSetChart
166
+ * document attributes
167
+
168
+ * QRCode
169
+ * data length validation for given EC level and character type
170
+ * see table: http://code.google.com/apis/chart/types.html#qrcodes
171
+ * may be irrelevant because URL_MAX_LENGTH == 2074
172
+
173
+ * data and encoding
174
+ * The best encoding type should be selected automatically (whatever is shortest with enough granularity). Avoid URLs longer than 2074 characters. Default to Extended, but use Simple if (1) URL would be too long, (2) image is less than 100px tall, or (3) not enough data point to justify it.
175
+ * data granularity adjustment (curve smoothing, rolling average?)
176
+ * see bottom: http://code.google.com/apis/chart/formats.html
177
+ * at least 1 pixel per data point
178
+
179
+
180
+
181
+ == References
182
+
183
+ Other Google Charts APIs:
184
+ http://groups.google.com/group/google-chart-api/web/useful-links-to-api-libraries
185
+
186
+
187
+ Copyright (c) 2009 Alex Reisner. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,56 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "smart_chart"
8
+ gem.summary = %Q{Easily create charts and graphs for the web (uses Google Charts).}
9
+ gem.description = %Q{Easily create charts and graphs for the web (uses Google Charts).}
10
+ gem.email = "alex@alexreisner.com"
11
+ gem.homepage = "http://github.com/alexreisner/smart_chart"
12
+ gem.authors = ["Alex Reisner"]
13
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
14
+ end
15
+ Jeweler::GemcutterTasks.new
16
+ rescue LoadError
17
+ puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
18
+ end
19
+
20
+ require 'rake/testtask'
21
+ Rake::TestTask.new(:test) do |test|
22
+ test.libs << 'lib' << 'test'
23
+ test.pattern = 'test/**/*_test.rb'
24
+ test.verbose = true
25
+ end
26
+
27
+ begin
28
+ require 'rcov/rcovtask'
29
+ Rcov::RcovTask.new do |test|
30
+ test.libs << 'test'
31
+ test.pattern = 'test/**/*_test.rb'
32
+ test.verbose = true
33
+ end
34
+ rescue LoadError
35
+ task :rcov do
36
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
37
+ end
38
+ end
39
+
40
+ task :test => :check_dependencies
41
+
42
+ task :default => :test
43
+
44
+ require 'rake/rdoctask'
45
+ Rake::RDocTask.new do |rdoc|
46
+ if File.exist?('VERSION')
47
+ version = File.read('VERSION')
48
+ else
49
+ version = ""
50
+ end
51
+
52
+ rdoc.rdoc_dir = 'rdoc'
53
+ rdoc.title = "SmartChart #{version}"
54
+ rdoc.rdoc_files.include('README*')
55
+ rdoc.rdoc_files.include('lib/**/*.rb')
56
+ end
@@ -0,0 +1,428 @@
1
+ module SmartChart
2
+
3
+ ##
4
+ # Maximum length of URL accepted by Google.
5
+ #
6
+ URL_MAX_LENGTH = 2074
7
+
8
+ ##
9
+ # Takes a decimal number and returns a string with up to +frac+
10
+ # digits to the right of the '.'.
11
+ #
12
+ def self.decimal_string(num, frac = 3)
13
+ str = "%.#{frac}f" % num
14
+ str = str[0...-1] while str[-1,1] == "0"
15
+ str = str[0...-1] if str[-1,1] == "." # leave zeros left of .
16
+ str
17
+ end
18
+
19
+ ##
20
+ # Method names are called attributes, data for URL are called parameters.
21
+ # Use attr_writers for all attributes, and wrte readers so
22
+ # they instantiate the correct object type.
23
+ #
24
+ class BaseChart
25
+
26
+ # dimensions of chart image, in pixels
27
+ attr_accessor :width, :height
28
+
29
+ # chart data
30
+ attr_accessor :data
31
+
32
+ # chart range
33
+ attr_accessor :y_min, :y_max
34
+
35
+ # chart background
36
+ attr_accessor :background
37
+
38
+ # chart margins
39
+ attr_accessor :margins
40
+
41
+ # legend properties
42
+ attr_accessor :legend
43
+
44
+ # bar chart orientation -- :vertical (default) or :horizontal
45
+ # pie chart orientation -- degrees of rotation
46
+ attr_accessor :orientation
47
+
48
+ # bar -- :grouped (default) or :stacked
49
+ # pie -- nil (2D, default), "3d", or :concentric
50
+ # radar -- nil (default) or :filled
51
+ attr_accessor :style
52
+
53
+ ##
54
+ # Accept attributes and attempt to assign each to an attribute.
55
+ #
56
+ def initialize(options = {})
57
+ options.each do |k,v|
58
+ begin
59
+ send("#{k}=", v)
60
+ rescue NoMethodError
61
+ raise NoAttributeError.new(self, k)
62
+ end
63
+ end
64
+ end
65
+
66
+ ##
67
+ # Get the chart URL query string.
68
+ #
69
+ def to_query_string(encode = true, validation = true)
70
+ validate if validation
71
+ query_string(encode)
72
+ end
73
+
74
+ ##
75
+ # Get the full chart URL.
76
+ #
77
+ def to_url(encode = true, validation = true)
78
+ "http://chart.apis.google.com/chart?" +
79
+ to_query_string(encode, validation)
80
+ end
81
+
82
+ ##
83
+ # Chart as an HTML tag.
84
+ #
85
+ def to_html(encode = true, validation = true)
86
+ '<img src="%s" />' % to_url(encode, validation)
87
+ end
88
+
89
+ ##
90
+ # Run validation (may raise exceptions).
91
+ #
92
+ def validate!
93
+ validate
94
+ end
95
+
96
+ ##
97
+ # Does the chart pass all validations?
98
+ #
99
+ def valid?
100
+ begin
101
+ validate
102
+ true
103
+ rescue ValidationError
104
+ false
105
+ end
106
+ end
107
+
108
+
109
+ private # ---------------------------------------------------------------
110
+
111
+ ##
112
+ # Chart type URL parameter, for example:
113
+ #
114
+ # :bvs # vertical bar
115
+ # :p # pie
116
+ # :p3 # 3D pie
117
+ #
118
+ # All subclasses *must* implement this method.
119
+ #
120
+ def type
121
+ fail
122
+ end
123
+
124
+ ##
125
+ # Get an array of values to be graphed.
126
+ # Subclasses *must* implement this method.
127
+ #
128
+ def data_values
129
+ fail
130
+ end
131
+
132
+ ##
133
+ # The number of data points represented along the x-axis.
134
+ #
135
+ def data_values_count
136
+ data_values.map{ |set| set.size }.max
137
+ end
138
+
139
+ ##
140
+ # Get the minimum Y-value for the chart (from data or explicitly set).
141
+ #
142
+ def y_min
143
+ @y_min || data_values.flatten.compact.min
144
+ end
145
+
146
+ ##
147
+ # Get the maximum Y-value for the chart (from data or explicitly set).
148
+ #
149
+ def y_max
150
+ @y_max || data_values.flatten.compact.max
151
+ end
152
+
153
+ ##
154
+ # Array of names of required attributes.
155
+ #
156
+ def required_attrs
157
+ [
158
+ :width,
159
+ :height,
160
+ :data
161
+ ]
162
+ end
163
+
164
+ ##
165
+ # Array of validations to be run on the chart.
166
+ #
167
+ def validations
168
+ [
169
+ :required_attrs,
170
+ :dimensions,
171
+ :data_format,
172
+ :labels,
173
+ :colors,
174
+ :url_length
175
+ ]
176
+ end
177
+
178
+ ##
179
+ # Make sure chart dimensions are within Google's 300,000 pixel limit.
180
+ #
181
+ def validate_dimensions
182
+ raise DimensionsError unless width * height <= 300000
183
+ end
184
+
185
+ ##
186
+ # Validate data format.
187
+ # Subclasses *must* implement this method.
188
+ #
189
+ def validate_data_format
190
+ fail
191
+ end
192
+
193
+ ##
194
+ # Make sure labels are specified in proper format
195
+ # Subclasses *must* implement this method.
196
+ #
197
+ def validate_labels
198
+ end
199
+
200
+ ##
201
+ # Make sure colors are valid hex codes.
202
+ # Subclasses should probably implement this method.
203
+ #
204
+ def validate_colors
205
+ validate_color(background)
206
+ end
207
+
208
+
209
+ # --- subclasses should not overwrite anything below this line ----------
210
+
211
+ ##
212
+ # The query string for the chart.
213
+ #
214
+ def query_string(encode = true)
215
+ values = query_string_params.map{ |p| format_param(p, encode) }
216
+ values.compact.join("&")
217
+ end
218
+
219
+ ##
220
+ # Format a query string parameter for a URL (string: name=value). Uses
221
+ # %-encoding unless second argument is false.
222
+ #
223
+ def format_param(name, encode = true)
224
+ unless (value = send(name).to_s) == ""
225
+ value = CGI.escape(value) if encode
226
+ name.to_s + '=' + value
227
+ end
228
+ end
229
+
230
+ ##
231
+ # Is the data given as a single bare array of values?
232
+ #
233
+ def bare_data_set?
234
+ data.is_a?(Array) and ![Array, Hash].include?(data.first.class)
235
+ end
236
+
237
+ ##
238
+ # Run all validations on the chart attributes.
239
+ #
240
+ def validate
241
+ validations.each{ |v| send "validate_#{v}" }
242
+ end
243
+
244
+ ##
245
+ # Make sure all required chart attributes are specified.
246
+ #
247
+ def validate_required_attrs
248
+ required_attrs.each do |param|
249
+ if send(param).nil?
250
+ raise MissingRequiredAttributeError.new(self, param)
251
+ end
252
+ end
253
+ end
254
+
255
+ ##
256
+ # Make sure encoded URL is no longer than the maximum allowed length.
257
+ #
258
+ def validate_url_length
259
+ raise UrlLengthError unless to_url(true, false).size <= URL_MAX_LENGTH
260
+ end
261
+
262
+ ##
263
+ # Validate a single color (this is not a normal validator).
264
+ #
265
+ def validate_color(c)
266
+ raise ColorFormatError unless (c.nil? or c.match(/^[0-9A-Fa-f]{6}$/))
267
+ end
268
+
269
+
270
+ # --- URL parameter list and methods ------------------------------------
271
+
272
+ ##
273
+ # Array of names of all possible query string parameters in the order
274
+ # in which they are output (for easier testing).
275
+ #
276
+ def query_string_params
277
+ [
278
+ :cht, # type
279
+ :chs, # size
280
+ :chd, # data
281
+
282
+ :chco, # color
283
+ :chf, # fill
284
+
285
+ :chl, # labels
286
+ :chxt, # axis_type
287
+ :chxs, # axis_style
288
+ :chxl, # axis_labels
289
+ :chxp, # axis_label_positions
290
+ :chxr, # axis_range
291
+ :chma, # margins
292
+
293
+ :chbh, # bar_spacing
294
+ :chp, # bar_chart_zero_line, pie chart rotation
295
+
296
+ :chm, # markers
297
+
298
+ :chtt, # title
299
+ :chdl, # legend
300
+ :chdlp, # legend_position
301
+
302
+ :chds # data_scaling -- never used
303
+ ]
304
+ end
305
+
306
+ #
307
+ # All parameter methods should return a string, or an object that
308
+ # renders itself as a string via the to_s method.
309
+ #
310
+
311
+ # cht
312
+ def cht
313
+ type
314
+ end
315
+
316
+ # chs
317
+ def chs
318
+ "#{width}x#{height}"
319
+ end
320
+
321
+ # chd
322
+ def chd
323
+ Encoder.encode(data_values, y_min, y_max)
324
+ end
325
+
326
+ # chco
327
+ def chco
328
+ data.map{ |d|
329
+ if d.is_a?(Hash) and c = d[:color]
330
+ c = [c] unless c.is_a?(Array)
331
+ c.join('|') # data point delimiter
332
+ end
333
+ }.compact.join(',') # data set delimiter
334
+ end
335
+
336
+ # chf
337
+ def chf
338
+ "bg,s,#{background}" if background
339
+ end
340
+
341
+ # chl
342
+ def chl
343
+ nil
344
+ end
345
+
346
+ # chxt
347
+ def chxt
348
+ nil
349
+ end
350
+
351
+ # chxl
352
+ def chxl
353
+ nil
354
+ end
355
+
356
+ # chxp
357
+ def chxp
358
+ nil
359
+ end
360
+
361
+ # chxr
362
+ def chxr
363
+ nil
364
+ end
365
+
366
+ # chxs
367
+ def chxs
368
+ nil
369
+ end
370
+
371
+ ##
372
+ # Are legend dimensions specified?
373
+ #
374
+ def legend_dimensions_given?
375
+ legend.is_a?(Hash) and (legend[:width] or legend[:height])
376
+ end
377
+
378
+ # chma
379
+ def chma
380
+ return nil unless (margins or legend_dimensions_given?)
381
+ value = ""
382
+ if margins.is_a?(Hash)
383
+ pixels = [:left, :right, :top, :bottom].map{ |i| margins[i] || 0 }
384
+ value << pixels.join(',')
385
+ end
386
+ if legend_dimensions_given?
387
+ value << "0,0,0,0" if value == ""
388
+ value << "|#{legend[:width] || 0},#{legend[:height] || 0}"
389
+ end
390
+ value
391
+ end
392
+
393
+ # chbh
394
+ def chbh
395
+ nil
396
+ end
397
+
398
+ # chp
399
+ def chp
400
+ nil
401
+ end
402
+
403
+ # chm
404
+ def chm
405
+ nil
406
+ end
407
+
408
+ # chtt
409
+ def chtt
410
+ nil
411
+ end
412
+
413
+ # chdl
414
+ def chdl
415
+ nil
416
+ end
417
+
418
+ # chdlp
419
+ def chdlp
420
+ nil
421
+ end
422
+
423
+ # chds -- never used
424
+ def chds
425
+ nil
426
+ end
427
+ end
428
+ end