sparklines 0.2.7 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (6) hide show
  1. data/CHANGELOG +6 -0
  2. data/README +12 -26
  3. data/lib/sparklines.rb +448 -421
  4. data/rakefile +16 -0
  5. data/test/all_test.rb +89 -44
  6. metadata +2 -2
data/CHANGELOG CHANGED
@@ -1,4 +1,10 @@
1
1
 
2
+ 0.3.0
3
+
4
+ * Changed to a Class for maintainability
5
+ * All values are normalized (except pie)
6
+ * A single value can be passed for the pie percentage (instead of an Array)
7
+
2
8
  0.2.7
3
9
 
4
10
  * Fixed bug where last element of bar graph wouldn't go to the bottom [Esad Hajdarevic esad@esse.at]
data/README CHANGED
@@ -1,36 +1,22 @@
1
- **********************************
2
- ** Spark Graph Library for Ruby **
3
- **********************************
1
+ == Sparklines
4
2
 
5
- Geoffrey Grosenbach
6
- boss@topfunky.com
7
- http://nubyonrails.topfunky.com
8
-
9
- Daniel Nugent
10
- nugend@gmail.com
11
-
12
-
13
- *** What is it? ***
3
+ A library for generating small sparkline graphs from Ruby. Use it in desktop apps or with Ruby on Rails.
14
4
 
15
- A library for generating small sparkline graphs from Ruby. Use it in desktop apps or Rails apps. See the samples in the 'samples' directory.
5
+ == Other info
16
6
 
7
+ http://nubyonrails.com/pages/sparklines
17
8
 
18
- *** How do I use it? ***
9
+ == Rails plugin
19
10
 
20
- Read the meager documentation in the enclosed 'docs' folder.
11
+ http://topfunky.net/svn/plugins/sparklines
21
12
 
22
- In Rails, copy the included files (sparklines_controller.rb, sparklines_helper.rb, sparklines.rb) into your controller, helper, and lib directories, respectively.
13
+ == Authors
23
14
 
24
- In your custom controller, do
25
- require_dependency 'sparklines'
26
- and
27
- helper :sparklines
28
-
29
- In your view, call it like this:
30
-
31
- <%= sparklines_tag [1,2,3,4,5,6] %> <!-- Gives you a smooth graph -->
15
+ Geoffrey Grosenbach
16
+ boss@topfunky.com
17
+ http://nubyonrails.com/pages/sparklines
32
18
 
33
- Or specify details:
19
+ Daniel Nugent
20
+ nugend@gmail.com
34
21
 
35
- <%= sparklines_tag [1,2,3,4,5,6], :type => 'discrete', :height => 10, :upper => 80, :above_color => 'green', :below_color => 'blue' %>
36
22
 
data/lib/sparklines.rb CHANGED
@@ -1,12 +1,13 @@
1
1
 
2
+ require 'rubygems'
2
3
  require 'RMagick'
3
4
  require 'mathn'
4
5
 
5
6
  =begin rdoc
6
7
 
7
- A library (in Ruby!) for generating sparklines.
8
+ A library for generating small unmarked graphs (sparklines).
8
9
 
9
- Can be used to write to a file or make a web service with Rails or other Ruby CGI apps.
10
+ Can be used to write an image to a file or make a web service with Rails or other Ruby CGI apps.
10
11
 
