├── .gitattributes ├── .gitignore ├── samples ├── area.png ├── pie.png ├── pie0.png ├── pie1.png ├── pie100.png ├── pie45.png ├── pie95.png ├── pie99.png ├── smooth.png ├── area-high.png ├── discrete.png ├── pie-large.png └── smooth-colored.png ├── test ├── expected │ ├── bar.png │ ├── pie.png │ ├── area.png │ ├── error.png │ ├── pie0.png │ ├── pie1.png │ ├── pie100.png │ ├── pie45.png │ ├── pie95.png │ ├── pie99.png │ ├── smooth.png │ ├── zeros.png │ ├── bar_tall.png │ ├── bar_wide.png │ ├── discrete.png │ ├── nil_area.png │ ├── nil_bar.png │ ├── nil_pie.png │ ├── pie_flat.png │ ├── whisker.png │ ├── area_high.png │ ├── bullet_tall.png │ ├── bullet_wide.png │ ├── labeled_bar.png │ ├── labeled_pie.png │ ├── nil_smooth.png │ ├── pie_large.png │ ├── area_min_max.png │ ├── bullet_basic.png │ ├── discrete_max.png │ ├── discrete_min.png │ ├── discrete_wide.png │ ├── labeled_area.png │ ├── nil_discrete.png │ ├── whisker_junk.png │ ├── bar_string.png.png │ ├── bullet_colorful.png │ ├── labeled_discrete.png │ ├── labeled_smooth.png │ ├── smooth_colored.png │ ├── bar_extreme_values.png │ ├── smooth_with_target.png │ ├── standard_deviation.png │ ├── whisker_with_step.png │ ├── bullet_full_featured.png │ ├── labeled_bar_sprintf.png │ ├── smooth_underneath_color.png │ ├── standard_deviation_tall.png │ ├── whisker_non_exceptional.png │ ├── labeled_whisker_decimals.png │ ├── standard_deviation_short.png │ ├── labeled_smooth_without_last.png │ └── smooth_similar_nonzero_values.png └── test_all.rb ├── fonts └── silkscreen │ ├── slkscr.ttf │ ├── slkscrb.ttf │ ├── slkscre.ttf │ ├── slkscreb.ttf │ └── readme.txt ├── README.md ├── Rakefile ├── MIT-LICENSE ├── lib ├── sparklines_helper.rb └── sparklines.rb ├── Manifest.txt ├── History.txt └── sparklines.gemspec /.gitattributes: -------------------------------------------------------------------------------- 1 | *.ttf -crlf -diff -merge 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | test/actual/* 3 | doc/* 4 | /test/output.html 5 | -------------------------------------------------------------------------------- /samples/area.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfunky/sparklines/HEAD/samples/area.png -------------------------------------------------------------------------------- /samples/pie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfunky/sparklines/HEAD/samples/pie.png -------------------------------------------------------------------------------- /samples/pie0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfunky/sparklines/HEAD/samples/pie0.png -------------------------------------------------------------------------------- /samples/pie1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfunky/sparklines/HEAD/samples/pie1.png -------------------------------------------------------------------------------- /samples/pie100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfunky/sparklines/HEAD/samples/pie100.png -------------------------------------------------------------------------------- /samples/pie45.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfunky/sparklines/HEAD/samples/pie45.png -------------------------------------------------------------------------------- /samples/pie95.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfunky/sparklines/HEAD/samples/pie95.png -------------------------------------------------------------------------------- /samples/pie99.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfunky/sparklines/HEAD/samples/pie99.png -------------------------------------------------------------------------------- /samples/smooth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfunky/sparklines/HEAD/samples/smooth.png -------------------------------------------------------------------------------- /samples/area-high.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfunky/sparklines/HEAD/samples/area-high.png -------------------------------------------------------------------------------- /samples/discrete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfunky/sparklines/HEAD/samples/discrete.png -------------------------------------------------------------------------------- /samples/pie-large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfunky/sparklines/HEAD/samples/pie-large.png -------------------------------------------------------------------------------- /test/expected/bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfunky/sparklines/HEAD/test/expected/bar.png -------------------------------------------------------------------------------- /test/expected/pie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfunky/sparklines/HEAD/test/expected/pie.png -------------------------------------------------------------------------------- /test/expected/area.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfunky/sparklines/HEAD/test/expected/area.png -------------------------------------------------------------------------------- /test/expected/error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfunky/sparklines/HEAD/test/expected/error.png -------------------------------------------------------------------------------- /test/expected/pie0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfunky/sparklines/HEAD/test/expected/pie0.png -------------------------------------------------------------------------------- /test/expected/pie1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfunky/sparklines/HEAD/test/expected/pie1.png -------------------------------------------------------------------------------- /test/expected/pie100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfunky/sparklines/HEAD/test/expected/pie100.png -------------------------------------------------------------------------------- /test/expected/pie45.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfunky/sparklines/HEAD/test/expected/pie45.png -------------------------------------------------------------------------------- /test/expected/pie95.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfunky/sparklines/HEAD/test/expected/pie95.png -------------------------------------------------------------------------------- /test/expected/pie99.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfunky/sparklines/HEAD/test/expected/pie99.png -------------------------------------------------------------------------------- /test/expected/smooth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfunky/sparklines/HEAD/test/expected/smooth.png -------------------------------------------------------------------------------- /test/expected/zeros.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfunky/sparklines/HEAD/test/expected/zeros.png -------------------------------------------------------------------------------- /samples/smooth-colored.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfunky/sparklines/HEAD/samples/smooth-colored.png -------------------------------------------------------------------------------- /test/expected/bar_tall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfunky/sparklines/HEAD/test/expected/bar_tall.png -------------------------------------------------------------------------------- /test/expected/bar_wide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfunky/sparklines/HEAD/test/expected/bar_wide.png -------------------------------------------------------------------------------- /test/expected/discrete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfunky/sparklines/HEAD/test/expected/discrete.png -------------------------------------------------------------------------------- /test/expected/nil_area.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfunky/sparklines/HEAD/test/expected/nil_area.png -------------------------------------------------------------------------------- /test/expected/nil_bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfunky/sparklines/HEAD/test/expected/nil_bar.png -------------------------------------------------------------------------------- /test/expected/nil_pie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfunky/sparklines/HEAD/test/expected/nil_pie.png -------------------------------------------------------------------------------- /test/expected/pie_flat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfunky/sparklines/HEAD/test/expected/pie_flat.png -------------------------------------------------------------------------------- /test/expected/whisker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfunky/sparklines/HEAD/test/expected/whisker.png -------------------------------------------------------------------------------- /fonts/silkscreen/slkscr.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfunky/sparklines/HEAD/fonts/silkscreen/slkscr.ttf -------------------------------------------------------------------------------- /fonts/silkscreen/slkscrb.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfunky/sparklines/HEAD/fonts/silkscreen/slkscrb.ttf -------------------------------------------------------------------------------- /fonts/silkscreen/slkscre.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfunky/sparklines/HEAD/fonts/silkscreen/slkscre.ttf -------------------------------------------------------------------------------- /fonts/silkscreen/slkscreb.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfunky/sparklines/HEAD/fonts/silkscreen/slkscreb.ttf -------------------------------------------------------------------------------- /test/expected/area_high.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfunky/sparklines/HEAD/test/expected/area_high.png -------------------------------------------------------------------------------- /test/expected/bullet_tall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfunky/sparklines/HEAD/test/expected/bullet_tall.png -------------------------------------------------------------------------------- /test/expected/bullet_wide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfunky/sparklines/HEAD/test/expected/bullet_wide.png -------------------------------------------------------------------------------- /test/expected/labeled_bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfunky/sparklines/HEAD/test/expected/labeled_bar.png -------------------------------------------------------------------------------- /test/expected/labeled_pie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfunky/sparklines/HEAD/test/expected/labeled_pie.png -------------------------------------------------------------------------------- /test/expected/nil_smooth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfunky/sparklines/HEAD/test/expected/nil_smooth.png -------------------------------------------------------------------------------- /test/expected/pie_large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfunky/sparklines/HEAD/test/expected/pie_large.png -------------------------------------------------------------------------------- /test/expected/area_min_max.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfunky/sparklines/HEAD/test/expected/area_min_max.png -------------------------------------------------------------------------------- /test/expected/bullet_basic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfunky/sparklines/HEAD/test/expected/bullet_basic.png -------------------------------------------------------------------------------- /test/expected/discrete_max.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfunky/sparklines/HEAD/test/expected/discrete_max.png -------------------------------------------------------------------------------- /test/expected/discrete_min.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfunky/sparklines/HEAD/test/expected/discrete_min.png -------------------------------------------------------------------------------- /test/expected/discrete_wide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfunky/sparklines/HEAD/test/expected/discrete_wide.png -------------------------------------------------------------------------------- /test/expected/labeled_area.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfunky/sparklines/HEAD/test/expected/labeled_area.png -------------------------------------------------------------------------------- /test/expected/nil_discrete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfunky/sparklines/HEAD/test/expected/nil_discrete.png -------------------------------------------------------------------------------- /test/expected/whisker_junk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfunky/sparklines/HEAD/test/expected/whisker_junk.png -------------------------------------------------------------------------------- /test/expected/bar_string.png.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfunky/sparklines/HEAD/test/expected/bar_string.png.png -------------------------------------------------------------------------------- /test/expected/bullet_colorful.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfunky/sparklines/HEAD/test/expected/bullet_colorful.png -------------------------------------------------------------------------------- /test/expected/labeled_discrete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfunky/sparklines/HEAD/test/expected/labeled_discrete.png -------------------------------------------------------------------------------- /test/expected/labeled_smooth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfunky/sparklines/HEAD/test/expected/labeled_smooth.png -------------------------------------------------------------------------------- /test/expected/smooth_colored.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfunky/sparklines/HEAD/test/expected/smooth_colored.png -------------------------------------------------------------------------------- /test/expected/bar_extreme_values.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfunky/sparklines/HEAD/test/expected/bar_extreme_values.png -------------------------------------------------------------------------------- /test/expected/smooth_with_target.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfunky/sparklines/HEAD/test/expected/smooth_with_target.png -------------------------------------------------------------------------------- /test/expected/standard_deviation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfunky/sparklines/HEAD/test/expected/standard_deviation.png -------------------------------------------------------------------------------- /test/expected/whisker_with_step.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfunky/sparklines/HEAD/test/expected/whisker_with_step.png -------------------------------------------------------------------------------- /test/expected/bullet_full_featured.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfunky/sparklines/HEAD/test/expected/bullet_full_featured.png -------------------------------------------------------------------------------- /test/expected/labeled_bar_sprintf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfunky/sparklines/HEAD/test/expected/labeled_bar_sprintf.png -------------------------------------------------------------------------------- /test/expected/smooth_underneath_color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfunky/sparklines/HEAD/test/expected/smooth_underneath_color.png -------------------------------------------------------------------------------- /test/expected/standard_deviation_tall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfunky/sparklines/HEAD/test/expected/standard_deviation_tall.png -------------------------------------------------------------------------------- /test/expected/whisker_non_exceptional.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfunky/sparklines/HEAD/test/expected/whisker_non_exceptional.png -------------------------------------------------------------------------------- /test/expected/labeled_whisker_decimals.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfunky/sparklines/HEAD/test/expected/labeled_whisker_decimals.png -------------------------------------------------------------------------------- /test/expected/standard_deviation_short.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfunky/sparklines/HEAD/test/expected/standard_deviation_short.png -------------------------------------------------------------------------------- /test/expected/labeled_smooth_without_last.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfunky/sparklines/HEAD/test/expected/labeled_smooth_without_last.png -------------------------------------------------------------------------------- /test/expected/smooth_similar_nonzero_values.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/topfunky/sparklines/HEAD/test/expected/smooth_similar_nonzero_values.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sparklines 2 | 3 | A library for generating small sparkline graphs from Ruby. Use it in desktop apps or with Ruby on Rails. 4 | 5 | ## Source 6 | 7 | http://github.com/topfunky/sparklines 8 | 9 | ## Other info 10 | 11 | http://nubyonrails.com/pages/sparklines 12 | 13 | ## Authors 14 | 15 | Geoffrey Grosenbach 16 | boss@topfunky.com 17 | http://nubyonrails.com/pages/sparklines 18 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | 2 | require 'rubygems' 3 | require 'hoe' 4 | $:.unshift(File.dirname(__FILE__) + "/lib") 5 | require 'sparklines' 6 | 7 | Hoe.new('Sparklines', Sparklines::VERSION) do |p| 8 | p.name = "sparklines" 9 | p.author = "Geoffrey Grosenbach" 10 | p.description = "Tiny graphs." 11 | p.email = 'boss@topfunky.com' 12 | p.summary = "Tiny graphs." 13 | p.url = "http://nubyonrails.com/pages/sparklines" 14 | p.clean_globs = ['test/actual', 'email.txt'] # Remove this directory on "rake clean" 15 | p.remote_rdoc_dir = '' # Release to root 16 | p.changes = p.paragraphs_of('History.txt', 0..1).join("\n\n") 17 | # * extra_deps - An array of rubygem dependencies. 18 | end 19 | 20 | desc "Release and publish documentation" 21 | task :repubdoc => [:release, :publish_docs] 22 | 23 | 24 | desc "Simple require on packaged files to make sure they are all there" 25 | task :verify => :package do 26 | # An error message will be displayed if files are missing 27 | if system %(ruby -e "require 'pkg/sparklines-#{Sparklines::VERSION}/lib/sparklines'") 28 | puts "\nThe library files are present" 29 | end 30 | end 31 | 32 | task :release => :verify 33 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2005 Geoffrey Grosenbach boss@topfunky.com 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | -------------------------------------------------------------------------------- /fonts/silkscreen/readme.txt: -------------------------------------------------------------------------------- 1 | Thank you for downloading Silkscreen, a type family for your Web graphics 2 | by Jason Kottke (jason@kottke.org). 3 | 4 | To install the Silkscreen type family, unzip this file and drag the files 5 | into the Fonts folder in the Control Panel. 6 | 7 | If you encounter any problems in using this font, please email me and I'll 8 | see if I can try and fix it. Please note that I can't help you with any 9 | installation issues. Please consult your system's help files for assistance. 10 | 11 | This font is free for personal and corporate use and may be redistributed in 12 | this unmodified form on your Web site. I would ask that you not modify and 13 | then redistribute this font...although you may modify it for your own 14 | personal use. If you really like this font and use it often, feel free to 15 | mail me (e- or snail mail) some small token of your appreciation. A URL 16 | of your work using Silkscreen would be appreciated as well. 17 | 18 | All future bug fixes, updates, and additions to the Silkscreen type family 19 | will be available on my Web site at the following URL: 20 | 21 | http://www.kottke.org/plus/type/silkscreen/index.html 22 | 23 | Again, thanks for downloading Silkscreen. Enjoy! 24 | 25 | -jason -------------------------------------------------------------------------------- /lib/sparklines_helper.rb: -------------------------------------------------------------------------------- 1 | require 'base64' 2 | # Provides a tag for embedding sparklines graphs into your Rails app. 3 | # 4 | module SparklinesHelper 5 | 6 | # Call with an array of data and a hash of params for the Sparklines module. 7 | # 8 | # sparkline_tag [42, 37, 43, 182], :type => 'bar', :line_color => 'black' 9 | # 10 | # You can also pass :class => 'some_css_class' ('sparkline' by default). 11 | def sparkline_tag(results=[], options={}) 12 | url = { :controller => 'sparklines' } 13 | url[:results] = results.join(',') unless results.nil? 14 | options = url.merge(options) 15 | attributes = %(class="#{options[:class] || 'sparkline'}" alt="Sparkline Graph" ) 16 | attributes << %(title="#{options[:title]}" ) if options[:title] 17 | 18 | attributes << %(width="#{options[:width]}" ) if options[:width] 19 | attributes << %(height="#{options[:height]}" ) if options[:height] 20 | 21 | # prefer to use a "data" URL scheme as described in {RFC 2397}[http://www.ietf.org/rfc/rfc2397.txt] 22 | # data_url = "data:image/png;base64,#{Base64.encode64(Sparklines.plot(results,options))}" 23 | # tag = %() 24 | 25 | # # respect some limits noted in RFC 2397 since the data: url is supposed to be 'short' 26 | # data_url.length <= 1024 && tag.length <= 2100 ? tag : 27 | %() 28 | 29 | end 30 | 31 | end 32 | -------------------------------------------------------------------------------- /Manifest.txt: -------------------------------------------------------------------------------- 1 | History.txt 2 | MIT-LICENSE 3 | Manifest.txt 4 | README.txt 5 | Rakefile 6 | lib/sparklines.rb 7 | lib/sparklines_helper.rb 8 | samples/area-high.png 9 | samples/area.png 10 | samples/discrete.png 11 | samples/pie-large.png 12 | samples/pie.png 13 | samples/pie0.png 14 | samples/pie1.png 15 | samples/pie100.png 16 | samples/pie45.png 17 | samples/pie95.png 18 | samples/pie99.png 19 | samples/smooth-colored.png 20 | samples/smooth.png 21 | test/expected/area.png 22 | test/expected/area_high.png 23 | test/expected/area_min_max.png 24 | test/expected/bar.png 25 | test/expected/bar_extreme_values.png 26 | test/expected/bar_string.png.png 27 | test/expected/bar_tall.png 28 | test/expected/bar_wide.png 29 | test/expected/bullet_basic.png 30 | test/expected/bullet_colorful.png 31 | test/expected/bullet_full_featured.png 32 | test/expected/bullet_tall.png 33 | test/expected/bullet_wide.png 34 | test/expected/discrete.png 35 | test/expected/discrete_wide.png 36 | test/expected/error.png 37 | test/expected/labeled_area.png 38 | test/expected/labeled_bar.png 39 | test/expected/labeled_discrete.png 40 | test/expected/labeled_pie.png 41 | test/expected/labeled_smooth.png 42 | test/expected/labeled_whisker_decimals.png 43 | test/expected/pie.png 44 | test/expected/pie0.png 45 | test/expected/pie1.png 46 | test/expected/pie100.png 47 | test/expected/pie45.png 48 | test/expected/pie95.png 49 | test/expected/pie99.png 50 | test/expected/pie_flat.png 51 | test/expected/pie_large.png 52 | test/expected/smooth.png 53 | test/expected/smooth_colored.png 54 | test/expected/smooth_similar_nonzero_values.png 55 | test/expected/smooth_underneath_color.png 56 | test/expected/smooth_with_target.png 57 | test/expected/standard_deviation.png 58 | test/expected/standard_deviation_short.png 59 | test/expected/standard_deviation_tall.png 60 | test/expected/whisker.png 61 | test/expected/whisker_junk.png 62 | test/expected/whisker_non_exceptional.png 63 | test/expected/whisker_with_step.png 64 | test/expected/zeros.png 65 | test/test_all.rb 66 | -------------------------------------------------------------------------------- /History.txt: -------------------------------------------------------------------------------- 1 | == 0.5.4 2 | 3 | * Support disabling last point when label is shown [Justin Love] 4 | * Handle missing data (nil) values in area, bar, discrete, smooth [Justin Love] 5 | * Extract method: normalize [Justin Love] 6 | * Support :min and :max options to set graph scale [Justin Love] 7 | 8 | == 0.5.2 9 | 10 | * Handle case where all data is zero [Silvia Pfeiffer] 11 | 12 | == 0.5.1 13 | 14 | * Fixed drawing and normalization of bar graph 15 | * Added :target and :target_color to bar graph (draws a horizontal line at a value) 16 | 17 | == 0.5.0 18 | 19 | * Documentation for bullet graph. 20 | * More bullet options for good_color, satisfactory_color, and bad_color. 21 | * Smooth options for target line value and target_color. 22 | * Sparklinks.plot_to_image returns a Magick::Image object for further manipulation. 23 | * Step option for wider whisker. 24 | 25 | == 0.4.8 26 | 27 | * Added bullet graph. See http://en.wikipedia.org/wiki/Bullet_graph or Stephen Few's book _Information Dashboard Design_. 28 | 29 | == 0.4.7 30 | 31 | * Improved normalization for better display of close, high-values. [Matt Van Horn] 32 | * Improved rendering of closed polyline on smooth graph with undercolor. Far left, right, and bottom of polyline are drawn offscreen so they don't show in the visible graph. 33 | 34 | == 0.4.6 35 | 36 | * Added :underneath_color option to smooth sparkline. [Cory Forsyth] 37 | 38 | == 0.4.5 39 | 40 | * Several fixes by Rob Biedenharn 41 | * lib/sparklines_helper.rb: Include example code for generating "data:" URLs (but leave it commented out due to lack of widespread testing) 42 | * lib/sparklines_helper.rb: allow results to be passed in the options hash (i.e., sparkline_tag(nil, :results => [ ... ], ...)) 43 | * lib/sparklines.rb: Move calculation of whisker endpoints out of loop 44 | * lib/sparklines.rb: Fix fencepost bug in whisker that caused plot to be 1 pixel short 45 | 46 | == 0.4.4 47 | 48 | * Fixed stddev rounding bug [Andrew Nutter-Upham] 49 | 50 | == 0.4.3 51 | 52 | * Minor change for use with Hoe. 53 | 54 | == 0.4.2 55 | 56 | * Added standard deviation bars [Andrew Nutter-Upham] 57 | 58 | == 0.4.1 59 | 60 | * Converted to Hoe for rakefile 61 | * Added whisker graph [Luke Francl] 62 | * General cleanup and bug fixes 63 | * Experimental label option 64 | 65 | == 0.3.0 66 | 67 | * Changed to a Class for maintainability 68 | * All values are normalized (except pie) 69 | * A single value can be passed for the pie percentage (instead of an Array) 70 | 71 | == 0.2.7 72 | 73 | * Fixed bug where last element of bar graph wouldn't go to the bottom [Esad Hajdarevic esad@esse.at] 74 | 75 | == 0.2.5 76 | 77 | * Tests now use Test::Unit 78 | * Bar type added 79 | 80 | == 0.2.1 81 | 82 | * Added line_color option for smooth graphs 83 | * Now available as a gem ('gem install sparklines') and as a rails generator ('gem install sparklines_generator') 84 | 85 | 86 | -------------------------------------------------------------------------------- /sparklines.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.name = %q{sparklines} 3 | s.version = "0.5.3" 4 | 5 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 6 | s.authors = ["Geoffrey Grosenbach"] 7 | s.date = %q{2008-08-20} 8 | s.description = %q{Tiny graphs.} 9 | s.email = %q{boss@topfunky.com} 10 | s.extra_rdoc_files = ["History.txt", "Manifest.txt", "README.txt"] 11 | s.files = ["History.txt", "MIT-LICENSE", "Manifest.txt", "README.txt", "Rakefile", "lib/sparklines.rb", "lib/sparklines_helper.rb", "samples/area-high.png", "samples/area.png", "samples/discrete.png", "samples/pie-large.png", "samples/pie.png", "samples/pie0.png", "samples/pie1.png", "samples/pie100.png", "samples/pie45.png", "samples/pie95.png", "samples/pie99.png", "samples/smooth-colored.png", "samples/smooth.png", "test/expected/area.png", "test/expected/area_high.png", "test/expected/area_min_max.png", "test/expected/bar.png", "test/expected/bar_extreme_values.png", "test/expected/bar_string.png.png", "test/expected/bar_tall.png", "test/expected/bar_wide.png", "test/expected/bullet_basic.png", "test/expected/bullet_colorful.png", "test/expected/bullet_full_featured.png", "test/expected/bullet_tall.png", "test/expected/bullet_wide.png", "test/expected/discrete.png", "test/expected/discrete_wide.png", "test/expected/error.png", "test/expected/labeled_area.png", "test/expected/labeled_bar.png", "test/expected/labeled_discrete.png", "test/expected/labeled_pie.png", "test/expected/labeled_smooth.png", "test/expected/labeled_whisker_decimals.png", "test/expected/pie.png", "test/expected/pie0.png", "test/expected/pie1.png", "test/expected/pie100.png", "test/expected/pie45.png", "test/expected/pie95.png", "test/expected/pie99.png", "test/expected/pie_flat.png", "test/expected/pie_large.png", "test/expected/smooth.png", "test/expected/smooth_colored.png", "test/expected/smooth_similar_nonzero_values.png", "test/expected/smooth_underneath_color.png", "test/expected/smooth_with_target.png", "test/expected/standard_deviation.png", "test/expected/standard_deviation_short.png", "test/expected/standard_deviation_tall.png", "test/expected/whisker.png", "test/expected/whisker_junk.png", "test/expected/whisker_non_exceptional.png", "test/expected/whisker_with_step.png", "test/expected/zeros.png", "test/test_all.rb"] 12 | s.has_rdoc = true 13 | s.homepage = %q{http://nubyonrails.com/pages/sparklines} 14 | s.rdoc_options = ["--main", "README.txt"] 15 | s.require_paths = ["lib"] 16 | s.rubyforge_project = %q{sparklines} 17 | s.rubygems_version = %q{1.2.0} 18 | s.summary = %q{Tiny graphs.} 19 | s.test_files = ["test/test_all.rb"] 20 | 21 | if s.respond_to? :specification_version then 22 | current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION 23 | s.specification_version = 2 24 | 25 | if current_version >= 3 then 26 | s.add_development_dependency(%q, [">= 1.7.0"]) 27 | else 28 | s.add_dependency(%q, [">= 1.7.0"]) 29 | end 30 | else 31 | s.add_dependency(%q, [">= 1.7.0"]) 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /test/test_all.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | 3 | require 'test/unit' 4 | require 'lib/sparklines' 5 | require 'fileutils' 6 | require 'tidy_table' 7 | require 'dust' 8 | 9 | class SparklinesTest < Test::Unit::TestCase 10 | 11 | def setup 12 | @output_dir = "test/actual" 13 | FileUtils.mkdir_p(@output_dir) 14 | 15 | @data = %w( 1 5 15 20 30 50 57 58 55 48 16 | 44 43 42 42 46 48 49 53 55 59 17 | 60 65 75 90 105 106 107 110 115 120 18 | 115 120 130 140 150 160 170 100 100 10).map {|i| i.to_f} 19 | @nil_data = @data.dup 20 | [5, 10, 11, 20, 25, 26, 27].each {|i| @nil_data[i] = nil} 21 | @constant_data = [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1] 22 | end 23 | 24 | test "basic bullet" do 25 | Sparklines.plot_to_file("#{@output_dir}/bullet_basic.png", 85, { 26 | :type => "bullet", 27 | :target => 80, 28 | :good => 100, 29 | :height => 15 30 | }) 31 | end 32 | 33 | test "full-featured bullet" do 34 | Sparklines.plot_to_file("#{@output_dir}/bullet_full_featured.png", 85, { 35 | :type => "bullet", 36 | :target => 90, 37 | :bad => 60, 38 | :satisfactory => 80, 39 | :good => 100, 40 | :height => 15 41 | }) 42 | end 43 | 44 | test "colorful bullet" do 45 | Sparklines.plot_to_file("#{@output_dir}/bullet_colorful.png", 85, { 46 | :type => "bullet", 47 | :target => 90, 48 | :bad => 60, 49 | :satisfactory => 80, 50 | :good => 100, 51 | :height => 15, 52 | :bad_color => '#c3e3bf', 53 | :satisfactory_color => '#96cf90', 54 | :good_color => "#6ab162" 55 | }) 56 | end 57 | 58 | test "tall bullet" do 59 | Sparklines.plot_to_file("#{@output_dir}/bullet_tall.png", 85, { 60 | :type => "bullet", 61 | :target => 90, 62 | :bad => 60, 63 | :satisfactory => 80, 64 | :good => 100, 65 | :height => 30 66 | }) 67 | end 68 | 69 | test "wide bullet" do 70 | Sparklines.plot_to_file("#{@output_dir}/bullet_wide.png", 85, { 71 | :type => "bullet", 72 | :target => 90, 73 | :bad => 60, 74 | :satisfactory => 80, 75 | :good => 100, 76 | :height => 15, 77 | :width => 200 78 | }) 79 | end 80 | 81 | test "smooth with target" do 82 | quick_graph("smooth_with_target", { 83 | :type => "smooth", 84 | :target => 50, 85 | :target_color => '#999999', 86 | :line_color => "#6699cc", 87 | :underneath_color => "#ebf3f6" 88 | }) 89 | end 90 | 91 | test "whisker with step" do 92 | quick_graph("whisker_with_step", { 93 | :type => "whisker", 94 | :step => 5 95 | }) 96 | end 97 | 98 | def test_each_graph 99 | %w{pie area discrete smooth bar}.each do |type| 100 | quick_graph("#{type}", :type => type) 101 | end 102 | end 103 | 104 | def test_each_graph_with_label 105 | %w{pie area discrete smooth bar}.each do |type| 106 | quick_graph("labeled_#{type}", :type => type, :label => 'Glucose') 107 | end 108 | end 109 | 110 | test "bar graph with sprintf for label" do 111 | quick_graph("labeled_bar_sprintf", :type => "bar", :label => "Formatted", :label_format => "%i") 112 | end 113 | 114 | def test_each_graph_with_nil 115 | %w{pie area discrete smooth bar}.each do |type| 116 | nil_graph("nil_#{type}", :type => type, :label => 'Glucose') 117 | end 118 | end 119 | 120 | def test_smooth_graph_with_target_and_constant_data 121 | constant_graph("constant_smooth", :type => 'smooth', :label => 'a-label', :target => 1) 122 | end 123 | 124 | def test_whisker_decimals 125 | @data = (1..200).map {|n| n.to_f/100 } 126 | quick_graph("labeled_whisker_decimals", { 127 | :height => 30, 128 | :type => 'smooth', 129 | :label => 'png' 130 | }) 131 | end 132 | 133 | def test_whisker_random 134 | # Need data ranging from -2 to +2 135 | @data = (1..40).map { |i| rand(3) * (rand(2) == 1 ? -1 : 1) } 136 | quick_graph("whisker", :type => 'whisker') 137 | end 138 | 139 | def test_whisker_non_exceptional 140 | @data = [1,1,1,1,1,1,1,1,1,1,1,1,1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1] 141 | quick_graph("whisker_non_exceptional", :type => 'whisker') 142 | end 143 | 144 | ## 145 | # Send random values in the range (-9..9) 146 | 147 | def test_whisker_junk 148 | @data = (1..40).map { |i| rand(10) * (rand(2) == 1 ? -1 : 1) } 149 | quick_graph("whisker_junk", :type => 'whisker') 150 | end 151 | 152 | def test_pie 153 | # Test extremes which previously did not work right 154 | [0, 1, 45, 95, 99, 100].each do |value| 155 | Sparklines.plot_to_file("#{@output_dir}/pie#{value}.png", 156 | value, { 157 | :type => 'pie', 158 | :diameter => 128 159 | }) 160 | end 161 | Sparklines.plot_to_file("#{@output_dir}/pie_flat.png", 162 | [60], 163 | { 164 | :type => 'pie' 165 | }) 166 | end 167 | 168 | def test_special_conditions 169 | tests = { 'smooth_colored' => 170 | { 171 | :type => 'smooth', 172 | :line_color => 'purple' 173 | }, 174 | 'labeled_smooth_without_last' => 175 | { 176 | :type => 'smooth', 177 | :label => 'Glucose', 178 | :has_last => false 179 | }, 180 | 'pie_large' => { 181 | :type => 'pie', 182 | :diameter => 200 183 | }, 184 | 'area_high' => { 185 | :type => 'area', 186 | :upper => 80, 187 | :step => 4, 188 | :height => 20 189 | }, 190 | 'discrete_wide' => { 191 | :type => 'discrete', 192 | :step => 8 193 | }, 194 | 'discrete_max' => { 195 | :type => 'discrete', 196 | :max => @data.max * 4 197 | }, 198 | 'discrete_min' => { 199 | :type => 'discrete', 200 | :min => @data.max * -4 201 | }, 202 | 'bar_wide' => { 203 | :type => 'bar', 204 | :step => 8 205 | }, 206 | 'bar_tall' => { 207 | :type => 'bar', 208 | :below_color => 'blue', 209 | :above_color => 'red', 210 | :upper => 90, 211 | :height => 50, 212 | :step => 8 213 | } 214 | } 215 | tests.each do |name, options| 216 | quick_graph(name, options) 217 | end 218 | end 219 | 220 | def test_bar_extreme_values 221 | Sparklines.plot_to_file("#{@output_dir}/bar_extreme_values.png", 222 | [0,1,100,2,99,3,98,4,97,5,96,6,95,7,94,8,93,9,92,10,91], { 223 | :type => 'bar', 224 | :below_color => 'blue', 225 | :above_color => 'red', 226 | :upper => 90, 227 | :step => 4 228 | }) 229 | end 230 | 231 | def test_string_args 232 | quick_graph("bar_string.png", { 233 | 'type' => 'bar', 234 | 'below_color' => 'blue', 235 | 'above_color' => 'red', 236 | 'upper' => 50, 237 | 'height' => 50, 238 | 'step' => 8 239 | }) 240 | end 241 | 242 | def test_area_min_max 243 | quick_graph("area_min_max", { 244 | :has_min => true, 245 | :has_max => true, 246 | :has_first => true, 247 | :has_last => true 248 | }) 249 | end 250 | 251 | def test_smooth_underneath_color 252 | quick_graph("smooth_underneath_color", { 253 | :type => 'smooth', 254 | :line_color => "#6699cc", 255 | :underneath_color => "#ebf3f6" 256 | }) 257 | end 258 | 259 | def test_close_values 260 | Sparklines.plot_to_file("#{@output_dir}/smooth_similar_nonzero_values.png", [100, 90, 95, 99, 80, 90], { 261 | :type => 'smooth', 262 | :line_color => "#6699cc", 263 | :underneath_color => "#ebf3f6" 264 | }) 265 | end 266 | 267 | def test_no_type 268 | Sparklines.plot_to_file("#{@output_dir}/error.png", 0, :type => 'nonexistent') 269 | end 270 | 271 | def test_standard_deviation 272 | quick_graph('standard_deviation', { 273 | :type => 'smooth', 274 | :height => 100, 275 | :line_color => '#666', 276 | :has_std_dev => true, 277 | :std_dev_color => '#cccccc' 278 | }) 279 | end 280 | 281 | def test_standard_deviation_tall 282 | quick_graph('standard_deviation_tall', { 283 | :type => 'smooth', 284 | :height => 300, 285 | :line_color => '#666', 286 | :has_std_dev => true, 287 | :std_dev_color => '#cccccc' 288 | }) 289 | end 290 | 291 | def test_standard_deviation_short 292 | quick_graph('standard_deviation_short', { 293 | :type => 'smooth', 294 | :height => 20, 295 | :line_color => '#666', 296 | :has_std_dev => true, 297 | :std_dev_color => '#cccccc' 298 | }) 299 | end 300 | 301 | def test_zeros 302 | @data = [0,0,0,0] 303 | quick_graph('zeros', { 304 | :type => 'smooth', 305 | }) 306 | end 307 | 308 | private 309 | 310 | def quick_graph(name, options) 311 | Sparklines.plot_to_file("#{@output_dir}/#{name}.png", @data, options) 312 | end 313 | 314 | def nil_graph(name, options) 315 | Sparklines.plot_to_file("#{@output_dir}/#{name}.png", @nil_data, options) 316 | end 317 | 318 | def constant_graph(name, options) 319 | Sparklines.plot_to_file("#{@output_dir}/#{name}.png", @constant_data, options) 320 | end 321 | 322 | end 323 | 324 | # HACK Make reference HTML file for viewing output 325 | END { 326 | def image_tag(image_path) 327 | %() 328 | end 329 | 330 | reference_files = Dir['test/expected/*'] 331 | output = TidyTable.new(reference_files).to_html(%w(Expected Actual)) do |record| 332 | [image_tag(record), image_tag(record.gsub('expected', 'actual'))] 333 | end 334 | FileUtils.mkdir_p("test/actual") 335 | File.open("test/output.html", "w") do |f| 336 | f.write <<-EOL 337 | 348 | EOL 349 | f.write output 350 | end 351 | 352 | # TODO: Squawk if there are files in actual that aren't in expected 353 | } 354 | -------------------------------------------------------------------------------- /lib/sparklines.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'rubygems' 3 | require 'rmagick' 4 | 5 | =begin rdoc 6 | 7 | A library for generating small unmarked graphs (sparklines). 8 | 9 | Can be used to write an image to a file or make a web service with Rails or other Ruby CGI apps. 10 | 11 | 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 | Requires the RMagick image library. 14 | 15 | ==Authors 16 | 17 | {Dan Nugent}[mailto:nugend@gmail.com] Original port from Python Sparklines library. 18 | 19 | {Geoffrey Grosenbach}[mailto:boss@topfunky.com] -- http://nubyonrails.topfunky.com 20 | -- Conversion to module and further maintenance. 21 | 22 | ==General Usage and Defaults 23 | 24 | To use in a script: 25 | 26 | require 'rubygems' 27 | require 'sparklines' 28 | Sparklines.plot([1,25,33,46,89,90,85,77,42], 29 | :type => 'discrete', 30 | :height => 20) 31 | 32 | An image blob will be returned which you can print, write to STDOUT, etc. 33 | 34 | For use with Ruby on Rails, see the sparklines plugin: 35 | 36 | http://nubyonrails.com/pages/sparklines 37 | 38 | In your view, call it like this: 39 | 40 | <%= sparkline_tag [1,2,3,4,5,6] %> 41 | 42 | Or specify details: 43 | 44 | <%= sparkline_tag [1,2,3,4,5,6], 45 | :type => 'discrete', 46 | :height => 10, 47 | :upper => 80, 48 | :above_color => 'green', 49 | :below_color => 'blue' %> 50 | 51 | Graph types: 52 | 53 | area 54 | discrete 55 | pie 56 | smooth 57 | bar 58 | bullet 59 | whisker 60 | 61 | General Defaults: 62 | 63 | :type => 'smooth' 64 | :height => 14px 65 | :upper => 50 66 | :above_color => 'red' 67 | :below_color => 'grey' 68 | :background_color => 'white' 69 | :line_color => 'lightgrey' 70 | :label => name_of_label_after_graph 71 | :label_format => sprintf_string_to_format_label 72 | 73 | ==License 74 | 75 | Licensed under the MIT license. 76 | 77 | =end 78 | class Sparklines 79 | 80 | VERSION = '0.5.3' 81 | 82 | @@label_margin = 5.0 83 | @@pointsize = 10.0 84 | 85 | class << self 86 | 87 | ## 88 | # Plots a sparkline and returns a Magic::Image object. 89 | 90 | def plot_to_image(data=[], options={}) 91 | defaults = { 92 | :type => 'smooth', 93 | :height => 14, 94 | :upper => 50, 95 | :diameter => 20, 96 | :step => 2, 97 | :line_color => 'lightgrey', 98 | 99 | :above_color => 'red', 100 | :below_color => 'grey', 101 | :background_color => 'white', 102 | :share_color => 'red', 103 | :remain_color => 'lightgrey', 104 | :min_color => 'blue', 105 | :max_color => 'green', 106 | :last_color => 'red', 107 | :std_dev_color => '#efefef', 108 | 109 | :min => nil, 110 | :max => nil, 111 | :has_min => false, 112 | :has_max => false, 113 | :has_last => nil, 114 | :has_std_dev => false, 115 | 116 | :label => nil 117 | } 118 | 119 | # HACK for HashWithIndifferentAccess 120 | options_sym = Hash.new 121 | options.keys.each do |key| 122 | options_sym[key.to_sym] = options[key] 123 | end 124 | 125 | options_sym = defaults.merge(options_sym) 126 | 127 | # Call the appropriate method for actual plotting. 128 | sparkline = self.new(data, options_sym) 129 | if %w(area bar bullet pie smooth discrete whisker).include? options_sym[:type] 130 | sparkline.send options_sym[:type] 131 | else 132 | sparkline.plot_error options_sym 133 | end 134 | end 135 | 136 | ## 137 | # Does the actual plotting of the graph. 138 | # Calls the appropriate subclass based on the :type argument. 139 | # Defaults to 'smooth'. 140 | # 141 | # Returns a blob. 142 | 143 | def plot(data=[], options={}) 144 | plot_to_image(data, options).to_blob 145 | end 146 | 147 | ## 148 | # Writes a graph to disk with the specified filename, or "sparklines.png". 149 | 150 | def plot_to_file(filename="sparklines.png", data=[], options={}) 151 | File.open( filename, 'wb' ) do |png| 152 | png << self.plot( data, options) 153 | end 154 | end 155 | 156 | end # class methods 157 | 158 | def initialize(data=[], options={}) 159 | @data = Array(data) 160 | @options = options 161 | normalize_data 162 | end 163 | 164 | ## 165 | # Creates a continuous area sparkline. Relevant options. 166 | # 167 | # :step - An integer that determines the distance between each point on the sparkline. Defaults to 2. 168 | # 169 | # :height - An integer that determines what the height of the sparkline will be. Defaults to 14 170 | # 171 | # :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. 172 | # 173 | # :has_min - Determines whether a dot will be drawn at the lowest value or not. Defaults to false. 174 | # 175 | # :has_max - Determines whether a dot will be drawn at the highest value or not. Defaults to false. 176 | # 177 | # :has_last - Determines whether a dot will be drawn at the last value or not. Defaults to false. 178 | # 179 | # :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. 180 | # 181 | # :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. 182 | # 183 | # :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. 184 | # 185 | # :above_color - A string or color code representing the color to draw values above or equal the upper value. Defaults to red. 186 | # 187 | # :below_color - A string or color code representing the color to draw values below the upper value. Defaults to gray. 188 | 189 | def area 190 | 191 | step = @options[:step].to_f 192 | height = @options[:height].to_f 193 | background_color = @options[:background_color] 194 | 195 | create_canvas((@norm_data.size - 1) * step + 4, height, background_color) 196 | 197 | upper = @options[:upper].to_f 198 | 199 | has_min = @options[:has_min] 200 | has_max = @options[:has_max] 201 | has_last = @options[:has_last] 202 | 203 | min_color = @options[:min_color] 204 | max_color = @options[:max_color] 205 | last_color = @options[:last_color] 206 | below_color = @options[:below_color] 207 | above_color = @options[:above_color] 208 | 209 | 210 | coords = [[0,(height - 3 - upper/(101.0/(height-4)))]] 211 | i=0 212 | @norm_data.each do |r| 213 | coords.push [(2 + i), (height - 3 - r/(101.0/(height-4)))] if r 214 | i += step 215 | end 216 | coords.push [(@norm_data.size - 1) * step + 4, (height - 3 - upper/(101.0/(height-4)))] 217 | 218 | # TODO Refactor! Should take a block and do both. 219 | # 220 | # Block off the bottom half of the image and draw the sparkline 221 | @draw.fill(above_color) 222 | @draw.define_clip_path('top') do 223 | @draw.rectangle(0,0,(@norm_data.size - 1) * step + 4,(height - 3 - upper/(101.0/(height-4)))) 224 | end 225 | @draw.clip_path('top') 226 | @draw.polygon(*coords.flatten) 227 | 228 | # Block off the top half of the image and draw the sparkline 229 | @draw.fill(below_color) 230 | @draw.define_clip_path('bottom') do 231 | @draw.rectangle(0,(height - 3 - upper/(101.0/(height-4))),(@norm_data.size - 1) * step + 4,height) 232 | end 233 | @draw.clip_path('bottom') 234 | @draw.polygon(*coords.flatten) 235 | 236 | # The sparkline looks kinda nasty if either the above_color or below_color gets the center line 237 | @draw.fill('black') 238 | @draw.line(0,(height - 3 - upper/(101.0/(height-4))),(@norm_data.size - 1) * step + 4,(height - 3 - upper/(101.0/(height-4)))) 239 | 240 | # After the parts have been masked, we need to let the whole canvas be drawable again 241 | # so a max dot can be displayed 242 | @draw.define_clip_path('all') do 243 | @draw.rectangle(0,0,@canvas.columns,@canvas.rows) 244 | end 245 | @draw.clip_path('all') 246 | 247 | drawbox(coords[@norm_data.index(@norm_data.compact.min)+1], 1, min_color) if has_min == true 248 | drawbox(coords[@norm_data.index(@norm_data.compact.max)+1], 1, max_color) if has_max == true 249 | 250 | drawbox(coords[-2], 1, last_color) if has_last == true 251 | 252 | @draw.draw(@canvas) 253 | @canvas 254 | end 255 | 256 | 257 | ## 258 | # A bar graph. 259 | # 260 | # Also takes :target option (a line will be drawn) and :upper (values 261 | # under will be drawn in :below_color, above in :above_color). 262 | 263 | def bar 264 | step = @options[:step].to_f 265 | height = @options[:height].to_f 266 | width = ((@norm_data.size - 1) * step).to_f 267 | background_color = @options[:background_color] 268 | 269 | create_canvas(@norm_data.length * step + 2, height, background_color) 270 | 271 | upper = @options[:upper].to_f 272 | below_color = @options[:below_color] 273 | above_color = @options[:above_color] 274 | 275 | target = @options.has_key?(:target) ? @options[:target].to_f : nil 276 | target_color = @options[:target_color] || 'white' 277 | 278 | i = 1 279 | # raise @norm_data.to_yaml 280 | max_normalized = normalize(@maximum_value) 281 | @norm_data.each_with_index do |r, index| 282 | color = ((@data[index] || @minimum_value) >= upper) ? above_color : below_color 283 | @draw.stroke('transparent') 284 | @draw.fill(color) 285 | bar_height_from_top = @canvas.rows - ( (r.to_f / max_normalized.to_f) * @canvas.rows) 286 | @draw.rectangle( i, @canvas.rows, i + step - 2, bar_height_from_top ) 287 | i += step 288 | end 289 | 290 | unless target.nil? 291 | adjusted_target_value = (height - 3 - normalize(target)/(101.0/(height-4))).to_i 292 | @draw.stroke(target_color) 293 | open_ended_polyline([[-5, adjusted_target_value], [width + 5, adjusted_target_value]]) 294 | end 295 | 296 | @draw.draw(@canvas) 297 | @canvas 298 | end 299 | 300 | 301 | ## 302 | # Creates a discretized sparkline 303 | # 304 | # :height - An integer that determines what the height of the sparkline will be. Defaults to 14 305 | # 306 | # :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. 307 | # 308 | # :above_color - A string or color code representing the color to draw values above or equal the upper value. Defaults to red. 309 | # 310 | # :below_color - A string or color code representing the color to draw values below the upper value. Defaults to gray. 311 | 312 | def discrete 313 | 314 | height = @options[:height].to_f 315 | upper = normalize(@options[:upper].to_f) 316 | background_color = @options[:background_color] 317 | step = @options[:step].to_f 318 | 319 | width = @norm_data.size * step - 1 320 | 321 | create_canvas(@norm_data.size * step - 1, height, background_color) 322 | 323 | below_color = @options[:below_color] 324 | above_color = @options[:above_color] 325 | std_dev_color = @options[:std_dev_color] 326 | 327 | drawstddevbox(width,height,std_dev_color) if @options[:has_std_dev] == true 328 | 329 | i = 0 330 | @norm_data.each do |r| 331 | if !r.nil? 332 | color = (r >= upper) ? above_color : below_color 333 | @draw.stroke(color) 334 | @draw.line(i, (@canvas.rows - r/(101.0/(height-4))-4).to_f, 335 | i, (@canvas.rows - r/(101.0/(height-4))).to_f) 336 | end 337 | i += step 338 | end 339 | 340 | @draw.draw(@canvas) 341 | @canvas 342 | end 343 | 344 | 345 | ## 346 | # Creates a pie-chart sparkline 347 | # 348 | # :diameter - An integer that determines what the size of the sparkline will be. Defaults to 20 349 | # 350 | # :share_color - A string or color code representing the color to draw the share of the pie represented by percent. Defaults to red. 351 | # 352 | # :remain_color - A string or color code representing the color to draw the pie not taken by the share color. Defaults to lightgrey. 353 | 354 | def pie 355 | diameter = @options[:diameter].to_f 356 | background_color = @options[:background_color] 357 | 358 | create_canvas(diameter, diameter, background_color) 359 | 360 | share_color = @options[:share_color] 361 | remain_color = @options[:remain_color] 362 | percent = @norm_data[0] 363 | 364 | # Adjust the radius so there's some edge left in the pie 365 | r = diameter/2.0 - 2 366 | @draw.fill(remain_color) 367 | @draw.ellipse(r + 2, r + 2, r , r , 0, 360) 368 | @draw.fill(share_color) 369 | 370 | # Special exceptions 371 | if percent == 0 372 | # For 0% return blank 373 | @draw.draw(@canvas) 374 | return @canvas 375 | elsif percent == 100 376 | # For 100% just draw a full circle 377 | @draw.ellipse(r + 2, r + 2, r , r , 0, 360) 378 | @draw.draw(@canvas) 379 | return @canvas 380 | end 381 | 382 | # Okay, this part is as confusing as hell, so pay attention: 383 | # This line determines the horizontal portion of the point on the circle where the X-Axis 384 | # should end. It's caculated by taking the center of the on-image circle and adding that 385 | # to the radius multiplied by the formula for determinig the point on a unit circle that a 386 | # angle corresponds to. 3.6 * percent gives us that angle, but it's in degrees, so we need to 387 | # convert, hence the muliplication by Pi over 180 388 | arc_end_x = r + 2 + (r * Math.cos((3.6 * percent)*(Math::PI/180))) 389 | 390 | # The same goes for here, except it's the vertical point instead of the horizontal one 391 | arc_end_y = r + 2 + (r * Math.sin((3.6 * percent)*(Math::PI/180))) 392 | 393 | # Because the SVG path format is seriously screwy, we need to set the large-arc-flag to 1 394 | # if the angle of an arc is greater than 180 degrees. I have no idea why this is, but it is. 395 | percent > 50? large_arc_flag = 1: large_arc_flag = 0 396 | 397 | # This is also confusing 398 | # M tells us to move to an absolute point on the image. We're moving to the center of the pie 399 | # h tells us to move to a relative point. We're moving to the right edge of the circle. 400 | # A tells us to start an absolute elliptical arc. The first two values are the radii of the ellipse 401 | # the third value is the x-axis-rotation (how to rotate the ellipse if we wanted to [could have some fun 402 | # with randomizing that maybe), the fourth value is our large-arc-flag, the fifth is the sweep-flag, 403 | # (again, confusing), the sixth and seventh values are the end point of the arc which we calculated previously 404 | # More info on the SVG path string format at: http://www.w3.org/TR/SVG/paths.html 405 | path = "M#{r + 2},#{r + 2} h#{r} A#{r},#{r} 0 #{large_arc_flag},1 #{arc_end_x},#{arc_end_y} z" 406 | @draw.path(path) 407 | 408 | @draw.draw(@canvas) 409 | @canvas 410 | end 411 | 412 | ## 413 | # Creates a smooth line graph sparkline. 414 | # 415 | # :step - An integer that determines the distance between each point on the sparkline. Defaults to 2. 416 | # 417 | # :height - An integer that determines what the height of the sparkline will be. Defaults to 14 418 | # 419 | # :has_min - Determines whether a dot will be drawn at the lowest value or not. Defaults to false. 420 | # 421 | # :has_max - Determines whether a dot will be drawn at the highest value or not. Defaults to false. 422 | # 423 | # :has_last - Determines whether a dot will be drawn at the last value or not. Defaults to false. 424 | # 425 | # :has_std_dev - Determines whether there will be a standard deviation bar behind the smooth graph or not. Defaults to false. 426 | # 427 | # :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. 428 | # 429 | # :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. 430 | # 431 | # :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. 432 | # 433 | # :std_dev_color - A string or color code representing the color that the standard deviation bar behind the smooth graph will be displayed as. Defaults to #efefef 434 | # 435 | # :underneath_color - A string or color code representing the color that will be used to fill in the area underneath the line. Optional. 436 | # 437 | # :target - A 1px horizontal line will be drawn at this value. Useful for showing an average. 438 | # 439 | # :target_color - Color of the target line. Defaults to white. 440 | # 441 | # :dot_values - Determines whether a dot will be drawn for all values between the first and last values. Defaults to false. 442 | # 443 | # :dot_size - The radius of the dots to be drawn if +:dot_values+ is +true+. 444 | 445 | def smooth 446 | if @options.has_key?(:dot_values) && @options[:dot_values] != false 447 | dot_size = @options.has_key?(:dot_size) ? @options[:dot_size].to_f : 2 448 | else 449 | dot_size = 0 450 | end 451 | step = @options[:step].to_f 452 | height = @options[:height].to_f + (dot_size) 453 | width = ((@norm_data.size - 1) * step + (dot_size * 2) + 2).to_f 454 | 455 | background_color = @options[:background_color] 456 | create_canvas(width, height, background_color) 457 | 458 | min_color = @options[:min_color] 459 | max_color = @options[:max_color] 460 | last_color = @options[:last_color] 461 | has_min = @options[:has_min] 462 | has_max = @options[:has_max] 463 | has_last = @options[:has_last] 464 | line_color = @options[:line_color] 465 | has_std_dev = @options[:has_std_dev] 466 | std_dev_color = @options[:std_dev_color] 467 | 468 | target = @options.has_key?(:target) ? @options[:target].to_f : nil 469 | target_color = @options[:target_color] || 'white' 470 | 471 | 472 | 473 | drawstddevbox(width,height,std_dev_color) if has_std_dev == true 474 | 475 | # @draw.stroke_antialias(false) 476 | # @draw.stroke_width = 2 477 | @draw.stroke(line_color) 478 | coords = [] 479 | i=0 480 | @norm_data.each do |r| 481 | if i == 0 482 | i += dot_size 483 | end 484 | 485 | coords.push [ i, (height - (dot_size / 2.0) - 3 - r/(101.0/(height-4-(dot_size)))) ] 486 | i += step 487 | end 488 | 489 | if @options[:underneath_color] 490 | closed_polygon(height, width, coords, dot_size) 491 | @draw.stroke(line_color) 492 | open_ended_polyline(coords) 493 | else 494 | open_ended_polyline(coords) 495 | end 496 | 497 | unless target.nil? 498 | normalized_target_value = ((target.to_f - @minimum_value)/(@maximum_value - @minimum_value)) * 100.0 499 | # adjusted_target_value = (height - 3 - normalized_target_value/(101.0/(height-4))).to_i 500 | adjusted_target_value = (height - (dot_size / 2.0) - 3 - normalized_target_value/(101.0/(height-4-(dot_size)))).to_i 501 | @draw.stroke(target_color) 502 | 503 | if @options[:dot_values] == false 504 | open_ended_polyline([[-5, adjusted_target_value], [width + 5, adjusted_target_value]]) 505 | else 506 | open_ended_polyline([[coords.first.first, adjusted_target_value], [coords.last.first, adjusted_target_value]]) 507 | end 508 | end 509 | 510 | if @options[:dot_values] != false 511 | @draw.fill('black') 512 | @draw.stroke('black') 513 | # Rails.logger.debug { "coords.length = #{coords.length}" } 514 | coords.each_with_index do |coord, i| 515 | # Rails.logger.debug { "coord = #{coord.inspect}" } 516 | @draw.ellipse(coord[0], coord[1], dot_size, dot_size, 0, 360) unless @data[i] > @maximum_value 517 | end 518 | @draw.stroke(line_color) 519 | end 520 | 521 | if @options[:dot_values] == false 522 | drawbox(coords[@norm_data.index(@norm_data.min)], 2, min_color) if has_min == true 523 | drawbox(coords[@norm_data.index(@norm_data.max)], 2, max_color) if has_max == true 524 | drawbox(coords[-1], 2, last_color) if has_last == true 525 | else 526 | draw_circle(coords[@norm_data.index(@norm_data.min)], dot_size, min_color) if has_min == true 527 | draw_circle(coords[@norm_data.index(@norm_data.max)], dot_size, max_color) if has_max == true 528 | draw_circle(coords[-1], dot_size, last_color) if has_last == true 529 | end 530 | 531 | @draw.draw(@canvas) 532 | @canvas 533 | end 534 | 535 | 536 | ## 537 | # Creates a whisker sparkline to track on/off type data. There are five states: 538 | # on, off, no value, exceptional on, exceptional off. On values create an up 539 | # whisker and off values create a down whisker. Exceptional values may be 540 | # colored differently than regular values to indicate, for example, a shut out. 541 | # No value produces an empty row to indicate a tie. 542 | # 543 | # * results - an array of integer values between -2 and 2. -2 is exceptional 544 | # down, -1 is regular down, 0 is no value, 1 is up, and 2 is exceptional up. 545 | # * options - a hash that takes parameters 546 | # 547 | # :height - height of the sparkline 548 | # 549 | # :whisker_color - the color of regular whiskers; defaults to black 550 | # 551 | # :exception_color - the color of exceptional whiskers; defaults to red 552 | # 553 | # :step - Spacing for whiskers. Includes the whisker itself. Default 2. 554 | 555 | def whisker 556 | 557 | step = @options[:step].to_i 558 | height = @options[:height].to_f 559 | background_color = @options[:background_color] 560 | 561 | create_canvas(@data.size * step - 1, height, background_color) 562 | 563 | whisker_color = @options[:whisker_color] || 'black' 564 | exception_color = @options[:exception_color] || 'red' 565 | 566 | on_row = (@canvas.rows/2.0 - 1).ceil 567 | off_row = (@canvas.rows/2.0).floor 568 | i = 0 569 | @data.each do |r| 570 | color = whisker_color 571 | 572 | if ( (r == 2 || r == -2) && exception_color ) 573 | color = exception_color 574 | end 575 | 576 | y_mid_point = (r >= 1) ? on_row : off_row 577 | 578 | y_end_point = y_mid_point 579 | if ( r > 0) 580 | y_end_point = 0 581 | end 582 | 583 | if ( r < 0 ) 584 | y_end_point = @canvas.rows 585 | end 586 | 587 | @draw.stroke( color ) 588 | @draw.line( i, y_mid_point, i, y_end_point ) 589 | i += step 590 | end 591 | 592 | @draw.draw(@canvas) 593 | @canvas 594 | end 595 | 596 | ## 597 | # A bullet graph, a la Stephen Few in "Information Dashboard Design." 598 | # 599 | # * data - A single value for the thermometer part of the bullet. 600 | # Represents the current value. 601 | # * options - a hash 602 | # 603 | # :good - Numeric. Maximum value that will be shown on the graph. Required. 604 | # 605 | # :height - Numeric. Defaults to 15. Should be a multiple of three. 606 | # 607 | # :width - This graph expands to any specified width. Defaults to 100. 608 | # 609 | # :bad - Numeric. A darker shade background will be drawn up to this point. 610 | # 611 | # :satisfactory - Numeric. A medium background will be drawn up to this point. 612 | # 613 | # :target - Numeric value. A thin vertical bar will be drawn. 614 | # 615 | # :good_color - Color for the rightmost section of the bullet. 616 | # 617 | # :satisfactory_color - Color for the middle background of the bullet. 618 | # 619 | # :bad_color - Color for the lowest, leftmost section. 620 | 621 | def bullet 622 | height = @options[:height].to_f 623 | @graph_width = @options.has_key?(:width) ? @options[:width].to_f : 100.0 624 | good_color = @options.has_key?(:good_color) ? @options[:good_color] : '#eeeeee' 625 | satisfactory_color = @options.has_key?(:satisfactory_color) ? @options[:satisfactory_color] : '#bbbbbb' 626 | bad_color = @options.has_key?(:bad_color) ? @options[:bad_color] : '#999999' 627 | bullet_color = @options.has_key?(:bullet_color) ? @options[:bullet_color] : 'black' 628 | @thickness = height/3.0 629 | 630 | create_canvas(@graph_width, height, good_color) 631 | 632 | @value = @norm_data 633 | @good_value = @options[:good].to_f 634 | 635 | @graph_height = @options[:height] 636 | 637 | qualitative_range_colors = [satisfactory_color, bad_color] 638 | [:satisfactory, :bad].each_with_index do |indicator, index| 639 | next unless @options.has_key?(indicator) 640 | @draw = @draw.fill(qualitative_range_colors[index]) 641 | indicator_width_x = @graph_width * (@options[indicator].to_f / @good_value) 642 | @draw = @draw.rectangle(0, 0, indicator_width_x.to_i, @graph_height) 643 | end 644 | 645 | if @options.has_key?(:target) 646 | @draw = @draw.fill(bullet_color) 647 | target_x = @graph_width * (@options[:target].to_f / @good_value) 648 | half_thickness = (@thickness / 2.0).to_i 649 | bar_width = 1.0 650 | @draw = @draw.rectangle(target_x.to_i, half_thickness, (target_x + bar_width).to_i, @thickness * 2 + half_thickness) 651 | end 652 | 653 | # Value 654 | @draw = @draw.fill(bullet_color) 655 | @draw = @draw.rectangle(0, @thickness.to_i, @graph_width * (@data.first.to_f / @good_value), (@thickness * 2.0).to_i) 656 | 657 | @draw.draw(@canvas) 658 | @canvas 659 | end 660 | 661 | ## 662 | # Draw the error Sparkline. 663 | 664 | def plot_error(options={}) 665 | create_canvas(40, 15, 'white') 666 | 667 | @draw.fill('red') 668 | @draw.line(0,0,40,15) 669 | @draw.line(0,15,40,0) 670 | 671 | @draw.draw(@canvas) 672 | @canvas 673 | end 674 | 675 | private 676 | 677 | def normalize_data 678 | case @options[:type].to_s 679 | when 'bar' 680 | @minimum_value = @options[:min] || 0.0 681 | else 682 | @minimum_value = @options[:min] || @data.compact.min 683 | end 684 | 685 | @maximum_value = @options[:max] || @data.compact.max 686 | 687 | case @options[:type].to_s 688 | when 'pie' 689 | @norm_data = @data 690 | when 'bullet' 691 | @norm_data = @data 692 | else 693 | @norm_data = @data.map do |value| 694 | if @maximum_value == @minimum_value 695 | value = @maximum_value 696 | else 697 | value = normalize(value) 698 | end 699 | end 700 | end 701 | end 702 | 703 | ## 704 | # :arr - an array of points (represented as two element arrays) 705 | 706 | def open_ended_polyline(arr) 707 | 0.upto(arr.length - 2) { |i| 708 | @draw.line(arr[i][0], arr[i][1], arr[i+1][0], arr[i+1][1]) 709 | } 710 | end 711 | 712 | # Fills in the area under the line (used for a smooth graph) 713 | def closed_polygon(height, width, coords, dot_size = 0) 714 | return if @options[:underneath_color].nil? 715 | 716 | list = [] 717 | 718 | list << [coords.first.first - 1 + (dot_size / 2.0), height - 3 - (dot_size / 2.0)] 719 | list << [coords.first.first - 1 + (dot_size / 2.0), coords.first.last] 720 | 721 | list << coords 722 | 723 | list << [coords.last.first + 1 - (dot_size / 2.0), coords.last.last] 724 | list << [coords.last.first + 1 - (dot_size / 2.0), height - 3 - (dot_size / 2.0)] 725 | 726 | @draw.stroke(@options[:underneath_color]) 727 | @draw.fill(@options[:underneath_color]) 728 | @draw.polygon( *list.flatten ) 729 | end 730 | 731 | ## 732 | # Create an image to draw on and a drawable to do the drawing with. 733 | # 734 | # TODO Refactor into smaller methods 735 | 736 | def create_canvas(w, h, bkg_col) 737 | @draw = Magick::Draw.new 738 | @draw.pointsize = @@pointsize # TODO Use height 739 | @draw.pointsize = @options[:font_size] if @options.has_key?(:font_size) 740 | @canvas = Magick::Image.new(w , h) { self.background_color = bkg_col } 741 | 742 | # Make room for label and last value 743 | unless @options[:label].nil? 744 | if (@options[:has_last].nil?) 745 | @options[:has_last] = true 746 | end 747 | @label_width = calculate_width(@options[:label]) 748 | @data_last_width = calculate_width(formatted_last_data_string) 749 | 750 | # TODO: Must figure out correct spacing 751 | @label_and_data_last_width = @label_width + @data_last_width + @@label_margin * 3.0 752 | w += @label_and_data_last_width 753 | end 754 | 755 | @canvas = Magick::Image.new(w , h) { self.background_color = bkg_col } 756 | @canvas.format = "PNG" 757 | 758 | # Draw label and last value 759 | unless @options[:label].nil? 760 | if ENV.has_key?('MAGICK_FONT_PATH') 761 | vera_font_path = File.expand_path('Vera.ttf', ENV['MAGICK_FONT_PATH']) 762 | @font = File.exists?(vera_font_path) ? vera_font_path : nil 763 | else 764 | @font = nil 765 | end 766 | @font = @options[:font] if @options.has_key?(:font) 767 | 768 | @draw.fill = 'black' 769 | @draw.font = @font if @font 770 | @draw.gravity = Magick::WestGravity 771 | @draw.annotate( @canvas, 772 | @label_width, 1.0, 773 | w - @label_and_data_last_width + @@label_margin, h - calculate_caps_height/2.0, 774 | @options[:label]) 775 | 776 | @draw.fill = 'red' 777 | @draw.annotate( @canvas, 778 | @data_last_width, 1.0, 779 | w - @data_last_width - @@label_margin, h - calculate_caps_height/2.0, 780 | formatted_last_data_string) 781 | end 782 | end 783 | 784 | def formatted_last_data_string 785 | if @options[:label_format] 786 | return sprintf(@options[:label_format], @data.last) 787 | end 788 | @data.last.to_s 789 | end 790 | 791 | ## 792 | # Utility to draw a coloured box 793 | # Centred on pt, offset off in each direction, fill color is col 794 | 795 | def drawbox(pt, offset, color) 796 | @draw.stroke 'transparent' 797 | @draw.fill(color) 798 | @draw.rectangle(pt[0]-offset, pt[1]-offset, pt[0]+offset, pt[1]+offset) 799 | end 800 | 801 | ## 802 | # Utility to draw a coloured circle 803 | # Centred on pt, radius of radius in each direction, fill color is color 804 | 805 | def draw_circle(pt, radius, color) 806 | # @draw.stroke('transparent') 807 | @draw.stroke(color) 808 | @draw.fill(color) 809 | @draw.ellipse(pt[0], pt[1], radius, radius, 0, 360) 810 | end 811 | 812 | 813 | ## 814 | # Utility to draw the standard deviation box 815 | # 816 | def drawstddevbox(width,height,color) 817 | mid=@norm_data.inject(0) {|sum,v| sum+=v}/@norm_data.size 818 | std_dev = standard_deviation(@norm_data) 819 | lower = (height - 3 - (mid-std_dev)/(101.0/(height-4))) 820 | upper = (height - 3 - (mid+std_dev)/(101.0/(height-4))) 821 | @draw.stroke 'transparent' 822 | @draw.fill(color) 823 | @draw.rectangle(0, lower, width, upper) 824 | end 825 | 826 | def calculate_width(text) 827 | @draw.get_type_metrics(@canvas, text.to_s).width 828 | end 829 | 830 | def calculate_caps_height 831 | @draw.get_type_metrics(@canvas, 'X').height 832 | end 833 | 834 | ## 835 | # Calculation helper for standard deviation. 836 | # 837 | # Thanks to Warren Seen 838 | # http://warrenseen.com/blog/2006/03/13/how-to-calculate-standard-deviation/ 839 | def variance(population) 840 | n = 0 841 | mean = 0.0 842 | s = 0.0 843 | population.each { |x| 844 | n = n + 1 845 | delta = x - mean 846 | mean = mean + (delta / n) 847 | s = s + delta * (x - mean) 848 | } 849 | # if you want to calculate std deviation 850 | # of a sample change this to "s / (n-1)" 851 | return s / n 852 | end 853 | 854 | ## 855 | # Calculate the standard deviation of a population 856 | # 857 | # accepts: an array, the population 858 | # returns: the standard deviation 859 | def standard_deviation(population) 860 | Math.sqrt(variance(population)) 861 | end 862 | 863 | def normalize(x) 864 | if x.nil? 865 | nil 866 | else 867 | @minimum_value == @maximum_value ? @maximum_value : 868 | (x.to_f - @minimum_value) * 100 / (@maximum_value - @minimum_value) 869 | end 870 | end 871 | 872 | end 873 | --------------------------------------------------------------------------------