smart_chart 0.0.1

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.
@@ -0,0 +1,47 @@
1
+ module SmartChart
2
+ module GridLines
3
+
4
+ def self.included(base)
5
+ base.class_eval do
6
+ attr_accessor :grid
7
+ end
8
+ end
9
+
10
+
11
+ private # ---------------------------------------------------------------
12
+
13
+ ##
14
+ # Grid lines parameter.
15
+ #
16
+ def chg
17
+ return nil unless (grid.is_a?(Hash) and (grid[:x] or grid[:y]))
18
+ style = line_style_to_array(grid)
19
+ [ x_grid_property(:every) || 0,
20
+ y_grid_property(:every) || 0,
21
+ style[1],
22
+ style[2],
23
+ x_grid_property(:offset) || 0,
24
+ y_grid_property(:offset) || 0
25
+ ].join(",")
26
+ end
27
+
28
+ ##
29
+ # Compute x-grid :every or :offset (as a string).
30
+ #
31
+ def x_grid_property(property)
32
+ return nil unless grid.is_a?(Hash)
33
+ return nil unless (grid[:x].is_a?(Hash) and s = grid[:x][property])
34
+ SmartChart.decimal_string(s.to_f * 100 / data_values_count.to_f)
35
+ end
36
+
37
+ ##
38
+ # Compute y-grid :every or :offset (as a string).
39
+ #
40
+ def y_grid_property(property)
41
+ return nil unless grid.is_a?(Hash)
42
+ return nil unless (grid[:y].is_a?(Hash) and s = grid[:y][property])
43
+ range = y_max - y_min
44
+ SmartChart.decimal_string(s.to_f * 100 / range.to_f)
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,23 @@
1
+ module SmartChart
2
+ module Labels
3
+
4
+ def self.included(base)
5
+ base.class_eval do
6
+ attr_accessor :labels
7
+ end
8
+ end
9
+
10
+
11
+ private # ---------------------------------------------------------------
12
+
13
+ ##
14
+ # Labels parameter.
15
+ #
16
+ def chdl
17
+ unless bare_data_set?
18
+ labels = data.map{ |d| d.is_a?(Hash) ? d[:label] : nil }
19
+ labels.compact.size > 0 ? labels.join("|") : nil
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,128 @@
1
+ module SmartChart
2
+ class MultipleDataSetChart < BaseChart
3
+ include Labels
4
+ include AxisLines
5
+
6
+ private # ---------------------------------------------------------------
7
+
8
+ ##
9
+ # Extract an array of arrays (data sets) from the +data+ attribute.
10
+ #
11
+ def data_values
12
+ if bare_data_set?
13
+ [data]
14
+ else
15
+ data.map{ |set| set.is_a?(Hash) ? set[:values] : set }
16
+ end
17
+ end
18
+
19
+ ##
20
+ # Array of all possible query string parameters.
21
+ #
22
+ def query_string_params
23
+ super + [:chls]
24
+ end
25
+
26
+ ##
27
+ # Line style parameter.
28
+ #
29
+ def chls
30
+ validate_data_format
31
+ return nil if bare_data_set?
32
+ lines = data.map do |set|
33
+ if set.is_a?(Hash)
34
+ line_style_to_array(set)
35
+ else
36
+ [1, 1, 0]
37
+ end
38
+ end
39
+ # only return non-nil if styles other than default are given
40
+ if lines.map{ |l| l == [1,1,0] ? nil : 1 }.compact.size > 0
41
+ lines.map{ |s| s.join(",") }.join("|")
42
+ end
43
+ end
44
+
45
+ ##
46
+ # Translate a line style to a three-element array:
47
+ #
48
+ # [thickness, solid, blank]
49
+ #
50
+ # Takes a hash or a symbol (shortcut for a pre-defined look).
51
+ #
52
+ def line_style_to_array(data)
53
+ return [1, 1, 0] if data.nil?
54
+ thickness = data[:thickness] || 1
55
+ style = data[:style]
56
+ style_arr = style.is_a?(Hash) ? [style[:solid], style[:blank]] :
57
+ line_style_definition(style, thickness)
58
+ [thickness] + style_arr
59
+ end
60
+
61
+ ##
62
+ # Translate a symbol into a line style: a two-element array (solid line
63
+ # length, blank line length). Takes a style name and line thickness.
64
+ #
65
+ def line_style_definition(symbol, thickness = 1)
66
+ self.class.line_styles(thickness)[symbol] || [1, 0]
67
+ end
68
+
69
+ ##
70
+ # Get a hash of line style definitions.
71
+ #
72
+ def self.line_styles(thickness = 1)
73
+ {
74
+ :solid => [thickness * 1, thickness * 0],
75
+ :dotted => [thickness * 1, thickness * 1],
76
+ :short => [thickness * 2, thickness * 4],
77
+ :dashed => [thickness * 4, thickness * 4],
78
+ :long => [thickness * 6, thickness * 4]
79
+ }
80
+ end
81
+
82
+ ##
83
+ # Array of validations to be run on the chart.
84
+ #
85
+ def validations
86
+ [:line_style_names] + super
87
+ end
88
+
89
+ ##
90
+ # Raise an exception unless the provided data is given as an array.
91
+ #
92
+ def validate_data_format
93
+ unless data.is_a?(Array)
94
+ raise DataFormatError, "Data set(s) should be given as an array"
95
+ end
96
+ end
97
+
98
+ ##
99
+ # Make sure colors are valid hex codes.
100
+ #
101
+ def validate_colors
102
+ super
103
+ data.each do |d|
104
+ if d.is_a?(Hash) and c = d[:color]
105
+ validate_color(c)
106
+ end
107
+ end
108
+ end
109
+
110
+ def validate_line_style_names
111
+ data.each do |d|
112
+ if d.is_a?(Hash) and d.is_a?(Hash)
113
+ if (style = d[:style]).is_a?(Symbol)
114
+ unless self.class.line_styles.keys.include?(style)
115
+ raise LineStyleNameError,
116
+ "Line style name '#{style}' is not valid. " +
117
+ "Try one of: #{self.class.line_styles.keys.join(', ')}"
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
124
+
125
+
126
+ class LineStyleNameError < ValidationError #:nodoc:
127
+ end
128
+ end
@@ -0,0 +1,56 @@
1
+ module SmartChart
2
+ class SingleDataSetChart < BaseChart
3
+
4
+ # colors to be used
5
+ attr_accessor :colors
6
+
7
+
8
+ private # ---------------------------------------------------------------
9
+
10
+ ##
11
+ # Data to be encoded.
12
+ #
13
+ def data_values
14
+ d = data.is_a?(Hash) ? data.to_a : data
15
+ [d.inject([]){ |values,i| values << i[1] }]
16
+ end
17
+
18
+ ##
19
+ # Texts of labels.
20
+ #
21
+ def labels
22
+ d = data.is_a?(Hash) ? data.to_a : data
23
+ d.inject([]){ |values,i| values << i[0] }
24
+ end
25
+
26
+ ##
27
+ # Raise an exception unless the provided data is given as an array.
28
+ #
29
+ def validate_data_format
30
+ unless data.is_a?(Hash)
31
+ raise DataFormatError, "Data should be given as a hash"
32
+ end
33
+ end
34
+
35
+ ##
36
+ # Make sure colors are valid hex codes.
37
+ #
38
+ def validate_colors
39
+ super
40
+ return if colors.nil?
41
+ colors.each{ |c| validate_color(c) }
42
+ end
43
+
44
+ ##
45
+ # Labels parameter.
46
+ #
47
+ def chdl
48
+ data.is_a?(Array) ? nil : data.keys.join("|")
49
+ end
50
+
51
+ # chco
52
+ def chco
53
+ colors.join(',') unless colors.nil?
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,30 @@
1
+ # external dependencies
2
+ require 'cgi'
3
+
4
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), "smart_chart"))
5
+
6
+ # common code
7
+ require 'encoder'
8
+ require 'exceptions'
9
+
10
+ # features
11
+ require 'features/axis_lines'
12
+ require 'features/grid_lines'
13
+ require 'features/labels'
14
+
15
+ # chart parents
16
+ require 'base_chart'
17
+ require 'single_data_set_chart'
18
+ require 'multiple_data_set_chart'
19
+
20
+ # chart types
21
+ require 'charts/bar'
22
+ require 'charts/line'
23
+ require 'charts/map'
24
+ require 'charts/meter'
25
+ require 'charts/pie'
26
+ require 'charts/qr_code'
27
+ require 'charts/radar'
28
+ require 'charts/scatter'
29
+ require 'charts/venn'
30
+
@@ -0,0 +1,400 @@
1
+ require 'test_helper'
2
+
3
+ class SmartChartTest < Test::Unit::TestCase
4
+
5
+ # --- labels --------------------------------------------------------------
6
+
7
+ def test_line_graph_labels
8
+ c = line_graph(:data => [{
9
+ :values => [1,2,3],
10
+ :label => "One"
11
+ },{
12
+ :values => [4,5,6],
13
+ :label => "Two"
14
+ }])
15
+ assert_equal "One|Two", c.send(:chdl)
16
+ end
17
+
18
+
19
+ # --- background color ----------------------------------------------------
20
+
21
+ def test_line_graph_background_color
22
+ c = line_graph(:background => "000000")
23
+ assert_equal "bg,s,000000", c.send(:chf)
24
+ end
25
+
26
+
27
+ # --- margins -------------------------------------------------------------
28
+
29
+ def test_margins
30
+ c = line_graph(
31
+ :margins => {
32
+ :top => 10,
33
+ :bottom => 15,
34
+ :left => 5,
35
+ :right => 25
36
+ }
37
+ )
38
+ assert_equal "5,25,10,15", c.send(:chma).to_s
39
+ end
40
+
41
+
42
+ # --- legend --------------------------------------------------------------
43
+
44
+ def test_legend_dimensions
45
+ c = line_graph(
46
+ :legend => {
47
+ :width => 50,
48
+ :height => 20
49
+ }
50
+ )
51
+ assert_equal "0,0,0,0|50,20", c.send(:chma).to_s
52
+ end
53
+
54
+
55
+ # --- line styles ---------------------------------------------------------
56
+
57
+ def test_line_styles
58
+ c = line_graph(
59
+ :data => [{
60
+ :values => [1,2,3],
61
+ :thickness => 4,
62
+ :style => {:solid => 3, :blank => 2}
63
+ },{
64
+ :values => [3,1,2],
65
+ :thickness => 2,
66
+ :style => {:solid => 1, :blank => 3}
67
+ }]
68
+ )
69
+ # handle multiple data sets correctly
70
+ assert_equal "4,3,2|2,1,3", c.send(:chls).to_s
71
+
72
+ # apply default line style automatically
73
+ c.data[1][:thickness] = nil
74
+ c.data[1][:style] = nil
75
+ assert_equal "4,3,2|1,1,0", c.send(:chls).to_s
76
+
77
+ # apply line style by name
78
+ c.data[0][:style] = :dotted
79
+ assert_equal "4,4,4|1,1,0", c.send(:chls).to_s
80
+
81
+ # raise exception on invalid style name
82
+ assert_raise(SmartChart::LineStyleNameError) do
83
+ c.data[0][:style] = :asdf
84
+ c.validate!
85
+ end
86
+
87
+ # omit chls parameter if no styles specified
88
+ c.data[0][:thickness] = nil
89
+ assert_equal "", c.send(:chls).to_s
90
+ end
91
+
92
+
93
+ # --- grid lines ----------------------------------------------------------
94
+
95
+ def test_grid_lines
96
+ c = line_graph(
97
+ :data => [0, 2, 3, 4, 5, 6, 7, 8],
98
+ :grid => {
99
+ :x => {:every => 2, :offset => 1},
100
+ :y => {:every => 4, :offset => 2},
101
+ :color => "AABBCC",
102
+ :style => :dotted
103
+ }
104
+ )
105
+ assert_equal "25,50,1,1,12.5,25", c.send(:chg).to_s
106
+
107
+ # test x/y-step decimal points
108
+ c.data = [0, 2, 3, 4, 5, 6]
109
+ assert_equal "33.333,66.667,1,1,16.667,33.333", c.send(:chg).to_s
110
+ end
111
+
112
+
113
+ # --- axis lines ----------------------------------------------------------
114
+
115
+ def test_axis_lines
116
+ c = line_graph(
117
+ :data => [0, 2, 3, 4, 5, 6, 7, 8],
118
+ :axis => {
119
+ :color => "AABBCC",
120
+ :style => :dotted
121
+ }
122
+ )
123
+ assert_nil c.send(:chxt)
124
+ assert_equal "???", c.send(:chxs).to_s
125
+
126
+ # test axis side specification
127
+ c.axis[:sides] = [:left, :bottom, :right]
128
+ assert_equal "???", c.send(:chxt).to_s
129
+
130
+ # test no axis specification
131
+ c.axis[:sides] = []
132
+ assert_equal "ls", c.send(:cht).to_s
133
+ assert_nil c.send(:chxt)
134
+ assert_nil c.send(:chxs)
135
+ end
136
+
137
+
138
+ # --- pie -----------------------------------------------------------------
139
+
140
+ def test_pie_chart_rotation
141
+ c = pie_chart(:rotate => 45)
142
+ assert_equal "5.498", c.send(:chp)
143
+
144
+ # 90-degree rotation is Google default
145
+ c.rotate = -90
146
+ assert_equal "3.142", c.send(:chp)
147
+
148
+ # 90-degree rotation is Google default
149
+ c.rotate = 90
150
+ assert_nil c.send(:chp)
151
+ end
152
+
153
+
154
+ # --- qr code ------------------------------------------------------------
155
+
156
+ def test_qr_code
157
+ c = SmartChart::QRCode.new(
158
+ :width => 200, :height => 200,
159
+ :data => "some data",
160
+ :encoding => :iso88591)
161
+ assert_equal "some data", c.send(:chl)
162
+ assert_equal "ISO-8859-1", c.send(:choe)
163
+ assert_nil c.send(:chld)
164
+
165
+ # omit default margin
166
+ c.margin = 4
167
+ assert_nil c.send(:chld)
168
+
169
+ # print non-default margin
170
+ c.margin = 6
171
+ assert_equal "L|6", c.send(:chld)
172
+
173
+ # print non-default margin
174
+ c.ec_level = :m
175
+ assert_equal "M|6", c.send(:chld)
176
+
177
+ # don't print default margin
178
+ c.margin = 4
179
+ assert_equal "M", c.send(:chld)
180
+ end
181
+
182
+
183
+ # --- line graph ----------------------------------------------------------
184
+
185
+ def test_line_graph
186
+ c = line_graph(
187
+ :data => [{
188
+ :values => [2,1,3,4,5,9],
189
+ :color => '552255'
190
+ },{
191
+ :values => [1,6,4,8,7,5],
192
+ :color => '225522'
193
+ }]
194
+ )
195
+ assert_equal "s:HAPWe9,AmW1te", c.send(:chd).to_s
196
+ assert_equal "552255,225522", c.send(:chco)
197
+ end
198
+
199
+
200
+ # --- map -----------------------------------------------------------------
201
+
202
+ def test_map_data_point_colors
203
+ c = map_chart(:colors => %w[111111 222222 333333])
204
+ assert_equal "FFFFFF,111111,222222,333333", c.send(:chco)
205
+ end
206
+
207
+ def test_map_data_and_labels
208
+ c = map_chart(:data => [
209
+ [:CA, 81],
210
+ [:US, 49],
211
+ [:AU, 96],
212
+ [:RU, 45],
213
+ [:MX, 14]
214
+ ])
215
+ assert_equal "s:xa9XA", c.send(:chd).to_s
216
+ assert_equal "CAUSAURUMX", c.send(:chld).to_s
217
+ end
218
+
219
+ def test_map_with_one_data_point
220
+ c = map_chart(:data => {:US => 7})
221
+ assert_nothing_raised {c.validate!}
222
+ end
223
+
224
+ def test_map_foreground_color
225
+ c = map_chart(:colors => %w[111111 222222 333333], :foreground => "BBBBBB")
226
+ assert_equal "BBBBBB,111111,222222,333333", c.send(:chco)
227
+ end
228
+
229
+ def test_map_background_color
230
+ c = map_chart(:background => "000000")
231
+ assert_equal "bg,s,000000", c.send(:chf)
232
+ end
233
+
234
+ def test_region_validation
235
+ invalids = ['middle east', 'china', 'USA']
236
+ valids = ['africa', 'europe']
237
+ code = lambda{ |region|
238
+ map_chart(:region => region).validate!
239
+ }
240
+ invalids.each do |i|
241
+ assert_raise(SmartChart::DataFormatError) { code.call(i) }
242
+ end
243
+ valids.each do |v|
244
+ assert_nothing_raised { code.call(v) }
245
+ end
246
+ end
247
+
248
+ def test_country_code_validation
249
+ invalids = ['USA', 'SW']
250
+ valids = ['US', 'CN', :CA]
251
+ code = lambda{ |c|
252
+ map_chart(:data => {:MX => 1, c => 2}).validate!
253
+ }
254
+ invalids.each do |i|
255
+ assert_raise(SmartChart::DataFormatError) { code.call(i) }
256
+ end
257
+ valids.each do |v|
258
+ assert_nothing_raised { code.call(v) }
259
+ end
260
+ end
261
+
262
+ def test_state_code_validation
263
+ invalids = ['JJ', 'DC']
264
+ valids = ['AL', 'MS', :GA]
265
+ code = lambda{ |s|
266
+ map_chart(:region => :usa, :data => {:NY => 1, s => 2}).validate!
267
+ }
268
+ invalids.each do |i|
269
+ assert_raise(SmartChart::DataFormatError) { code.call(i) }
270
+ end
271
+ valids.each do |v|
272
+ assert_nothing_raised { code.call(v) }
273
+ end
274
+ end
275
+
276
+
277
+ # --- encoding ------------------------------------------------------------
278
+
279
+ def test_simple_encoding
280
+ [
281
+ [[0, 10], nil, nil, 's:A9'],
282
+ [[0, 10], 0, 61, 's:AK'],
283
+ [[0, 11], 0, 61, 's:AL'],
284
+ [[0, 10, 26, 27], nil, nil, 's:AW69'],
285
+ [[0, 10, nil, 27], nil, nil, 's:AW_9']
286
+
287
+ ].each do |data,min,max,encoding|
288
+ assert_equal encoding,
289
+ SmartChart::Encoder::Simple.new([data], min, max).to_s
290
+ end
291
+ end
292
+
293
+ def test_text_encoding
294
+ [
295
+ [[0, 10], nil, nil, 't:0,100'],
296
+ [[0, 10], 0, 100, 't:0,10'],
297
+ [[0, 11], 0, 100, 't:0,11'],
298
+ [[0, 10, 26, 27], nil, nil, 't:0,37,96,100'],
299
+ [[0, 10, nil, 27], nil, nil, 't:0,37,-1,100']
300
+
301
+ ].each do |data,min,max,encoding|
302
+ assert_equal encoding,
303
+ SmartChart::Encoder::Text.new([data], min, max).to_s
304
+ end
305
+ end
306
+
307
+ def test_extended_encoding
308
+ [
309
+ [[0, 10], nil, nil, 'e:AA..'],
310
+ [[0, 10], 0, 4095, 'e:AAAK'],
311
+ [[0, 11], 0, 4095, 'e:AAAL'],
312
+ [[0, 10, 26, 27], nil, nil, 'e:AAXs9n..'],
313
+ [[0, 10, nil, 27], nil, nil, 'e:AAXs__..']
314
+
315
+ ].each do |data,min,max,encoding|
316
+ assert_equal encoding,
317
+ SmartChart::Encoder::Extended.new([data], min, max).to_s
318
+ end
319
+ end
320
+
321
+ def test_encoding_multiple_data_sets
322
+ assert_equal "s:ANbo,GUhv",
323
+ SmartChart::Encoder::Simple.new([[1,3,5,7], [2,4,6,8]], 1, 10).to_s
324
+ end
325
+
326
+ def test_mixed_data_set_formats
327
+ c = SmartChart::Line.new(
328
+ :width => 400,
329
+ :height => 200,
330
+ :data => [ [2,1,3,4,5,9], {:values => [1,6,4,8,7,5]} ]
331
+ )
332
+ assert_equal "s:HAPWe9,AmW1te", c.send(:chd).to_s
333
+ end
334
+
335
+ def test_encoding_with_y_min_and_max
336
+ c = line_graph(
337
+ :y_min => -20,
338
+ :y_max => 10,
339
+ :data => [2,1,3,4,5,9]
340
+ )
341
+ assert_equal "s:squwy6", c.send(:chd).to_s
342
+ end
343
+
344
+
345
+ # --- validation ----------------------------------------------------------
346
+
347
+ def test_required_attributes_validation
348
+ assert_raise SmartChart::MissingRequiredAttributeError do
349
+ SmartChart::Map.new(:width => 500, :height => 50).validate!
350
+ end
351
+ assert_raise SmartChart::MissingRequiredAttributeError do
352
+ SmartChart::Map.new(:data => []).validate!
353
+ end
354
+ end
355
+
356
+ def test_map_width_validation
357
+ assert_raise SmartChart::DimensionsError do
358
+ map_chart(:width => 500).validate!
359
+ end
360
+ end
361
+
362
+ def test_map_height_validation
363
+ assert_raise SmartChart::DimensionsError do
364
+ map_chart(:height => 300).validate!
365
+ end
366
+ end
367
+
368
+ def test_dimensions_validation
369
+ assert_raise SmartChart::DimensionsError do
370
+ line_graph(:width => 800, :height => 600).validate!
371
+ end
372
+ end
373
+
374
+ def test_color_validation
375
+ valids = %w[fcfcfc FCFCFC 123456 a1b2c3]
376
+ invalids = %w[fcf 012 12345q 1234567]
377
+ code = lambda{ |color|
378
+ line_graph(:data => [{
379
+ :values => [1,2,3],
380
+ :color => color
381
+ }]
382
+ ).validate!
383
+ }
384
+ invalids.each do |color|
385
+ assert_raise(SmartChart::ColorFormatError) { code.call(color) }
386
+ end
387
+ valids.each do |color|
388
+ assert_nothing_raised { code.call(color) }
389
+ end
390
+ end
391
+
392
+
393
+ # --- exceptions ----------------------------------------------------------
394
+
395
+ def test_chart_attribute_error
396
+ assert_raise SmartChart::NoAttributeError do
397
+ line_graph(:asdf => "hi")
398
+ end
399
+ end
400
+ end