@election.results unless @election.borda_result
data, labels = get_borda_points(@election.borda_result)
+ size = "400x300"
+ size = "580x300" if @election.candidates.size >= 5
+
+ if @election.candidates.size >= 5
+ marker_font_size = 17
+ else
+ marker_font_size = 20
+ end
+
graph = GruffGraff.new( :graph_type => Gruff::Bar,
:data_name => @election.name,
:data => data,
:interval_labels => labels,
+ :size => size,
:title => "Points Per Candidate",
:marker_color => '#999999',
+ :marker_font_size => marker_font_size,
:y_axis_label => "Points",
- :x_axis_label => "Candidate")
+ :x_axis_label => "Candidates")
send_data(*graph.output)
end
#Acording to Tufte, small, concomparitive, highly labeled data sets usually
@results = @election.results
@candidates = {}
@election.candidates.each {|c| @candidates[c.id] = c}
+ @names = @election.names_by_id
@sidebar_content = render_to_string :partial => 'results_sidebar'
end
def other_methods
if election_method
- @other_methods = ELECTION_TYPES.reject {|i| i == election_method}
+ @other_methods = ELECTION_TYPES.keys.reject {|i| i == election_method}
else
@other_methods = nil
end
--- /dev/null
+<table class="preftable">
+ <tr>
+ <td>Candidate</td>
+ <% @election.approval_result.points.keys.sort.each do |candidate| %>
+ <th><%=h @names[candidate] %></th>
+ <% end -%>
+ </tr>
+
+ <tr>
+ <td> Times Approved </td>
+ <% @election.approval_result.points.values.sort.each do |points| %>
+ <td><%= points %></td>
+ <% end -%>
+ </tr>
+</table>
\ No newline at end of file
<% voters = @election.voters.size %>
<% matrix = @election.condorcet_result.matrix %>
<% victories = @election.condorcet_result.victories_and_ties %>
-<% names = @election.names_by_id %>
<!-- This table shows how many times each choice was ranked above the other,
with percentages-->
<tr>
<td></td>
<% candidates.each do |candidate| -%>
- <th><%=h names[candidate] -%></th>
+ <th><%=h @names[candidate] -%></th>
<% end -%>
</tr>
<% candidates.each do |winner| -%>
<tr>
- <th><%=h names[winner] %></th>
+ <th><%=h @names[winner] %></th>
<% candidates.each do |loser| -%>
<% if winner == loser -%>
<td> -- </td>
<table class="preftable">
<% candidates.each do |victor| %>
<tr>
- <th><%=h names[victor] %></th>
+ <th><%=h @names[victor] %></th>
<% victories[victor].keys.each do |loser| %>
<% margin = victories[victor][loser]%>
- <td><%=h names[loser] %>
+ <td><%=h @names[loser] %>
<% if margin == 0%>
Tied!
<% else -%>
range that voters are allowed to express is extremely constrained:
accept or not.</p>
-</div>
\ No newline at end of file
+</div>
+
+<%= render :partial => 'approval_table' %>
\ No newline at end of file
-<div id="page-title">
+<div id="title-header">
<span class="header">Quickvote</span>
<span class="subheader">Create New QuickVote</span>
- <div style="clear:both;"></div>
</div>
<p><label for="quickvote_candidate_names">Choices</p>
<%= text_field 'quickvote', 'description', :size => 50 %></p>
<div id="advanced" style="display: none">
- <div class="plain-header">
- <span class="header">Advanced Options</span>
- <span class="subheader"></span>
+ <div class="normal-header">
+ <span class="header">Advanced Options</span>
+ <span class="subheader"></span>
</div>
<div class="clear-div"></div>
-<div id="page-title">
+<div id="title-header">
<span class="header">Quickvote</span>
<span class="subheader"><%=h @voter.election.description.capitalize %></span>
</div>
<% require 'whois/whois' %>
-<div id="page-title">
+<div id="title-header">
<span class="header">Quickvote</span>
<span class="subheader">Results</span>
</div>
<%= @election.voters.reject {|v| not v.voted? }.length %> (see below for details)
</blockquote>
-<div class="plain-header">
+<div class="normal-header">
<span class="header">Winner</span>
- <span class="subheader">The chosen method is:
- <%= @election.election_method.capitalize %></span>
+ <span class="subheader">Computed with
+ <%= ELECTION_TYPES[@election.election_method] %></span>
</div>
<div class="mainresultbox">
:object => @results[@election.election_method] %>
</div>
-<div class="plain-header">
+<div class="normal-header">
<span class="header">Other Voting Methods</span>
<span class="subheader"></span>
</div>
-<div id="page-title">
+<div id="title-header">
<span class="header">Quickvote</span>
<span class="subheader">New QuickVote Created</span>
- <div style="clear:both;"></div>
</div>
<p>Please direct voters to:</p>
-<div id="page-title">
+<div id="title-header">
<span class="header">Quickvote</span>
<span class="subheader">Vote Recorded Successfully</span>
</div>
require 'uniq_token'
require 'randarray'
-require 'gruff'
+require 'gruff-0.2.8/lib/gruff'
require 'sparklines'
require 'rubyvote'
-ELECTION_TYPES = %w(ssd plurality approval condorcet borda)
+ELECTION_TYPES = {'ssd' => "Schulze Sequential Dropping",
+ 'plurality' => "Plurality/First Past the Post",
+ 'approval' => "Approval (Top Two)",
+ 'condorcet' => "Simple Condorcet",
+ 'borda' => "Borda Count"}
class String
# alternate capitalization method that does not lowercase the rest of
--- /dev/null
+CHANGELOG
+
+== 0.2.8
+
+* New accumulator bar graph (experimental)
+* Better mini graphs
+* Bug fixes
+
+== 0.2.7
+
+* Regenerated Manifest.txt
+* Added scene sample to package
+* Added mini side_bar (EXPERIMENTAL)
+* Added @zero_degree option to Gruff::Pie so first slice can start somewhere other than 3 o'clock
+* Increased size of numbers in Gruff::Mini::Pie
+* Added legend_box_size accessor
+
+== 0.2.6
+
+* Fixed missing side_bar.rb in Manifest.txt
+
+== 0.2.5
+
+* New mini graph types (Experimental)
+* Marker lines can be different color than text labels
+* Theme definition cleanup
+
+== 0.2.4
+
+* Added option to hide line numbers
+* Fixed code that was causing warnings
+
+== 0.2.3
+
+* Cleaned up measurements so the graph expands to fill the available space
+* Added x-axis and y-axis label options
+
+== 0.1.2
+
+* minimum_value and maximum_value can be set after data() to manually scale the graph
+* Fixed infinite loop bug when values are all equal
+* Added experimental net and spider graphs
+* Added non-linear scene graph for a simple interface to complex layered graphs
+* Initial refactoring of tests
+* A host of other bug fixes
+
+== 0.0.8
+
+* NEW Sidestacked Bar Graphs. [Alun Eyre]
+* baseline_value larger than data will now show correctly. [Mike Perham]
+* hide_dots and hide_lines are now options for line graphs.
+
+== 0.0.6
+
+* Fixed hang when no data is passed.
+
+== 0.0.4
+
+* Added bar graphs
+* Added area graphs
+* Added pie graphs
+* Added render_image_background for using images as background on a theme
+* Fixed small size legend centering issue
+* Added initial line marker rounding to significant digits (Christian Winkler)
+* Line graphs line width is scaled with number of points being drawn (Christian Winkler)
+
+== 0.0.3
+
+* Added option to draw line graphs without the lines (points only), thanks to Eric Hodel
+* Removed font-minimum check so graphs look better at 300px width
+
+== 0.0.2
+
+* Fixed to_blob (thanks to Carlos Villela)
+* Added bar graphs (initial functionality...will be enhanced)
+* Removed rendered test output from gem
+
+== 0.0.1
+
+* Initial release.
+* Line graphs only. Other graph styles coming soon.
--- /dev/null
+Copyright (c) 2005 Geoffrey Grosenbach boss@topfunky.com
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
--- /dev/null
+CHANGELOG
+Manifest.txt
+MIT-LICENSE
+Rakefile
+README.txt
+assets/pc306715.jpg
+assets/city_scene/background/0000.png
+assets/city_scene/background/0600.png
+assets/city_scene/background/2000.png
+assets/city_scene/clouds/cloudy.png
+assets/city_scene/clouds/partly_cloudy.png
+assets/city_scene/clouds/stormy.png
+assets/city_scene/grass/default.png
+assets/city_scene/haze/true.png
+assets/city_scene/number_sample/1.png
+assets/city_scene/number_sample/2.png
+assets/city_scene/number_sample/default.png
+assets/city_scene/sky/0000.png
+assets/city_scene/sky/0200.png
+assets/city_scene/sky/0400.png
+assets/city_scene/sky/0600.png
+assets/city_scene/sky/0800.png
+assets/city_scene/sky/1000.png
+assets/city_scene/sky/1200.png
+assets/city_scene/sky/1400.png
+assets/city_scene/sky/1500.png
+assets/city_scene/sky/1700.png
+assets/city_scene/sky/2000.png
+lib/gruff.rb
+lib/gruff/area.rb
+lib/gruff/bar.rb
+lib/gruff/bar_conversion.rb
+lib/gruff/accumulator_bar.rb
+lib/gruff/base.rb
+lib/gruff/deprecated.rb
+lib/gruff/line.rb
+lib/gruff/net.rb
+lib/gruff/photo_bar.rb
+lib/gruff/pie.rb
+lib/gruff/scene.rb
+lib/gruff/side_bar.rb
+lib/gruff/side_stacked_bar.rb
+lib/gruff/spider.rb
+lib/gruff/stacked_bar.rb
+lib/gruff/mini/bar.rb
+lib/gruff/mini/legend.rb
+lib/gruff/mini/pie.rb
+lib/gruff/mini/side_bar.rb
+test/gruff_test_case.rb
+test/output
+test/test_area.rb
+test/test_accumulator_bar.rb
+test/test_bar.rb
+test/test_base.rb
+test/test_legend.rb
+test/test_line.rb
+test/test_mini_bar.rb
+test/test_mini_pie.rb
+test/test_mini_side_bar.rb
+test/test_net.rb
+test/test_photo.rb
+test/test_pie.rb
+test/test_scene.rb
+test/test_side_bar.rb
+test/test_sidestacked_bar.rb
+test/test_spider.rb
+test/test_stacked_bar.rb
--- /dev/null
+== Gruff Graphs
+
+A library for making beautiful graphs.
+
+See samples at http://nubyonrails.com/pages/gruff
+
+See the test suite in test/line_test.rb for examples.
+
+== Documentation
+
+Most of the documentation is in the Gruff::Base class.
+
+== WARNING
+
+This is beta-quality software. It works well according to my tests, but the API may change and other features will be added.
--- /dev/null
+require 'rubygems'
+require 'hoe'
+$:.unshift(File.dirname(__FILE__) + "/lib")
+require 'gruff'
+
+Hoe.new('Gruff', Gruff::VERSION) do |p|
+ p.name = "gruff"
+ p.author = "Geoffrey Grosenbach"
+ p.description = "Beautiful graphs for one or multiple datasets. Can be used on websites or in documents."
+ p.email = 'boss@topfunky.com'
+ p.summary = "Beautiful graphs for one or multiple datasets."
+ p.url = "http://nubyonrails.com/pages/gruff"
+ p.clean_globs = ['test/output/*.png']
+end
+
+desc "Simple test on packaged files to make sure they are all there"
+task :verify => :package do
+ # An error message will be displayed if files are missing
+ if system %(ruby -e "require 'pkg/gruff-#{Gruff::VERSION}/lib/gruff'")
+ puts "\nThe library files are present"
+ end
+end
+
+##
+# Catch unmatched tasks and run them as a unit test.
+#
+# Makes it possible to do
+#
+# rake pie
+#
+# To run the +test/test_pie+ file.
+
+rule '' do |t|
+ system "ruby test/test_#{t.name}.rb"
+end
--- /dev/null
+# Extra full path added to fix loading errors on some installations.
+
+%w(
+ base
+ area
+ bar
+ line
+ pie
+ spider
+ net
+ stacked_bar
+ side_stacked_bar
+ side_bar
+ accumulator_bar
+
+ scene
+
+ mini/legend
+ mini/bar
+ mini/pie
+ mini/side_bar
+).each do |filename|
+ require File.dirname(__FILE__) + "/gruff/#{filename}"
+end
--- /dev/null
+require File.dirname(__FILE__) + '/base'
+
+##
+# A special bar graph that shows a single dataset as a set of
+# stacked bars. The bottom bar shows the running total and
+# the top bar shows the new value being added to the array.
+
+class Gruff::AccumulatorBar < Gruff::StackedBar
+
+ def draw
+ raise(Gruff::IncorrectNumberOfDatasetsException) unless @data.length == 1
+
+ accumulator_array = []
+ index = 0
+
+ increment_array = @data.first[DATA_VALUES_INDEX].inject([]) {|memo, value|
+ memo << ((index > 0) ? (value + memo.max) : value)
+ accumulator_array << memo[index] - value
+ index += 1
+ memo
+ }
+ data "Accumulator", accumulator_array
+
+ super
+ end
+
+end
--- /dev/null
+
+require File.dirname(__FILE__) + '/base'
+
+class Gruff::Area < Gruff::Base
+
+ def draw
+ super
+
+ return unless @has_data
+
+ @x_increment = @graph_width / (@column_count - 1).to_f
+ @d = @d.stroke 'transparent'
+
+ @norm_data.each do |data_row|
+ poly_points = Array.new
+ prev_x = prev_y = 0.0
+ @d = @d.fill data_row[DATA_COLOR_INDEX]
+
+ data_row[1].each_with_index do |data_point, index|
+ # Use incremented x and scaled y
+ new_x = @graph_left + (@x_increment * index)
+ new_y = @graph_top + (@graph_height - data_point * @graph_height)
+
+ if prev_x > 0 and prev_y > 0 then
+ poly_points << new_x
+ poly_points << new_y
+
+ #@d = @d.polyline(prev_x, prev_y, new_x, new_y)
+ else
+ poly_points << @graph_left
+ poly_points << @graph_bottom - 1
+ poly_points << new_x
+ poly_points << new_y
+
+ #@d = @d.polyline(@graph_left, @graph_bottom, new_x, new_y)
+ end
+
+ draw_label(new_x, index)
+
+ prev_x = new_x
+ prev_y = new_y
+ end
+
+ # Add closing points, draw polygon
+ poly_points << @graph_right
+ poly_points << @graph_bottom - 1
+ poly_points << @graph_left
+ poly_points << @graph_bottom - 1
+
+ @d = @d.polyline(*poly_points)
+
+ end
+
+ @d.draw(@base_image)
+ end
+
+
+end
--- /dev/null
+
+require File.dirname(__FILE__) + '/base'
+require File.dirname(__FILE__) + '/bar_conversion'
+
+class Gruff::Bar < Gruff::Base
+
+ def draw
+ # Labels will be centered over the left of the bar if
+ # there are more labels than columns. This is basically the same
+ # as where it would be for a line graph.
+ @center_labels_over_point = (@labels.keys.length > @column_count ? true : false)
+
+ super
+ return unless @has_data
+
+ draw_bars
+ end
+
+protected
+
+ def draw_bars
+ # Setup spacing.
+ #
+ # Columns sit side-by-side.
+ spacing_factor = 0.9 # space between the bars
+ @bar_width = @graph_width / (@column_count * @data.length).to_f
+
+ @d = @d.stroke_opacity 0.0
+
+ # Setup the BarConversion Object
+ conversion = Gruff::BarConversion.new()
+ conversion.graph_height = @graph_height
+ conversion.graph_top = @graph_top
+
+ # Set up the right mode [1,2,3] see BarConversion for further explanation
+ if @minimum_value >= 0 then
+ # all bars go from zero to positiv
+ conversion.mode = 1
+ else
+ # all bars go from 0 to negativ
+ if @maximum_value <= 0 then
+ conversion.mode = 2
+ else
+ # bars either go from zero to negativ or to positiv
+ conversion.mode = 3
+ conversion.spread = @spread
+ conversion.minimum_value = @minimum_value
+ conversion.zero = -@minimum_value/@spread
+ end
+ end
+
+ # iterate over all normalised data
+ @norm_data.each_with_index do |data_row, row_index|
+ count = 0
+ data_row[1].each_with_index do |data_point, point_index|
+
+ # Use incremented x and scaled y
+ # x
+ left_x = @graph_left + (@bar_width * (row_index + point_index + ((@data.length - 1) * point_index)))
+ right_x = left_x + @bar_width * spacing_factor
+ # y
+ conv = []
+ conversion.getLeftYRightYscaled( data_point, conv )
+
+ # create new bar
+ @d = @d.fill data_row[DATA_COLOR_INDEX]
+ @d = @d.rectangle(left_x, conv[0], right_x, conv[1])
+
+ # Calculate center based on bar_width and current row
+ label_center = @graph_left +
+ (@data.length * @bar_width * point_index) +
+ (@data.length * @bar_width / 2.0)
+ # Subtract half a bar width to center left if requested
+ draw_label(label_center - (@center_labels_over_point ? @bar_width / 2.0 : 0.0), point_index, count)
+ # custom modification to allow graph labels to alternate in height on
+ # bar graphs so they don't overlap
+ count += 1
+ end
+
+ end
+
+ # Draw the last label if requested
+ draw_label(@graph_right, @column_count) if @center_labels_over_point
+
+ @d.draw(@base_image)
+ end
+
+ def draw_axis_labels
+ unless @x_axis_label.nil?
+ # X Axis
+ # Centered vertically and horizontally by setting the
+ # height to 1.0 and the width to the width of the graph.
+ x_axis_label_y_coordinate = @graph_bottom + LABEL_MARGIN * 2.5 + @marker_caps_height
+
+ # TODO Center between graph area
+ @d.fill = @font_color
+ @d.font = @font if @font
+ @d.stroke('transparent')
+ @d.pointsize = scale_fontsize(@marker_font_size) * 1.2
+ @d.gravity = NorthGravity
+ @d = @d.annotate_scaled( @base_image,
+ @raw_columns, 1.0,
+ 0.0, x_axis_label_y_coordinate,
+ @x_axis_label, @scale)
+ debug { @d.line 0.0, x_axis_label_y_coordinate, @raw_columns, x_axis_label_y_coordinate }
+ end
+
+ unless @y_axis_label.nil?
+ # Y Axis, rotated vertically
+ @d.rotation = 90.0
+ @d.gravity = CenterGravity
+ @d = @d.annotate_scaled( @base_image,
+ 1.0, @raw_rows,
+ LEFT_MARGIN + @marker_caps_height / 2.0, 0.0,
+ @y_axis_label, @scale)
+ @d.rotation = -90.0
+ end
+ end
+
+end
--- /dev/null
+##
+# Original Author: David Stokar
+#
+# This class perfoms the y coordinats conversion for the bar class.
+#
+# There are three cases:
+#
+# 1. Bars all go from zero in positive direction
+# 2. Bars all go from zero to negative direction
+# 3. Bars either go from zero to positive or from zero to negative
+#
+class Gruff::BarConversion
+ attr_writer :mode
+ attr_writer :zero
+ attr_writer :graph_top
+ attr_writer :graph_height
+ attr_writer :minimum_value
+ attr_writer :spread
+
+ def getLeftYRightYscaled( data_point, result )
+ case @mode
+ when 1 then # Case one
+ # minimum value >= 0 ( only positiv values )
+ result[0] = @graph_top + @graph_height*(1 - data_point) + 1
+ result[1] = @graph_top + @graph_height - 1
+ when 2 then # Case two
+ # only negativ values
+ result[0] = @graph_top + 1
+ result[1] = @graph_top + @graph_height*(1 - data_point) - 1
+ when 3 then # Case three
+ # positiv and negativ values
+ val = data_point-@minimum_value/@spread
+ if ( data_point >= @zero ) then
+ result[0] = @graph_top + @graph_height*(1 - (val-@zero)) + 1
+ result[1] = @graph_top + @graph_height*(1 - @zero) - 1
+ else
+ result[0] = @graph_top + @graph_height*(1 - (val-@zero)) + 1
+ result[1] = @graph_top + @graph_height*(1 - @zero) - 1
+ end
+ else
+ result[0] = 0.0
+ result[1] = 0.0
+ end
+ end
+
+end
--- /dev/null
+#
+# = Gruff. Graphs.
+#
+# Author:: Geoffrey Grosenbach boss@topfunky.com
+#
+# Originally Created:: October 23, 2005
+#
+# Extra thanks to Tim Hunter for writing RMagick,
+# and also contributions by
+# Jarkko Laine, Mike Perham, Andreas Schwarz,
+# Alun Eyre, Guillaume Theoret, David Stokar,
+# Paul Rogers, Dave Woodward, Frank Oxener,
+# Kevin Clark, Cies Breijs, Richard Cowin,
+# and a cast of thousands.
+#
+
+require 'rubygems'
+require 'RMagick'
+require File.dirname(__FILE__) + '/deprecated'
+
+module Gruff
+
+ VERSION = '0.2.8'
+
+ class Base
+
+ include Magick
+ include Deprecated
+
+ # Draw extra lines showing where the margins and text centers are
+ DEBUG = false
+
+ # Used for navigating the array of data to plot
+ DATA_LABEL_INDEX = 0
+ DATA_VALUES_INDEX = 1
+ DATA_COLOR_INDEX = 2
+
+ # Blank space around the edges of the graph
+ TOP_MARGIN = BOTTOM_MARGIN = RIGHT_MARGIN = LEFT_MARGIN = 20.0
+
+ # Space around text elements. Mostly used for vertical spacing
+ LEGEND_MARGIN = TITLE_MARGIN = LABEL_MARGIN = 10.0
+
+ # A hash of names for the individual columns, where the key is the array index for the column this label represents.
+ #
+ # Not all columns need to be named.
+ #
+ # Example: 0 => 2005, 3 => 2006, 5 => 2007, 7 => 2008
+ attr_accessor :labels
+
+ # Used internally for spacing.
+ #
+ # By default, labels are centered over the point they represent.
+ attr_accessor :center_labels_over_point
+
+ # Used internally for horizontal graph types.
+ attr_accessor :has_left_labels
+
+ # A label for the bottom of the graph
+ attr_accessor :x_axis_label
+
+ # A label for the left side of the graph
+ attr_accessor :y_axis_label
+
+ # attr_accessor :x_axis_increment
+
+ # Manually set increment of the horizontal marking lines
+ attr_accessor :y_axis_increment
+
+ # Get or set the list of colors that will be used to draw the bars or lines.
+ attr_accessor :colors
+
+ # The large title of the graph displayed at the top
+ attr_accessor :title
+
+ # Font used for titles, labels, etc. Works best if you provide the full path to the TTF font file.
+ # RMagick must be built with the Freetype libraries for this to work properly.
+ #
+ # Tries to find Bitstream Vera (Vera.ttf) in the location specified by
+ # ENV['MAGICK_FONT_PATH']. Uses default RMagick font otherwise.
+ attr_reader :font
+
+ attr_accessor :font_color
+
+ # Hide various elements
+ attr_accessor :hide_line_markers, :hide_legend, :hide_title, :hide_line_numbers
+
+ # Message shown when there is no data. Fits up to 20 characters. Defaults to "No Data."
+ attr_accessor :no_data_message
+
+ # The font size of the large title at the top of the graph
+ attr_accessor :title_font_size
+
+ # Optionally set the size of the font. Based on an 800x600px graph. Default is 20.
+ #
+ # Will be scaled down if graph is smaller than 800px wide.
+ attr_accessor :legend_font_size
+
+ # The font size of the labels around the graph
+ attr_accessor :marker_font_size
+
+ # The color of the auxiliary lines
+ attr_accessor :marker_color
+
+ # The number of horizontal lines shown for reference
+ attr_accessor :marker_count
+
+
+ # You can manually set a minimum value instead of having the values guessed for you.
+ #
+ # Set it after you have given all your data to the graph object.
+ attr_accessor :minimum_value
+
+ # You can manually set a maximum value, such as a percentage-based graph that always goes to 100.
+ #
+ # If you use this, you must set it after you have given all your data to the graph object.
+ attr_accessor :maximum_value
+
+ # Set to false if you don't want the data to be sorted with largest avg values at the back.
+ attr_accessor :sort
+
+ # Experimental
+ attr_accessor :additional_line_values
+
+ # Experimental
+ attr_accessor :stacked
+
+
+ # Optionally set the size of the colored box by each item in the legend. Default is 20.0
+ #
+ # Will be scaled down if graph is smaller than 800px wide.
+ attr_accessor :legend_box_size
+
+
+ # If one numerical argument is given, the graph is drawn at 4/3 ratio according to the given width (800 results in 800x600, 400 gives 400x300, etc.).
+ #
+ # Or, send a geometry string for other ratios ('800x400', '400x225').
+ #
+ # Looks for Bitstream Vera as the default font. Expects an environment var of MAGICK_FONT_PATH to be set. (Uses RMagick's default font otherwise.)
+ def initialize(target_width=800)
+
+ if not Numeric === target_width
+ geometric_width, geometric_height = target_width.split('x')
+ @columns = geometric_width.to_f
+ @rows = geometric_height.to_f
+ else
+ @columns = target_width.to_f
+ @rows = target_width.to_f * 0.75
+ end
+
+ initialize_ivars
+
+ reset_themes
+ theme_keynote
+ end
+
+ ##
+ # Set instance variables for this object.
+ #
+ # Subclasses can override this, call super, then set values separately.
+ #
+ # This makes it possible to set defaults in a subclass but still allow
+ # developers to change this values in their program.
+
+ def initialize_ivars
+ # Internal for calculations
+ @raw_columns = 800.0
+ @raw_rows = 800.0 * (@rows/@columns)
+ @column_count = 0
+ @marker_count = nil
+ @maximum_value = @minimum_value = nil
+ @has_data = false
+ @data = Array.new
+ @labels = Hash.new
+ @labels_seen = Hash.new
+ @sort = true
+ @title = nil
+
+ @scale = @columns / @raw_columns
+
+ vera_font_path = File.expand_path('Vera.ttf', ENV['MAGICK_FONT_PATH'])
+ @font = File.exists?(vera_font_path) ? vera_font_path : nil
+
+ @marker_font_size = 21.0
+ @legend_font_size = 20.0
+ @title_font_size = 36.0
+
+ @legend_box_size = 20.0
+
+ @no_data_message = "No Data"
+
+ @hide_line_markers = @hide_legend = @hide_title = @hide_line_numbers = false
+ @center_labels_over_point = true
+ @has_left_labels = false
+
+ @additional_line_values = []
+ @additional_line_colors = []
+ @theme_options = {}
+
+ @x_axis_label = @y_axis_label = nil
+ @y_axis_increment = nil
+ @stacked = nil
+ @norm_data = nil
+ end
+
+ def font=(font_path)
+ @font = font_path
+ @d.font = @font
+ end
+
+ # Add a color to the list of available colors for lines.
+ #
+ # Example:
+ # add_color('#c0e9d3')
+ def add_color(colorname)
+ @colors << colorname
+ end
+
+
+ # Replace the entire color list with a new array of colors. You need to have one more color
+ # than the number of datasets you intend to draw. Also aliased as the colors= setter method.
+ #
+ # Example:
+ # replace_colors('#cc99cc', '#d9e043', '#34d8a2')
+ def replace_colors(color_list=[])
+ @colors = color_list
+ end
+
+
+ # You can set a theme manually. Assign a hash to this method before you send your data.
+ #
+ # graph.theme = {
+ # :colors => %w(orange purple green white red),
+ # :marker_color => 'blue',
+ # :background_colors => %w(black grey)
+ # }
+ #
+ # :background_image => 'squirrel.png' is also possible.
+ #
+ # (Or hopefully something better looking than that.)
+ #
+ def theme=(options)
+ reset_themes()
+
+ defaults = {
+ :colors => ['black', 'white'],
+ :additional_line_colors => [],
+ :marker_color => 'white',
+ :font_color => 'black',
+ :background_colors => nil,
+ :background_image => nil
+ }
+ @theme_options = defaults.merge options
+
+ @colors = @theme_options[:colors]
+ @marker_color = @theme_options[:marker_color]
+ @font_color = @theme_options[:font_color] || @marker_color
+ @additional_line_colors = @theme_options[:additional_line_colors]
+
+ render_background
+ end
+
+ # A color scheme similar to the popular presentation software.
+ def theme_keynote
+ # Colors
+ @blue = '#6886B4'
+ @yellow = '#FDD84E'
+ @green = '#72AE6E'
+ @red = '#D1695E'
+ @purple = '#8A6EAF'
+ @orange = '#EFAA43'
+ @white = 'white'
+ @colors = [@yellow, @blue, @green, @red, @purple, @orange, @white]
+
+ self.theme = {
+ :colors => @colors,
+ :marker_color => 'white',
+ :font_color => 'white',
+ :background_colors => ['black', '#4a465a']
+ }
+ end
+
+ # A color scheme plucked from the colors on the popular usability blog.
+ def theme_37signals
+ # Colors
+ @green = '#339933'
+ @purple = '#cc99cc'
+ @blue = '#336699'
+ @yellow = '#FFF804'
+ @red = '#ff0000'
+ @orange = '#cf5910'
+ @black = 'black'
+ @colors = [@yellow, @blue, @green, @red, @purple, @orange, @black]
+
+ self.theme = {
+ :colors => @colors,
+ :marker_color => 'black',
+ :font_color => 'black',
+ :background_colors => ['#d1edf5', 'white']
+ }
+ end
+
+ # A color scheme from the colors used on the 2005 Rails keynote presentation at RubyConf.
+ def theme_rails_keynote
+ # Colors
+ @green = '#00ff00'
+ @grey = '#333333'
+ @orange = '#ff5d00'
+ @red = '#f61100'
+ @white = 'white'
+ @light_grey = '#999999'
+ @black = 'black'
+ @colors = [@green, @grey, @orange, @red, @white, @light_grey, @black]
+
+ self.theme = {
+ :colors => @colors,
+ :marker_color => 'white',
+ :font_color => 'white',
+ :background_colors => ['#0083a3', '#0083a3']
+ }
+ end
+
+ # A color scheme similar to that used on the popular podcast site.
+ def theme_odeo
+ # Colors
+ @grey = '#202020'
+ @white = 'white'
+ @dark_pink = '#a21764'
+ @green = '#8ab438'
+ @light_grey = '#999999'
+ @dark_blue = '#3a5b87'
+ @black = 'black'
+ @colors = [@grey, @white, @dark_blue, @dark_pink, @green, @light_grey, @black]
+
+ self.theme = {
+ :colors => @colors,
+ :marker_color => 'white',
+ :font_color => 'white',
+ :background_colors => ['#ff47a4', '#ff1f81']
+ }
+ end
+
+ # Parameters are an array where the first element is the name of the dataset
+ # and the value is an array of values to plot.
+ #
+ # Can be called multiple times with different datasets for a multi-valued graph.
+ #
+ # If the color argument is nil, the next color from the default theme will be used.
+ #
+ # NOTE: If you want to use a preset theme, you must set it before calling data().
+ #
+ # Example:
+ #
+ # data("Bart S.", [95, 45, 78, 89, 88, 76], '#ffcc00')
+ #
+ def data(name, data_points=[], color=nil)
+ data_points = Array(data_points) # make sure it's an array
+ @data << [name, data_points, (color || increment_color)]
+ # Set column count if this is larger than previous counts
+ @column_count = (data_points.length > @column_count) ? data_points.length : @column_count
+
+ # Pre-normalize
+ data_points.each_with_index do |data_point, index|
+ next if data_point.nil?
+
+ # Setup max/min so spread starts at the low end of the data points
+ if @maximum_value.nil? && @minimum_value.nil?
+ @maximum_value = @minimum_value = data_point
+ end
+
+ # TODO Doesn't work with stacked bar graphs
+ # Original: @maximum_value = larger_than_max?(data_point, index) ? max(data_point, index) : @maximum_value
+ @maximum_value = larger_than_max?(data_point) ? data_point : @maximum_value
+ @has_data = true if @maximum_value > 0
+
+ @minimum_value = less_than_min?(data_point) ? data_point : @minimum_value
+ @has_data = true if @minimum_value < 0
+ end
+ end
+
+ # Writes the graph to a file. Defaults to 'graph.png'
+ #
+ # Example: write('graphs/my_pretty_graph.png')
+ def write(filename="graph.png")
+ draw()
+ @base_image.write(filename)
+ end
+
+ # Return the graph as a rendered binary blob.
+ def to_blob(fileformat='PNG')
+ draw()
+ return @base_image.to_blob do
+ self.format = fileformat
+ end
+ end
+
+protected
+
+ # Overridden by subclasses to do the actual plotting of the graph.
+ #
+ # Subclasses should start by calling super() for this method.
+ def draw
+ make_stacked if @stacked
+ setup_drawing
+
+ debug {
+ # Outer margin
+ @d.rectangle( LEFT_MARGIN, TOP_MARGIN,
+ @raw_columns - RIGHT_MARGIN, @raw_rows - BOTTOM_MARGIN)
+ # Graph area box
+ @d.rectangle( @graph_left, @graph_top, @graph_right, @graph_bottom)
+ }
+ end
+
+ ##
+ # Calculates size of drawable area and draws the decorations.
+ #
+ # * line markers
+ # * legend
+ # * title
+
+ def setup_drawing
+ # Maybe should be done in one of the following functions for more granularity.
+ unless @has_data
+ draw_no_data()
+ return
+ end
+
+ normalize()
+ setup_graph_measurements()
+ sort_norm_data() if @sort # Sort norm_data with avg largest values set first (for display)
+
+ draw_legend()
+ draw_line_markers()
+ draw_axis_labels()
+ draw_title
+ end
+
+ # Make copy of data with values scaled between 0-100
+ def normalize(force=false)
+ if @norm_data.nil? || force
+ @norm_data = []
+ return unless @has_data
+
+ calculate_spread
+
+ @data.each do |data_row|
+ norm_data_points = []
+ data_row[DATA_VALUES_INDEX].each do |data_point|
+ if data_point.nil?
+ norm_data_points << nil
+ else
+ norm_data_points << ((data_point.to_f - @minimum_value.to_f ) / @spread)
+ end
+ end
+ @norm_data << [data_row[DATA_LABEL_INDEX], norm_data_points, data_row[DATA_COLOR_INDEX]]
+ end
+ end
+ end
+
+ def calculate_spread
+ @spread = @maximum_value.to_f - @minimum_value.to_f
+ @spread = @spread > 0 ? @spread : 1
+ end
+
+ ##
+ # Calculates size of drawable area, general font dimensions, etc.
+
+ def setup_graph_measurements
+ @marker_caps_height = calculate_caps_height(@marker_font_size)
+ @title_caps_height = calculate_caps_height(@title_font_size)
+ @legend_caps_height = calculate_caps_height(@legend_font_size)
+
+ if @hide_line_markers
+ (@graph_left,
+ @graph_right_margin,
+ @graph_bottom_margin) = [LEFT_MARGIN, RIGHT_MARGIN, BOTTOM_MARGIN]
+ else
+ longest_left_label_width = 0
+ if @has_left_labels
+ longest_left_label_width = calculate_width(@marker_font_size,
+ labels.values.inject('') { |value, memo| (value.to_s.length > memo.to_s.length) ? value : memo }) * 1.25
+ else
+ longest_left_label_width = calculate_width(@marker_font_size,
+ label(@maximum_value.to_f))
+ end
+
+ # Shift graph if left line numbers are hidden
+ line_number_width = @hide_line_numbers && !@has_left_labels ?
+ 0.0 :
+ (longest_left_label_width + LABEL_MARGIN * 2)
+
+ @graph_left = LEFT_MARGIN +
+ line_number_width +
+ (@y_axis_label.nil? ? 0.0 : @marker_caps_height + LABEL_MARGIN * 2)
+ # Make space for half the width of the rightmost column label.
+ # Might be greater than the number of columns if between-style bar markers are used.
+ last_label = @labels.keys.sort.last.to_i
+ extra_room_for_long_label = (last_label >= (@column_count-1) && @center_labels_over_point) ?
+ calculate_width(@marker_font_size, @labels[last_label])/2.0 :
+ 0
+ @graph_right_margin = RIGHT_MARGIN + extra_room_for_long_label
+
+ @graph_bottom_margin = BOTTOM_MARGIN +
+ @marker_caps_height + LABEL_MARGIN
+ end
+
+ @graph_right = @raw_columns - @graph_right_margin
+ @graph_width = @raw_columns - @graph_left - @graph_right_margin
+
+ # When @hide title, leave a TITLE_MARGIN space for aesthetics.
+ # Same with @hide_legend
+ @graph_top = TOP_MARGIN +
+ (@hide_title ? TITLE_MARGIN : @title_caps_height + TITLE_MARGIN * 2) +
+ (@hide_legend ? LEGEND_MARGIN : @legend_caps_height + LEGEND_MARGIN * 2)
+
+ @graph_bottom = @raw_rows - @graph_bottom_margin -
+ (@x_axis_label.nil? ? 0.0 : @marker_caps_height + LABEL_MARGIN)
+
+ @graph_height = @graph_bottom - @graph_top
+ end
+
+ # Draw the optional labels for the x axis and y axis.
+ def draw_axis_labels
+ unless @x_axis_label.nil?
+ # X Axis
+ # Centered vertically and horizontally by setting the
+ # height to 1.0 and the width to the width of the graph.
+ x_axis_label_y_coordinate = @graph_bottom + LABEL_MARGIN * 2 + @marker_caps_height
+
+ # TODO Center between graph area
+ @d.fill = @font_color
+ @d.font = @font if @font
+ @d.stroke('transparent')
+ @d.pointsize = scale_fontsize(@marker_font_size)
+ @d.gravity = NorthGravity
+ @d = @d.annotate_scaled( @base_image,
+ @raw_columns, 1.0,
+ 0.0, x_axis_label_y_coordinate,
+ @x_axis_label, @scale)
+ debug { @d.line 0.0, x_axis_label_y_coordinate, @raw_columns, x_axis_label_y_coordinate }
+ end
+
+ unless @y_axis_label.nil?
+ # Y Axis, rotated vertically
+ @d.rotation = 90.0
+ @d.gravity = CenterGravity
+ @d = @d.annotate_scaled( @base_image,
+ 1.0, @raw_rows,
+ LEFT_MARGIN + @marker_caps_height / 2.0, 0.0,
+ @y_axis_label, @scale)
+ @d.rotation = -90.0
+ end
+ end
+
+ # Draws horizontal background lines and labels
+ def draw_line_markers
+ return if @hide_line_markers
+
+ @d = @d.stroke_antialias false
+
+ if @y_axis_increment.nil?
+ # Try to use a number of horizontal lines that will come out even.
+ #
+ # TODO Do the same for larger numbers...100, 75, 50, 25
+ if @marker_count.nil?
+ (3..7).each do |lines|
+ if @spread % lines == 0.0
+ @marker_count = lines
+ break
+ end
+ end
+ @marker_count ||= 4
+ end
+ @increment = (@spread > 0) ? significant(@spread / @marker_count) : 1
+ else
+ # TODO Make this work for negative values
+ @maximum_value = [@maximum_value.ceil, @y_axis_increment].max
+ @minimum_value = @minimum_value.floor
+ calculate_spread
+ normalize(true)
+
+ @marker_count = (@spread / @y_axis_increment).to_i
+ @increment = @y_axis_increment
+ end
+ @increment_scaled = @graph_height.to_f / (@spread / @increment)
+
+ # Draw horizontal line markers and annotate with numbers
+ (0..@marker_count).each do |index|
+ y = @graph_top + @graph_height - index.to_f * @increment_scaled
+
+ @d = @d.stroke(@marker_color)
+ @d = @d.stroke_width 1
+ @d = @d.line(@graph_left, y, @graph_right, y)
+
+ marker_label = index * @increment + @minimum_value.to_f
+
+ unless @hide_line_numbers
+ @d.fill = @font_color
+ @d.font = @font if @font
+ @d.stroke('transparent')
+ @d.pointsize = scale_fontsize(@marker_font_size)
+ @d.gravity = EastGravity
+
+ # Vertically center with 1.0 for the height
+ @d = @d.annotate_scaled( @base_image,
+ @graph_left - LABEL_MARGIN, 1.0,
+ 0.0, y,
+ label(marker_label), @scale)
+ end
+ end
+
+ # # Submitted by a contibutor...the utility escapes me
+ # i = 0
+ # @additional_line_values.each do |value|
+ # @increment_scaled = @graph_height.to_f / (@maximum_value.to_f / value)
+ #
+ # y = @graph_top + @graph_height - @increment_scaled
+ #
+ # @d = @d.stroke(@additional_line_colors[i])
+ # @d = @d.line(@graph_left, y, @graph_right, y)
+ #
+ #
+ # @d.fill = @additional_line_colors[i]
+ # @d.font = @font if @font
+ # @d.stroke('transparent')
+ # @d.pointsize = scale_fontsize(@marker_font_size)
+ # @d.gravity = EastGravity
+ # @d = @d.annotate_scaled( @base_image,
+ # 100, 20,
+ # -10, y - (@marker_font_size/2.0),
+ # "", @scale)
+ # i += 1
+ # end
+
+ @d = @d.stroke_antialias true
+ end
+
+ # Draws a legend with the names of the datasets
+ # matched to the colors used to draw them.
+ def draw_legend
+ return if @hide_legend
+
+ @legend_labels = @data.collect {|item| item[DATA_LABEL_INDEX] }
+
+ legend_square_width = @legend_box_size # small square with color of this item
+
+ # May fix legend drawing problem at small sizes
+ @d.font = @font if @font
+ @d.pointsize = @legend_font_size
+
+ metrics = @d.get_type_metrics(@base_image, @legend_labels.join(''))
+ legend_text_width = metrics.width
+ legend_width = legend_text_width +
+ (@legend_labels.length * legend_square_width * 2.7)
+ legend_left = (@raw_columns - legend_width) / 2
+ legend_increment = legend_width / @legend_labels.length.to_f
+
+ current_x_offset = legend_left
+ current_y_offset = @hide_title ?
+ TOP_MARGIN + LEGEND_MARGIN :
+ TOP_MARGIN +
+ TITLE_MARGIN + @title_caps_height +
+ LEGEND_MARGIN
+
+ debug { @d.line 0.0, current_y_offset, @raw_columns, current_y_offset }
+
+ @legend_labels.each_with_index do |legend_label, index|
+
+ # Draw label
+ @d.fill = @font_color
+ @d.font = @font if @font
+ @d.pointsize = scale_fontsize(@legend_font_size)
+ @d.stroke('transparent')
+ @d.font_weight = NormalWeight
+ @d.gravity = WestGravity
+ @d = @d.annotate_scaled( @base_image,
+ @raw_columns, 1.0,
+ current_x_offset + (legend_square_width * 1.7), current_y_offset,
+ legend_label.to_s, @scale)
+
+ # Now draw box with color of this dataset
+ @d = @d.stroke('transparent')
+ @d = @d.fill @data[index][DATA_COLOR_INDEX]
+ @d = @d.rectangle(current_x_offset,
+ current_y_offset - legend_square_width / 2.0,
+ current_x_offset + legend_square_width,
+ current_y_offset + legend_square_width / 2.0)
+
+ @d.pointsize = @legend_font_size
+ metrics = @d.get_type_metrics(@base_image, legend_label.to_s)
+ current_string_offset = metrics.width + (legend_square_width * 2.7)
+ current_x_offset += current_string_offset
+ end
+ @color_index = 0
+ end
+
+ def draw_title
+ return if (@hide_title || @title.nil?)
+
+ @d.fill = @font_color
+ @d.font = @font if @font
+ @d.stroke('transparent')
+ @d.pointsize = scale_fontsize(@title_font_size)
+ @d.font_weight = BoldWeight
+ @d.gravity = NorthGravity
+ @d = @d.annotate_scaled( @base_image,
+ @raw_columns, 1.0,
+ 0, TOP_MARGIN,
+ @title, @scale)
+ end
+
+ ##
+ # Draws column labels below graph, centered over x_offset
+ #
+ # TODO Allow WestGravity as an option
+
+ def draw_label(x_offset, index, count=nil)
+ return if @hide_line_markers
+
+ if !@labels[index].nil? && @labels_seen[index].nil?
+ y_offset = @graph_bottom + LABEL_MARGIN
+
+ if count && (count % 2) == 1
+ y_offset += 1.5 * LABEL_MARGIN
+ end
+
+ @d.fill = @font_color
+ @d.font = @font if @font
+ @d.stroke('transparent')
+ @d.font_weight = NormalWeight
+ @d.pointsize = scale_fontsize(@marker_font_size)
+ @d.gravity = NorthGravity
+ @d = @d.annotate_scaled(@base_image,
+ 1.0, 1.0,
+ x_offset, y_offset,
+ @labels[index], @scale)
+ @labels_seen[index] = 1
+ debug { @d.line 0.0, y_offset, @raw_columns, y_offset }
+ end
+ end
+
+ def draw_no_data
+ @d.fill = @font_color
+ @d.font = @font if @font
+ @d.stroke('transparent')
+ @d.font_weight = NormalWeight
+ @d.pointsize = scale_fontsize(80)
+ @d.gravity = CenterGravity
+ @d = @d.annotate_scaled( @base_image,
+ @raw_columns, @raw_rows/2.0,
+ 0, 10,
+ @no_data_message, @scale)
+ end
+
+ ##
+ # Finds the best background to render based on the provided theme options.
+ #
+ # Creates a @base_image to draw on.
+ #
+ def render_background
+ case @theme_options[:background_colors]
+ when Array
+ @base_image = render_gradiated_background(*@theme_options[:background_colors])
+ when String
+ @base_image = render_solid_background(@theme_options[:background_colors])
+ else
+ @base_image = render_image_background(*@theme_options[:background_image])
+ end
+ end
+
+ ##
+ # Make a new image at the current size with a solid +color+.
+
+ def render_solid_background(color)
+ Image.new(@columns, @rows) {
+ self.background_color = color
+ }
+ end
+
+ # Use with a theme definition method to draw a gradiated background.
+ def render_gradiated_background(top_color, bottom_color)
+ Image.new(@columns, @rows,
+ GradientFill.new(0, 0, 100, 0, top_color, bottom_color))
+ end
+
+ # Use with a theme to use an image (800x600 original) background.
+ def render_image_background(image_path)
+ image = Image.read(image_path)
+ if @scale != 1.0
+ image[0].resize!(@scale) # TODO Resize with new scale (crop if necessary for wide graph)
+ end
+ image[0]
+ end
+
+ # Use with a theme to make a transparent background
+ def render_transparent_background
+ Image.new(@columns, @rows) do
+ self.background_color = 'transparent'
+ end
+ end
+
+ def reset_themes
+ @color_index = 0
+ @labels_seen = {}
+ @theme_options = {}
+
+ @d = Draw.new
+ # Scale down from 800x600 used to calculate drawing.
+ @d = @d.scale(@scale, @scale)
+ end
+
+ def scale(value)
+ value * @scale
+ end
+
+ # Return a comparable fontsize for the current graph.
+ def scale_fontsize(value)
+ new_fontsize = value * @scale
+ # return new_fontsize < 10.0 ? 10.0 : new_fontsize
+ return new_fontsize
+ end
+
+ def clip_value_if_greater_than(value, max_value)
+ (value > max_value) ? max_value : value
+ end
+
+ # Overridden by subclasses such as stacked bar.
+ def larger_than_max?(data_point, index=0)
+ data_point > @maximum_value
+ end
+
+ def less_than_min?(data_point, index=0)
+ data_point < @minimum_value
+ end
+
+ ##
+ # Overridden by subclasses that need it.
+ def max(data_point, index)
+ data_point
+ end
+
+ ##
+ # Overridden by subclasses that need it.
+ def min(data_point, index)
+ data_point
+ end
+
+ def significant(inc)
+ return 1.0 if inc == 0 # Keep from going into infinite loop
+ factor = 1.0
+ while (inc < 10)
+ inc *= 10
+ factor /= 10
+ end
+
+ while (inc > 100)
+ inc /= 10
+ factor *= 10
+ end
+
+ res = inc.floor * factor
+ if (res.to_i.to_f == res)
+ res.to_i
+ else
+ res
+ end
+ end
+
+ # Sort with largest overall summed value at front of array
+ # so it shows up correctly in the drawn graph.
+ def sort_norm_data
+ @norm_data.sort! { |a,b| sums(b[1]) <=> sums(a[1]) }
+ end
+
+ def sums(data_set)
+ total_sum = 0
+ data_set.collect {|num| total_sum += num.to_f }
+ total_sum
+ end
+
+ ##
+ # Used by StackedBar and child classes.
+ #
+ # May need to be moved to the StackedBar class.
+
+ def get_maximum_by_stack
+ # Get sum of each stack
+ max_hash = {}
+ @data.each do |data_set|
+ data_set[DATA_VALUES_INDEX].each_with_index do |data_point, i|
+ max_hash[i] = 0.0 unless max_hash[i]
+ max_hash[i] += data_point.to_f
+ end
+ end
+
+ # @maximum_value = 0
+ max_hash.keys.each do |key|
+ @maximum_value = max_hash[key] if max_hash[key] > @maximum_value
+ end
+ @minimum_value = 0
+ end
+
+ def make_stacked
+ stacked_values = Array.new(@column_count, 0)
+ @data.each do |value_set|
+ value_set[1].each_with_index do |value, index|
+ stacked_values[index] += value
+ end
+ value_set[1] = stacked_values.dup
+ end
+ end
+
+private
+
+ # Takes a block and draws it if DEBUG is true.
+ #
+ # debug { @d.rectangle x1, y1, x2, y2 }
+ #
+ def debug
+ if DEBUG
+ @d = @d.fill 'transparent'
+ @d = @d.stroke 'turquoise'
+ @d = yield
+ end
+ end
+
+ def increment_color
+ if @color_index == 0
+ @color_index += 1
+ return @colors[0]
+ else
+ if @color_index < @colors.length
+ @color_index += 1
+ return @colors[@color_index - 1]
+ else
+ # Start over
+ @color_index = 0
+ return @colors[-1]
+ end
+ end
+ end
+
+ ##
+ # Return a formatted string representing a number value that should be printed as a label.
+
+ def label(value)
+ if (@spread.to_f % @marker_count.to_f == 0) || !@y_axis_increment.nil?
+ return value.to_i.to_s
+ end
+
+ if @spread > 10.0
+ sprintf("%0i", value)
+ elsif @spread >= 3.0
+ sprintf("%0.2f", value)
+ else
+ value.to_s
+ end
+ end
+
+ ##
+ # Returns the height of the capital letter 'X' for the current font and size.
+ #
+ # Not scaled since it deals with dimensions that the regular
+ # scaling will handle.
+ #
+ def calculate_caps_height(font_size)
+ @d.pointsize = font_size
+ @d.get_type_metrics(@base_image, 'X').height
+ end
+
+ ##
+ # Returns the width of a string at this pointsize.
+ #
+ # Not scaled since it deals with dimensions that the regular
+ # scaling will handle.
+ #
+ def calculate_width(font_size, text)
+ @d.pointsize = font_size
+ @d.get_type_metrics(@base_image, text.to_s).width
+ end
+
+ end # Gruff::Base
+
+ class IncorrectNumberOfDatasetsException < StandardError; end
+
+end # Gruff
+
+
+module Magick
+
+ class Draw
+
+ # Additional method since Draw.scale doesn't affect annotations.
+ def annotate_scaled(img, width, height, x, y, text, scale)
+ scaled_width = (width * scale) >= 1 ? (width * scale) : 1
+ scaled_height = (height * scale) >= 1 ? (height * scale) : 1
+
+ self.annotate( img,
+ scaled_width, scaled_height,
+ x * scale, y * scale,
+ text)
+ end
+
+ end
+
+end # Magick
+
--- /dev/null
+
+##
+# A mixin for methods that need to be deleted or have been
+# replaced by cleaner code.
+
+module Gruff
+ module Deprecated
+
+ def scale_measurements
+ setup_graph_measurements
+ end
+
+ def total_height
+ @rows + 10
+ end
+
+ def graph_top
+ @graph_top * @scale
+ end
+
+ def graph_height
+ @graph_height * @scale
+ end
+
+ def graph_left
+ @graph_left * @scale
+ end
+
+ def graph_width
+ @graph_width * @scale
+ end
+
+ # TODO Should be calculate_graph_height
+ # def setup_graph_height
+ # @graph_height = @graph_bottom - @graph_top
+ # end
+
+ end
+end
--- /dev/null
+
+require File.dirname(__FILE__) + '/base'
+
+class Gruff::Line < Gruff::Base
+
+ # Draw a dashed line at the given value
+ attr_accessor :baseline_value
+
+ # Color of the baseline
+ attr_accessor :baseline_color
+
+ # Hide parts of the graph to fit more datapoints, or for a different appearance.
+ attr_accessor :hide_dots, :hide_lines
+
+ # Call with target pixel width of graph (800, 400, 300), and/or 'false' to omit lines (points only).
+ #
+ # g = Gruff::Line.new(400) # 400px wide with lines
+ #
+ # g = Gruff::Line.new(400, false) # 400px wide, no lines (for backwards compatibility)
+ #
+ # g = Gruff::Line.new(false) # Defaults to 800px wide, no lines (for backwards compatibility)
+ #
+ # The preferred way is to call hide_dots or hide_lines instead.
+ def initialize(*args)
+ raise ArgumentError, "Wrong number of arguments" if args.length > 2
+ if args.empty? or ((not Numeric === args.first) && (not String === args.first)) then
+ super()
+ else
+ super args.shift
+ end
+
+ @hide_dots = @hide_lines = false
+ @baseline_color = 'red'
+ @baseline_value = nil
+ end
+
+ def draw
+ super
+
+ return unless @has_data
+
+ # Check to see if more than one datapoint was given. NaN can result otherwise.
+ @x_increment = (@column_count > 1) ? (@graph_width / (@column_count - 1).to_f) : @graph_width
+
+ if (defined?(@norm_baseline)) then
+ level = @graph_top + (@graph_height - @norm_baseline * @graph_height)
+ @d = @d.push
+ @d.stroke_color @baseline_color
+ @d.fill_opacity 0.0
+ @d.stroke_dasharray(10, 20)
+ @d.stroke_width 5
+ @d.line(@graph_left, level, @graph_left + @graph_width, level)
+ @d = @d.pop
+ end
+
+ @norm_data.each do |data_row|
+ prev_x = prev_y = nil
+
+ data_row[1].each_with_index do |data_point, index|
+ new_x = @graph_left + (@x_increment * index)
+ next if data_point.nil?
+
+ draw_label(new_x, index)
+
+ new_y = @graph_top + (@graph_height - data_point * @graph_height)
+
+ # Reset each time to avoid thin-line errors
+ @d = @d.stroke data_row[DATA_COLOR_INDEX]
+ @d = @d.fill data_row[DATA_COLOR_INDEX]
+ @d = @d.stroke_opacity 1.0
+ @d = @d.stroke_width clip_value_if_greater_than(@columns / (@norm_data.first[1].size * 4), 5.0)
+
+ if !@hide_lines and !prev_x.nil? and !prev_y.nil? then
+ @d = @d.line(prev_x, prev_y, new_x, new_y)
+ end
+ circle_radius = clip_value_if_greater_than(@columns / (@norm_data.first[1].size * 2.5), 5.0)
+ @d = @d.circle(new_x, new_y, new_x - circle_radius, new_y) unless @hide_dots
+
+ prev_x = new_x
+ prev_y = new_y
+ end
+
+ end
+
+ @d.draw(@base_image)
+ end
+
+ def normalize
+ @maximum_value = [@maximum_value.to_f, @baseline_value.to_f].max
+ super
+ @norm_baseline = (@baseline_value.to_f / @maximum_value.to_f) if @baseline_value
+ end
+
+end
--- /dev/null
+##
+#
+# Makes a small bar graph suitable for display at 200px or even smaller.
+#
+module Gruff
+ module Mini
+
+ class Bar < Gruff::Bar
+
+ include Gruff::Mini::Legend
+
+ def draw
+ @hide_legend = true
+ @hide_title = true
+ @hide_line_numbers = true
+
+ @marker_font_size = 50.0
+ @minimum_value = 0.0
+ @legend_font_size = 60.0
+
+ expand_canvas_for_vertical_legend
+
+ super
+
+ draw_vertical_legend
+ @d.draw(@base_image)
+ end
+
+ end
+
+ end
+end
--- /dev/null
+module Gruff
+ module Mini
+ module Legend
+
+ ##
+ # The canvas needs to be bigger so we can put the legend beneath it.
+
+ def expand_canvas_for_vertical_legend
+ @original_rows = @raw_rows
+ @rows += @data.length * calculate_caps_height(scale_fontsize(@legend_font_size)) * 1.7
+ render_background
+ end
+
+ ##
+ # Draw the legend beneath the existing graph.
+
+ def draw_vertical_legend
+
+ @legend_labels = @data.collect {|item| item[Gruff::Base::DATA_LABEL_INDEX] }
+
+ legend_square_width = 40.0 # small square with color of this item
+ legend_square_margin = 10.0
+ @legend_left_margin = 40.0
+ legend_top_margin = 40.0
+
+ # May fix legend drawing problem at small sizes
+ @d.font = @font if @font
+ @d.pointsize = @legend_font_size
+
+ current_x_offset = @graph_left + @legend_left_margin
+ current_y_offset = @original_rows + legend_top_margin
+
+ debug { @d.line 0.0, current_y_offset, @raw_columns, current_y_offset }
+
+ @legend_labels.each_with_index do |legend_label, index|
+
+ # Draw label
+ @d.fill = @font_color
+ @d.font = @font if @font
+ @d.pointsize = scale_fontsize(@legend_font_size)
+ @d.stroke = 'transparent'
+ @d.font_weight = Magick::NormalWeight
+ @d.gravity = Magick::WestGravity
+ @d = @d.annotate_scaled( @base_image,
+ @raw_columns, 1.0,
+ current_x_offset + (legend_square_width * 1.7), current_y_offset,
+ truncate_legend_label(legend_label), @scale)
+
+ # Now draw box with color of this dataset
+ @d = @d.stroke 'transparent'
+ @d = @d.fill @data[index][Gruff::Base::DATA_COLOR_INDEX]
+ @d = @d.rectangle(current_x_offset,
+ current_y_offset - legend_square_width / 2.0,
+ current_x_offset + legend_square_width,
+ current_y_offset + legend_square_width / 2.0)
+
+ current_y_offset += calculate_caps_height(@legend_font_size) * 1.7
+ end
+ @color_index = 0
+ end
+
+ ##
+ # Shorten long labels so they will fit on the canvas.
+ #
+ # Department of Hu...
+
+ def truncate_legend_label(label)
+ truncated_label = label.to_s
+ while calculate_width(scale_fontsize(@legend_font_size), truncated_label) > (@columns - @legend_left_margin - Gruff::Base::RIGHT_MARGIN) && (truncated_label.length > 1)
+ truncated_label = truncated_label[0..truncated_label.length-2]
+ end
+ truncated_label + (truncated_label.length < label.to_s.length ? "…" : '')
+ end
+
+ end
+ end
+end
--- /dev/null
+##
+#
+# Makes a small pie graph suitable for display at 200px or even smaller.
+#
+module Gruff
+ module Mini
+
+ class Pie < Gruff::Pie
+
+ include Gruff::Mini::Legend
+
+ def initialize_ivars
+ super
+
+ @hide_legend = true
+ @hide_title = true
+ @hide_line_numbers = true
+
+ @marker_font_size = 60.0
+ @legend_font_size = 60.0
+ end
+
+ def draw
+ expand_canvas_for_vertical_legend
+
+ super
+
+ draw_vertical_legend
+
+ @d.draw(@base_image)
+ end # def draw
+
+ end # class Pie
+
+ end
+end
--- /dev/null
+##
+#
+# Makes a small pie graph suitable for display at 200px or even smaller.
+#
+module Gruff
+ module Mini
+
+ class SideBar < Gruff::SideBar
+
+ def initialize_ivars
+ super
+ @hide_legend = true
+ @hide_title = true
+ @hide_line_numbers = true
+
+ @marker_font_size = 50.0
+ end
+
+ end
+
+ end
+end
--- /dev/null
+
+require File.dirname(__FILE__) + '/base'
+
+# Experimental!!! See also the Spider graph.
+class Gruff::Net < Gruff::Base
+
+ # Hide parts of the graph to fit more datapoints, or for a different appearance.
+ attr_accessor :hide_dots
+
+ def initialize(*args)
+ super
+
+ @hide_dots = false
+ end
+
+ def draw
+
+ super
+
+ return unless @has_data
+
+ @radius = @graph_height / 2.0
+ @center_x = @graph_left + (@graph_width / 2.0)
+ @center_y = @graph_top + (@graph_height / 2.0) - 10 # Move graph up a bit
+
+ @x_increment = @graph_width / (@column_count - 1).to_f
+ circle_radius = clip_value_if_greater_than(@columns / (@norm_data.first[1].size * 2.5), 5.0)
+
+ @d = @d.stroke_opacity 1.0
+ @d = @d.stroke_width clip_value_if_greater_than(@columns / (@norm_data.first[1].size * 4), 5.0)
+
+ if (defined?(@norm_baseline)) then
+ level = @graph_top + (@graph_height - @norm_baseline * @graph_height)
+ @d = @d.push
+ @d.stroke_color @baseline_color
+ @d.fill_opacity 0.0
+ @d.stroke_dasharray(10, 20)
+ @d.stroke_width 5
+ @d.line(@graph_left, level, @graph_left + @graph_width, level)
+ @d = @d.pop
+ end
+
+ @norm_data.each do |data_row|
+ prev_x = prev_y = nil
+ @d = @d.stroke data_row[DATA_COLOR_INDEX]
+ @d = @d.fill data_row[DATA_COLOR_INDEX]
+
+ data_row[1].each_with_index do |data_point, index|
+ next if data_point.nil?
+
+ rad_pos = index * Math::PI * 2 / @column_count
+ point_distance = data_point * @radius
+ start_x = @center_x + Math::sin(rad_pos) * point_distance
+ start_y = @center_y - Math::cos(rad_pos) * point_distance
+
+ next_index = index + 1 < data_row[1].length ? index + 1 : 0
+
+ next_rad_pos = next_index * Math::PI * 2 / @column_count
+ next_point_distance = data_row[1][next_index] * @radius
+ end_x = @center_x + Math::sin(next_rad_pos) * next_point_distance
+ end_y = @center_y - Math::cos(next_rad_pos) * next_point_distance
+
+ @d = @d.line(start_x, start_y, end_x, end_y)
+
+ @d = @d.circle(start_x, start_y, start_x - circle_radius, start_y) unless @hide_dots
+ end
+
+ end
+
+ @d.draw(@base_image)
+ end
+
+
+ # the lines connecting in the center, with the first line vertical
+ def draw_line_markers
+ return if @hide_line_markers
+
+
+ # have to do this here (AGAIN)... see draw() in this class
+ # because this funtion is called before the @radius, @center_x and @center_y are set
+ @radius = @graph_height / 2.0
+ @center_x = @graph_left + (@graph_width / 2.0)
+ @center_y = @graph_top + (@graph_height / 2.0) - 10 # Move graph up a bit
+
+
+ # Draw horizontal line markers and annotate with numbers
+ @d = @d.stroke(@marker_color)
+ @d = @d.stroke_width 1
+
+
+ (0..@column_count-1).each do |index|
+ rad_pos = index * Math::PI * 2 / @column_count
+
+ @d = @d.line(@center_x, @center_y, @center_x + Math::sin(rad_pos) * @radius, @center_y - Math::cos(rad_pos) * @radius)
+
+
+ marker_label = labels[index] ? labels[index].to_s : '000'
+
+ draw_label(@center_x, @center_y, rad_pos * 360 / (2 * Math::PI), @radius, marker_label)
+ end
+ end
+
+private
+
+ def draw_label(center_x, center_y, angle, radius, amount)
+ r_offset = 1.1
+ x_offset = center_x # + 15 # The label points need to be tweaked slightly
+ y_offset = center_y # + 0 # This one doesn't though
+ x = x_offset + (radius * r_offset * Math.sin(angle.deg2rad))
+ y = y_offset - (radius * r_offset * Math.cos(angle.deg2rad))
+
+ # Draw label
+ @d.fill = @marker_color
+ @d.font = @font if @font
+ @d.pointsize = scale_fontsize(20)
+ @d.stroke = 'transparent'
+ @d.font_weight = BoldWeight
+ s = angle.deg2rad / (2*Math::PI)
+ @d.gravity = SouthGravity if s >= 0.96 or s < 0.04
+ @d.gravity = SouthWestGravity if s >= 0.04 or s < 0.21
+ @d.gravity = WestGravity if s >= 0.21 or s < 0.29
+ @d.gravity = NorthWestGravity if s >= 0.29 or s < 0.46
+ @d.gravity = NorthGravity if s >= 0.46 or s < 0.54
+ @d.gravity = NorthEastGravity if s >= 0.54 or s < 0.71
+ @d.gravity = EastGravity if s >= 0.71 or s < 0.79
+ @d.gravity = SouthEastGravity if s >= 0.79 or s < 0.96
+# @d.gravity = NorthGravity
+ @d.annotate_scaled(@base_image, 0, 0, x, y, amount, @scale)
+ end
+
+end
+
+# # This method is already in Float
+# class Float
+# # Used for degree => radian conversions
+# def deg2rad
+# self * (Math::PI/180.0)
+# end
+# end
+
+
+
--- /dev/null
+require File.dirname(__FILE__) + '/base'
+
+# EXPERIMENTAL!
+#
+# Doesn't work yet.
+#
+class Gruff::PhotoBar < Gruff::Base
+
+# TODO
+#
+# define base and cap in yml
+# allow for image directory to be located elsewhere
+# more exact measurements for bar heights (go all the way to the bottom of the graph)
+# option to tile images instead of use a single image
+# drop base label a few px lower so photo bar graphs can have a base dropping over the lower marker line
+#
+
+ # The name of a pre-packaged photo-based theme.
+ attr_reader :theme
+
+# def initialize(target_width=800)
+# super
+# init_photo_bar_graphics()
+# end
+
+ def draw
+ super
+ return unless @has_data
+
+ return # TODO Remove for further development
+
+ init_photo_bar_graphics()
+
+ #Draw#define_clip_path()
+ #Draw#clip_path(pathname)
+ #Draw#composite....with bar graph image OverCompositeOp
+ #
+ # See also
+ #
+ # Draw.pattern # define an image to tile as the filling of a draw object
+ #
+
+ # Setup spacing.
+ #
+ # Columns sit side-by-side.
+ spacing_factor = 0.9
+ @bar_width = @norm_data[0][DATA_COLOR_INDEX].columns
+
+ @norm_data.each_with_index do |data_row, row_index|
+
+ data_row[1].each_with_index do |data_point, point_index|
+ data_point = 0 if data_point.nil?
+ # Use incremented x and scaled y
+ left_x = @graph_left + (@bar_width * (row_index + point_index + ((@data.length - 1) * point_index)))
+ left_y = @graph_top + (@graph_height - data_point * @graph_height) + 1
+ right_x = left_x + @bar_width * spacing_factor
+ right_y = @graph_top + @graph_height - 1
+
+ bar_image_width = data_row[DATA_COLOR_INDEX].columns
+ bar_image_height = right_y.to_f - left_y.to_f
+
+ # Crop to scale for data
+ bar_image = data_row[DATA_COLOR_INDEX].crop(0, 0, bar_image_width, bar_image_height)
+
+ @d.gravity = NorthWestGravity
+ @d = @d.composite(left_x, left_y, bar_image_width, bar_image_height, bar_image)
+
+ # Calculate center based on bar_width and current row
+ label_center = @graph_left + (@data.length * @bar_width * point_index) + (@data.length * @bar_width / 2.0)
+ draw_label(label_center, point_index)
+ end
+
+ end
+
+ @d.draw(@base_image)
+ end
+
+
+ # Return the chosen theme or the default
+ def theme
+ @theme || 'plastik'
+ end
+
+protected
+
+ # Sets up colors with a list of images that will be used.
+ # Images should be 340px tall
+ def init_photo_bar_graphics
+ color_list = Array.new
+ theme_dir = File.dirname(__FILE__) + '/../../assets/' + theme
+
+ Dir.open(theme_dir).each do |file|
+ next unless /\.png$/.match(file)
+ color_list << Image.read("#{theme_dir}/#{file}").first
+ end
+ @colors = color_list
+ end
+
+end
+
--- /dev/null
+
+require File.dirname(__FILE__) + '/base'
+
+class Gruff::Pie < Gruff::Base
+
+ TEXT_OFFSET_PERCENTAGE = 0.15
+
+ # Can be used to make the pie start cutting slices at the top (-90.0)
+ # or at another angle. Default is 0.0, which starts at 3 o'clock.
+ attr_accessor :zero_degree
+
+ def initialize_ivars
+ super
+ @zero_degree = 0.0
+ end
+
+ def draw
+ @hide_line_markers = true
+
+ super
+
+ return unless @has_data
+
+ diameter = @graph_height
+ radius = ([@graph_width, @graph_height].min / 2.0) * 0.8
+ top_x = @graph_left + (@graph_width - diameter) / 2.0
+ center_x = @graph_left + (@graph_width / 2.0)
+ center_y = @graph_top + (@graph_height / 2.0) - 10 # Move graph up a bit
+ total_sum = sums_for_pie()
+ prev_degrees = @zero_degree
+
+ # Use full data since we can easily calculate percentages
+ @data.sort{ |a, b| a[DATA_VALUES_INDEX][0] <=> b[DATA_VALUES_INDEX][0] }.each do |data_row|
+ if data_row[DATA_VALUES_INDEX][0] > 0
+ @d = @d.stroke data_row[DATA_COLOR_INDEX]
+ @d = @d.fill 'transparent'
+ @d.stroke_width(radius) # stroke width should be equal to radius. we'll draw centered on (radius / 2)
+
+ current_degrees = (data_row[DATA_VALUES_INDEX][0] / total_sum) * 360.0
+
+ # ellipse will draw the the stroke centered on the first two parameters offset by the second two.
+ # therefore, in order to draw a circle of the proper diameter we must center the stroke at
+ # half the radius for both x and y
+ @d = @d.ellipse(center_x, center_y,
+ radius / 2.0, radius / 2.0,
+ prev_degrees, prev_degrees + current_degrees + 0.5) # <= +0.5 'fudge factor' gets rid of the ugly gaps
+
+ half_angle = prev_degrees + ((prev_degrees + current_degrees) - prev_degrees) / 2
+
+ # End the string with %% to escape the single %.
+ # RMagick must use sprintf with the string and % has special significance.
+ label_string = ((data_row[DATA_VALUES_INDEX][0] / total_sum) * 100.0).round.to_s + '%%'
+ @d = draw_label(center_x,center_y,
+ half_angle, radius + (radius * TEXT_OFFSET_PERCENTAGE),
+ label_string)
+
+ prev_degrees += current_degrees
+ end
+ end
+
+ # TODO debug a circle where the text is drawn...
+
+ @d.draw(@base_image)
+ end
+
+private
+
+ ##
+ # Labels are drawn around a slightly wider ellipse to give room for
+ # labels on the left and right.
+ def draw_label(center_x, center_y, angle, radius, amount)
+ # TODO Don't use so many hard-coded numbers
+ r_offset = 20.0 # The distance out from the center of the pie to get point
+ x_offset = center_x # + 15.0 # The label points need to be tweaked slightly
+ y_offset = center_y # This one doesn't though
+ radius_offset = (radius + r_offset)
+ ellipse_factor = radius_offset * 0.15
+ x = x_offset + ((radius_offset + ellipse_factor) * Math.cos(angle.deg2rad))
+ y = y_offset + (radius_offset * Math.sin(angle.deg2rad))
+
+ # Draw label
+ @d.fill = @font_color
+ @d.font = @font if @font
+ @d.pointsize = scale_fontsize(@marker_font_size)
+ @d.stroke = 'transparent'
+ @d.font_weight = BoldWeight
+ @d.gravity = CenterGravity
+ @d.annotate_scaled( @base_image,
+ 0, 0,
+ x, y,
+ amount, @scale)
+ end
+
+ def sums_for_pie
+ total_sum = 0.0
+ @data.collect {|data_row| total_sum += data_row[DATA_VALUES_INDEX][0] }
+ total_sum
+ end
+
+end
+
+
+class Float
+ # Used for degree => radian conversions
+ def deg2rad
+ self * (Math::PI/180.0)
+ end
+end
--- /dev/null
+
+require "observer"
+require File.dirname(__FILE__) + '/base'
+
+##
+# A scene is a non-linear graph that assembles layers together to tell a story.
+# Layers are folders with appropriately named files (see below). You can group
+# layers and control them together or just set their values individually.
+#
+# Examples:
+#
+# * A city scene that changes with the time of day and the weather conditions.
+# * A traffic map that shows red lines on streets that are crowded and green on free-flowing ones.
+#
+# Usage:
+#
+# g = Gruff::Scene.new("500x100", "artwork/city_scene")
+# g.layers = %w(background haze sky clouds)
+# g.weather_group = %w(clouds)
+# g.time_group = %w(background sky)
+# g.weather = "cloudy"
+# g.time = Time.now
+# g.haze = true
+# g.write "hazy_daytime_city_scene.png"
+#
+#
+#
+# If there is a file named 'default.png', it will be selected (unless other values are provided to override it).
+#
+class Gruff::Scene < Gruff::Base
+
+ # An array listing the foldernames that will be rendered, from back to front.
+ #
+ # g.layers = %w(sky clouds buildings street people)
+ #
+ attr_reader :layers
+
+ def initialize(target_width, base_dir)
+ @base_dir = base_dir
+ @groups = {}
+ @layers = []
+ super target_width
+ end
+
+ def draw
+ # Join all the custom paths and filter out the empty ones
+ image_paths = @layers.map { |layer| layer.path }.select { |path| !path.empty? }
+ images = Magick::ImageList.new(*image_paths)
+ @base_image = images.flatten_images
+ end
+
+ def layers=(ordered_list)
+ ordered_list.each do |layer_name|
+ @layers << Gruff::Layer.new(@base_dir, layer_name)
+ end
+ end
+
+ # Group layers to input values
+ #
+ # g.weather_group = ["sky", "sea", "clouds"]
+ #
+ # Set input values
+ #
+ # g.weather = "cloudy"
+ #
+ def method_missing(method_name, *args)
+ case method_name.to_s
+ when /^(\w+)_group=$/
+ add_group $1, *args
+ return
+ when /^(\w+)=$/
+ set_input $1, args.first
+ return
+ end
+ super
+ end
+
+private
+
+ def add_group(input_name, layer_names)
+ @groups[input_name] = Gruff::Group.new(input_name, @layers.select { |layer| layer_names.include?(layer.name) })
+ end
+
+ def set_input(input_name, input_value)
+ if not @groups[input_name].nil?
+ @groups[input_name].send_updates(input_value)
+ else
+ if chosen_layer = @layers.detect { |layer| layer.name == input_name }
+ chosen_layer.update input_value
+ end
+ end
+ end
+
+end
+
+
+class Gruff::Group
+
+ include Observable
+ attr_reader :name
+
+ def initialize(folder_name, layers)
+ @name = folder_name
+ layers.each do |layer|
+ layer.observe self
+ end
+ end
+
+ def send_updates(value)
+ changed
+ notify_observers value
+ end
+
+end
+
+
+class Gruff::Layer
+
+ attr_reader :name
+
+ def initialize(base_dir, folder_name)
+ @base_dir = base_dir.to_s
+ @name = folder_name.to_s
+ @filenames = Dir.open(File.join(base_dir, folder_name)).entries.select { |file| file =~ /^[^.]+\.png$/ }
+ @selected_filename = select_default
+ end
+
+ # Register this layer so it receives updates from the group
+ def observe(obj)
+ obj.add_observer self
+ end
+
+ # Choose the appropriate filename for this layer, based on the input
+ def update(value)
+ @selected_filename = case value.to_s
+ when /^(true|false)$/
+ select_boolean value
+ when /^(\w|\s)+$/
+ select_string value
+ when /^-?(\d+\.)?\d+$/
+ select_numeric value
+ when /(\d\d):(\d\d):\d\d/
+ select_time "#{$1}#{$2}"
+ else
+ select_default
+ end
+ # Finally, try to use 'default' if we're still blank
+ @selected_filename ||= select_default
+ end
+
+ # Returns the full path to the selected image, or a blank string
+ def path
+ unless @selected_filename.nil? || @selected_filename.empty?
+ return File.join(@base_dir, @name, @selected_filename)
+ end
+ ''
+ end
+
+private
+
+ # Match "true.png" or "false.png"
+ def select_boolean(value)
+ file_exists_or_blank value.to_s
+ end
+
+ # Match -5 to _5.png
+ def select_numeric(value)
+ file_exists_or_blank value.to_s.gsub('-', '_')
+ end
+
+ def select_time(value)
+ times = @filenames.map { |filename| filename.gsub('.png', '') }
+ times.each_with_index do |time, index|
+ if (time > value) && (index > 0)
+ return "#{times[index - 1]}.png"
+ end
+ end
+ return "#{times.last}.png"
+ end
+
+ # Match "partly cloudy" to "partly_cloudy.png"
+ def select_string(value)
+ file_exists_or_blank value.to_s.gsub(' ', '_')
+ end
+
+ def select_default
+ @filenames.include?("default.png") ? "default.png" : ''
+ end
+
+ # Returns the string "#{filename}.png", if it exists.
+ #
+ # Failing that, it returns default.png, or '' if that doesn't exist.
+ def file_exists_or_blank(filename)
+ @filenames.include?("#{filename}.png") ? "#{filename}.png" : select_default
+ end
+
+end
--- /dev/null
+require File.dirname(__FILE__) + '/base'
+
+##
+# EXPERIMENTAL
+#
+# Graph with horizontal bars instead of vertical.
+#
+# TODO SideStackedBar should probably inherit from this
+# to consolidate the line marker drawing.
+class Gruff::SideBar < Gruff::Base
+
+ # Instead of base class version, draws vertical background lines and label
+ def draw_line_markers
+
+ return if @hide_line_markers
+
+ @d = @d.stroke_antialias false
+
+ # Draw horizontal line markers and annotate with numbers
+ @d = @d.stroke(@marker_color)
+ @d = @d.stroke_width 1
+ number_of_lines = 5
+
+ # TODO Round maximum marker value to a round number like 100, 0.1, 0.5, etc.
+ increment = significant(@maximum_value.to_f / number_of_lines)
+ (0..number_of_lines).each do |index|
+
+ line_diff = (@graph_right - @graph_left) / number_of_lines
+ x = @graph_right - (line_diff * index) - 1
+ @d = @d.line(x, @graph_bottom, x, @graph_top)
+ diff = index - number_of_lines
+ marker_label = diff.abs * increment
+
+ unless @hide_line_numbers
+ @d.fill = @font_color
+ @d.font = @font if @font
+ @d.stroke = 'transparent'
+ @d.pointsize = scale_fontsize(@marker_font_size)
+ # @d.gravity = NorthGravity
+ @d = @d.annotate_scaled( @base_image,
+ 100, 20,
+ x - (@marker_font_size/1.5), @graph_bottom + 40,
+ marker_label.to_s, @scale)
+ end # unless
+ @d = @d.stroke_antialias true
+ end
+ end
+
+ ##
+ # Draw on the Y axis instead of the X
+
+ def draw_label(y_offset, index)
+ if !@labels[index].nil? && @labels_seen[index].nil?
+ @d.fill = @font_color
+ @d.font = @font if @font
+ @d.stroke = 'transparent'
+ @d.font_weight = NormalWeight
+ @d.pointsize = scale_fontsize(@marker_font_size)
+ @d.gravity = EastGravity
+ @d = @d.annotate_scaled(@base_image,
+ 1, 1,
+ -@graph_left + LABEL_MARGIN * 2.0, y_offset,
+ @labels[index], @scale)
+ @labels_seen[index] = 1
+ end
+ end
+
+ def draw
+ @has_left_labels = true
+ super
+
+ return unless @has_data
+
+ # Setup spacing.
+ #
+ # Columns sit stacked.
+ spacing_factor = 0.9
+
+ @bar_width = @graph_height / @column_count.to_f
+ @d = @d.stroke_opacity 0.0
+ height = Array.new(@column_count, 0)
+ length = Array.new(@column_count, @graph_left)
+
+ @norm_data.each_with_index do |data_row, row_index|
+ @d = @d.fill data_row[DATA_COLOR_INDEX]
+
+ data_row[1].each_with_index do |data_point, point_index|
+
+ # Using the original calcs from the stacked bar chart
+ # to get the difference between
+ # part of the bart chart we wish to stack.
+ temp1 = @graph_left + (@graph_width -
+ data_point * @graph_width -
+ height[point_index]) + 1
+ temp2 = @graph_left + @graph_width - height[point_index] - 1
+ difference = temp2 - temp1
+
+ left_x = length[point_index] #+ 1
+ left_y = @graph_top + (@bar_width * point_index)
+ right_x = left_x + difference
+ right_y = left_y + @bar_width * spacing_factor
+ length[point_index] += difference
+ height[point_index] += (data_point * @graph_width - 2)
+
+ @d = @d.rectangle(left_x, left_y, right_x, right_y)
+
+ # Calculate center based on bar_width and current row
+ label_center = @graph_top + (@bar_width * point_index) + (@bar_width * spacing_factor / 2.0)
+ draw_label(label_center, point_index)
+ end
+
+ end
+
+ @d.draw(@base_image)
+ end
+
+end
--- /dev/null
+##
+# New gruff graph type added to enable sideways stacking bar charts (basically looks like a x/y
+# flip of a standard stacking bar chart)
+#
+# alun.eyre@googlemail.com
+#
+require File.dirname(__FILE__) + '/base'
+
+class Gruff::SideStackedBar < Gruff::Base
+
+ # instead of base class version, draws vertical background lines and label
+ def draw_line_markers
+
+ return if @hide_line_markers
+
+ # Draw horizontal line markers and annotate with numbers
+ @d = @d.stroke(@marker_color)
+ @d = @d.stroke_width 1
+ number_of_lines = 5
+
+ # TODO Round maximum marker value to a round number like 100, 0.1, 0.5, etc.
+ increment = significant(@maximum_value.to_f / number_of_lines)
+ (0..number_of_lines).each do |index|
+
+ line_diff = (@graph_right - @graph_left) / number_of_lines
+ x = @graph_right - (line_diff * index) - 1
+ @d = @d.line(x, @graph_bottom, x, @graph_top)
+
+ diff = index - number_of_lines
+ marker_label = diff.abs * increment
+
+ @d.fill = @marker_color
+ @d.font = @font if @font
+ @d.stroke = 'transparent'
+ @d.pointsize = scale_fontsize(@marker_font_size)
+# @d.gravity = NorthGravity
+ @d = @d.annotate_scaled( @base_image,
+ 100, 20,
+ x - (@marker_font_size/1.5), @graph_bottom + 40,
+ marker_label.to_s, @scale)
+
+ end
+ end
+
+ # instead of base class version, modified to enable us to draw on the Y axis instead of X
+ def draw_label(y_offset, index)
+ if !@labels[index].nil? && @labels_seen[index].nil?
+ @d.fill = @marker_color
+ @d.font = @font if @font
+ @d.stroke = 'transparent'
+ @d.font_weight = NormalWeight
+ @d.pointsize = scale_fontsize(@marker_font_size)
+ @d.gravity = CenterGravity
+ @d = @d.annotate_scaled(@base_image,
+ 1, 1,
+ @graph_left / 2, y_offset,
+ @labels[index], @scale)
+ @labels_seen[index] = 1
+ end
+ end
+
+ def draw
+ @has_left_labels = true
+ get_maximum_by_stack
+ super
+
+ return unless @has_data
+
+ # Setup spacing.
+ #
+ # Columns sit stacked.
+ spacing_factor = 0.9
+
+ @bar_width = @graph_height / @column_count.to_f
+ @d = @d.stroke_opacity 0.0
+ height = Array.new(@column_count, 0)
+ length = Array.new(@column_count, @graph_left)
+
+ @norm_data.each_with_index do |data_row, row_index|
+ @d = @d.fill data_row[DATA_COLOR_INDEX]
+
+ data_row[1].each_with_index do |data_point, point_index|
+
+ ## using the original calcs from the stacked bar chart to get the difference between
+ ## part of the bart chart we wish to stack.
+ temp1 = @graph_left + (@graph_width -
+ data_point * @graph_width -
+ height[point_index]) + 1
+ temp2 = @graph_left + @graph_width - height[point_index] - 1
+ difference = temp2 - temp1
+
+ left_x = length[point_index] #+ 1
+ left_y = @graph_top + (@bar_width * point_index)
+ right_x = left_x + difference
+ right_y = left_y + @bar_width * spacing_factor
+ length[point_index] += difference
+ height[point_index] += (data_point * @graph_width - 2)
+
+ @d = @d.rectangle(left_x, left_y, right_x, right_y)
+
+ # Calculate center based on bar_width and current row
+ label_center = @graph_top + (@bar_width * point_index) + (@bar_width * spacing_factor / 2.0)
+ draw_label(label_center, point_index)
+ end
+
+ end
+
+ @d.draw(@base_image)
+ end
+
+ protected
+
+ def larger_than_max?(data_point, index=0)
+ max(data_point, index) > @maximum_value
+ end
+
+ def max(data_point, index)
+ @data.inject(0) {|sum, item| sum + item[1][index]}
+ end
+
+end
--- /dev/null
+
+require File.dirname(__FILE__) + '/base'
+
+# Experimental!!! See also the Net graph.
+#
+# Submitted by Kevin Clark http://glu.ttono.us/
+class Gruff::Spider < Gruff::Base
+
+ # Hide all text
+ attr_reader :hide_text
+ attr_accessor :hide_axes
+ attr_reader :transparent_background
+
+ def transparent_background=(value)
+ @transparent_background = value
+ @base_image = render_transparent_background if value
+ end
+
+ def hide_text=(value)
+ @hide_title = @hide_text = value
+ end
+
+ def initialize(max_value, target_width = 800)
+ super(target_width)
+ @max_value = max_value
+ @hide_legend = true;
+ end
+
+ def draw
+ @hide_line_markers = true
+
+ super
+
+ return unless @has_data
+
+ # Setup basic positioning
+ diameter = @graph_height
+ radius = @graph_height / 2.0
+ top_x = @graph_left + (@graph_width - diameter) / 2.0
+ center_x = @graph_left + (@graph_width / 2.0)
+ center_y = @graph_top + (@graph_height / 2.0) - 25 # Move graph up a bit
+
+ @unit_length = radius / @max_value
+
+
+ total_sum = sums_for_spider
+ prev_degrees = 0.0
+ additive_angle = (2 * Math::PI)/ @data.size
+
+ current_angle = 0.0
+
+ # Draw axes
+ draw_axes(center_x, center_y, radius, additive_angle) unless hide_axes
+
+ # Draw polygon
+ draw_polygon(center_x, center_y, additive_angle)
+
+
+ @d.draw(@base_image)
+ end
+
+private
+
+ def normalize_points(value)
+ value * @unit_length
+ end
+
+ def draw_label(center_x, center_y, angle, radius, amount)
+ r_offset = 50 # The distance out from the center of the pie to get point
+ x_offset = center_x # The label points need to be tweaked slightly
+ y_offset = center_y + 0 # This one doesn't though
+ x = x_offset + ((radius + r_offset) * Math.cos(angle))
+ y = y_offset + ((radius + r_offset) * Math.sin(angle))
+
+ # Draw label
+ @d.fill = @marker_color
+ @d.font = @font if @font
+ @d.pointsize = scale_fontsize(legend_font_size)
+ @d.stroke = 'transparent'
+ @d.font_weight = BoldWeight
+ @d.gravity = CenterGravity
+ @d.annotate_scaled( @base_image,
+ 0, 0,
+ x, y,
+ amount, @scale)
+ end
+
+ def draw_axes(center_x, center_y, radius, additive_angle, line_color = nil)
+ return if hide_axes
+
+ current_angle = 0.0
+
+ @data.each do |data_row|
+ @d.stroke(line_color || data_row[DATA_COLOR_INDEX])
+ @d.stroke_width 5.0
+
+ x_offset = radius * Math.cos(current_angle)
+ y_offset = radius * Math.sin(current_angle)
+
+ @d.line(center_x, center_y,
+ center_x + x_offset,
+ center_y + y_offset)
+
+ draw_label(center_x, center_y, current_angle, radius, data_row[0].to_s) unless hide_text
+
+ current_angle += additive_angle
+ end
+ end
+
+ def draw_polygon(center_x, center_y, additive_angle, color = nil)
+ points = []
+ current_angle = 0.0
+ @data.each do |data_row|
+ points << center_x + normalize_points(data_row[1][0]) * Math.cos(current_angle)
+ points << center_y + normalize_points(data_row[1][0]) * Math.sin(current_angle)
+ current_angle += additive_angle
+ end
+
+ @d.stroke_width 1.0
+ @d.stroke(color || @marker_color)
+ @d.fill(color || @marker_color)
+ @d.fill_opacity 0.4
+ @d.polygon(*points)
+ end
+
+ def sums_for_spider
+ @data.inject(0.0) {|sum, data_row| sum += data_row[1][0]}
+ end
+
+end
--- /dev/null
+
+require File.dirname(__FILE__) + '/base'
+
+class Gruff::StackedBar < Gruff::Base
+
+ # Draws a bar graph, but multiple sets are stacked on top of each other.
+ def draw
+ get_maximum_by_stack
+ super
+ return unless @has_data
+
+ # Setup spacing.
+ #
+ # Columns sit stacked.
+ spacing_factor = 0.9
+ @bar_width = @graph_width / @column_count.to_f
+
+ @d = @d.stroke_opacity 0.0
+
+ height = Array.new(@column_count, 0)
+
+ @norm_data.each_with_index do |data_row, row_index|
+ @d = @d.fill data_row[DATA_COLOR_INDEX]
+
+ data_row[1].each_with_index do |data_point, point_index|
+ # Use incremented x and scaled y
+ left_x = @graph_left + (@bar_width * point_index)
+ left_y = @graph_top + (@graph_height -
+ data_point * @graph_height -
+ height[point_index]) + 1
+ right_x = left_x + @bar_width * spacing_factor
+ right_y = @graph_top + @graph_height - height[point_index] - 1
+
+ # update the total height of the current stacked bar
+ height[point_index] += (data_point * @graph_height - 2)
+
+ @d = @d.rectangle(left_x, left_y, right_x, right_y)
+
+ # Calculate center based on bar_width and current row
+ label_center = @graph_left + (@bar_width * point_index) + (@bar_width * spacing_factor / 2.0)
+ draw_label(label_center, point_index)
+ end
+
+ end
+
+ @d.draw(@base_image)
+ end
+
+end
--- /dev/null
+$:.unshift(File.dirname(__FILE__) + "/../lib/")
+
+require 'test/unit'
+require 'gruff'
+# require 'test_timer'
+
+class GruffTestCase < Test::Unit::TestCase
+
+ def setup
+ @datasets = [
+ [:Jimmy, [25, 36, 86, 39, 25, 31, 79, 88]],
+ [:Charles, [80, 54, 67, 54, 68, 70, 90, 95]],
+ [:Julie, [22, 29, 35, 38, 36, 40, 46, 57]],
+ [:Jane, [95, 95, 95, 90, 85, 80, 88, 100]],
+ [:Philip, [90, 34, 23, 12, 78, 89, 98, 88]],
+ ["Arthur", [5, 10, 13, 11, 6, 16, 22, 32]],
+ ]
+
+ @labels = {
+ 0 => '5/6',
+ 1 => '5/15',
+ 2 => '5/24',
+ 3 => '5/30',
+ 4 => '6/4',
+ 5 => '6/12',
+ 6 => '6/21',
+ 7 => '6/28',
+ }
+ end
+
+ def setup_single_dataset
+ @datasets = [
+ [:Jimmy, [25, 36, 86]]
+ ]
+
+ @labels = {
+ 0 => 'You',
+ 1 => 'Average',
+ 2 => 'Lifetime'
+ }
+ end
+
+ def setup_wide_dataset
+ @datasets = [
+ ["Auto", 25],
+ ["Food", 5],
+ ["Entertainment", 15]
+ ]
+
+ @labels = { 0 => 'This Month' }
+ end
+
+ def test_dummy
+ assert true
+ end
+
+protected
+
+ # Generate graphs at several sizes.
+ #
+ # Also writes the graph to disk.
+ #
+ # graph_sized 'bar_basic' do |g|
+ # g.data('students', [1, 2, 3, 4])
+ # end
+ #
+ def graph_sized(filename, sizes=['', 400])
+ class_name = self.class.name.gsub(/^TestGruff/, '')
+ Array(sizes).each do |size|
+ g = instance_eval("Gruff::#{class_name}.new #{size}")
+ g.title = "#{class_name} Graph"
+ yield g
+ write_test_file g, "#{filename}_#{size}.png"
+ end
+ end
+
+ def write_test_file(graph, filename)
+ graph.write(File.dirname(__FILE__) + "/output/#{filename}")
+ end
+
+ ##
+ # Example:
+ #
+ # setup_basic_graph Gruff::Pie, 400
+ #
+ def setup_basic_graph(*args)
+ klass, size = Gruff::Bar, 400
+ # Allow args to be klass, size or just klass or just size.
+ #
+ # TODO Refactor
+ case args.length
+ when 1
+ case args[0]
+ when Fixnum
+ size = args[0]
+ klass = eval("Gruff::#{self.class.name.gsub(/^TestGruff/, '')}")
+ when String
+ size = args[0]
+ klass = eval("Gruff::#{self.class.name.gsub(/^TestGruff/, '')}")
+ else
+ klass = args[0]
+ end
+ when 2
+ klass, size = args[0], args[1]
+ end
+
+ g = klass.new(size)
+ g.title = "My Bar Graph"
+ g.labels = @labels
+
+
+ @datasets.each do |data|
+ g.data(data[0], data[1])
+ end
+ g
+ end
+
+end
--- /dev/null
+require File.dirname(__FILE__) + "/gruff_test_case"
+
+class TestGruffAccumulatorBar < GruffTestCase
+
+ # TODO Delete old output files once when starting tests
+
+ def setup
+ @datasets = [
+ (1..20).to_a.map { rand(10) }
+ ]
+ end
+
+ def test_accumulator
+ g = Gruff::AccumulatorBar.new 500
+ g.title = "Your Savings"
+ g.hide_legend = true
+
+ # g.font = File.expand_path(File.dirname(__FILE__) + "/../assets/fonts/ATMA____.TTF")
+
+ g.marker_font_size = 18
+
+ g.theme = {
+ :colors => ['#aedaa9', '#12a702'], # 3077a9 blue, aedaa9 light green
+ :marker_color => '#dddddd',
+ :font_color => 'black',
+ :background_colors => "white"
+ # :background_image => File.expand_path(File.dirname(__FILE__) + "/../assets/backgrounds/43things.png")
+ }
+
+ # Attempt at negative numbers
+ # g.data 'Savings', (1..20).to_a.map { rand(10) * (rand(2) > 0 ? 1 : -1) }
+ g.data 'Savings', (1..12).to_a.map { rand(100) }
+ g.labels = (0..11).to_a.inject({}) {|memo, index| {index => '12-26'}.merge(memo)}
+
+ g.maximum_value = 1000
+ g.minimum_value = 0
+
+ g.write("test/output/accum_bar.png")
+ end
+
+ def test_too_many_args
+ assert_raise(Gruff::IncorrectNumberOfDatasetsException) {
+ g = Gruff::AccumulatorBar.new
+ g.data 'First', [1,1,1]
+ g.data 'Too Many', [1,1,1]
+ g.write("test/output/_SHOULD_NOT_ACTUALLY_BE_WRITTEN.png")
+ }
+ end
+
+end
--- /dev/null
+#!/usr/bin/ruby
+
+require File.dirname(__FILE__) + "/gruff_test_case"
+
+class TestGruffArea < GruffTestCase
+
+ def setup
+ @datasets = [
+ [:Jimmy, [25, 36, 86, 39, 25, 31, 79, 88]],
+ [:Charles, [80, 54, 67, 54, 68, 70, 90, 95]],
+ [:Julie, [22, 29, 35, 38, 36, 40, 46, 57]],
+ [:Jane, [95, 95, 95, 90, 85, 80, 88, 100]],
+ [:Philip, [90, 34, 23, 12, 78, 89, 98, 88]],
+ ["Arthur", [5, 10, 13, 11, 6, 16, 22, 32]],
+ ]
+ @sample_labels = {
+ 0 => '5/6',
+ 1 => '5/15',
+ 2 => '5/24',
+ 3 => '5/30',
+ 4 => '6/4',
+ 5 => '6/12',
+ 6 => '6/21',
+ 7 => '6/28',
+ }
+
+ end
+
+ def test_area_graph
+ g = Gruff::Area.new
+ g.title = "Visual Multi-Area Graph Test"
+ g.labels = {
+ 0 => '5/6',
+ 2 => '5/15',
+ 4 => '5/24',
+ 6 => '5/30',
+ }
+ @datasets.each do |data|
+ g.data(data[0], data[1])
+ end
+
+ # Default theme
+ g.write("test/output/area_keynote.png")
+ end
+
+ def test_resize
+ g = Gruff::Area.new(400)
+ g.title = "Small Size Multi-Area Graph Test"
+ g.labels = {
+ 0 => '5/6',
+ 2 => '5/15',
+ 4 => '5/24',
+ 6 => '5/30',
+ }
+ @datasets.each do |data|
+ g.data(data[0], data[1])
+ end
+
+ # Default theme
+ g.write("test/output/area_keynote_small.png")
+ end
+
+ def test_many_datapoints
+ g = Gruff::Area.new
+ g.title = "Many Multi-Area Graph Test"
+ g.labels = {
+ 0 => 'June',
+ 10 => 'July',
+ 30 => 'August',
+ 50 => 'September',
+ }
+ g.data('many points', (0..50).collect {|i| rand(100) })
+
+ # Default theme
+ g.write("test/output/area_many.png")
+ end
+
+ def test_many_areas_graph_small
+ g = Gruff::Area.new(400)
+ g.title = "Many Values Area Test 400px"
+ g.labels = {
+ 0 => '5/6',
+ 10 => '5/15',
+ 20 => '5/24',
+ 30 => '5/30',
+ 40 => '6/4',
+ 50 => '6/16'
+ }
+ %w{jimmy jane philip arthur julie bert}.each do |student_name|
+ g.data(student_name, (0..50).collect { |i| rand 100 })
+ end
+
+ # Default theme
+ g.write("test/output/area_many_areas_small.png")
+ end
+
+ def test_area_graph_tiny
+ g = Gruff::Area.new(300)
+ g.title = "Area Test 300px"
+ g.labels = {
+ 0 => '5/6',
+ 10 => '5/15',
+ 20 => '5/24',
+ 30 => '5/30',
+ 40 => '6/4',
+ 50 => '6/16'
+ }
+ %w{jimmy jane philip arthur julie bert}.each do |student_name|
+ g.data(student_name, (0..50).collect { |i| rand 100 })
+ end
+
+ # Default theme
+ g.write("test/output/area_tiny.png")
+ end
+
+ def test_wide
+ g = setup_basic_graph('800x400')
+ g.title = "Area Wide"
+ g.write("test/output/area_wide.png")
+ end
+
+protected
+
+ def setup_basic_graph(size=800)
+ g = Gruff::Area.new(size)
+ g.title = "My Graph Title"
+ g.labels = @sample_labels
+ @datasets.each do |data|
+ g.data(data[0], data[1])
+ end
+ return g
+ end
+
+end
--- /dev/null
+#!/usr/bin/ruby
+
+require File.dirname(__FILE__) + "/gruff_test_case"
+
+class TestGruffBar < GruffTestCase
+
+ # TODO Delete old output files once when starting tests
+
+ def setup
+ @datasets = [
+ [:Jimmy, [25, 36, 86, 39]],
+ [:Charles, [80, 54, 67, 54]],
+ [:Julie, [22, 29, 35, 38]],
+ #[:Jane, [95, 95, 95, 90, 85, 80, 88, 100]],
+ #[:Philip, [90, 34, 23, 12, 78, 89, 98, 88]],
+ #["Arthur", [5, 10, 13, 11, 6, 16, 22, 32]],
+ ]
+ end
+
+ def test_bar_graph
+ g = setup_basic_graph
+ g.title = "Bar Graph Test"
+ g.write("test/output/bar_keynote.png")
+
+ g = setup_basic_graph
+ g.title = "Visual Multi-Line Bar Graph Test"
+ g.theme_rails_keynote
+ g.write("test/output/bar_rails_keynote.png")
+
+ g = setup_basic_graph
+ g.title = "Visual Multi-Line Bar Graph Test"
+ g.theme_odeo
+ g.write("test/output/bar_odeo.png")
+ end
+
+ def test_bar_graph_set_colors
+ g = Gruff::Bar.new
+ g.title = "Bar Graph With Manual Colors"
+ g.labels = {
+ 0 => '5/6',
+ 1 => '5/15',
+ 2 => '5/24',
+ 3 => '5/30',
+ }
+ g.data(:Art, [0, 5, 8, 15], '#990000')
+ g.data(:Philosophy, [10, 3, 2, 8], '#009900')
+ g.data(:Science, [2, 15, 8, 11], '#990099')
+
+ g.minimum_value = 0
+
+ g.write("test/output/bar_manual_colors.png")
+ end
+
+ def test_bar_graph_small
+ g = Gruff::Bar.new(400)
+ g.title = "Visual Multi-Line Bar Graph Test"
+ g.labels = {
+ 0 => '5/6',
+ 1 => '5/15',
+ 2 => '5/24',
+ 3 => '5/30',
+ }
+ @datasets.each do |data|
+ g.data(data[0], data[1])
+ end
+
+ g.write("test/output/bar_keynote_small.png")
+ end
+
+ # Somewhat worthless test. Should an error be thrown?
+ # def test_nil_font
+ # g = setup_basic_graph 400
+ # g.title = "Nil Font"
+ # g.font = nil
+ # g.write "test/output/bar_nil_font.png"
+ # end
+
+
+ def test_no_line_markers
+ g = setup_basic_graph(400)
+ g.title = "No Line Markers"
+ g.hide_line_markers = true
+ g.write("test/output/bar_no_line_markers.png")
+ end
+
+ def test_no_legend
+ g = setup_basic_graph(400)
+ g.title = "No Legend"
+ g.hide_legend = true
+ g.write("test/output/bar_no_legend.png")
+ end
+
+ def test_no_title
+ g = setup_basic_graph(400)
+ g.title = "No Title"
+ g.hide_title = true
+ g.write("test/output/bar_no_title.png")
+ end
+
+ def test_no_title_or_legend
+ g = setup_basic_graph(400)
+ g.title = "No Title or Legend"
+ g.hide_legend = true
+ g.hide_title = true
+ g.write("test/output/bar_no_title_or_legend.png")
+ end
+
+ def test_set_marker_count
+ g = setup_basic_graph(400)
+ g.title = "Set marker"
+ g.marker_count = 10
+ g.write("test/output/bar_set_marker.png")
+ end
+
+ def test_set_legend_box_size
+ g = setup_basic_graph(400)
+ g.title = "Set Legend Box Size"
+ g.legend_box_size = 10.0
+ g.write("test/output/bar_set_legend_box_size_sm.png")
+
+ g = setup_basic_graph(400)
+ g.title = "Set Legend Box Size"
+ g.legend_box_size = 50.0
+ g.write("test/output/bar_set_legend_box_size_lg.png")
+ end
+
+ def test_x_y_labels
+ g = setup_basic_graph(400)
+ g.title = "X Y Labels"
+ g.x_axis_label = 'Score (%)'
+ g.y_axis_label = "Students"
+ g.write("test/output/bar_x_y_labels.png")
+ end
+
+ def test_wide_graph
+ g = setup_basic_graph('800x400')
+ g.title = "Wide Graph"
+ g.write("test/output/bar_wide_graph.png")
+
+ g = setup_basic_graph('400x200')
+ g.title = "Wide Graph Small"
+ g.write("test/output/bar_wide_graph_small.png")
+ end
+
+
+ def test_tall_graph
+ g = setup_basic_graph('400x600')
+ g.title = "Tall Graph"
+ g.write("test/output/bar_tall_graph.png")
+
+ g = setup_basic_graph('200x400')
+ g.title = "Tall Graph Small"
+ g.write("test/output/bar_tall_graph_small.png")
+ end
+
+
+ def test_one_value
+ g = Gruff::Bar.new
+ g.title = "One Value Graph Test"
+ g.labels = {
+ 0 => '1',
+ 1 => '2'
+ }
+ g.data('one', [1,1])
+
+ g.write("test/output/bar_one_value.png")
+ end
+
+
+ def test_negative
+ g = Gruff::Bar.new
+ g.title = "Pos/Neg Bar Graph Test"
+ g.labels = {
+ 0 => '5/6',
+ 1 => '5/15',
+ 2 => '5/24',
+ 3 => '5/30',
+ }
+ g.data(:apples, [-1, 0, 4, -4])
+ g.data(:peaches, [10, 8, 6, 3])
+
+ g.write("test/output/bar_pos_neg.png")
+ end
+
+
+ def test_nearly_zero
+ g = Gruff::Bar.new
+ g.title = "Nearly Zero Graph"
+ g.labels = {
+ 0 => '5/6',
+ 1 => '5/15',
+ 2 => '5/24',
+ 3 => '5/30',
+ }
+ g.data(:apples, [1, 2, 3, 4])
+ g.data(:peaches, [4, 3, 2, 1])
+ g.minimum_value = 0
+ g.maximum_value = 10
+ g.write("test/output/bar_nearly_zero_max_10.png")
+ end
+
+ def test_y_axis_increment
+ generate_with_y_axis_increment 2.0
+ generate_with_y_axis_increment 1
+ generate_with_y_axis_increment 5
+ generate_with_y_axis_increment 20
+ end
+
+ def generate_with_y_axis_increment(increment)
+ g = Gruff::Bar.new
+ g.title = "Y Axis Set to #{increment}"
+ g.labels = {
+ 0 => '5/6',
+ 1 => '5/15',
+ 2 => '5/24',
+ 3 => '5/30',
+ }
+ g.y_axis_increment = increment
+ g.data(:apples, [1, 0.2, 0.5, 0.7])
+ g.data(:peaches, [2.5, 2.3, 2, 6.1])
+ g.write("test/output/bar_y_increment_#{increment}.png")
+ end
+
+
+ def test_custom_theme
+ g = Gruff::Bar.new
+ g.title = "Custom Theme"
+ g.font = File.expand_path('CREABBRG.TTF', ENV['MAGICK_FONT_PATH'])
+ g.title_font_size = 60
+ g.legend_font_size = 32
+ g.marker_font_size = 32
+ g.theme = {
+ :colors => %w(#efd250 #666699 #e5573f #9595e2),
+ :marker_color => 'white',
+ :font_color => 'blue',
+ :background_image => "assets/pc306715.jpg"
+ }
+ g.labels = {
+ 0 => '5/6',
+ 1 => '5/15',
+ 2 => '5/24',
+ 3 => '5/30',
+ }
+ g.data(:vancouver, [1, 2, 3, 4])
+ g.data(:seattle, [2, 4, 6, 8])
+ g.data(:portland, [3, 1, 7, 3])
+ g.data(:victoria, [4, 3, 5, 7])
+ g.minimum_value = 0
+ g.write("test/output/bar_themed.png")
+ end
+
+ def test_july_enhancements
+ g = Gruff::Bar.new(600)
+ g.hide_legend = true
+ g.title = "Full speed ahead"
+ g.labels = (0..10).inject({}) { |memo, i| memo.merge({ i => (i*10).to_s}) }
+ g.data(:apples, (0..9).map { rand(20)/10.0 })
+ g.y_axis_increment = 1.0
+ g.x_axis_label = 'Score (%)'
+ g.y_axis_label = 'Students'
+ write_test_file g, 'enhancements.png'
+ end
+
+
+protected
+
+ def setup_basic_graph(size=800)
+ g = Gruff::Bar.new(size)
+ g.title = "My Bar Graph"
+ g.labels = {
+ 0 => '5/6',
+ 1 => '5/15',
+ 2 => '5/24',
+ 3 => '5/30',
+ }
+ @datasets.each do |data|
+ g.data(data[0], data[1])
+ end
+ g
+ end
+
+
+end
+
--- /dev/null
+#!/usr/bin/ruby
+
+require File.dirname(__FILE__) + "/gruff_test_case"
+
+class TestGruffBase < GruffTestCase
+
+
+end
\ No newline at end of file
--- /dev/null
+require File.dirname(__FILE__) + "/gruff_test_case"
+
+class TestGruffLegend < GruffTestCase
+
+ def setup
+ @datasets = [
+ [:Jimmy, [25, 36, 86, 39, 25, 31, 79, 88]],
+ [:Charles, [80, 54, 67, 54, 68, 70, 90, 95]],
+ [:Julie, [22, 29, 35, 38, 36, 40, 46, 57]],
+ [:Jane, [95, 95, 95, 90, 85, 80, 88, 100]],
+ [:Philip, [90, 34, 23, 12, 78, 89, 98, 88]],
+ ["Arthur", [5, 10, 13, 11, 6, 16, 22, 32]],
+ ["Vincent", [5, 10, 13, 11, 6, 16, 22, 32]],
+ ["Jake", [5, 10, 13, 11, 6, 16, 22, 32]],
+ ["Stephen", [5, 10, 13, 11, 6, 16, 22, 32]],
+ ]
+
+ @sample_labels = {
+ 0 => '5/6',
+ 1 => '5/15',
+ 2 => '5/24',
+ 3 => '5/30',
+ 4 => '6/4',
+ 5 => '6/12',
+ 6 => '6/21',
+ 7 => '6/28',
+ }
+ end
+
+ ## TODO Fix implementation
+
+ # def test_bar_legend_wrap
+ # [800, 400].each do |width|
+ # [nil, 4, 16, 30].each do |font_size|
+ # g = Gruff::Bar.new(width)
+ # g.title = "Wrapped Legend Bar Test #{font_size}pts #{width}px"
+ # g.labels = @sample_labels
+ # 0xEFD250.step(0xFF0000, 60) do |num|
+ # g.colors << "#%x" % num
+ # end
+ #
+ # @datasets.each do |data|
+ # g.data(data[0], data[1])
+ # end
+ #
+ # g.legend_font_size = font_size unless font_size.nil?
+ # g.write("test/output/bar_wrapped_legend_#{font_size}_#{width}.png")
+ # end
+ # end
+ # end
+ #
+ # def test_pie_legend_wrap
+ # [800, 400].each do |width|
+ # [nil, 4, 16, 30].each do |font_size|
+ # g = Gruff::Pie.new(width)
+ # g.title = "Wrapped Legend Pie Test #{font_size}pts #{width}px"
+ # g.labels = @sample_labels
+ # 0xEFD250.step(0xFF0000, 60) do |num|
+ # g.colors << "#%x" % num
+ # end
+ #
+ # @datasets.each do |data|
+ # g.data(data[0], data[1])
+ # end
+ #
+ # g.legend_font_size = font_size unless font_size.nil?
+ # g.write("test/output/pie_wrapped_legend_#{font_size}_#{width}.png")
+ # end
+ # end
+ # end
+end
--- /dev/null
+#!/usr/bin/ruby
+
+require File.dirname(__FILE__) + "/gruff_test_case"
+
+class TestGruffLine < GruffTestCase
+
+ # TODO Delete old output files once when starting tests
+
+ def test_line_graph_with_themes
+ line_graph_with_themes()
+ line_graph_with_themes(400)
+ end
+
+ def test_one_value
+ g = Gruff::Line.new
+ g.title = "One Value"
+ g.labels = {
+ 0 => '1',
+ 1 => '2'
+ }
+ g.data('one', 1)
+
+ g.write("test/output/line_one_value.png")
+ end
+
+ def test_one_value_array
+ g = Gruff::Line.new
+ g.title = "One Value in an Array"
+ g.labels = {
+ 0 => '1',
+ 1 => '2'
+ }
+ g.data('one', [1])
+
+ g.write("test/output/line_one_value_array.png")
+ end
+
+
+ def test_should_not_hang_with_0_0_100
+ g = Gruff::Line.new(320)
+ g.title = "Hang Value Graph Test"
+ g.data('test', [0,0,100])
+
+ g.write("test/output/line_hang_value.png")
+ end
+
+ # TODO
+ # def test_fix_crash
+ # g = Gruff::Line.new(370)
+ # g.title = "Crash Test"
+ # g.data "ichi", [5]
+ # g.data "ni", [0]
+ # g.data "san", [0]
+ # g.data "shi", [0]
+ # g.write("test/output/line_crash_fix_test.png")
+ # end
+
+
+ def test_line_small_values
+ @datasets = [
+ [:small, [0.1, 0.14356, 0.0, 0.5674839, 0.456]],
+ [:small2, [0.2, 0.3, 0.1, 0.05, 0.9]]
+ ]
+
+ g = Gruff::Line.new
+ g.title = "Small Values Line Graph Test"
+ @datasets.each do |data|
+ g.data(data[0], data[1])
+ end
+ g.write("test/output/line_small.png")
+
+ g = Gruff::Line.new(400)
+ g.title = "Small Values Line Graph Test 400px"
+ @datasets.each do |data|
+ g.data(data[0], data[1])
+ end
+ g.write("test/output/line_small_small.png")
+ end
+
+ def test_line_starts_with_zero
+ @datasets = [
+ [:first0, [0, 5, 10, 8, 18]],
+ [:normal, [1, 2, 3, 4, 5]]
+ ]
+
+ g = Gruff::Line.new
+ g.title = "Small Values Line Graph Test"
+ @datasets.each do |data|
+ g.data(data[0], data[1])
+ end
+ g.write("test/output/line_small_zero.png")
+
+ g = Gruff::Line.new(400)
+ g.title = "Small Values Line Graph Test 400px"
+ @datasets.each do |data|
+ g.data(data[0], data[1])
+ end
+ g.write("test/output/line_small_small_zero.png")
+ end
+
+
+ def test_line_large_values
+ @datasets = [
+ [:large, [100_005, 35_000, 28_000, 27_000]],
+ [:large2, [35_000, 28_000, 27_000, 100_005]],
+ [:large3, [28_000, 27_000, 100_005, 35_000]],
+ [:large4, [1_238, 39_092, 27_938, 48_876]]
+ ]
+
+ g = Gruff::Line.new
+ g.title = "Very Large Values Line Graph Test"
+ g.baseline_value = 50_000
+ g.baseline_color = 'green'
+ @datasets.each do |data|
+ g.data(data[0], data[1])
+ end
+
+ g.write("test/output/line_large.png")
+ end
+
+ def test_long_title
+
+ end
+
+ def test_add_colors
+
+ end
+
+ def test_request_too_many_colors
+
+ end
+
+ def test_add_data
+
+ end
+
+ def test_many_datapoints
+ g = Gruff::Line.new
+ g.title = "Many Multi-Line Graph Test"
+ g.labels = {
+ 0 => 'June',
+ 10 => 'July',
+ 30 => 'August',
+ 50 => 'September',
+ }
+ g.data('many points', (0..50).collect {|i| rand(100) })
+ g.x_axis_label = "Months"
+
+ # Default theme
+ g.write("test/output/line_many.png")
+ end
+
+
+ def test_similar_high_end_values
+ @dataset = %w(29.43 29.459 29.498 29.53 29.548 29.589 29.619 29.66 29.689 29.849 29.878 29.74 29.769 29.79 29.808 29.828).collect {|i| i.to_f}
+
+ g = Gruff::Line.new
+ g.title = "Similar High End Values Test"
+ g.data('similar points', @dataset )
+ g.write("test/output/line_similar_high_end_values.png")
+
+ g = Gruff::Line.new
+ g.title = "Similar High End Values With Floor"
+ g.data('similar points', @dataset )
+ g.minimum_value = 0
+ g.y_axis_label = "Barometric Pressure"
+ g.write("test/output/line_similar_high_end_values_with_floor.png")
+ end
+
+ def test_many_lines_graph_small
+ g = Gruff::Line.new(400)
+ g.title = "Many Values Line Test 400px"
+ g.labels = {
+ 0 => '5/6',
+ 10 => '5/15',
+ 20 => '5/24',
+ 30 => '5/30',
+ 40 => '6/4',
+ 50 => '6/16'
+ }
+ %w{jimmy jane philip arthur julie bert}.each do |student_name|
+ g.data(student_name, (0..50).collect { |i| rand 100 })
+ end
+
+ # Default theme
+ g.write("test/output/line_many_lines_small.png")
+ end
+
+ def test_graph_tiny
+ g = Gruff::Line.new(300)
+ g.title = "Tiny Test 300px"
+ g.labels = {
+ 0 => '5/6',
+ 10 => '5/15',
+ 20 => '5/24',
+ 30 => '5/30',
+ 40 => '6/4',
+ 50 => '6/16'
+ }
+ %w{jimmy jane philip arthur julie bert}.each do |student_name|
+ g.data(student_name, (0..50).collect { |i| rand 100 })
+ end
+
+ # Default theme
+ g.write("test/output/line_tiny.png")
+ end
+
+ def test_no_data
+ g = Gruff::Line.new(400)
+ g.title = "No Data"
+ # Default theme
+ g.write("test/output/line_no_data.png")
+
+ g = Gruff::Line.new(400)
+ g.title = "No Data Title"
+ g.no_data_message = 'There is no data'
+ g.write("test/output/line_no_data_msg.png")
+ end
+
+
+ def test_all_zeros
+ g = Gruff::Line.new(400)
+ g.title = "All Zeros"
+
+ g.data(:gus, [0,0,0,0])
+
+ # Default theme
+ g.write("test/output/line_no_data_other.png")
+ end
+
+
+ def test_some_nil_points
+ g = Gruff::Line.new
+ g.title = "Some Nil Points"
+
+ @datasets = [
+ [:data1, [1, 2, 3, nil, 3, 5, 6]],
+ [:data2, [5, nil, nil, nil, nil, nil, 5]],
+ [:data3, [4, nil, 2, 1, 0]],
+ [:data4, [nil, nil, 3, 1, 2]]
+ ]
+
+ @datasets.each do |data|
+ g.data(*data)
+ end
+
+ # Default theme
+ g.write("test/output/line_some_nil_points.png")
+ end
+
+ def test_no_title
+ g = Gruff::Line.new(400)
+ g.labels = @labels
+ @datasets.each do |data|
+ g.data(data[0], data[1])
+ end
+
+ g.write("test/output/line_no_title.png")
+ end
+
+ def test_no_line_markers
+ g = setup_basic_graph(400)
+ g.title = "No Line Markers"
+ g.hide_line_markers = true
+ g.write("test/output/line_no_line_markers.png")
+ end
+
+ def test_no_legend
+ g = setup_basic_graph(400)
+ g.title = "No Legend"
+ g.hide_legend = true
+ g.write("test/output/line_no_legend.png")
+ end
+
+ def test_nothing_but_the_graph
+ g = setup_basic_graph(400)
+ g.title = "THIS TITLE SHOULD NOT DISPLAY!!!"
+ g.hide_line_markers = true
+ g.hide_legend = true
+ g.hide_title = true
+ g.write("test/output/line_nothing_but_the_graph.png")
+ end
+
+ def test_baseline_larger_than_data
+ g = setup_basic_graph(400)
+ g.title = "Baseline Larger Than Data"
+ g.baseline_value = 150
+ g.write("test/output/line_large_baseline.png")
+ end
+
+
+ def test_hide_dots
+ g = setup_basic_graph(400)
+ g.title = "Hide Dots"
+ g.hide_dots = true
+ g.write("test/output/line_hide_dots.png")
+ end
+
+ def test_hide_lines
+ g = setup_basic_graph(400)
+ g.title = "Hide Lines"
+ g.hide_lines = true
+ g.write("test/output/line_hide_lines.png")
+ end
+
+ def test_wide_graph
+ g = setup_basic_graph('800x400')
+ g.title = "Wide Graph"
+ g.write("test/output/line_wide_graph.png")
+
+ g = setup_basic_graph('400x200')
+ g.title = "Wide Graph Small"
+ g.write("test/output/line_wide_graph_small.png")
+ end
+
+ def test_negative
+ g = setup_pos_neg(800)
+ g.write("test/output/line_pos_neg.png")
+
+ g = setup_pos_neg(400)
+ g.title = 'Pos/Neg Line Test Small'
+ g.write("test/output/line_pos_neg_400.png")
+ end
+
+ def test_all_negative
+ g = setup_all_neg(800)
+ g.write("test/output/line_all_neg.png")
+
+ g = setup_all_neg(400)
+ g.title = 'All Neg Line Test Small'
+ g.write("test/output/line_all_neg_400.png")
+ end
+
+ def test_many_numbers
+ g = Gruff::Line.new('400x170')
+ g.title = "Line Test, Many Numbers"
+
+ data = [
+ { :date => '01',
+ :wpm => 0,
+ :errors => 0,
+ :accuracy => 0 },
+ { :date => '02',
+ :wpm => 10,
+ :errors => 2,
+ :accuracy => 80 },
+ { :date => '03',
+ :wpm => 15,
+ :errors => 0,
+ :accuracy => 100 },
+ { :date => '04',
+ :wpm => 16,
+ :errors => 2,
+ :accuracy => 87 },
+ { :date => '05',
+ :wpm => nil,
+ :errors => nil,
+ :accuracy => nil },
+ { :date => '06',
+ :wpm => 18,
+ :errors => 1,
+ :accuracy => 94 },
+ { :date => '07'},
+ { :date => '08' },
+ { :date => '09',
+ :wpm => 21,
+ :errors => 1,
+ :accuracy => 95 },
+ { :date => '10'},
+ { :date => '11'},
+ { :date => '12'},
+ { :date => '13'},
+ { :date => '14'},
+ { :date => '15'},
+ { :date => '16'},
+ { :date => '17'},
+ { :date => '18'},
+ { :date => '19',
+ :wpm => 28,
+ :errors => 5,
+ :accuracy => 82 },
+ { :date => '20'},
+ { :date => '21'},
+ { :date => '22'},
+ { :date => '23'},
+ { :date => '24'},
+ { :date => '25'},
+ { :date => '26'},
+ { :date => '27',
+ :wpm => 37,
+ :errors => 3,
+ :accuracy => 92 },
+ ]
+
+ [:wpm, :errors, :accuracy].each do |field|
+ g.data(field.to_s, data.collect {|d| d[field] })
+ end
+
+ labels = Hash.new
+ data.each_with_index do |d, i|
+ labels[i] = d[:date]
+ end
+ g.labels = labels
+
+ g.write('test/output/line_many_numbers.png')
+ end
+
+ def test_no_hide_line_no_labels
+ g = Gruff::Line.new
+ g.title = "No Hide Line No Labels"
+ @datasets.each do |data|
+ g.data(data[0], data[1])
+ end
+ g.hide_line_markers = false
+ g.write('test/output/line_no_hide.png')
+ end
+
+protected
+
+ # TODO Reset data after each theme
+ def line_graph_with_themes(size=nil)
+ g = Gruff::Line.new(size)
+ g.title = "Multi-Line Graph Test #{size}"
+ g.labels = @labels
+ g.baseline_value = 90
+ @datasets.each do |data|
+ g.data(data[0], data[1])
+ end
+ # Default theme
+ g.write("test/output/line_theme_keynote_#{size}.png")
+
+ g = Gruff::Line.new(size)
+ g.title = "Multi-Line Graph Test #{size}"
+ g.labels = @labels
+ g.baseline_value = 90
+ g.theme_37signals
+ @datasets.each do |data|
+ g.data(data[0], data[1])
+ end
+ g.write("test/output/line_theme_37signals_#{size}.png")
+
+
+ g = Gruff::Line.new(size)
+ g.title = "Multi-Line Graph Test #{size}"
+ g.labels = @labels
+ g.baseline_value = 90
+ g.theme_rails_keynote
+ @datasets.each do |data|
+ g.data(data[0], data[1])
+ end
+ g.write("test/output/line_theme_rails_keynote_#{size}.png")
+
+ g = Gruff::Line.new(size)
+ g.title = "Multi-Line Graph Test #{size}"
+ g.labels = @labels
+ g.baseline_value = 90
+ g.theme_odeo
+ @datasets.each do |data|
+ g.data(data[0], data[1])
+ end
+ g.write("test/output/line_theme_odeo_#{size}.png")
+ end
+
+ def setup_pos_neg(size=800)
+ g = Gruff::Line.new(size)
+ g.title = "Pos/Neg Line Graph Test"
+ g.labels = {
+ 0 => '5/6',
+ 1 => '5/15',
+ 2 => '5/24',
+ 3 => '5/30',
+ }
+ g.data(:apples, [-1, 0, 4, -4])
+ g.data(:peaches, [10, 8, 6, 3])
+ return g
+ end
+
+
+ def setup_all_neg(size=800)
+ g = Gruff::Line.new(size)
+ g.title = "All Neg Line Graph Test"
+ g.labels = {
+ 0 => '5/6',
+ 1 => '5/15',
+ 2 => '5/24',
+ 3 => '5/30',
+ }
+ g.data(:apples, [-1, -5, -20, -4])
+ g.data(:peaches, [-10, -8, -6, -3])
+ g
+ end
+
+end
--- /dev/null
+
+require File.dirname(__FILE__) + "/gruff_test_case"
+
+class TestMiniBar < GruffTestCase
+
+ def test_simple_bar
+ setup_single_dataset
+ g = setup_basic_graph(Gruff::Mini::Bar, 200)
+ write_test_file g, 'mini_bar.png'
+ end
+
+ # def test_simple_bar_wide_dataset
+ # setup_wide_dataset
+ # g = setup_basic_graph(Gruff::Mini::Bar, 200)
+ # write_test_file g, 'mini_bar_wide_data.png'
+ # end
+ #
+ # def test_code_sample
+ # g = Gruff::Mini::Bar.new(200)
+ # g.data "Jim", [200, 500, 400]
+ # g.labels = { 0 => 'This Month', 1 => 'Average', 2 => 'Overall'}
+ # g.write "mini_bar_one_color.png"
+ #
+ # g = Gruff::Mini::Bar.new(200)
+ # g.data "Car", 200
+ # g.data "Food", 500
+ # g.data "Art", 1000
+ # g.data "Music", 16
+ # g.write "mini_bar_many_colors.png"
+ # end
+
+end
--- /dev/null
+
+require File.dirname(__FILE__) + "/gruff_test_case"
+
+class TestMiniPie < GruffTestCase
+
+ def test_simple_pie
+ g = setup_basic_graph(Gruff::Mini::Pie, 200)
+ write_test_file g, 'mini_pie.png'
+ end
+
+ # def test_code_sample
+ # g = Gruff::Mini::Pie.new(200)
+ # g.data "Car", 200
+ # g.data "Food", 500
+ # g.data "Art", 1000
+ # g.data "Music", 16
+ # g.write "mini_pie.png"
+ # end
+
+end
--- /dev/null
+
+require File.dirname(__FILE__) + "/gruff_test_case"
+
+class TestMiniSideBar < GruffTestCase
+
+ def test_one_color
+ # Use a single data set
+ @datasets = [
+ [:Jimmy, [25, 36, 86, 39]]
+ ]
+ @labels = {
+ 0 => 'Auto',
+ 1 => 'Entertainment',
+ 2 => 'Food',
+ 3 => 'Bus'
+ }
+
+ g = setup_basic_graph(Gruff::Mini::SideBar, 200)
+ write_test_file g, 'mini_side_bar.png'
+ end
+
+ def test_multi_color
+ # @datasets = [
+ # [:Jimmy, [25, 36, 86, 39]]
+ # ]
+ # @labels = {
+ # 0 => 'Auto',
+ # 1 => 'Entertainment',
+ # 2 => 'Food',
+ # 3 => 'Bus'
+ # }
+
+ g = setup_basic_graph(Gruff::Mini::SideBar, 200)
+ write_test_file g, 'mini_side_bar_multi_color.png'
+ end
+
+end
--- /dev/null
+#!/usr/bin/ruby
+
+require File.dirname(__FILE__) + "/gruff_test_case"
+
+class TestGruffNet < GruffTestCase
+
+ def setup
+ @datasets = [
+ [:Jimmy, [25, 36, 86, 39, 25, 31, 79, 88]],
+ [:Charles, [80, 54, 67, 54, 68, 70, 90, 95]],
+ [:Julie, [22, 29, 35, 38, 36, 40, 46, 57]],
+ [:Jane, [95, 95, 95, 90, 85, 80, 88, 100]],
+ [:Philip, [90, 34, 23, 12, 78, 89, 98, 88]],
+ ["Arthur", [5, 10, 13, 11, 6, 16, 22, 32]],
+ ]
+
+ @sample_labels = {
+ 0 => '5/6',
+ 1 => '5/15',
+ 2 => '5/24',
+ 3 => '5/30',
+ 4 => '6/4',
+ 5 => '6/12',
+ 6 => '6/21',
+ 7 => '6/28',
+ }
+ end
+
+ def test_net_small_values
+ @datasets = [
+ [:small, [0.1, 0.14356, 0.0, 0.5674839, 0.456]],
+ [:small2, [0.2, 0.3, 0.1, 0.05, 0.9]]
+ ]
+
+ g = Gruff::Net.new
+ g.title = "Small Values Net Graph Test"
+ @datasets.each do |data|
+ g.data(data[0], data[1])
+ end
+ g.write("test/output/net_small.png")
+
+ g = Gruff::Net.new(400)
+ g.title = "Small Values Net Graph Test 400px"
+ @datasets.each do |data|
+ g.data(data[0], data[1])
+ end
+ g.write("test/output/net_small_small.png")
+ end
+
+ def test_net_starts_with_zero
+ @datasets = [
+ [:first0, [0, 5, 10, 8, 18]],
+ [:normal, [1, 2, 3, 4, 5]]
+ ]
+
+ g = Gruff::Net.new
+ g.title = "Small Values Net Graph Test"
+ @datasets.each do |data|
+ g.data(data[0], data[1])
+ end
+ g.write("test/output/net_small_zero.png")
+
+ g = Gruff::Net.new(400)
+ g.title = "Small Values Net Graph Test 400px"
+ @datasets.each do |data|
+ g.data(data[0], data[1])
+ end
+ g.write("test/output/net_small_small_zero.png")
+ end
+
+
+ def test_net_large_values
+ @datasets = [
+ [:large, [100_005, 35_000, 28_000, 27_000]],
+ [:large2, [35_000, 28_000, 27_000, 100_005]],
+ [:large3, [28_000, 27_000, 100_005, 35_000]],
+ [:large4, [1_238, 39_092, 27_938, 48_876]]
+ ]
+
+ g = Gruff::Net.new
+ g.title = "Very Large Values Net Graph Test"
+ @datasets.each do |data|
+ g.data(data[0], data[1])
+ end
+
+ g.write("test/output/net_large.png")
+ end
+
+ def test_many_datapoints
+ g = Gruff::Net.new
+ g.title = "Many Multi-Net Graph Test"
+ g.labels = {
+ 0 => 'June',
+ 10 => 'July',
+ 30 => 'August',
+ 50 => 'September',
+ }
+ g.data('many points', (0..50).collect {|i| rand(100) })
+
+ # Default theme
+ g.write("test/output/net_many.png")
+ end
+
+
+ def test_similar_high_end_values
+ g = Gruff::Net.new
+ g.title = "Similar High End Values Test"
+ g.data('similar points', %w(29.43 29.459 29.498 29.53 29.548 29.589 29.619 29.66 29.689 29.849 29.878 29.74 29.769 29.79 29.808 29.828).collect {|i| i.to_f} )
+
+ # Default theme
+ g.write("test/output/net_similar_high_end_values.png")
+ end
+
+ def test_many_nets_graph_small
+ g = Gruff::Net.new(400)
+ g.title = "Many Values Net Test 400px"
+ g.labels = {
+ 0 => '5/6',
+ 10 => '5/15',
+ 20 => '5/24',
+ 30 => '5/30',
+ 40 => '6/4',
+ 50 => '6/16'
+ }
+ %w{jimmy jane philip arthur julie bert}.each do |student_name|
+ g.data(student_name, (0..50).collect { |i| rand 100 })
+ end
+
+ # Default theme
+ g.write("test/output/net_many_nets_small.png")
+ end
+
+ def test_dots_graph_tiny
+ g = Gruff::Net.new(300)
+ g.title = "Dots Test 300px"
+ g.labels = {
+ 0 => '5/6',
+ 10 => '5/15',
+ 20 => '5/24',
+ 30 => '5/30',
+ 40 => '6/4',
+ 50 => '6/16'
+ }
+ %w{jimmy jane philip arthur julie bert}.each do |student_name|
+ g.data(student_name, (0..50).collect { |i| rand 100 })
+ end
+
+ # Default theme
+ g.write("test/output/net_dots_tiny.png")
+ end
+
+ def test_no_data
+ g = Gruff::Net.new(400)
+ g.title = "No Data"
+ # Default theme
+ g.write("test/output/net_no_data.png")
+
+ g = Gruff::Net.new(400)
+ g.title = "No Data Title"
+ g.no_data_message = 'There is no data'
+ g.write("test/output/net_no_data_msg.png")
+ end
+
+
+ def test_all_zeros
+ g = Gruff::Net.new(400)
+ g.title = "All Zeros"
+
+ g.data(:gus, [0,0,0,0])
+
+ # Default theme
+ g.write("test/output/net_no_data_other.png")
+ end
+
+ def test_no_title
+ g = Gruff::Net.new(400)
+ g.labels = @sample_labels
+ @datasets.each do |data|
+ g.data(data[0], data[1])
+ end
+
+ g.write("test/output/net_no_title.png")
+ end
+
+ def test_no_net_markers
+ g = setup_basic_graph(400)
+ g.title = "No Net Markers"
+ g.hide_line_markers = true
+ g.write("test/output/net_no_net_markers.png")
+ end
+
+ def test_no_legend
+ g = setup_basic_graph(400)
+ g.title = "No Legend"
+ g.hide_legend = true
+ g.write("test/output/net_no_legend.png")
+ end
+
+ def test_nothing_but_the_graph
+ g = setup_basic_graph(400)
+ g.title = "THIS TITLE SHOULD NOT DISPLAY!!!"
+ g.hide_line_markers = true
+ g.hide_legend = true
+ g.hide_title = true
+ g.write("test/output/net_nothing_but_the_graph.png")
+ end
+
+ def test_wide_graph
+ g = setup_basic_graph('800x400')
+ g.title = "Wide Graph"
+ g.write("test/output/net_wide_graph.png")
+
+ g = setup_basic_graph('400x200')
+ g.title = "Wide Graph Small"
+ g.write("test/output/net_wide_graph_small.png")
+ end
+
+protected
+
+ def setup_basic_graph(size=800)
+ g = Gruff::Net.new(size)
+ g.title = "My Graph Title"
+ g.labels = @sample_labels
+ @datasets.each do |data|
+ g.data(data[0], data[1])
+ end
+ return g
+ end
+
+end
--- /dev/null
+#!/usr/bin/ruby
+
+require File.dirname(__FILE__) + "/gruff_test_case"
+
+class TestGruffPhotoBar < GruffTestCase
+
+# def setup
+# @datasets = [
+# [:Jimmy, [25, 36, 86, 39]],
+# [:Charles, [80, 54, 67, 54]],
+# # [:Charity, [0, nil, 100, 90]],
+# ]
+# end
+#
+# def test_bar_graph
+# bar_graph_sized
+# bar_graph_sized(400)
+# end
+#
+#
+# protected
+#
+# def bar_graph_sized(size=800)
+# g = Gruff::PhotoBar.new(size)
+# g.title = "Photo Bar Graph Test #{size}px"
+# g.labels = {
+# 0 => '5/6',
+# 1 => '5/15',
+# 2 => '5/24',
+# 3 => '5/30',
+# }
+# @datasets.each do |data|
+# g.data(*data)
+# end
+#
+# g.theme = 'plastik'
+#
+# g.write("test/output/photo_plastik_#{size}.png")
+# end
+
+end
--- /dev/null
+#!/usr/bin/ruby
+
+require File.dirname(__FILE__) + "/gruff_test_case"
+
+class TestGruffPie < GruffTestCase
+
+ def setup
+ @datasets = [
+ [:Darren, [25]],
+ [:Chris, [80]],
+ [:Egbert, [22]],
+ [:Adam, [95]],
+ [:Bill, [90]],
+ ["Frank", [5]],
+ ["Zero", [0]],
+ ]
+ end
+
+ def test_pie_graph
+ g = Gruff::Pie.new
+ g.title = "Visual Pie Graph Test"
+ @datasets.each do |data|
+ g.data(data[0], data[1])
+ end
+
+ # Default theme
+ g.write("test/output/pie_keynote.png")
+ end
+
+ def test_pie_graph_small
+ g = Gruff::Pie.new(400)
+ g.title = "Visual Pie Graph Test Small"
+ @datasets.each do |data|
+ g.data(data[0], data[1])
+ end
+
+ # Default theme
+ g.write("test/output/pie_keynote_small.png")
+ end
+
+ def test_pie_graph_nearly_equal
+ g = Gruff::Pie.new
+ g.title = "Pie Graph Nearly Equal"
+
+ g.data(:Blake, [41])
+ g.data(:Aaron, [42])
+# g.data(:Grouch, [40])
+# g.data(:Snuffleupagus, [43])
+
+ g.write("test/output/pie_nearly_equal.png")
+ end
+
+ def test_pie_graph_equal
+ g = Gruff::Pie.new
+ g.title = "Pie Graph Equal"
+
+ g.data(:Bert, [41])
+ g.data(:Adam, [41])
+
+ g.write("test/output/pie_equal.png")
+ end
+
+ def test_pie_graph_zero
+ g = Gruff::Pie.new
+ g.title = "Pie Graph One Zero"
+
+ g.data(:Bert, [0])
+ g.data(:Adam, [1])
+
+ g.write("test/output/pie_zero.png")
+ end
+
+
+ def test_pie_graph_one_val
+ g = Gruff::Pie.new
+ g.title = "Pie Graph One Val"
+
+ g.data(:Bert, 53)
+ g.data(:Adam, 29)
+
+ g.write("test/output/pie_one_val.png")
+ end
+
+
+ def test_wide
+ g = setup_basic_graph('800x400')
+ g.title = "Wide Pie"
+ g.write("test/output/pie_wide.png")
+ end
+
+ def test_label_size
+ g = setup_basic_graph()
+ g.title = "Pie With Small Legend"
+ g.legend_font_size = 10
+ g.write("test/output/pie_legend.png")
+
+ g = setup_basic_graph(400)
+ g.title = "Small Pie With Small Legend"
+ g.legend_font_size = 10
+ g.write("test/output/pie_legend_small.png")
+ end
+
+
+ def test_tiny_simple_pie
+ @datasets = (1..5).map {|n| ['Auto', [rand(100)]]}
+
+ g = setup_basic_graph 200
+ g.hide_legend = true
+ g.hide_title = true
+ g.hide_line_numbers = true
+
+ g.marker_font_size = 40.0
+ g.minimum_value = 0.0
+
+ write_test_file g, "pie_simple.png"
+ end
+
+protected
+
+ def setup_basic_graph(size=800)
+ g = Gruff::Pie.new(size)
+ g.title = "My Graph Title"
+ @datasets.each do |data|
+ g.data(data[0], data[1])
+ end
+ return g
+ end
+
+end
--- /dev/null
+#!/usr/bin/ruby
+
+require File.dirname(__FILE__) + "/gruff_test_case"
+require 'yaml'
+
+class LayerStub < Gruff::Layer; attr_reader :base_dir, :filenames, :selected_filename; end
+
+class TestGruffScene < GruffTestCase
+
+ def test_hazy
+ g = setup_scene
+ g.weather = "cloudy"
+ g.haze = true
+ g.time = Time.mktime(2006, 7, 4, 4, 35)
+ g.write "test/output/scene_hazy_night.png"
+ end
+
+ def test_stormy_night
+ g = setup_scene
+ g.weather = "stormy"
+ g.time = Time.mktime(2006, 7, 4, 0, 0)
+ g.write "test/output/scene_stormy_night.png"
+ end
+
+
+ def test_not_hazy
+ g = setup_scene
+ g.weather = "cloudy"
+ g.haze = false
+ g.time = Time.mktime(2006, 7, 4, 6, 00)
+ g.write "test/output/scene_not_hazy_day.png"
+ end
+
+ def test_partly_cloudy
+ g = setup_scene
+ g.weather = "partly cloudy"
+ g.haze = false
+ g.time = Time.mktime(2006, 7, 4, 13, 00)
+ g.write "test/output/scene_partly_cloudy_day.png"
+ end
+
+
+ def test_stormy_day
+ g = setup_scene
+ g.weather = "stormy"
+ g.haze = false
+ g.time = Time.mktime(2006, 7, 4, 8, 00)
+ g.write "test/output/scene_stormy_day.png"
+ end
+
+
+ def test_layer
+ l = LayerStub.new(File.expand_path("../assets/city_scene", File.dirname(__FILE__)), "clouds")
+ assert_equal %w(cloudy.png partly_cloudy.png stormy.png), l.filenames
+
+ l = LayerStub.new(File.expand_path("../assets/city_scene", File.dirname(__FILE__)), "grass")
+ assert_equal 'default.png', l.selected_filename
+
+ l = LayerStub.new(File.expand_path("../assets/city_scene", File.dirname(__FILE__)), "sky")
+ l.update Time.mktime(2006, 7, 4, 12, 35) # 12:35, July 4, 2006
+ assert_equal '1200.png', l.selected_filename
+
+ l = LayerStub.new(File.expand_path("../assets/city_scene", File.dirname(__FILE__)), "sky")
+ l.update Time.mktime(2006, 7, 4, 0, 0) # 00:00, July 4, 2006
+ assert_equal '0000.png', l.selected_filename
+
+ l = LayerStub.new(File.expand_path("../assets/city_scene", File.dirname(__FILE__)), "sky")
+ l.update Time.mktime(2006, 7, 4, 23, 35) # 23:35, July 4, 2006
+ assert_equal '2000.png', l.selected_filename
+
+ l = LayerStub.new(File.expand_path("../assets/city_scene", File.dirname(__FILE__)), "sky")
+ l.update Time.mktime(2006, 7, 4, 0, 1) # 00:01, July 4, 2006
+ assert_equal '0000.png', l.selected_filename
+
+ l = LayerStub.new(File.expand_path("../assets/city_scene", File.dirname(__FILE__)), "sky")
+ l.update Time.mktime(2006, 7, 4, 2, 0) # 02:00, July 4, 2006
+ assert_equal '0200.png', l.selected_filename
+
+ l = LayerStub.new(File.expand_path("../assets/city_scene", File.dirname(__FILE__)), "sky")
+ l.update Time.mktime(2006, 7, 4, 4, 00) # 04:00, July 4, 2006
+ assert_equal '0400.png', l.selected_filename
+
+ # TODO Need number_sample folder
+ # l = LayerStub.new(File.expand_path("../assets/city_scene", File.dirname(__FILE__)), "number_sample")
+ # assert_equal %w(1.png 2.png default.png), l.filenames
+ # l.update 3
+ # assert_equal 'default.png', l.selected_filename
+ end
+
+private
+
+ def setup_scene
+ g = Gruff::Scene.new("500x100", File.expand_path("../assets/city_scene", File.dirname(__FILE__)) )
+ g.layers = %w(background haze sky clouds)
+ g.weather_group = %w(clouds)
+ g.time_group = %w(background sky)
+ g
+ end
+
+end
--- /dev/null
+
+require File.dirname(__FILE__) + "/gruff_test_case"
+
+class TestGruffSideBar < GruffTestCase
+
+ def test_bar_graph
+ g = setup_basic_graph(Gruff::SideBar, 800)
+ write_test_file g, 'side_bar.png'
+ end
+
+end
+
--- /dev/null
+#!/usr/bin/ruby
+
+require File.dirname(__FILE__) + "/gruff_test_case"
+
+class TestGruffSideStackedBar < GruffTestCase
+
+ def setup
+ @datasets = [
+ [:Jimmy, [25, 36, 86, 39]],
+ [:Charles, [80, 54, 67, 54]],
+ [:Julie, [22, 29, 35, 38]],
+ #[:Jane, [95, 95, 95, 90, 85, 80, 88, 100]],
+ #[:Philip, [90, 34, 23, 12, 78, 89, 98, 88]],
+ #["Arthur", [5, 10, 13, 11, 6, 16, 22, 32]],
+ ]
+ @sample_labels = {
+ 0 => '5/6',
+ 1 => '5/15',
+ 2 => '5/24'
+ }
+
+ end
+
+ def test_bar_graph
+ g = Gruff::SideStackedBar.new
+ g.title = "Visual Stacked Bar Graph Test"
+ g.labels = {
+ 0 => '5/6',
+ 1 => '5/15',
+ 2 => '5/24',
+ 3 => '5/30',
+ }
+ @datasets.each do |data|
+ g.data(data[0], data[1])
+ end
+ g.write "test/output/side_stacked_bar_keynote.png"
+ end
+
+
+ def test_bar_graph_small
+ g = Gruff::SideStackedBar.new(400)
+ g.title = "Visual Stacked Bar Graph Test"
+ g.labels = {
+ 0 => '5/6',
+ 1 => '5/15',
+ 2 => '5/24',
+ 3 => '5/30',
+ }
+ @datasets.each do |data|
+ g.data(data[0], data[1])
+ end
+ g.write "test/output/side_stacked_bar_keynote_small.png"
+ end
+
+ def test_wide
+ g = setup_basic_graph('800x400')
+ g.title = "Wide SSBar"
+ g.write "test/output/side_stacked_bar_wide.png"
+ end
+
+ def test_should_space_long_left_labels_appropriately
+ g = Gruff::SideStackedBar.new
+ g.title = "Stacked Bar Long Label"
+ g.labels = {
+ 0 => 'September',
+ 1 => 'Oct',
+ 2 => 'Nov',
+ 3 => 'Dec',
+ }
+ @datasets.each do |data|
+ g.data(data[0], data[1])
+ end
+ g.write "test/output/side_stacked_bar_long_label.png"
+ end
+
+protected
+
+ def setup_basic_graph(size=800)
+ g = Gruff::SideStackedBar.new(size)
+ g.title = "My Graph Title"
+ g.labels = @sample_labels
+ @datasets.each do |data|
+ g.data(data[0], data[1])
+ end
+ return g
+ end
+
+end
+
--- /dev/null
+#!/usr/bin/ruby
+
+require File.dirname(__FILE__) + "/gruff_test_case"
+
+class TestGruffSpider < GruffTestCase
+
+ def setup
+ @datasets = [
+ [:Strength, [10]],
+ [:Dexterity, [16]],
+ [:Constitution, [12]],
+ [:Intelligence, [12]],
+ [:Wisdom, [10]],
+ ["Charisma", [16]],
+ ]
+
+# @datasets = [
+# [:Darren, [25]],
+# [:Chris, [80]],
+# [:Egbert, [22]],
+# [:Adam, [95]],
+# [:Bill, [90]],
+# ["Frank", [5]],
+# ["Zero", [0]],
+# ]
+ end
+
+ def test_spider_graph
+ g = Gruff::Spider.new(20)
+ g.title = "Spider Graph Test"
+ @datasets.each do |data|
+ g.data(data[0], data[1])
+ end
+
+ # Default theme
+ g.write("test/output/spider_keynote.png")
+ end
+
+ def test_pie_graph_small
+ g = Gruff::Spider.new(20, 400)
+ g.title = "Visual Spider Graph Test Small"
+ @datasets.each do |data|
+ g.data(data[0], data[1])
+ end
+
+ # Default theme
+ g.write("test/output/spider_small.png")
+ end
+
+ def test_spider_graph_nearly_equal
+ g = Gruff::Spider.new(50)
+ g.title = "Spider Graph Nearly Equal"
+
+ g.data(:Blake, [41])
+ g.data(:Aaron, [42])
+ g.data(:Grouch, [40])
+# g.data(:Snuffleupagus, [43])
+
+ g.write("test/output/spider_nearly_equal.png")
+ end
+
+ def test_pie_graph_equal
+ g = Gruff::Spider.new(50)
+ g.title = "Spider Graph Equal"
+
+ g.data(:Bert, [41])
+ g.data(:Adam, [41])
+ g.data(:Joe, [41])
+
+ g.write("test/output/spider_equal.png")
+ end
+
+ def test_pie_graph_zero
+ g = Gruff::Spider.new(2)
+ g.title = "Pie Graph Two One Zero"
+
+ g.data(:Bert, [0])
+ g.data(:Adam, [1])
+ g.data(:Sam, [2])
+
+ g.write("test/output/spider_zero.png")
+ end
+
+ def test_wide
+ g = setup_basic_graph('800x400')
+ g.title = "Wide spider"
+ g.write("test/output/spider_wide.png")
+ end
+
+ def test_label_size
+ g = setup_basic_graph()
+ g.title = "Spider With Small Legend"
+ g.legend_font_size = 10
+ g.write("test/output/spider_legend.png")
+
+ g = setup_basic_graph(400)
+ g.title = "Small spider With Small Legend"
+ g.legend_font_size = 10
+ g.write("test/output/spider_legend_small.png")
+ end
+
+ def test_theme_37signals
+ g = Gruff::Spider.new(20)
+ g.title = "Spider Graph Test"
+ @datasets.each do |data|
+ g.data(data[0], data[1])
+ end
+
+ g.theme_37signals
+
+ # Default theme
+ g.write("test/output/spider_37signals.png")
+ end
+
+ def test_no_axes
+ g = Gruff::Spider.new(20)
+ g.title = "Look ma, no axes"
+ g.hide_axes = true
+ @datasets.each do |data|
+ g.data(data[0], data[1])
+ end
+ g.write("test/output/spider_no_axes.png")
+
+ end
+
+ def test_no_print
+ g = Gruff::Spider.new(20)
+ g.title = "Should not print"
+ g.hide_text = true
+ @datasets.each do |data|
+ g.data(data[0], data[1])
+ end
+ g.write("test/output/spider_no_print.png")
+ end
+
+ def test_transparency
+ g = Gruff::Spider.new(20)
+ g.title = "Transparent background"
+ g.hide_text = true
+ g.transparent_background = true
+ g.hide_axes = true
+ @datasets.each do |data|
+ g.data(data[0], data[1])
+ end
+ g.write("test/output/spider_no_background.png")
+ end
+
+ def test_overlay
+ g = Gruff::Spider.new(20)
+ g.title = "George (blue) vs Sarah (white)"
+ @datasets.each do |data|
+ g.data(data[0], data[1])
+ end
+ g.write("test/output/spider_overlay_1.png")
+
+ g = Gruff::Spider.new(20)
+ g.title = "Transparent background"
+ g.hide_text = true
+ g.hide_axes = true
+ g.transparent_background = true
+ @datasets = [
+ [:Strength, [18]],
+ [:Dexterity, [10]],
+ [:Constitution, [18]],
+ [:Intelligence, [8]],
+ [:Wisdom, [14]],
+ ["Charisma", [4]],
+ ]
+ @datasets.each do |data|
+ g.data(data[0], data[1])
+ end
+ g.marker_color = "#4F6EFF"
+ g.write("test/output/spider_overlay_2.png")
+ end
+
+ def test_lots_of_data
+ g = Gruff::Spider.new(10)
+ @datasets = [[:a, [1]], [:b, [5]], [:c, [3]], [:d, [9]], [:e, [4]],
+ [:f, [7]], [:g, [0]], [:h, [4]], [:i, [6]], [:j, [0]],
+ [:k, [4]], [:l, [8]]]
+
+ @datasets.each do |data|
+ g.data(data[0], data[1])
+ end
+
+ g.title = "Sample Data"
+ g.write("test/output/spider_lots_of_data.png")
+ end
+
+ def test_lots_of_data_with_large_names
+ g = Gruff::Spider.new(10)
+ @datasets = [[:anteaters, [1]], [:bulls, [5]], [:cats, [3]], [:dogs, [9]], [:elephants, [4]],
+ [:frogs, [7]], [:giraffes, [0]], [:hamsters, [4]], [:iguanas, [6]],
+ [:jaguar, [0]], [:kangaroo, [4]], [:locust, [8]]]
+
+ @datasets.each do |data|
+ g.data(data[0], data[1])
+ end
+
+ g.title = "Zoo Inventory"
+ g.write("test/output/spider_lots_of_data_normal_names.png")
+ end
+
+
+protected
+
+ def setup_basic_graph(size=800, max = 20)
+ g = Gruff::Spider.new(max, size)
+ g.title = "My Graph Title"
+ @datasets.each do |data|
+ g.data(data[0], data[1])
+ end
+ return g
+ end
+
+end
--- /dev/null
+#!/usr/bin/ruby
+
+require File.dirname(__FILE__) + "/gruff_test_case"
+
+class TestGruffStackedBar < GruffTestCase
+
+ def setup
+ @datasets = [
+ [:Jimmy, [25, 36, 86, 39]],
+ [:Charles, [80, 54, 67, 54]],
+ [:Julie, [22, 29, 35, 38]],
+ ]
+ @sample_labels = {
+ 0 => '5/6',
+ 1 => '5/15',
+ 2 => '5/24'
+ }
+
+ end
+
+ def test_bar_graph
+ g = Gruff::StackedBar.new
+ g.title = "Visual Stacked Bar Graph Test"
+ g.labels = {
+ 0 => '5/6',
+ 1 => '5/15',
+ 2 => '5/24',
+ 3 => '5/30',
+ }
+ @datasets.each do |data|
+ g.data(data[0], data[1])
+ end
+ g.write "test/output/stacked_bar_keynote.png"
+ end
+
+
+ def test_bar_graph_small
+ g = Gruff::StackedBar.new(400)
+ g.title = "Visual Stacked Bar Graph Test"
+ g.labels = {
+ 0 => '5/6',
+ 1 => '5/15',
+ 2 => '5/24',
+ 3 => '5/30',
+ }
+ @datasets.each do |data|
+ g.data(data[0], data[1])
+ end
+ g.write "test/output/stacked_bar_keynote_small.png"
+ end
+
+end
vertical-align: baseline;
}
+body {
+ font-size: 11px;
+ font-family: Trebuchet, Verdana, Arial, Helvetica, sans-serif;
+}
+
/* tables still need 'cellspacing="0"' in the markup */
table {
border-collapse: separate;
#footer {
font-size: 11px;
- font-family: "Trebuchet MS", Trebuchet, Verdana, Arial, Helvetica, sans-serif;
+ font-family: Verdana, Arial, Helvetica, sans-serif;
margin-top: 40px;
text-align: center;
}
color: #dc0d13;
}
-#page-title .header {
+#title-header {
+ background-color: #dc0d13;
+}
+
+#title-header .header {
background-color: #dc0d13;
}
-#page-title .subheader {
+#title-header .subheader {
color: #dc0d13;
background-color: #e5e5e5;
}
overflow: hidden;
}
-#page-title {
+#title-header {
margin: -20px -20px 1em -20px;
+ height: 32px;
+ margin-bottom: 14px;
+ background-color: #e5e5e5;
}
-.plain-header {
- margin: 1em -20px 1em -20px;
+.normal-header {
+ margin: 0 -20px 1em -20px;
+ height: 32px;
+ background-color: #e5e5e5;
}
.header {
- display: block;
- font-family: "trebuchet ms",trebuchet,verdana,arial,sans-serif;
- font-size: 30px;
- text-transform: uppercase;
- text-align: center;
- color: white;
- background-color: #999999;
- height: 32px;
- float: left;
- padding: 0px .75em 0px .75em;
- margin-right: 10px;
- margin-bottom: 14px;
+ display: block;
+ font-family: verdana,arial,helvetica,sans-serif;
+ font-size: 30px;
+ text-transform: uppercase;
+ text-align: center;
+ color: white;
+ background-color: #999999;
+ height: 32px;
+ float: left;
+ padding: 0px .75em 0px .75em;
}
.subheader {
- display: block;
- font-family: "trebuchet ms",trebuchet,verdana,arial,sans-serif;
- font-size: 18px;
- text-align: left;
- color: #666666;
- background-color: #CCCCCC;
- height: 26px;
- padding: 6px 1em 0px 20px;
- margin-bottom: 14px;
-}
+ float: left;
+ font-family: verdana,arial,helvetica,sans-serif;
+ font-size: 18px;
+ color: #666666;
+ background-color: #e5e5e5;
+ height: 26px;
+ padding: 6px 1em 0px 10px;
+}
#main-box {
float: right;
color: #569a00;
}
-#page-title .header {
+#title-header .header {
background-color: #74ce00;
}
-#page-title .subheader {
+#title-header .subheader {
color: #74ce00;
- background-color: #e5e5e5;
}
color: #005cd9;
}
-#page-title .header {
+#title-header {
background-color: #005cd9;
}
-#page-title .subheader {
+#title-header .header {
+ background-color: #005cd9;
+}
+
+#title-header .subheader {
color: #005cd9;
background-color: #e5e5e5;
}