11
12
  Idea and much of the outline for the source lifted directly from {Joe Gregorio's Python Sparklines web service script}[http://bitworking.org/projects/sparklines].
12
13
 
@@ -14,28 +15,10 @@ Requires the RMagick image library.
14
15
 
15
16
  ==Authors
16
17
 
17
- {Dan Nugent}[mailto:nugend@gmail.com]
18
- Original port from Python Sparklines library.
19
-
18
+ {Dan Nugent}[mailto:nugend@gmail.com] Original port from Python Sparklines library.
20
19
 
21
20
  {Geoffrey Grosenbach}[mailto:boss@topfunky.com] -- http://nubyonrails.topfunky.com
22
- -- Conversion to module and addition of functions for using with Rails. Also changed functions to use Rails-style option hashes for parameters.
23
-
24
- ===Tangent regarding RMagick
25
-
26
- The easiest way to use RMagick on Mac OS X is to use darwinports. There are packages for libpng, freetype, and all the other libraries you need.
27
-
28
- I had a heck of a time getting RMagick to work on my system so in the interests of saving other people the trouble here's a little set of instructions on how to get RMagick working properly and with the right image formats.
29
-
30
- 1. Install the zlib[http://www.libpng.org/pub/png/libpng.html] library
31
- 2. With zlib in the same directory as the libpng[http://www.libpng.org/pub/png/libpng.html] library, install libpng
32
- 3. Option step: Install the {jpeg library}[ftp://ftp.uu.net/graphics/jpeg/jpegsrc.v6b.tar.gz] (You need it to use jpegs and you might want to have it)
33
- 4. Install ImageMagick from *source*[http://www.imagemagick.org/script/install-source.php]. RMagick requires the ImageMagick headers, so this is important.
34
- 5. Install RMagick from source[http://rubyforge.org/projects/rmagick/]. The gem is not reliable.
35
- 6. Edit Magick-conf if necessary. I had to remove -lcms and -ltiff since I didn't want those to be built and the libraries weren't on my system.
36
-
37
- Please keep in mind that these were only the steps that made RMagick work on my machine. This is a tricky library to get working.
38
- Consider using Joe Gregorio's version for Python if the installation proves to be too cumbersome.
21
+ -- Conversion to module and further maintenance.
39
22
 
40
23
  ==General Usage and Defaults
41
24
 
@@ -43,29 +26,28 @@ To use in a script:
43
26
 
44
27
  require 'rubygems'
45
28
  require 'sparklines'
46
- Sparklines.plot([1,25,33,46,89,90,85,77,42], :type => 'discrete', :height => 20)
29
+ Sparklines.plot([1,25,33,46,89,90,85,77,42],
30
+ :type => 'discrete',
31
+ :height => 20)
47
32
 
48
33
  An image blob will be returned which you can print, write to STDOUT, etc.
49
34
 
50
- In Rails,
51
-
52
- * Install the 'sparklines_generator' gem ('gem install sparklines_generator')
53
- * Call 'ruby script/generate sparklines'. This will copy the Sparklines controller and helper to your rails directories
54
- * Add "require 'sparklines'" to the bottom of your config/environment.rb
55
- * Restart your fcgi's or your WEBrick if necessary
35
+ For use with Ruby on Rails, see the sparklines plugin:
56
36
 
57
- And finally, add this to the controller whose view will be using sparklines:
58
-
59
- helper :sparklines
37
+ http://nubyonrails.com/pages/sparklines
60
38
 
61
39
  In your view, call it like this:
62
40
 
63
- <%= sparkline_tag [1,2,3,4,5,6] %> <!-- Gives you a smooth graph -->
41
+ <%= sparkline_tag [1,2,3,4,5,6] %>
64
42
 
65
43
  Or specify details:
66
44
 
67
- <%= sparkline_tag [1,2,3,4,5,6], :type => 'discrete', :height => 10, :upper => 80, :above_color => 'green', :below_color => 'blue' %>
68
-
45
+ <%= sparkline_tag [1,2,3,4,5,6],
46
+ :type => 'discrete',
47
+ :height => 10,
48
+ :upper => 80,
49
+ :above_color => 'green',
50
+ :below_color => 'blue' %>
69
51
 
70
52
  Graph types:
71
53
 
@@ -73,413 +55,458 @@ Graph types:
73
55
  discrete
74
56
  pie
75
57
  smooth
76
- bar (results will be normalized, i.e. scaled to take up the full height of the graph)
58
+ bar
77
59
 
78
60
  General Defaults:
79
61
 
80
- :type => 'smooth'
81
- :height => 14px
82
- :upper => 50
83
- :above_color => 'red'
84
- :below_color => 'grey'
85
- :background_color => 'white'
86
- :line_color => 'lightgrey'
62
+ :type => 'smooth'
63
+ :height => 14px
64
+ :upper => 50
65
+ :above_color => 'red'
66
+ :below_color => 'grey'
67
+ :background_color => 'white'
68
+ :line_color => 'lightgrey'
87
69
 
88
70
  ==License
89
71
 
90
72
  Licensed under the MIT license.
91
73
 
92
74
  =end
75
+ class Sparklines
76
+
77
+ VERSION = '0.4.0'
78
+
79
+ @@label_margin = 5.0
80
+ @@pointsize = 10.0
81
+
82
+ class << self
83
+
84
+ # Does the actual plotting of the graph.
85
+ # Calls the appropriate subclass based on the :type argument.
86
+ # Defaults to 'smooth'
87
+ def plot(data=[], options={})
88
+ defaults = {
89
+ :type => 'smooth',
90
+ :height => 14,
91
+ :upper => 50,
92
+ :diameter => 20,
93
+ :step => 2,
94
+ :line_color => 'lightgrey',
95
+
96
+ :above_color => 'red',
97
+ :below_color => 'grey',
98
+ :background_color => 'white',
99
+ :share_color => 'red',
100
+ :remain_color => 'lightgrey',
101
+ :min_color => 'blue',
102
+ :max_color => 'green',
103
+ :last_color => 'red',
104
+
105
+ :has_min => false,
106
+ :has_max => false,
107
+ :has_last => false,
108
+
109
+ :label => nil
110
+ }
111
+
112
+ # Hack for HashWithIndifferentAccess
113
+ options_sym = Hash.new
114
+ options.keys.each do |key|
115
+ options_sym[key.to_sym] = options[key]
116
+ end
117
+
118
+ options_sym = defaults.merge(options_sym)
119
+
120
+ # Call the appropriate method for actual plotting.
121
+ sparkline = self.new(data, options_sym)
122
+ if %w(area bar pie smooth discrete).include? options_sym[:type]
123
+ sparkline.send options_sym[:type]
124
+ else
125
+ sparkline.plot_error options_sym
126
+ end
127
+ end
93
128
 
94
- module Sparklines
95
- VERSION = '0.2.7'
96
-
97
- # Does the actually plotting of the graph. Calls the appropriate function based on the :type value passed. Defaults to 'smooth.'
98
- def Sparklines.plot(results=[], options={})
99
- defaults = { :type => 'smooth',
100
- :height => 14,
101
- :upper => 50,
102
- :diameter => 20,
103
- :step => 2,
104
- :line_color => 'lightgrey',
105
-
106
- :above_color => 'red',
107
- :below_color => 'grey',
108
- :background_color => 'white',
109
- :share_color => 'blue',
110
- :remain_color => 'lightgrey',
111
- :min_color => 'blue',
112
- :max_color => 'green',
113
- :last_color => 'red',
114
-
115
- :has_min => false,
116
- :has_max => false,
117
- :has_last => false
118
- }
119
-
120
- # Have to do this to get around HashWithIndifferentAccess
121
- options_sym = Hash.new
122
- options.keys.reverse.each do |key|
123
- options_sym[key.to_sym] = options[key]
124
- end
125
-
126
- options_sym = defaults.merge(options_sym)
127
-
128
- # Minimal normalization
129
- maximum_value = self.get_max_value(results).to_f
130
-
131
- # Call the appropriate function for actual plotting
132
- self.send(options_sym[:type], results, options_sym, maximum_value)
133
- end
134
-
135
- # Writes a graph to disk with the specified filename, or "Sparklines.png"
136
- def Sparklines.plot_to_file(filename="sparklines.png", results=[], options={})
137
- File.open( filename, 'wb' ) do |png|
138
- png << self.plot( results, options)
139
- end
140
- end
141
-
142
- # Creates a pie-chart sparkline
143
- #
144
- # * results - an array of integer values between 0 and 100 inclusive. Only the first integer will be accepted. It will be used to determine the percentage of the pie that is filled by the share_color
145
- #
146
- # * options - a hash that takes parameters:
147
- #
148
- # :diameter - An integer that determines what the size of the sparkline will be. Defaults to 20
149
- #
150
- # :share_color - A string or color code representing the color to draw the share of the pie represented by percent. Defaults to blue.
151
- #
152
- # :remain_color - A string or color code representing the color to draw the pie not taken by the share color. Defaults to lightgrey.
153
- def self.pie(results=[],options={}, maximum_value=100.0)
154
-
155
- diameter = options[:diameter].to_i
156
- share_color = options[:share_color]
157
- remain_color = options[:remain_color]
158
- percent = results[0]
159
-
160
- img = Magick::Image.new(diameter , diameter) {self.background_color = options[:background_color]}
161
- img.format = "PNG"
162
- draw = Magick::Draw.new
163
-
164
- #Adjust the radius so there's some edge left n the pie
165
- r = diameter/2.0 - 2
166
- draw.fill(remain_color)
167
- draw.ellipse(r + 2, r + 2, r , r , 0, 360)
168
- draw.fill(share_color)
169
-
170
- # Special exceptions
171
- if percent == 0
172
- # For 0% return blank
173
- draw.draw(img)
174
- return img.to_blob
175
- elsif percent == 100
176
- # For 100% just draw a full circle
177
- draw.ellipse(r + 2, r + 2, r , r , 0, 360)
178
- draw.draw(img)
179
- return img.to_blob
180
- end
181
-
182
- #Okay, this part is as confusing as hell, so pay attention:
183
- #This line determines the horizontal portion of the point on the circle where the X-Axis
184
- #should end. It's caculated by taking the center of the on-image circle and adding that
185
- #to the radius multiplied by the formula for determinig the point on a unit circle that a
186
- #angle corresponds to. 3.6 * percent gives us that angle, but it's in degrees, so we need to
187
- #convert, hence the muliplication by Pi over 180
188
- arc_end_x = r + 2 + (r * Math.cos((3.6 * percent)*(Math::PI/180)))
189
-
190
- #The same goes for here, except it's the vertical point instead of the horizontal one
191
- arc_end_y = r + 2 + (r * Math.sin((3.6 * percent)*(Math::PI/180)))
192
-
193
- #Because the SVG path format is seriously screwy, we need to set the large-arc-flag to 1
194
- #if the angle of an arc is greater than 180 degrees. I have no idea why this is, but it is.
195
- percent > 50? large_arc_flag = 1: large_arc_flag = 0
196
-
197
- #This is also confusing
198
- #M tells us to move to an absolute point on the image. We're moving to the center of the pie
199
- #h tells us to move to a relative point. We're moving to the right edge of the circle.
200
- #A tells us to start an absolute elliptical arc. The first two values are the radii of the ellipse
201
- #the third value is the x-axis-rotation (how to rotate the ellipse if we wanted to [could have some fun
202
- #with randomizing that maybe), the fourth value is our large-arc-flag, the fifth is the sweep-flag,
203
- #(again, confusing), the sixth and seventh values are the end point of the arc which we calculated previously
204
- #More info on the SVG path string format at: http://www.w3.org/TR/SVG/paths.html
205
- path = "M#{r + 2},#{r + 2} h#{r} A#{r},#{r} 0 #{large_arc_flag},1 #{arc_end_x},#{arc_end_y} z"
206
- draw.path(path)
207
-
208
- draw.draw(img)
209
- img.to_blob
210
- end
211
-
212
- # Creates a discretized sparkline
213
- #
214
- # * results is an array of integer values between 0 and 100 inclusive
215
- #
216
- # * options is a hash that takes 4 parameters:
217
- #
218
- # :height - An integer that determines what the height of the sparkline will be. Defaults to 14
219
- #
220
- # :upper - An integer that determines the threshold for colorization purposes. Any value less than upper will be colored using below_color, anything above and equal to upper will use above_color. Defaults to 50.
221
- #
222
- # :above_color - A string or color code representing the color to draw values above or equal the upper value. Defaults to red.
223
- #
224
- # :below_color - A string or color code representing the color to draw values below the upper value. Defaults to gray.
225
- def self.discrete(results=[], options = {}, maximum_value=100.0)
226
-
227
- height = options[:height].to_i
228
- upper = options[:upper].to_i
229
- below_color = options[:below_color]
230
- above_color = options[:above_color]
231
-
232
- img = Magick::Image.new(results.size * 2 - 1, height) {self.background_color = options[:background_color]}
233
- img.format = "PNG"
234
- draw = Magick::Draw.new
235
-
236
- i = 0
237
- results.each do |r|
238
- color = (r >= upper) ? above_color : below_color
239
- draw.stroke(color)
240
- draw.line(i, (img.rows - r/(101.0/(height-4))-4).to_i,
241
- i, (img.rows - r/(101.0/(height-4))).to_i)
242
-
243
- i += 2
244
- end
245
-
246
- draw.draw(img)
247
- img.to_blob
248
- end
249
-
250
- # Creates a continuous area sparkline
251
- #
252
- # * results is an array of integer values between 0 and 100 inclusive
253
- #
254
- # * options is a hash that takes 4 parameters:
255
- #
256
- # :step - An integer that determines the distance between each point on the sparkline. Defaults to 2.
257
- #
258
- # :height - An integer that determines what the height of the sparkline will be. Defaults to 14
259
- #
260
- # :upper - An ineger that determines the threshold for colorization purposes. Any value less than upper will be colored using below_color, anything above and equal to upper will use above_color. Defaults to 50.
261
- #
262
- # :has_min - Determines whether a dot will be drawn at the lowest value or not. Defaulst to false.
263
- #
264
- # :has_max - Determines whether a dot will be drawn at the highest value or not. Defaulst to false.
265
- #
266
- # :has_last - Determines whether a dot will be drawn at the last value or not. Defaulst to false.
267
- #
268
- # :min_color - A string or color code representing the color that the dot drawn at the smallest value will be displayed as. Defaults to blue.
269
- #
270
- # :max_color - A string or color code representing the color that the dot drawn at the largest value will be displayed as. Defaults to green.
271
- #
272
- # :last_color - A string or color code representing the color that the dot drawn at the last value will be displayed as. Defaults to red.
273
- #
274
- # :above_color - A string or color code representing the color to draw values above or equal the upper value. Defaults to red.
275
- #
276
- # :below_color - A string or color code representing the color to draw values below the upper value. Defaults to gray.
277
- def self.area(results=[], options={}, maximum_value=100.0)
278
-
279
- step = options[:step].to_i
280
- height = options[:height].to_i
281
- upper = options[:upper].to_i
282
-
283
- has_min = options[:has_min]
284
- has_max = options[:has_max]
285
- has_last = options[:has_last]
286
-
287
- min_color = options[:min_color]
288
- max_color = options[:max_color]
289
- last_color = options[:last_color]
290
- below_color = options[:below_color]
291
- above_color = options[:above_color]
292
-
293
- img = Magick::Image.new((results.size - 1) * step + 4, height) {self.background_color = options[:background_color]}
294
- img.format = "PNG"
295
- draw = Magick::Draw.new
296
-
297
- coords = [[0,(height - 3 - upper/(101.0/(height-4)))]]
298
- i=0
299
- results.each do |r|
300
- coords.push [(2 + i), (height - 3 - r/(101.0/(height-4)))]
301
- i += step
302
- end
303
- coords.push [(results.size - 1) * step + 4, (height - 3 - upper/(101.0/(height-4)))]
304
-
305
- #Block off the bottom half of the image and draw the sparkline
306
- draw.fill(above_color)
307
- draw.define_clip_path('top') do
308
- draw.rectangle(0,0,(results.size - 1) * step + 4,(height - 3 - upper/(101.0/(height-4))))
309
- end
310
- draw.clip_path('top')
311
- draw.polygon *coords.flatten
312
-
313
- #Block off the top half of the image and draw the sparkline
314
- draw.fill(below_color)
315
- draw.define_clip_path('bottom') do
316
- draw.rectangle(0,(height - 3 - upper/(101.0/(height-4))),(results.size - 1) * step + 4,height)
317
- end
318
- draw.clip_path('bottom')
319
- draw.polygon *coords.flatten
320
-
321
- #The sparkline looks kinda nasty if either the above_color or below_color gets the center line
322
- draw.fill('black')
323
- draw.line(0,(height - 3 - upper/(101.0/(height-4))),(results.size - 1) * step + 4,(height - 3 - upper/(101.0/(height-4))))
324
-
325
- #After the parts have been masked, we need to let the whole canvas be drawable again
326
- #so a max dot can be displayed
327
- draw.define_clip_path('all') do
328
- draw.rectangle(0,0,img.columns,img.rows)
329
- end
330
- draw.clip_path('all')
331
- if has_min == 'true'
332
- min_pt = coords[results.index(results.min)+1]
333
- draw.fill(min_color)
334
- draw.rectangle(min_pt[0]-1, min_pt[1]-1, min_pt[0]+1, min_pt[1]+1)
335
- end
336
- if has_max == 'true'
337
- max_pt = coords[results.index(results.max)+1]
338
- draw.fill(max_color)
339
- draw.rectangle(max_pt[0]-1, max_pt[1]-1, max_pt[0]+1, max_pt[1]+1)
340
- end
341
- if has_last == 'true'
342
- last_pt = coords[-2]
343
- draw.fill(last_color)
344
- draw.rectangle(last_pt[0]-1, last_pt[1]-1, last_pt[0]+1, last_pt[1]+1)
345
- end
346
-
347
- draw.draw(img)
348
- img.to_blob
349
- end
350
-
351
- # Creates a smooth sparkline
352
- #
353
- # * results - an array of integer values between 0 and 100 inclusive
354
- #
355
- # * options - a hash that takes these optional parameters:
356
- #
357
- # :step - An integer that determines the distance between each point on the sparkline. Defaults to 2.
358
- #
359
- # :height - An integer that determines what the height of the sparkline will be. Defaults to 14
360
- #
361
- # :has_min - Determines whether a dot will be drawn at the lowest value or not. Defaulst to false.
362
- #
363
- # :has_max - Determines whether a dot will be drawn at the highest value or not. Defaulst to false.
364
- #
365
- # :has_last - Determines whether a dot will be drawn at the last value or not. Defaulst to false.
366
- #
367
- # :min_color - A string or color code representing the color that the dot drawn at the smallest value will be displayed as. Defaults to blue.
368
- #
369
- # :max_color - A string or color code representing the color that the dot drawn at the largest value will be displayed as. Defaults to green.
370
- #
371
- # :last_color - A string or color code representing the color that the dot drawn at the last value will be displayed as. Defaults to red.
372
- def self.smooth(results, options, maximum_value=100.0)
373
-
374
- step = options[:step].to_i
375
- height = options[:height].to_i
376
- min_color = options[:min_color]
377
- max_color = options[:max_color]
378
- last_color = options[:last_color]
379
- has_min = options[:has_min]
380
- has_max = options[:has_max]
381
- has_last = options[:has_last]
382
- line_color = options[:line_color]
383
-
384
- img = Magick::Image.new((results.size - 1) * step + 4, height.to_i) {self.background_color = options[:background_color]}
385
- img.format = "PNG"
386
- draw = Magick::Draw.new
387
-
388
- draw.stroke(line_color)
389
- coords = []
390
- i=0
391
- results.each do |r|
392
- coords.push [ 2 + i, (height - 3 - r/(101.0/(height-4))) ]
393
- i += step
394
- end
395
-
396
- my_polyline(draw, coords)
397
-
398
- if has_min == true
399
- min_pt = coords[results.index(results.min)]
400
- draw.fill(min_color)
401
- draw.rectangle(min_pt[0]-2, min_pt[1]-2, min_pt[0]+2, min_pt[1]+2)
402
- end
403
- if has_max == true
404
- max_pt = coords[results.index(results.max)]
405
- draw.fill(max_color)
406
- draw.rectangle(max_pt[0]-2, max_pt[1]-2, max_pt[0]+2, max_pt[1]+2)
407
- end
408
- if has_last == true
409
- last_pt = coords[-1]
410
- draw.fill(last_color)
411
- draw.rectangle(last_pt[0]-2, last_pt[1]-2, last_pt[0]+2, last_pt[1]+2)
412
- end
413
-
414
- draw.draw(img)
415
- img.to_blob
416
- end
417
-
418
-
419
- # This is a function to replace the RMagick polyline function because it doesn't seem to work properly.
420
- #
421
- # * draw - a RMagick::Draw object.
422
- #
423
- # * arr - an array of points (represented as two element arrays)
424
- def self.my_polyline (draw, arr)
425
- i = 0
426
- while i < arr.size - 1
427
- draw.line(arr[i][0], arr[i][1], arr[i+1][0], arr[i+1][1])
428
- i += 1
429
- end
430
- end
431
-
432
- # Draw the error Sparkline. Not implemented yet.
433
- def self.plot_error(options={})
434
- img = Magick::Image.new(40,15) {self.background_color = options[:background_color]}
435
- img.format = "PNG"
436
- draw = Magick::Draw.new
437
- draw.fill('red')
438
- draw.line(0,0,40,15)
439
- draw.line(0,15,40,0)
440
- draw.draw(img)
441
-
442
- img.to_blob
443
- end
444
-
445
-
446
- # Draws a bar graph, normalized.
129
+ # Writes a graph to disk with the specified filename, or "sparklines.png"
130
+ def plot_to_file(filename="sparklines.png", data=[], options={})
131
+ File.open( filename, 'wb' ) do |png|
132
+ png << self.plot( data, options)
133
+ end
134
+ end
135
+
136
+ end
137
+
138
+ def initialize(data=[], options={})
139
+ @data = Array(data)
140
+ @options = options
141
+ normalize_data
142
+ end
143
+
144
+ # Creates a continuous area sparkline. Relevant options.
145
+ #
146
+ # :step - An integer that determines the distance between each point on the sparkline. Defaults to 2.
147
+ #
148
+ # :height - An integer that determines what the height of the sparkline will be. Defaults to 14
149
+ #
150
+ # :upper - An integer that determines the threshold for colorization purposes. Any value less than upper will be colored using below_color, anything above and equal to upper will use above_color. Defaults to 50.
151
+ #
152
+ # :has_min - Determines whether a dot will be drawn at the lowest value or not. Defaults to false.
153
+ #
154
+ # :has_max - Determines whether a dot will be drawn at the highest value or not. Defaults to false.
155
+ #
156
+ # :has_last - Determines whether a dot will be drawn at the last value or not. Defaults to false.
157
+ #
158
+ # :min_color - A string or color code representing the color that the dot drawn at the smallest value will be displayed as. Defaults to blue.
159
+ #
160
+ # :max_color - A string or color code representing the color that the dot drawn at the largest value will be displayed as. Defaults to green.
447
161
  #
448
- # BUG: Last column never goes all the way to the bottom.
449
- def self.bar(results=[], options = {}, maximum_value=100.0)
450
- step = options[:step].to_i
451
- height = options[:height].to_f
452
- upper = options[:upper].to_i
453
- below_color = options[:below_color]
454
- above_color = options[:above_color]
455
-
456
- img = Magick::Image.new((results.size) * step + 2, height) {
457
- self.background_color = options[:background_color]
458
- }
459
- img.format = "PNG"
460
- draw = Magick::Draw.new
162
+ # :last_color - A string or color code representing the color that the dot drawn at the last value will be displayed as. Defaults to red.
163
+ #
164
+ # :above_color - A string or color code representing the color to draw values above or equal the upper value. Defaults to red.
165
+ #
166
+ # :below_color - A string or color code representing the color to draw values below the upper value. Defaults to gray.
167
+ def area
168
+
169
+ step = @options[:step].to_i
170
+ height = @options[:height].to_i
171
+ background_color = @options[:background_color]
172
+
173
+ create_canvas((@norm_data.size - 1) * step + 4, height, background_color)
174
+
175
+ upper = @options[:upper].to_i
176
+
177
+ has_min = @options[:has_min]
178
+ has_max = @options[:has_max]
179
+ has_last = @options[:has_last]
180
+
181
+ min_color = @options[:min_color]
182
+ max_color = @options[:max_color]
183
+ last_color = @options[:last_color]
184
+ below_color = @options[:below_color]
185
+ above_color = @options[:above_color]
186
+
187
+
188
+ coords = [[0,(height - 3 - upper/(101.0/(height-4)))]]
189
+ i=0
190
+ @norm_data.each do |r|
191
+ coords.push [(2 + i), (height - 3 - r/(101.0/(height-4)))]
192
+ i += step
193
+ end
194
+ coords.push [(@norm_data.size - 1) * step + 4, (height - 3 - upper/(101.0/(height-4)))]
195
+
196
+ # TODO Refactor! Should take a block and do both.
197
+ #
198
+ # Block off the bottom half of the image and draw the sparkline
199
+ @draw.fill(above_color)
200
+ @draw.define_clip_path('top') do
201
+ @draw.rectangle(0,0,(@norm_data.size - 1) * step + 4,(height - 3 - upper/(101.0/(height-4))))
202
+ end
203
+ @draw.clip_path('top')
204
+ @draw.polygon *coords.flatten
205
+
206
+ # Block off the top half of the image and draw the sparkline
207
+ @draw.fill(below_color)
208
+ @draw.define_clip_path('bottom') do
209
+ @draw.rectangle(0,(height - 3 - upper/(101.0/(height-4))),(@norm_data.size - 1) * step + 4,height)
210
+ end
211
+ @draw.clip_path('bottom')
212
+ @draw.polygon *coords.flatten
213
+
214
+ # The sparkline looks kinda nasty if either the above_color or below_color gets the center line
215
+ @draw.fill('black')
216
+ @draw.line(0,(height - 3 - upper/(101.0/(height-4))),(@norm_data.size - 1) * step + 4,(height - 3 - upper/(101.0/(height-4))))
217
+
218
+ # After the parts have been masked, we need to let the whole canvas be drawable again
219
+ # so a max dot can be displayed
220
+ @draw.define_clip_path('all') do
221
+ @draw.rectangle(0,0,@canvas.columns,@canvas.rows)
222
+ end
223
+ @draw.clip_path('all')
224
+
225
+ drawbox(coords[@norm_data.index(@norm_data.min)+1], 1, min_color) if has_min == true
226
+ drawbox(coords[@norm_data.index(@norm_data.max)+1], 1, max_color) if has_max == true
227
+
228
+ drawbox(coords[-2], 1, last_color) if has_last == true
229
+
230
+ @draw.draw(@canvas)
231
+ @canvas.to_blob
232
+ end
233
+
234
+
235
+ # Draws a bar graph.
236
+ #
237
+ def bar
238
+ step = @options[:step].to_i
239
+ height = @options[:height].to_f
240
+ background_color = @options[:background_color]
241
+
242
+ create_canvas(@norm_data.length * step + 2, height, background_color)
243
+
244
+ upper = @options[:upper].to_i
245
+ below_color = @options[:below_color]
246
+ above_color = @options[:above_color]
461
247
 
462
248
  i = 1
463
- results.each_with_index do |r, index|
249
+ @norm_data.each_with_index do |r, index|
464
250
  color = (r >= upper) ? above_color : below_color
465
- draw.stroke('transparent')
466
- draw.fill(color)
467
- draw.rectangle( i, img.rows,
468
- i + step - 2, img.rows - ((r / maximum_value) * img.rows) )
251
+ @draw.stroke('transparent')
252
+ @draw.fill(color)
253
+ @draw.rectangle( i, @canvas.rows,
254
+ i + step - 2, @canvas.rows - ( (r / @maximum_value) * @canvas.rows) )
255
+ i += step
256
+ end
257
+
258
+ @draw.draw(@canvas)
259
+ @canvas.to_blob
260
+ end
261
+
262
+
263
+ # Creates a discretized sparkline
264
+ #
265
+ # :height - An integer that determines what the height of the sparkline will be. Defaults to 14
266
+ #
267
+ # :upper - An integer that determines the threshold for colorization purposes. Any value less than upper will be colored using below_color, anything above and equal to upper will use above_color. Defaults to 50.
268
+ #
269
+ # :above_color - A string or color code representing the color to draw values above or equal the upper value. Defaults to red.
270
+ #
271
+ # :below_color - A string or color code representing the color to draw values below the upper value. Defaults to gray.
272
+ def discrete
273
+
274
+ height = @options[:height].to_i
275
+ upper = @options[:upper].to_i
276
+ background_color = @options[:background_color]
277
+ step = @options[:step].to_i
278
+
279
+ create_canvas(@norm_data.size * step - 1, height, background_color)
280
+
281
+ below_color = @options[:below_color]
282
+ above_color = @options[:above_color]
283
+
284
+ i = 0
285
+ @norm_data.each do |r|
286
+ color = (r >= upper) ? above_color : below_color
287
+ @draw.stroke(color)
288
+ @draw.line(i, (@canvas.rows - r/(101.0/(height-4))-4).to_i,
289
+ i, (@canvas.rows - r/(101.0/(height-4))).to_i)
290
+ i += step
291
+ end
292
+
293
+ @draw.draw(@canvas)
294
+ @canvas.to_blob
295
+ end
296
+
297
+
298
+ # Creates a pie-chart sparkline
299
+ #
300
+ # :diameter - An integer that determines what the size of the sparkline will be. Defaults to 20
301
+ #
302
+ # :share_color - A string or color code representing the color to draw the share of the pie represented by percent. Defaults to red.
303
+ #
304
+ # :remain_color - A string or color code representing the color to draw the pie not taken by the share color. Defaults to lightgrey.
305
+ def pie
306
+
307
+ diameter = @options[:diameter].to_i
308
+ background_color = @options[:background_color]
309
+
310
+ create_canvas(diameter, diameter, background_color)
311
+
312
+ share_color = @options[:share_color]
313
+ remain_color = @options[:remain_color]
314
+ percent = @norm_data[0]
315
+
316
+ # Adjust the radius so there's some edge left in the pie
317
+ r = diameter/2.0 - 2
318
+ @draw.fill(remain_color)
319
+ @draw.ellipse(r + 2, r + 2, r , r , 0, 360)
320
+ @draw.fill(share_color)
321
+
322
+ # Special exceptions
323
+ if percent == 0
324
+ # For 0% return blank
325
+ @draw.draw(@canvas)
326
+ return @canvas.to_blob
327
+ elsif percent == 100
328
+ # For 100% just draw a full circle
329
+ @draw.ellipse(r + 2, r + 2, r , r , 0, 360)
330
+ @draw.draw(@canvas)
331
+ return @canvas.to_blob
332
+ end
333
+
334
+ # Okay, this part is as confusing as hell, so pay attention:
335
+ # This line determines the horizontal portion of the point on the circle where the X-Axis
336
+ # should end. It's caculated by taking the center of the on-image circle and adding that
337
+ # to the radius multiplied by the formula for determinig the point on a unit circle that a
338
+ # angle corresponds to. 3.6 * percent gives us that angle, but it's in degrees, so we need to
339
+ # convert, hence the muliplication by Pi over 180
340
+ arc_end_x = r + 2 + (r * Math.cos((3.6 * percent)*(Math::PI/180)))
341
+
342
+ # The same goes for here, except it's the vertical point instead of the horizontal one
343
+ arc_end_y = r + 2 + (r * Math.sin((3.6 * percent)*(Math::PI/180)))
344
+
345
+ # Because the SVG path format is seriously screwy, we need to set the large-arc-flag to 1
346
+ # if the angle of an arc is greater than 180 degrees. I have no idea why this is, but it is.
347
+ percent > 50? large_arc_flag = 1: large_arc_flag = 0
348
+
349
+ # This is also confusing
350
+ # M tells us to move to an absolute point on the image. We're moving to the center of the pie
351
+ # h tells us to move to a relative point. We're moving to the right edge of the circle.
352
+ # A tells us to start an absolute elliptical arc. The first two values are the radii of the ellipse
353
+ # the third value is the x-axis-rotation (how to rotate the ellipse if we wanted to [could have some fun
354
+ # with randomizing that maybe), the fourth value is our large-arc-flag, the fifth is the sweep-flag,
355
+ # (again, confusing), the sixth and seventh values are the end point of the arc which we calculated previously
356
+ # More info on the SVG path string format at: http://www.w3.org/TR/SVG/paths.html
357
+ path = "M#{r + 2},#{r + 2} h#{r} A#{r},#{r} 0 #{large_arc_flag},1 #{arc_end_x},#{arc_end_y} z"
358
+ @draw.path(path)
359
+
360
+ @draw.draw(@canvas)
361
+ @canvas.to_blob
362
+ end
363
+
364
+
365
+ # Creates a smooth sparkline.
366
+ #
367
+ # :step - An integer that determines the distance between each point on the sparkline. Defaults to 2.
368
+ #
369
+ # :height - An integer that determines what the height of the sparkline will be. Defaults to 14
370
+ #
371
+ # :has_min - Determines whether a dot will be drawn at the lowest value or not. Defaults to false.
372
+ #
373
+ # :has_max - Determines whether a dot will be drawn at the highest value or not. Defaults to false.
374
+ #
375
+ # :has_last - Determines whether a dot will be drawn at the last value or not. Defaults to false.
376
+ #
377
+ # :min_color - A string or color code representing the color that the dot drawn at the smallest value will be displayed as. Defaults to blue.
378
+ #
379
+ # :max_color - A string or color code representing the color that the dot drawn at the largest value will be displayed as. Defaults to green.
380
+ #
381
+ # :last_color - A string or color code representing the color that the dot drawn at the last value will be displayed as. Defaults to red.
382
+ def smooth
383
+
384
+ step = @options[:step].to_i
385
+ height = @options[:height].to_i
386
+ background_color = @options[:background_color]
387
+
388
+ create_canvas((@norm_data.size - 1) * step + 4, height, background_color)
389
+
390
+ min_color = @options[:min_color]
391
+ max_color = @options[:max_color]
392
+ last_color = @options[:last_color]
393
+ has_min = @options[:has_min]
394
+ has_max = @options[:has_max]
395
+ has_last = @options[:has_last]
396
+ line_color = @options[:line_color]
397
+
398
+ @draw.stroke(line_color)
399
+ coords = []
400
+ i=0
401
+ @norm_data.each do |r|
402
+ coords.push [ 2 + i, (height - 3 - r/(101.0/(height-4))) ]
469
403
  i += step
470
404
  end
405
+
406
+ open_ended_polyline(coords)
407
+
408
+ drawbox(coords[@norm_data.index(@norm_data.min)], 2, min_color) if has_min == true
409
+
410
+ drawbox(coords[@norm_data.index(@norm_data.max)], 2, max_color) if has_max == true
411
+
412
+ drawbox(coords[-1], 2, last_color) if has_last == true
413
+
414
+ @draw.draw(@canvas)
415
+ @canvas.to_blob
416
+ end
417
+
418
+
419
+ # Draw the error Sparkline.
420
+ def plot_error(options={})
421
+ create_canvas(40, 15, 'white')
422
+
423
+ @draw.fill('red')
424
+ @draw.line(0,0,40,15)
425
+ @draw.line(0,15,40,0)
426
+
427
+ @draw.draw(@canvas)
428
+ @canvas.to_blob
429
+ end
471
430
 
472
- draw.draw(img)
473
- img.to_blob
431
+ private
432
+
433
+ def normalize_data
434
+ @maximum_value = @data.max
435
+ if @options[:type].to_s == 'pie'
436
+ @norm_data = @data
437
+ else
438
+ @norm_data = @data.map { |value| value = (value.to_f / @maximum_value) * 100.0 }
439
+ end
474
440
  end
475
441
 
442
+ # * arr - an array of points (represented as two element arrays)
443
+ def open_ended_polyline(arr)
444
+ 0.upto(arr.length - 2) { |i|
445
+ @draw.line(arr[i][0], arr[i][1], arr[i+1][0], arr[i+1][1])
446
+ }
447
+ end
476
448
 
477
- def self.get_max_value(values=[])
478
- max_value = 0
479
- values.each do |value|
480
- max_value = (value > max_value) ? value : max_value
449
+ # Create an image to draw on and a drawable to do the drawing with.
450
+ #
451
+ # TODO Refactor into smaller functions
452
+ def create_canvas(w, h, bkg_col)
453
+ @draw = Magick::Draw.new
454
+ @draw.pointsize = @@pointsize # TODO Use height
455
+ @canvas = Magick::Image.new(w , h) { self.background_color = bkg_col }
456
+
457
+ # Make room for label and last value
458
+ if !@options[:label].nil?
459
+ @options[:has_last] = true
460
+ @label_width = calculate_width(@options[:label])
461
+ @data_last_width = calculate_width(@data.last)
462
+ # HACK The 7.0 is a severe hack. Must figure out correct spacing
463
+ @label_and_data_last_width = @label_width + @data_last_width + @@label_margin * 7.0
464
+ w += @label_and_data_last_width
465
+ end
466
+
467
+ @canvas = Magick::Image.new(w , h) { self.background_color = bkg_col }
468
+ @canvas.format = "PNG"
469
+
470
+ # Draw label and last value
471
+ if !@options[:label].nil?
472
+ if ENV.has_key?('MAGICK_FONT_PATH')
473
+ vera_font_path = File.expand_path('Vera.ttf', ENV['MAGICK_FONT_PATH'])
474
+ @font = File.exists?(vera_font_path) ? vera_font_path : nil
475
+ else
476
+ @font = nil
477
+ end
478
+
479
+ @draw.fill = 'black'
480
+ @draw.font = @font if @font
481
+ @draw.gravity = Magick::WestGravity
482
+ @draw.annotate( @canvas,
483
+ @label_width, 1.0,
484
+ w - @label_and_data_last_width + @@label_margin, h - calculate_caps_height/2.0,
485
+ @options[:label])
486
+
487
+ @draw.fill = 'red'
488
+ @draw.annotate( @canvas,
489
+ @data_last_width, 1.0,
490
+ w - @data_last_width - @@label_margin * 2.0, h - calculate_caps_height/2.0,
491
+ @data.last.to_s)
481
492
  end
482
- return max_value
483
493
  end
484
494
 
495
+ # Utility to draw a coloured box
496
+ # Centred on pt, offset off in each direction, fill color is col
497
+ def drawbox(pt, offset, color)
498
+ @draw.stroke 'transparent'
499
+ @draw.fill(color)
500
+ @draw.rectangle(pt[0]-offset, pt[1]-offset, pt[0]+offset, pt[1]+offset)
501
+ end
502
+
503
+ def calculate_width(text)
504
+ @draw.get_type_metrics(@canvas, text.to_s).width
505
+ end
506
+
507
+ def calculate_caps_height
508
+ @draw.get_type_metrics(@canvas, 'X').height
509
+ end
510
+
511
+
485
512
  end
data/rakefile CHANGED
@@ -27,6 +27,17 @@ task :clean do
27
27
  rm_rf "pkg"
28
28
  end
29
29
 
30
+ # Build a graphic file by running a single test.
31
+ #
32
+ # rake bar_extreme_values.png
33
+ # => Runs test_extreme_values
34
+
35
+ rule ".png" do |t|
36
+ test_name = t.name.gsub(/\.png/, '')
37
+ Rake::Task[:clean].invoke
38
+ sh "ruby -Ilib:test test/all_test.rb -n /^test_#{test_name}/"
39
+ end
40
+
30
41
  # Run the unit tests
31
42
  Rake::TestTask.new { |t|
32
43
  t.libs << "test"
@@ -76,6 +87,11 @@ Rake::GemPackageTask.new(spec) do |p|
76
87
  p.need_zip = true
77
88
  end
78
89
 
90
+ desc "Hackish copy to sparklines plugin for Rails"
91
+ task :update_plugin do
92
+ cp "lib/sparklines.rb", "../plugins/sparklines/lib/sparklines.rb"
93
+ end
94
+
79
95
  desc "Publish the API documentation"
80
96
  task :pgem => [:package] do
81
97
  Rake::SshFilePublisher.new("boss@topfunky.com", "public_html/gems/gems", "pkg", "#{PKG_FILE_NAME}.gem").upload
data/test/all_test.rb CHANGED
@@ -6,71 +6,116 @@ require 'lib/sparklines'
6
6
  class SparklinesTest < Test::Unit::TestCase
7
7
 
8
8
  def setup
9
- @data = (1..30).map { rand 100 }
9
+ @output_dir = "test/output"
10
+ @data = %w( 1 5 15 20 30 50 57 58 55 48
11
+ 44 43 42 42 46 48 49 53 55 59
12
+ 60 65 75 90 105 106 107 110 115 120
13
+ 115 120 130 140 150 160 170 100 100 10).map {|i| i.to_f}
10
14
  end
11
15
 
12
16
  def test_each_graph
13
17
  %w{pie area discrete smooth bar}.each do |type|
14
- Sparklines.plot_to_file("test/output/#{type}.png", @data, :type => type)
18
+ quick_graph("#{type}", :type => type)
15
19
  end
16
20
  end
17
21
 
18
- def test_pie_graph
22
+ def test_each_graph_with_label
23
+ %w{pie area discrete smooth bar}.each do |type|
24
+ quick_graph("labeled_#{type}", :type => type, :label => 'Comments for you')
25
+ end
26
+ end
27
+
28
+ def test_pie
19
29
  # Test extremes which previously did not work right
20
30
  [0, 1, 45, 95, 99, 100].each do |value|
21
- Sparklines.plot_to_file("test/output/pie#{value}.png", [value], :type => 'pie', :diameter => 128)
22
- end
31
+ Sparklines.plot_to_file("#{@output_dir}/pie#{value}.png",
32
+ value,
33
+ :type => 'pie',
34
+ :diameter => 128)
35
+ end
36
+ Sparklines.plot_to_file("#{@output_dir}/pie_flat.png",
37
+ [60],
38
+ :type => 'pie')
23
39
  end
24
40
 
25
- def test_special
26
- # Run special tests
27
- tests = { 'smooth-colored' => { :type => 'smooth', :line_color => 'purple'},
28
- 'pie-large' => { :type => 'pie', :diameter => 200 },
29
- 'area-high' => { :type => 'area',
30
- :upper => 80,
31
- :step => 4,
32
- :height => 20},
33
- 'discrete-wide' => { :type => 'discrete',
34
- :step => 8 },
35
- 'bar-wide' => { :type => 'bar',
36
- :step => 8 }
41
+ def test_special_conditions
42
+ tests = { 'smooth_colored' => {
43
+ :type => 'smooth',
44
+ :line_color => 'purple'
45
+ },
46
+ 'pie_large' => {
47
+ :type => 'pie',
48
+ :diameter => 200
49
+ },
50
+ 'area_high' => {
51
+ :type => 'area',
52
+ :upper => 80,
53
+ :step => 4,
54
+ :height => 20
55
+ },
56
+ 'discrete_wide' => {
57
+ :type => 'discrete',
58
+ :step => 8
59
+ },
60
+ 'bar_wide' => {
61
+ :type => 'bar',
62
+ :step => 8
63
+ },
64
+ 'bar_tall' => {
65
+ :type => 'bar',
66
+ :below_color => 'blue',
67
+ :above_color => 'red',
68
+ :upper => 90,
69
+ :height => 50,
70
+ :step => 8
71
+ }
37
72
  }
38
- tests.keys.each do |key|
39
- Sparklines.plot_to_file("test/output/#{key}.png", @data, tests[key])
73
+ tests.each do |name, options|
74
+ quick_graph(name, options)
40
75
  end
41
76
  end
42
77
 
43
- def test_smooth_graph
44
-
78
+ # def test_smooth_graph
79
+ #
80
+ # end
81
+
82
+ def test_bar_extreme_values
83
+ Sparklines.plot_to_file("#{@output_dir}/bar_extreme_values.png",
84
+ [0,1,100,2,99,3,98,4,97,5,96,6,95,7,94,8,93,9,92,10,91],
85
+ :type => 'bar',
86
+ :below_color => 'blue',
87
+ :above_color => 'red',
88
+ :upper => 90,
89
+ :step => 4 )
45
90
  end
46
91
 
47
- def test_bar_tall
48
- Sparklines.plot_to_file('test/output/bar-tall.png', @data,
49
- :type => 'bar',
50
- :below_color => 'blue',
51
- :above_color => 'red',
52
- :upper => 90,
53
- :height => 50,
54
- :step => 8 )
92
+ def test_string_args
93
+ quick_graph("bar_string.png",
94
+ 'type' => 'bar',
95
+ 'below_color' => 'blue',
96
+ 'above_color' => 'red',
97
+ 'upper' => 50,
98
+ 'height' => 50,
99
+ 'step' => 8 )
55
100
  end
56
101
 
57
- def test_bar_extreme_values
58
- Sparklines.plot_to_file('test/output/bar-extreme-values.png', [0,1,100,2,99,3,98,4,97,5,96,6,95,7,94,8,93,9,92,10,91],
59
- :type => 'bar',
60
- :below_color => 'blue',
61
- :above_color => 'red',
62
- :upper => 90,
63
- :step => 4 )
102
+ def test_area_min_max
103
+ quick_graph("area_min_max",
104
+ :has_min => true,
105
+ :has_max => true,
106
+ :has_first => true,
107
+ :has_last => true)
64
108
  end
65
109
 
66
- def test_string_args
67
- Sparklines.plot_to_file('test/output/bar-string.png', @data,
68
- 'type' => 'bar',
69
- 'below_color' => 'blue',
70
- 'above_color' => 'red',
71
- 'upper' => 50,
72
- 'height' => 50,
73
- 'step' => 8 )
110
+
111
+ def test_no_type
112
+ Sparklines.plot_to_file("#{@output_dir}/error.png", 0, :type => 'nonexistent')
113
+ end
114
+
115
+ private
116
+
117
+ def quick_graph(name, options)
118
+ Sparklines.plot_to_file("#{@output_dir}/#{name}.png", @data, options)
74
119
  end
75
120
 
76
121
  end
metadata CHANGED
@@ -3,8 +3,8 @@ rubygems_version: 0.8.11
3
3
  specification_version: 1
4
4
  name: sparklines
5
5
  version: !ruby/object:Gem::Version
6
- version: 0.2.7
7
- date: 2006-05-17 00:00:00 -07:00
6
+ version: 0.4.0
7
+ date: 2006-07-31 00:00:00 -07:00
8
8
  summary: Tiny graphs for concise data.
9
9
  require_paths:
10
10
  - lib