lib/sparklines.rb in sparklines-0.2.7 vs lib/sparklines.rb in sparklines-0.4.0
- old
+ new
@@ -1,485 +1,512 @@
+require 'rubygems'
require 'RMagick'
require 'mathn'
=begin rdoc
-A library (in Ruby!) for generating sparklines.
+A library for generating small unmarked graphs (sparklines).
-Can be used to write to a file or make a web service with Rails or other Ruby CGI apps.
+Can be used to write an image to a file or make a web service with Rails or other Ruby CGI apps.
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].
Requires the RMagick image library.
==Authors
-{Dan Nugent}[mailto:nugend@gmail.com]
-Original port from Python Sparklines library.
+{Dan Nugent}[mailto:nugend@gmail.com] Original port from Python Sparklines library.
-
{Geoffrey Grosenbach}[mailto:boss@topfunky.com] -- http://nubyonrails.topfunky.com
--- Conversion to module and addition of functions for using with Rails. Also changed functions to use Rails-style option hashes for parameters.
+-- Conversion to module and further maintenance.
-===Tangent regarding RMagick
-
-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.
-
-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.
-
-1. Install the zlib[http://www.libpng.org/pub/png/libpng.html] library
-2. With zlib in the same directory as the libpng[http://www.libpng.org/pub/png/libpng.html] library, install libpng
-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)
-4. Install ImageMagick from *source*[http://www.imagemagick.org/script/install-source.php]. RMagick requires the ImageMagick headers, so this is important.
-5. Install RMagick from source[http://rubyforge.org/projects/rmagick/]. The gem is not reliable.
-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.
-
-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.
-Consider using Joe Gregorio's version for Python if the installation proves to be too cumbersome.
-
==General Usage and Defaults
To use in a script:
require 'rubygems'
require 'sparklines'
- Sparklines.plot([1,25,33,46,89,90,85,77,42], :type => 'discrete', :height => 20)
+ Sparklines.plot([1,25,33,46,89,90,85,77,42],
+ :type => 'discrete',
+ :height => 20)
An image blob will be returned which you can print, write to STDOUT, etc.
-In Rails,
+For use with Ruby on Rails, see the sparklines plugin:
-* Install the 'sparklines_generator' gem ('gem install sparklines_generator')
-* Call 'ruby script/generate sparklines'. This will copy the Sparklines controller and helper to your rails directories
-* Add "require 'sparklines'" to the bottom of your config/environment.rb
-* Restart your fcgi's or your WEBrick if necessary
+ http://nubyonrails.com/pages/sparklines
-And finally, add this to the controller whose view will be using sparklines:
-
- helper :sparklines
-
In your view, call it like this:
-<%= sparkline_tag [1,2,3,4,5,6] %> <!-- Gives you a smooth graph -->
+ <%= sparkline_tag [1,2,3,4,5,6] %>
Or specify details:
-<%= sparkline_tag [1,2,3,4,5,6], :type => 'discrete', :height => 10, :upper => 80, :above_color => 'green', :below_color => 'blue' %>
+ <%= sparkline_tag [1,2,3,4,5,6],
+ :type => 'discrete',
+ :height => 10,
+ :upper => 80,
+ :above_color => 'green',
+ :below_color => 'blue' %>
-
Graph types:
area
discrete
pie
smooth
- bar (results will be normalized, i.e. scaled to take up the full height of the graph)
+ bar
General Defaults:
- :type => 'smooth'
- :height => 14px
- :upper => 50
- :above_color => 'red'
- :below_color => 'grey'
- :background_color => 'white'
- :line_color => 'lightgrey'
+ :type => 'smooth'
+ :height => 14px
+ :upper => 50
+ :above_color => 'red'
+ :below_color => 'grey'
+ :background_color => 'white'
+ :line_color => 'lightgrey'
==License
Licensed under the MIT license.
=end
+class Sparklines
-module Sparklines
- VERSION = '0.2.7'
+ VERSION = '0.4.0'
- # Does the actually plotting of the graph. Calls the appropriate function based on the :type value passed. Defaults to 'smooth.'
- def Sparklines.plot(results=[], options={})
- defaults = { :type => 'smooth',
- :height => 14,
- :upper => 50,
- :diameter => 20,
- :step => 2,
- :line_color => 'lightgrey',
+ @@label_margin = 5.0
+ @@pointsize = 10.0
- :above_color => 'red',
- :below_color => 'grey',
- :background_color => 'white',
- :share_color => 'blue',
- :remain_color => 'lightgrey',
- :min_color => 'blue',
- :max_color => 'green',
- :last_color => 'red',
+ class << self
- :has_min => false,
- :has_max => false,
- :has_last => false
- }
+ # Does the actual plotting of the graph.
+ # Calls the appropriate subclass based on the :type argument.
+ # Defaults to 'smooth'
+ def plot(data=[], options={})
+ defaults = {
+ :type => 'smooth',
+ :height => 14,
+ :upper => 50,
+ :diameter => 20,
+ :step => 2,
+ :line_color => 'lightgrey',
- # Have to do this to get around HashWithIndifferentAccess
- options_sym = Hash.new
- options.keys.reverse.each do |key|
- options_sym[key.to_sym] = options[key]
- end
+ :above_color => 'red',
+ :below_color => 'grey',
+ :background_color => 'white',
+ :share_color => 'red',
+ :remain_color => 'lightgrey',
+ :min_color => 'blue',
+ :max_color => 'green',
+ :last_color => 'red',
- options_sym = defaults.merge(options_sym)
-
- # Minimal normalization
- maximum_value = self.get_max_value(results).to_f
-
- # Call the appropriate function for actual plotting
- self.send(options_sym[:type], results, options_sym, maximum_value)
- end
+ :has_min => false,
+ :has_max => false,
+ :has_last => false,
- # Writes a graph to disk with the specified filename, or "Sparklines.png"
- def Sparklines.plot_to_file(filename="sparklines.png", results=[], options={})
- File.open( filename, 'wb' ) do |png|
- png << self.plot( results, options)
- end
- end
+ :label => nil
+ }
-# Creates a pie-chart sparkline
-#
-# * 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
-#
-# * options - a hash that takes parameters:
-#
-# :diameter - An integer that determines what the size of the sparkline will be. Defaults to 20
-#
-# :share_color - A string or color code representing the color to draw the share of the pie represented by percent. Defaults to blue.
-#
-# :remain_color - A string or color code representing the color to draw the pie not taken by the share color. Defaults to lightgrey.
- def self.pie(results=[],options={}, maximum_value=100.0)
+ # Hack for HashWithIndifferentAccess
+ options_sym = Hash.new
+ options.keys.each do |key|
+ options_sym[key.to_sym] = options[key]
+ end
- diameter = options[:diameter].to_i
- share_color = options[:share_color]
- remain_color = options[:remain_color]
- percent = results[0]
+ options_sym = defaults.merge(options_sym)
+
+ # Call the appropriate method for actual plotting.
+ sparkline = self.new(data, options_sym)
+ if %w(area bar pie smooth discrete).include? options_sym[:type]
+ sparkline.send options_sym[:type]
+ else
+ sparkline.plot_error options_sym
+ end
+ end
- img = Magick::Image.new(diameter , diameter) {self.background_color = options[:background_color]}
- img.format = "PNG"
- draw = Magick::Draw.new
-
- #Adjust the radius so there's some edge left n the pie
- r = diameter/2.0 - 2
- draw.fill(remain_color)
- draw.ellipse(r + 2, r + 2, r , r , 0, 360)
- draw.fill(share_color)
+ # Writes a graph to disk with the specified filename, or "sparklines.png"
+ def plot_to_file(filename="sparklines.png", data=[], options={})
+ File.open( filename, 'wb' ) do |png|
+ png << self.plot( data, options)
+ end
+ end
- # Special exceptions
- if percent == 0
- # For 0% return blank
- draw.draw(img)
- return img.to_blob
- elsif percent == 100
- # For 100% just draw a full circle
- draw.ellipse(r + 2, r + 2, r , r , 0, 360)
- draw.draw(img)
- return img.to_blob
- end
-
- #Okay, this part is as confusing as hell, so pay attention:
- #This line determines the horizontal portion of the point on the circle where the X-Axis
- #should end. It's caculated by taking the center of the on-image circle and adding that
- #to the radius multiplied by the formula for determinig the point on a unit circle that a
- #angle corresponds to. 3.6 * percent gives us that angle, but it's in degrees, so we need to
- #convert, hence the muliplication by Pi over 180
- arc_end_x = r + 2 + (r * Math.cos((3.6 * percent)*(Math::PI/180)))
-
- #The same goes for here, except it's the vertical point instead of the horizontal one
- arc_end_y = r + 2 + (r * Math.sin((3.6 * percent)*(Math::PI/180)))
-
- #Because the SVG path format is seriously screwy, we need to set the large-arc-flag to 1
- #if the angle of an arc is greater than 180 degrees. I have no idea why this is, but it is.
- percent > 50? large_arc_flag = 1: large_arc_flag = 0
-
- #This is also confusing
- #M tells us to move to an absolute point on the image. We're moving to the center of the pie
- #h tells us to move to a relative point. We're moving to the right edge of the circle.
- #A tells us to start an absolute elliptical arc. The first two values are the radii of the ellipse
- #the third value is the x-axis-rotation (how to rotate the ellipse if we wanted to [could have some fun
- #with randomizing that maybe), the fourth value is our large-arc-flag, the fifth is the sweep-flag,
- #(again, confusing), the sixth and seventh values are the end point of the arc which we calculated previously
- #More info on the SVG path string format at: http://www.w3.org/TR/SVG/paths.html
- path = "M#{r + 2},#{r + 2} h#{r} A#{r},#{r} 0 #{large_arc_flag},1 #{arc_end_x},#{arc_end_y} z"
- draw.path(path)
-
- draw.draw(img)
- img.to_blob
- end
+ end
-# Creates a discretized sparkline
-#
-# * results is an array of integer values between 0 and 100 inclusive
-#
-# * options is a hash that takes 4 parameters:
-#
-# :height - An integer that determines what the height of the sparkline will be. Defaults to 14
-#
-# :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.
-#
-# :above_color - A string or color code representing the color to draw values above or equal the upper value. Defaults to red.
-#
-# :below_color - A string or color code representing the color to draw values below the upper value. Defaults to gray.
- def self.discrete(results=[], options = {}, maximum_value=100.0)
+ def initialize(data=[], options={})
+ @data = Array(data)
+ @options = options
+ normalize_data
+ end
- height = options[:height].to_i
- upper = options[:upper].to_i
- below_color = options[:below_color]
- above_color = options[:above_color]
+ # Creates a continuous area sparkline. Relevant options.
+ #
+ # :step - An integer that determines the distance between each point on the sparkline. Defaults to 2.
+ #
+ # :height - An integer that determines what the height of the sparkline will be. Defaults to 14
+ #
+ # :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.
+ #
+ # :has_min - Determines whether a dot will be drawn at the lowest value or not. Defaults to false.
+ #
+ # :has_max - Determines whether a dot will be drawn at the highest value or not. Defaults to false.
+ #
+ # :has_last - Determines whether a dot will be drawn at the last value or not. Defaults to false.
+ #
+ # :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.
+ #
+ # :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.
+ #
+ # :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.
+ #
+ # :above_color - A string or color code representing the color to draw values above or equal the upper value. Defaults to red.
+ #
+ # :below_color - A string or color code representing the color to draw values below the upper value. Defaults to gray.
+ def area
- img = Magick::Image.new(results.size * 2 - 1, height) {self.background_color = options[:background_color]}
- img.format = "PNG"
- draw = Magick::Draw.new
+ step = @options[:step].to_i
+ height = @options[:height].to_i
+ background_color = @options[:background_color]
+
+ create_canvas((@norm_data.size - 1) * step + 4, height, background_color)
- i = 0
- results.each do |r|
- color = (r >= upper) ? above_color : below_color
- draw.stroke(color)
- draw.line(i, (img.rows - r/(101.0/(height-4))-4).to_i,
- i, (img.rows - r/(101.0/(height-4))).to_i)
+ upper = @options[:upper].to_i
- i += 2
- end
+ has_min = @options[:has_min]
+ has_max = @options[:has_max]
+ has_last = @options[:has_last]
- draw.draw(img)
- img.to_blob
- end
+ min_color = @options[:min_color]
+ max_color = @options[:max_color]
+ last_color = @options[:last_color]
+ below_color = @options[:below_color]
+ above_color = @options[:above_color]
-# Creates a continuous area sparkline
-#
-# * results is an array of integer values between 0 and 100 inclusive
-#
-# * options is a hash that takes 4 parameters:
-#
-# :step - An integer that determines the distance between each point on the sparkline. Defaults to 2.
-#
-# :height - An integer that determines what the height of the sparkline will be. Defaults to 14
-#
-# :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.
-#
-# :has_min - Determines whether a dot will be drawn at the lowest value or not. Defaulst to false.
-#
-# :has_max - Determines whether a dot will be drawn at the highest value or not. Defaulst to false.
-#
-# :has_last - Determines whether a dot will be drawn at the last value or not. Defaulst to false.
-#
-# :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.
-#
-# :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.
-#
-# :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.
-#
-# :above_color - A string or color code representing the color to draw values above or equal the upper value. Defaults to red.
-#
-# :below_color - A string or color code representing the color to draw values below the upper value. Defaults to gray.
- def self.area(results=[], options={}, maximum_value=100.0)
-
- step = options[:step].to_i
- height = options[:height].to_i
- upper = options[:upper].to_i
- has_min = options[:has_min]
- has_max = options[:has_max]
- has_last = options[:has_last]
+ coords = [[0,(height - 3 - upper/(101.0/(height-4)))]]
+ i=0
+ @norm_data.each do |r|
+ coords.push [(2 + i), (height - 3 - r/(101.0/(height-4)))]
+ i += step
+ end
+ coords.push [(@norm_data.size - 1) * step + 4, (height - 3 - upper/(101.0/(height-4)))]
- min_color = options[:min_color]
- max_color = options[:max_color]
- last_color = options[:last_color]
- below_color = options[:below_color]
- above_color = options[:above_color]
+ # TODO Refactor! Should take a block and do both.
+ #
+ # Block off the bottom half of the image and draw the sparkline
+ @draw.fill(above_color)
+ @draw.define_clip_path('top') do
+ @draw.rectangle(0,0,(@norm_data.size - 1) * step + 4,(height - 3 - upper/(101.0/(height-4))))
+ end
+ @draw.clip_path('top')
+ @draw.polygon *coords.flatten
- img = Magick::Image.new((results.size - 1) * step + 4, height) {self.background_color = options[:background_color]}
- img.format = "PNG"
- draw = Magick::Draw.new
+ # Block off the top half of the image and draw the sparkline
+ @draw.fill(below_color)
+ @draw.define_clip_path('bottom') do
+ @draw.rectangle(0,(height - 3 - upper/(101.0/(height-4))),(@norm_data.size - 1) * step + 4,height)
+ end
+ @draw.clip_path('bottom')
+ @draw.polygon *coords.flatten
- coords = [[0,(height - 3 - upper/(101.0/(height-4)))]]
- i=0
- results.each do |r|
- coords.push [(2 + i), (height - 3 - r/(101.0/(height-4)))]
- i += step
- end
- coords.push [(results.size - 1) * step + 4, (height - 3 - upper/(101.0/(height-4)))]
+ # The sparkline looks kinda nasty if either the above_color or below_color gets the center line
+ @draw.fill('black')
+ @draw.line(0,(height - 3 - upper/(101.0/(height-4))),(@norm_data.size - 1) * step + 4,(height - 3 - upper/(101.0/(height-4))))
- #Block off the bottom half of the image and draw the sparkline
- draw.fill(above_color)
- draw.define_clip_path('top') do
- draw.rectangle(0,0,(results.size - 1) * step + 4,(height - 3 - upper/(101.0/(height-4))))
- end
- draw.clip_path('top')
- draw.polygon *coords.flatten
+ # After the parts have been masked, we need to let the whole canvas be drawable again
+ # so a max dot can be displayed
+ @draw.define_clip_path('all') do
+ @draw.rectangle(0,0,@canvas.columns,@canvas.rows)
+ end
+ @draw.clip_path('all')
+
+ drawbox(coords[@norm_data.index(@norm_data.min)+1], 1, min_color) if has_min == true
+ drawbox(coords[@norm_data.index(@norm_data.max)+1], 1, max_color) if has_max == true
+
+ drawbox(coords[-2], 1, last_color) if has_last == true
- #Block off the top half of the image and draw the sparkline
- draw.fill(below_color)
- draw.define_clip_path('bottom') do
- draw.rectangle(0,(height - 3 - upper/(101.0/(height-4))),(results.size - 1) * step + 4,height)
- end
- draw.clip_path('bottom')
- draw.polygon *coords.flatten
+ @draw.draw(@canvas)
+ @canvas.to_blob
+ end
- #The sparkline looks kinda nasty if either the above_color or below_color gets the center line
- draw.fill('black')
- draw.line(0,(height - 3 - upper/(101.0/(height-4))),(results.size - 1) * step + 4,(height - 3 - upper/(101.0/(height-4))))
- #After the parts have been masked, we need to let the whole canvas be drawable again
- #so a max dot can be displayed
- draw.define_clip_path('all') do
- draw.rectangle(0,0,img.columns,img.rows)
- end
- draw.clip_path('all')
- if has_min == 'true'
- min_pt = coords[results.index(results.min)+1]
- draw.fill(min_color)
- draw.rectangle(min_pt[0]-1, min_pt[1]-1, min_pt[0]+1, min_pt[1]+1)
- end
- if has_max == 'true'
- max_pt = coords[results.index(results.max)+1]
- draw.fill(max_color)
- draw.rectangle(max_pt[0]-1, max_pt[1]-1, max_pt[0]+1, max_pt[1]+1)
- end
- if has_last == 'true'
- last_pt = coords[-2]
- draw.fill(last_color)
- draw.rectangle(last_pt[0]-1, last_pt[1]-1, last_pt[0]+1, last_pt[1]+1)
- end
+ # Draws a bar graph.
+ #
+ def bar
+ step = @options[:step].to_i
+ height = @options[:height].to_f
+ background_color = @options[:background_color]
- draw.draw(img)
- img.to_blob
- end
+ create_canvas(@norm_data.length * step + 2, height, background_color)
+
+ upper = @options[:upper].to_i
+ below_color = @options[:below_color]
+ above_color = @options[:above_color]
-# Creates a smooth sparkline
-#
-# * results - an array of integer values between 0 and 100 inclusive
-#
-# * options - a hash that takes these optional parameters:
-#
-# :step - An integer that determines the distance between each point on the sparkline. Defaults to 2.
-#
-# :height - An integer that determines what the height of the sparkline will be. Defaults to 14
-#
-# :has_min - Determines whether a dot will be drawn at the lowest value or not. Defaulst to false.
-#
-# :has_max - Determines whether a dot will be drawn at the highest value or not. Defaulst to false.
-#
-# :has_last - Determines whether a dot will be drawn at the last value or not. Defaulst to false.
-#
-# :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.
-#
-# :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.
-#
-# :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.
- def self.smooth(results, options, maximum_value=100.0)
-
- step = options[:step].to_i
- height = options[:height].to_i
- min_color = options[:min_color]
- max_color = options[:max_color]
- last_color = options[:last_color]
- has_min = options[:has_min]
- has_max = options[:has_max]
- has_last = options[:has_last]
- line_color = options[:line_color]
+ i = 1
+ @norm_data.each_with_index do |r, index|
+ color = (r >= upper) ? above_color : below_color
+ @draw.stroke('transparent')
+ @draw.fill(color)
+ @draw.rectangle( i, @canvas.rows,
+ i + step - 2, @canvas.rows - ( (r / @maximum_value) * @canvas.rows) )
+ i += step
+ end
- img = Magick::Image.new((results.size - 1) * step + 4, height.to_i) {self.background_color = options[:background_color]}
- img.format = "PNG"
- draw = Magick::Draw.new
-
- draw.stroke(line_color)
- coords = []
- i=0
- results.each do |r|
- coords.push [ 2 + i, (height - 3 - r/(101.0/(height-4))) ]
- i += step
- end
-
- my_polyline(draw, coords)
+ @draw.draw(@canvas)
+ @canvas.to_blob
+ end
- if has_min == true
- min_pt = coords[results.index(results.min)]
- draw.fill(min_color)
- draw.rectangle(min_pt[0]-2, min_pt[1]-2, min_pt[0]+2, min_pt[1]+2)
- end
- if has_max == true
- max_pt = coords[results.index(results.max)]
- draw.fill(max_color)
- draw.rectangle(max_pt[0]-2, max_pt[1]-2, max_pt[0]+2, max_pt[1]+2)
- end
- if has_last == true
- last_pt = coords[-1]
- draw.fill(last_color)
- draw.rectangle(last_pt[0]-2, last_pt[1]-2, last_pt[0]+2, last_pt[1]+2)
- end
- draw.draw(img)
- img.to_blob
- end
+ # Creates a discretized sparkline
+ #
+ # :height - An integer that determines what the height of the sparkline will be. Defaults to 14
+ #
+ # :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.
+ #
+ # :above_color - A string or color code representing the color to draw values above or equal the upper value. Defaults to red.
+ #
+ # :below_color - A string or color code representing the color to draw values below the upper value. Defaults to gray.
+ def discrete
+ height = @options[:height].to_i
+ upper = @options[:upper].to_i
+ background_color = @options[:background_color]
+ step = @options[:step].to_i
+
+ create_canvas(@norm_data.size * step - 1, height, background_color)
+
+ below_color = @options[:below_color]
+ above_color = @options[:above_color]
- # This is a function to replace the RMagick polyline function because it doesn't seem to work properly.
- #
- # * draw - a RMagick::Draw object.
- #
- # * arr - an array of points (represented as two element arrays)
- def self.my_polyline (draw, arr)
- i = 0
- while i < arr.size - 1
- draw.line(arr[i][0], arr[i][1], arr[i+1][0], arr[i+1][1])
- i += 1
- end
- end
+ i = 0
+ @norm_data.each do |r|
+ color = (r >= upper) ? above_color : below_color
+ @draw.stroke(color)
+ @draw.line(i, (@canvas.rows - r/(101.0/(height-4))-4).to_i,
+ i, (@canvas.rows - r/(101.0/(height-4))).to_i)
+ i += step
+ end
- # Draw the error Sparkline. Not implemented yet.
- def self.plot_error(options={})
- img = Magick::Image.new(40,15) {self.background_color = options[:background_color]}
- img.format = "PNG"
- draw = Magick::Draw.new
- draw.fill('red')
- draw.line(0,0,40,15)
- draw.line(0,15,40,0)
- draw.draw(img)
+ @draw.draw(@canvas)
+ @canvas.to_blob
+ end
- img.to_blob
- end
+ # Creates a pie-chart sparkline
+ #
+ # :diameter - An integer that determines what the size of the sparkline will be. Defaults to 20
+ #
+ # :share_color - A string or color code representing the color to draw the share of the pie represented by percent. Defaults to red.
+ #
+ # :remain_color - A string or color code representing the color to draw the pie not taken by the share color. Defaults to lightgrey.
+ def pie
- # Draws a bar graph, normalized.
+ diameter = @options[:diameter].to_i
+ background_color = @options[:background_color]
+
+ create_canvas(diameter, diameter, background_color)
+
+ share_color = @options[:share_color]
+ remain_color = @options[:remain_color]
+ percent = @norm_data[0]
+
+ # Adjust the radius so there's some edge left in the pie
+ r = diameter/2.0 - 2
+ @draw.fill(remain_color)
+ @draw.ellipse(r + 2, r + 2, r , r , 0, 360)
+ @draw.fill(share_color)
+
+ # Special exceptions
+ if percent == 0
+ # For 0% return blank
+ @draw.draw(@canvas)
+ return @canvas.to_blob
+ elsif percent == 100
+ # For 100% just draw a full circle
+ @draw.ellipse(r + 2, r + 2, r , r , 0, 360)
+ @draw.draw(@canvas)
+ return @canvas.to_blob
+ end
+
+ # Okay, this part is as confusing as hell, so pay attention:
+ # This line determines the horizontal portion of the point on the circle where the X-Axis
+ # should end. It's caculated by taking the center of the on-image circle and adding that
+ # to the radius multiplied by the formula for determinig the point on a unit circle that a
+ # angle corresponds to. 3.6 * percent gives us that angle, but it's in degrees, so we need to
+ # convert, hence the muliplication by Pi over 180
+ arc_end_x = r + 2 + (r * Math.cos((3.6 * percent)*(Math::PI/180)))
+
+ # The same goes for here, except it's the vertical point instead of the horizontal one
+ arc_end_y = r + 2 + (r * Math.sin((3.6 * percent)*(Math::PI/180)))
+
+ # Because the SVG path format is seriously screwy, we need to set the large-arc-flag to 1
+ # if the angle of an arc is greater than 180 degrees. I have no idea why this is, but it is.
+ percent > 50? large_arc_flag = 1: large_arc_flag = 0
+
+ # This is also confusing
+ # M tells us to move to an absolute point on the image. We're moving to the center of the pie
+ # h tells us to move to a relative point. We're moving to the right edge of the circle.
+ # A tells us to start an absolute elliptical arc. The first two values are the radii of the ellipse
+ # the third value is the x-axis-rotation (how to rotate the ellipse if we wanted to [could have some fun
+ # with randomizing that maybe), the fourth value is our large-arc-flag, the fifth is the sweep-flag,
+ # (again, confusing), the sixth and seventh values are the end point of the arc which we calculated previously
+ # More info on the SVG path string format at: http://www.w3.org/TR/SVG/paths.html
+ path = "M#{r + 2},#{r + 2} h#{r} A#{r},#{r} 0 #{large_arc_flag},1 #{arc_end_x},#{arc_end_y} z"
+ @draw.path(path)
+
+ @draw.draw(@canvas)
+ @canvas.to_blob
+ end
+
+
+ # Creates a smooth sparkline.
#
- # BUG: Last column never goes all the way to the bottom.
- def self.bar(results=[], options = {}, maximum_value=100.0)
- step = options[:step].to_i
- height = options[:height].to_f
- upper = options[:upper].to_i
- below_color = options[:below_color]
- above_color = options[:above_color]
+ # :step - An integer that determines the distance between each point on the sparkline. Defaults to 2.
+ #
+ # :height - An integer that determines what the height of the sparkline will be. Defaults to 14
+ #
+ # :has_min - Determines whether a dot will be drawn at the lowest value or not. Defaults to false.
+ #
+ # :has_max - Determines whether a dot will be drawn at the highest value or not. Defaults to false.
+ #
+ # :has_last - Determines whether a dot will be drawn at the last value or not. Defaults to false.
+ #
+ # :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.
+ #
+ # :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.
+ #
+ # :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.
+ def smooth
- img = Magick::Image.new((results.size) * step + 2, height) {
- self.background_color = options[:background_color]
- }
- img.format = "PNG"
- draw = Magick::Draw.new
+ step = @options[:step].to_i
+ height = @options[:height].to_i
+ background_color = @options[:background_color]
- i = 1
- results.each_with_index do |r, index|
- color = (r >= upper) ? above_color : below_color
- draw.stroke('transparent')
- draw.fill(color)
- draw.rectangle( i, img.rows,
- i + step - 2, img.rows - ((r / maximum_value) * img.rows) )
+ create_canvas((@norm_data.size - 1) * step + 4, height, background_color)
+
+ min_color = @options[:min_color]
+ max_color = @options[:max_color]
+ last_color = @options[:last_color]
+ has_min = @options[:has_min]
+ has_max = @options[:has_max]
+ has_last = @options[:has_last]
+ line_color = @options[:line_color]
+
+ @draw.stroke(line_color)
+ coords = []
+ i=0
+ @norm_data.each do |r|
+ coords.push [ 2 + i, (height - 3 - r/(101.0/(height-4))) ]
i += step
end
+
+ open_ended_polyline(coords)
- draw.draw(img)
- img.to_blob
+ drawbox(coords[@norm_data.index(@norm_data.min)], 2, min_color) if has_min == true
+
+ drawbox(coords[@norm_data.index(@norm_data.max)], 2, max_color) if has_max == true
+
+ drawbox(coords[-1], 2, last_color) if has_last == true
+
+ @draw.draw(@canvas)
+ @canvas.to_blob
end
- def self.get_max_value(values=[])
- max_value = 0
- values.each do |value|
- max_value = (value > max_value) ? value : max_value
+ # Draw the error Sparkline.
+ def plot_error(options={})
+ create_canvas(40, 15, 'white')
+
+ @draw.fill('red')
+ @draw.line(0,0,40,15)
+ @draw.line(0,15,40,0)
+
+ @draw.draw(@canvas)
+ @canvas.to_blob
+ end
+
+private
+
+ def normalize_data
+ @maximum_value = @data.max
+ if @options[:type].to_s == 'pie'
+ @norm_data = @data
+ else
+ @norm_data = @data.map { |value| value = (value.to_f / @maximum_value) * 100.0 }
end
- return max_value
end
+
+ # * arr - an array of points (represented as two element arrays)
+ def open_ended_polyline(arr)
+ 0.upto(arr.length - 2) { |i|
+ @draw.line(arr[i][0], arr[i][1], arr[i+1][0], arr[i+1][1])
+ }
+ end
+
+ # Create an image to draw on and a drawable to do the drawing with.
+ #
+ # TODO Refactor into smaller functions
+ def create_canvas(w, h, bkg_col)
+ @draw = Magick::Draw.new
+ @draw.pointsize = @@pointsize # TODO Use height
+ @canvas = Magick::Image.new(w , h) { self.background_color = bkg_col }
+
+ # Make room for label and last value
+ if !@options[:label].nil?
+ @options[:has_last] = true
+ @label_width = calculate_width(@options[:label])
+ @data_last_width = calculate_width(@data.last)
+ # HACK The 7.0 is a severe hack. Must figure out correct spacing
+ @label_and_data_last_width = @label_width + @data_last_width + @@label_margin * 7.0
+ w += @label_and_data_last_width
+ end
+
+ @canvas = Magick::Image.new(w , h) { self.background_color = bkg_col }
+ @canvas.format = "PNG"
+
+ # Draw label and last value
+ if !@options[:label].nil?
+ if ENV.has_key?('MAGICK_FONT_PATH')
+ vera_font_path = File.expand_path('Vera.ttf', ENV['MAGICK_FONT_PATH'])
+ @font = File.exists?(vera_font_path) ? vera_font_path : nil
+ else
+ @font = nil
+ end
+
+ @draw.fill = 'black'
+ @draw.font = @font if @font
+ @draw.gravity = Magick::WestGravity
+ @draw.annotate( @canvas,
+ @label_width, 1.0,
+ w - @label_and_data_last_width + @@label_margin, h - calculate_caps_height/2.0,
+ @options[:label])
+
+ @draw.fill = 'red'
+ @draw.annotate( @canvas,
+ @data_last_width, 1.0,
+ w - @data_last_width - @@label_margin * 2.0, h - calculate_caps_height/2.0,
+ @data.last.to_s)
+ end
+ end
+ # Utility to draw a coloured box
+ # Centred on pt, offset off in each direction, fill color is col
+ def drawbox(pt, offset, color)
+ @draw.stroke 'transparent'
+ @draw.fill(color)
+ @draw.rectangle(pt[0]-offset, pt[1]-offset, pt[0]+offset, pt[1]+offset)
+ end
+
+ def calculate_width(text)
+ @draw.get_type_metrics(@canvas, text.to_s).width
+ end
+
+ def calculate_caps_height
+ @draw.get_type_metrics(@canvas, 'X').height
+ end
+
+
end