Changesets can be listed by changeset number.
The Git repository is here.
- Revision:
- 15
- Log:
Attempt to update Typo to a Typo SVN HEAD release from around the
time the prototype installation was set up on the RISC OS Open Limited
web site. Timestamps place this at 04-Jul so a revision from 05-Jul or
earlier was pulled and copied over the 2.6.0 tarball stable code.
- Author:
- adh
- Date:
- Sat Jul 22 23:27:35 +0100 2006
- Size:
- 15997 Bytes
1 | require 'RMagick' |
2 | require 'mathn' |
3 | |
4 | =begin rdoc |
5 | |
6 | A library (in Ruby!) for generating sparklines. |
7 | |
8 | Can be used to write to a file or make a web service with Rails or other Ruby CGI apps. |
9 | |
10 | 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]. |
11 | |
12 | Requires the RMagick image library. |
13 | |
14 | ==Authors |
15 | |
16 | {Dan Nugent}[mailto:nugend@gmail.com] |
17 | Original port from Python Sparklines library. |
18 | |
19 | |
20 | {Geoffrey Grosenbach}[mailto:boss@topfunky.com] -- http://nubyonrails.topfunky.com |
21 | -- Conversion to module and addition of functions for using with Rails. Also changed functions to use Rails-style option hashes for parameters. |
22 | |
23 | ===Tangent regarding RMagick |
24 | |
25 | 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. |
26 | |
27 | 1. Install the zlib[http://www.libpng.org/pub/png/libpng.html] library |
28 | 2. With zlib in the same directory as the libpng[http://www.libpng.org/pub/png/libpng.html] library, install libpng |
29 | 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) |
30 | 4. Install ImageMagick from *source*[http://www.imagemagick.org/script/install-source.php]. RMagick requires the ImageMagick headers, so this is important. |
31 | 5. Install RMagick from source[http://rubyforge.org/projects/rmagick/]. The gem is not reliable. |
32 | 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. |
33 | |
34 | 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. |
35 | Consider using Joe Gregorio's version for Python if the installation proves to be too cumbersome. |
36 | |
37 | ==General Usage and Defaults |
38 | |
39 | To use in a script: |
40 | |
41 | require 'rubygems' |
42 | require 'sparklines' |
43 | Sparklines.plot([1,25,33,46,89,90,85,77,42], :type => 'discrete', :height => 20) |
44 | |
45 | An image blob will be returned which you can print, write to STDOUT, etc. |
46 | |
47 | In Rails, |
48 | |
49 | * Install the 'sparklines_generator' gem ('gem install sparklines_generator') |
50 | * Call 'ruby script/generate sparklines'. This will copy the Sparklines controller and helper to your rails directories |
51 | * Add "require 'sparklines'" to the bottom of your config/environment.rb |
52 | * Restart your fcgi's or your WEBrick if necessary |
53 | |
54 | And finally, add this to the controller whose view will be using sparklines: |
55 | |
56 | helper :sparklines |
57 | |
58 | In your view, call it like this: |
59 | |
60 | <%= sparkline_tag [1,2,3,4,5,6] %> <!-- Gives you a smooth graph --> |
61 | |
62 | Or specify details: |
63 | |
64 | <%= sparkline_tag [1,2,3,4,5,6], :type => 'discrete', :height => 10, :upper => 80, :above_color => 'green', :below_color => 'blue' %> |
65 | |
66 | |
67 | Graph types: |
68 | |
69 | area |
70 | discrete |
71 | pie |
72 | smooth |
73 | |
74 | General Defaults: |
75 | |
76 | :type => 'smooth' |
77 | :height => 14px |
78 | :upper => 50 |
79 | :above_color => 'red' |
80 | :below_color => 'grey' |
81 | :background_color => 'white' |
82 | :line_color => 'lightgrey' |
83 | |
84 | ==License |
85 | |
86 | Licensed under the MIT license. |
87 | |
88 | =end |
89 | |
90 | module Sparklines |
91 | $VERSION = '0.2.2' |
92 | |
93 | # Does the actually plotting of the graph. Calls the appropriate function based on the :type value passed. Defaults to 'smooth.' |
94 | def Sparklines.plot(results=[], options={}) |
95 | defaults = { :type => 'smooth', |
96 | :height => 14, |
97 | :upper => 50, |
98 | :diameter => 20, |
99 | :step => 2, |
100 | :line_color => 'lightgrey', |
101 | |
102 | :above_color => 'red', |
103 | :below_color => 'grey', |
104 | :background_color => 'white', |
105 | :share_color => 'blue', |
106 | :remain_color => 'lightgrey', |
107 | :min_color => 'blue', |
108 | :max_color => 'green', |
109 | :last_color => 'red', |
110 | |
111 | :has_min => false, |
112 | :has_max => false, |
113 | :has_last => false |
114 | } |
115 | |
116 | # This symbol->string->symbol is kind of awkward. Is there a more elegant way? |
117 | |
118 | # Convert all symbol keys to strings |
119 | defaults.keys.reverse.each do |key| |
120 | defaults[key.to_s] = defaults[key] |
121 | end |
122 | options.keys.reverse.each do |key| |
123 | options[key.to_s] = options[key] |
124 | end |
125 | |
126 | options = defaults.merge(options) |
127 | |
128 | # Convert options string keys back to symbols |
129 | options.keys.reverse.each do |key| |
130 | options[key.to_sym] = options[key] |
131 | end |
132 | |
133 | |
134 | # Call the appropriate function for actual plotting |
135 | #self.send('smooth', results, options) |
136 | self.send(options[:type], results, options) |
137 | end |
138 | |
139 | # Writes a graph to disk with the specified filename, or "Sparklines.png" |
140 | def Sparklines.plot_to_file(filename="sparklines.png", results=[], options={}) |
141 | File.open( filename, 'wb' ) do |png| |
142 | png << self.plot( results, options) |
143 | end |
144 | end |
145 | |
146 | # Creates a pie-chart sparkline |
147 | # |
148 | # * 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 |
149 | # |
150 | # * options - a hash that takes parameters: |
151 | # |
152 | # :diameter - An integer that determines what the size of the sparkline will be. Defaults to 20 |
153 | # |
154 | # :share_color - A string or color code representing the color to draw the share of the pie represented by percent. Defaults to blue. |
155 | # |
156 | # :remain_color - A string or color code representing the color to draw the pie not taken by the share color. Defaults to lightgrey. |
157 | def self.pie(results=[],options={}) |
158 | |
159 | diameter = options[:diameter].to_i |
160 | share_color = options[:share_color] |
161 | remain_color = options[:remain_color] |
162 | percent = results[0] |
163 | |
164 | img = Magick::Image.new(diameter , diameter) {self.background_color = options[:background_color]} |
165 | img.format = "PNG" |
166 | draw = Magick::Draw.new |
167 | |
168 | #Adjust the radius so there's some edge left n the pie |
169 | r = diameter/2.0 - 2 |
170 | draw.fill(remain_color) |
171 | draw.ellipse(r + 2, r + 2, r , r , 0, 360) |
172 | draw.fill(share_color) |
173 | |
174 | #Okay, this part is as confusing as hell, so pay attention: |
175 | #This line determines the horizontal portion of the point on the circle where the X-Axis |
176 | #should end. It's caculated by taking the center of the on-image circle and adding that |
177 | #to the radius multiplied by the formula for determinig the point on a unit circle that a |
178 | #angle corresponds to. 3.6 * percent gives us that angle, but it's in degrees, so we need to |
179 | #convert, hence the muliplication by Pi over 180 |
180 | arc_end_x = r + 2 + (r * Math.cos((3.6 * percent)*(Math::PI/180))) |
181 | |
182 | #The same goes for here, except it's the vertical point instead of the horizontal one |
183 | arc_end_y = r + 2 + (r * Math.sin((3.6 * percent)*(Math::PI/180))) |
184 | |
185 | #Because the SVG path format is seriously screwy, we need to set the large-arc-flag to 1 |
186 | #if the angle of an arc is greater than 180 degrees. I have no idea why this is, but it is. |
187 | percent > 50? large_arc_flag = 1: large_arc_flag = 0 |
188 | |
189 | #This is also confusing |
190 | #M tells us to move to an absolute point on the image. We're moving to the center of the pie |
191 | #h tells us to move to a relative point. We're moving to the right edge of the circle. |
192 | #A tells us to start an absolute elliptical arc. The first two values are the radii of the ellipse |
193 | #the third value is the x-axis-rotation (how to rotate the ellipse if we wanted to [could have some fun |
194 | #with randomizing that maybe), the fourth value is our large-arc-flag, the fifth is the sweep-flag, |
195 | #(again, confusing), the sixth and seventh values are the end point of the arc which we calculated previously |
196 | #More info on the SVG path string format at: http://www.w3.org/TR/SVG/paths.html |
197 | path = "M#{r + 2},#{r + 2} h#{r} A#{r},#{r} 0 #{large_arc_flag},1 #{arc_end_x},#{arc_end_y} z" |
198 | draw.path(path) |
199 | |
200 | draw.draw(img) |
201 | img.to_blob |
202 | end |
203 | |
204 | # Creates a discretized sparkline |
205 | # |
206 | # * results is an array of integer values between 0 and 100 inclusive |
207 | # |
208 | # * options is a hash that takes 4 parameters: |
209 | # |
210 | # :height - An integer that determines what the height of the sparkline will be. Defaults to 14 |
211 | # |
212 | # :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. |
213 | # |
214 | # :above_color - A string or color code representing the color to draw values above or equal the upper value. Defaults to red. |
215 | # |
216 | # :below_color - A string or color code representing the color to draw values below the upper value. Defaults to gray. |
217 | def self.discrete(results=[], options = {}) |
218 | |
219 | height = options[:height].to_i |
220 | upper = options[:upper].to_i |
221 | below_color = options[:below_color] |
222 | above_color = options[:above_color] |
223 | |
224 | img = Magick::Image.new(results.size * 2 - 1, height) {self.background_color = options[:background_color]} |
225 | img.format = "PNG" |
226 | draw = Magick::Draw.new |
227 | |
228 | i=0 |
229 | results.each do |r| |
230 | color = (r >= upper) && above_color || below_color |
231 | draw.stroke(color) |
232 | draw.line(i, (img.rows - r/(101.0/(height-4))-4).to_i,i,(img.rows - r/(101.0/(height-4))).to_i) |
233 | i+=2 |
234 | end |
235 | |
236 | draw.draw(img) |
237 | img.to_blob |
238 | end |
239 | |
240 | # Creates a continuous area sparkline |
241 | # |
242 | # * results is an array of integer values between 0 and 100 inclusive |
243 | # |
244 | # * options is a hash that takes 4 parameters: |
245 | # |
246 | # :step - An integer that determines the distance between each point on the sparkline. Defaults to 2. |
247 | # |
248 | # :height - An integer that determines what the height of the sparkline will be. Defaults to 14 |
249 | # |
250 | # :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. |
251 | # |
252 | # :has_min - Determines whether a dot will be drawn at the lowest value or not. Defaulst to false. |
253 | # |
254 | # :has_max - Determines whether a dot will be drawn at the highest value or not. Defaulst to false. |
255 | # |
256 | # :has_last - Determines whether a dot will be drawn at the last value or not. Defaulst to false. |
257 | # |
258 | # :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. |
259 | # |
260 | # :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. |
261 | # |
262 | # :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. |
263 | # |
264 | # :above_color - A string or color code representing the color to draw values above or equal the upper value. Defaults to red. |
265 | # |
266 | # :below_color - A string or color code representing the color to draw values below the upper value. Defaults to gray. |
267 | def self.area(results=[], options={}) |
268 | |
269 | step = options[:step].to_i |
270 | height = options[:height].to_i |
271 | upper = options[:upper].to_i |
272 | |
273 | has_min = options[:has_min] |
274 | has_max = options[:has_max] |
275 | has_last = options[:has_last] |
276 | |
277 | min_color = options[:min_color] |
278 | max_color = options[:max_color] |
279 | last_color = options[:last_color] |
280 | below_color = options[:below_color] |
281 | above_color = options[:above_color] |
282 | |
283 | img = Magick::Image.new((results.size - 1) * step + 4, height) {self.background_color = options[:background_color]} |
284 | img.format = "PNG" |
285 | draw = Magick::Draw.new |
286 | |
287 | coords = [[0,(height - 3 - upper/(101.0/(height-4)))]] |
288 | i=0 |
289 | results.each do |r| |
290 | coords.push [(2 + i), (height - 3 - r/(101.0/(height-4)))] |
291 | i += step |
292 | end |
293 | coords.push [(results.size - 1) * step + 4, (height - 3 - upper/(101.0/(height-4)))] |
294 | |
295 | #Block off the bottom half of the image and draw the sparkline |
296 | draw.fill(above_color) |
297 | draw.define_clip_path('top') do |
298 | draw.rectangle(0,0,(results.size - 1) * step + 4,(height - 3 - upper/(101.0/(height-4)))) |
299 | end |
300 | draw.clip_path('top') |
301 | draw.polygon *coords.flatten |
302 | |
303 | #Block off the top half of the image and draw the sparkline |
304 | draw.fill(below_color) |
305 | draw.define_clip_path('bottom') do |
306 | draw.rectangle(0,(height - 3 - upper/(101.0/(height-4))),(results.size - 1) * step + 4,height) |
307 | end |
308 | draw.clip_path('bottom') |
309 | draw.polygon *coords.flatten |
310 | |
311 | #The sparkline looks kinda nasty if either the above_color or below_color gets the center line |
312 | draw.fill('black') |
313 | draw.line(0,(height - 3 - upper/(101.0/(height-4))),(results.size - 1) * step + 4,(height - 3 - upper/(101.0/(height-4)))) |
314 | |
315 | #After the parts have been masked, we need to let the whole canvas be drawable again |
316 | #so a max dot can be displayed |
317 | draw.define_clip_path('all') do |
318 | draw.rectangle(0,0,img.columns,img.rows) |
319 | end |
320 | draw.clip_path('all') |
321 | if has_min == 'true' |
322 | min_pt = coords[results.index(results.min)+1] |
323 | draw.fill(min_color) |
324 | draw.rectangle(min_pt[0]-1, min_pt[1]-1, min_pt[0]+1, min_pt[1]+1) |
325 | end |
326 | if has_max == 'true' |
327 | max_pt = coords[results.index(results.max)+1] |
328 | draw.fill(max_color) |
329 | draw.rectangle(max_pt[0]-1, max_pt[1]-1, max_pt[0]+1, max_pt[1]+1) |
330 | end |
331 | if has_last == 'true' |
332 | last_pt = coords[-2] |
333 | draw.fill(last_color) |
334 | draw.rectangle(last_pt[0]-1, last_pt[1]-1, last_pt[0]+1, last_pt[1]+1) |
335 | end |
336 | |
337 | draw.draw(img) |
338 | img.to_blob |
339 | end |
340 | |
341 | # Creates a smooth sparkline |
342 | # |
343 | # * results - an array of integer values between 0 and 100 inclusive |
344 | # |
345 | # * options - a hash that takes these optional parameters: |
346 | # |
347 | # :step - An integer that determines the distance between each point on the sparkline. Defaults to 2. |
348 | # |
349 | # :height - An integer that determines what the height of the sparkline will be. Defaults to 14 |
350 | # |
351 | # :has_min - Determines whether a dot will be drawn at the lowest value or not. Defaulst to false. |
352 | # |
353 | # :has_max - Determines whether a dot will be drawn at the highest value or not. Defaulst to false. |
354 | # |
355 | # :has_last - Determines whether a dot will be drawn at the last value or not. Defaulst to false. |
356 | # |
357 | # :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. |
358 | # |
359 | # :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. |
360 | # |
361 | # :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. |
362 | def self.smooth(results, options) |
363 | |
364 | step = options[:step].to_i |
365 | height = options[:height].to_i |
366 | min_color = options[:min_color] |
367 | max_color = options[:max_color] |
368 | last_color = options[:last_color] |
369 | has_min = options[:has_min] |
370 | has_max = options[:has_max] |
371 | has_last = options[:has_last] |
372 | line_color = options[:line_color] |
373 | |
374 | img = Magick::Image.new((results.size - 1) * step + 4, height.to_i) {self.background_color = options[:background_color]} |
375 | img.format = "PNG" |
376 | draw = Magick::Draw.new |
377 | |
378 | draw.stroke(line_color) |
379 | coords = [] |
380 | i=0 |
381 | results.each do |r| |
382 | coords.push [ 2 + i, (height - 3 - r/(101.0/(height-4))) ] |
383 | i += step |
384 | end |
385 | |
386 | my_polyline(draw, coords) |
387 | |
388 | if has_min == true |
389 | min_pt = coords[results.index(results.min)] |
390 | draw.fill(min_color) |
391 | draw.rectangle(min_pt[0]-2, min_pt[1]-2, min_pt[0]+2, min_pt[1]+2) |
392 | end |
393 | if has_max == true |
394 | max_pt = coords[results.index(results.max)] |
395 | draw.fill(max_color) |
396 | draw.rectangle(max_pt[0]-2, max_pt[1]-2, max_pt[0]+2, max_pt[1]+2) |
397 | end |
398 | if has_last == true |
399 | last_pt = coords[-1] |
400 | draw.fill(last_color) |
401 | draw.rectangle(last_pt[0]-2, last_pt[1]-2, last_pt[0]+2, last_pt[1]+2) |
402 | end |
403 | |
404 | draw.draw(img) |
405 | img.to_blob |
406 | end |
407 | |
408 | |
409 | # This is a function to replace the RMagick polyline function because it doesn't seem to work properly. |
410 | # |
411 | # * draw - a RMagick::Draw object. |
412 | # |
413 | # * arr - an array of points (represented as two element arrays) |
414 | def self.my_polyline (draw, arr) |
415 | i = 0 |
416 | while i < arr.size - 1 |
417 | draw.line(arr[i][0], arr[i][1], arr[i+1][0], arr[i+1][1]) |
418 | i += 1 |
419 | end |
420 | end |
421 | |
422 | # Draw the error Sparkline. Not implemented yet. |
423 | def self.plot_error(options={}) |
424 | img = Magick::Image.new(40,15) {self.background_color = options[:background_color]} |
425 | img.format = "PNG" |
426 | draw = Magick::Draw.new |
427 | draw.fill('red') |
428 | draw.line(0,0,40,15) |
429 | draw.line(0,15,40,0) |
430 | draw.draw(img) |
431 | |
432 | img.to_blob |
433 | end |
434 | |
435 | end |