Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Merge branch 'scatter'

  • Loading branch information...
commit 33d6a062bfbf1e8eb789bb2dcce4bb709cb4b8f9 2 parents 4d7b583 + d11fa16
Uwe Kubosch donv authored
Showing with 502 additions and 0 deletions.
  1. +1 −0  lib/gruff.rb
  2. +268 −0 lib/gruff/scatter.rb
  3. +233 −0 test/test_scatter.rb
1  lib/gruff.rb
View
@@ -11,6 +11,7 @@
pie
spider
net
+ scatter
stacked_area
stacked_bar
side_stacked_bar
268 lib/gruff/scatter.rb
View
@@ -0,0 +1,268 @@
+require File.dirname(__FILE__) + '/base'
+
+# Here's how to set up an XY Scatter Chart
+#
+# g = Gruff::Scatter.new(800)
+# g.data(:apples, [1,2,3,4], [4,3,2,1])
+# g.data('oranges', [5,7,8], [4,1,7])
+# g.write('test/output/scatter.png')
+#
+#
+class Gruff::Scatter < Gruff::Base
+
+ # Maximum X Value. The value will get overwritten by the max in the
+ # datasets.
+ attr_accessor :maximum_x_value
+
+ # Minimum X Value. The value will get overwritten by the min in the
+ # datasets.
+ attr_accessor :minimum_x_value
+
+ # The number of vertical lines shown for reference
+ attr_accessor :marker_x_count
+
+ #~ # Draw a dashed horizontal line at the given y value
+ #~ attr_accessor :baseline_y_value
+
+ #~ # Color of the horizontal baseline
+ #~ attr_accessor :baseline_y_color
+
+ #~ # Draw a dashed horizontal line at the given y value
+ #~ attr_accessor :baseline_x_value
+
+ #~ # Color of the horizontal baseline
+ #~ attr_accessor :baseline_x_color
+
+
+ # Gruff::Scatter takes the same parameters as the Gruff::Line graph
+ #
+ # ==== Example
+ #
+ # g = Gruff::Scatter.new
+ #
+ def initialize(*args)
+ super(*args)
+
+ @maximum_x_value = @minimum_x_value = nil
+ @baseline_x_color = @baseline_y_color = 'red'
+ @baseline_x_value = @baseline_y_value = nil
+ @marker_x_count = nil
+ end
+
+ def draw
+ calculate_spread
+ @sort = false
+
+ # TODO Need to get x-axis labels working. Current behavior will be to not allow.
+ @labels = {}
+
+ # Translate our values so that we can use the base methods for drawing
+ # the standard chart stuff
+ @column_count = @x_spread
+
+ 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_y_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
+
+ #~ if (defined?(@norm_x_baseline)) then
+
+ #~ end
+
+ @norm_data.each do |data_row|
+ prev_x = prev_y = nil
+
+ data_row[DATA_VALUES_INDEX].each_with_index do |data_point, index|
+ x_value = data_row[DATA_VALUES_X_INDEX][index]
+ next if data_point.nil? || x_value.nil?
+
+ new_x = getXCoord(x_value, @graph_width, @graph_left)
+ 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)
+
+ 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)
+
+ prev_x = new_x
+ prev_y = new_y
+ end
+ end
+
+ @d.draw(@base_image)
+ end
+
+ # The first parameter is the name of the dataset. The next two are the
+ # x and y axis data points contain in their own array in that respective
+ # order. The final parameter is the color.
+ #
+ # 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().
+ #
+ # ==== Parameters
+ # name:: String or Symbol containing the name of the dataset.
+ # x_data_points:: An Array of of x-axis data points.
+ # y_data_points:: An Array of of y-axis data points.
+ # color:: The hex string for the color of the dataset. Defaults to nil.
+ #
+ # ==== Exceptions
+ # Data points contain nil values::
+ # This error will get raised if either the x or y axis data points array
+ # contains a <tt>nil</tt> value. The graph will not make an assumption
+ # as how to graph <tt>nil</tt>
+ # x_data_points is empty::
+ # This error is raised when the array for the x-axis points are empty
+ # y_data_points is empty::
+ # This error is raised when the array for the y-axis points are empty
+ # x_data_points.length != y_data_points.length::
+ # Error means that the x and y axis point arrays do not match in length
+ #
+ # ==== Examples
+ # g = Gruff::Scatter.new
+ # g.data(:apples, [1,2,3], [3,2,1])
+ # g.data('oranges', [1,1,1], [2,3,4])
+ # g.data('bitter_melon', [3,5,6], [6,7,8], '#000000')
+ #
+ def data(name, x_data_points=[], y_data_points=[], color=nil)
+
+ raise ArgumentError, "Data Points contain nil Value!" if x_data_points.include?(nil) || y_data_points.include?(nil)
+ raise ArgumentError, "x_data_points is empty!" if x_data_points.empty?
+ raise ArgumentError, "y_data_points is empty!" if y_data_points.empty?
+ raise ArgumentError, "x_data_points.length != y_data_points.length!" if x_data_points.length != y_data_points.length
+
+ # Call the existing data routine for the y axis data
+ super(name, y_data_points, color)
+
+ #append the x data to the last entry that was just added in the @data member
+ lastElem = @data.length()-1
+ @data[lastElem] << x_data_points
+
+ if @maximum_x_value.nil? && @minimum_x_value.nil?
+ @maximum_x_value = @minimum_x_value = x_data_points.first
+ end
+
+ @maximum_x_value = x_data_points.max > @maximum_x_value ?
+ x_data_points.max : @maximum_x_value
+ @minimum_x_value = x_data_points.min < @minimum_x_value ?
+ x_data_points.min : @minimum_x_value
+ end
+
+protected
+
+ def calculate_spread #:nodoc:
+ super
+ @x_spread = @maximum_x_value.to_f - @minimum_x_value.to_f
+ @x_spread = @x_spread > 0 ? @x_spread : 1
+ end
+
+ def normalize(force=@xy_normalize)
+ if @norm_data.nil? || force
+ @norm_data = []
+ return unless @has_data
+
+ @data.each do |data_row|
+ norm_data_points = [data_row[DATA_LABEL_INDEX]]
+ norm_data_points << data_row[DATA_VALUES_INDEX].map do |r|
+ (r.to_f - @minimum_value.to_f) / @spread
+ end
+ norm_data_points << data_row[DATA_COLOR_INDEX]
+ norm_data_points << data_row[DATA_VALUES_X_INDEX].map do |r|
+ (r.to_f - @minimum_x_value.to_f) / @x_spread
+ end
+ @norm_data << norm_data_points
+ end
+ end
+ #~ @norm_y_baseline = (@baseline_y_value.to_f / @maximum_value.to_f) if @baseline_y_value
+ #~ @norm_x_baseline = (@baseline_x_value.to_f / @maximum_x_value.to_f) if @baseline_x_value
+ end
+
+ def draw_line_markers
+ # do all of the stuff for the horizontal lines on the y-axis
+ super
+ return if @hide_line_markers
+
+ @d = @d.stroke_antialias false
+
+ if @x_axis_increment.nil?
+ # TODO Do the same for larger numbers...100, 75, 50, 25
+ if @marker_x_count.nil?
+ (3..7).each do |lines|
+ if @x_spread % lines == 0.0
+ @marker_x_count = lines
+ break
+ end
+ end
+ @marker_x_count ||= 4
+ end
+ @x_increment = (@x_spread > 0) ? significant(@x_spread / @marker_x_count) : 1
+ else
+ # TODO Make this work for negative values
+ @maximum_x_value = [@maximum_value.ceil, @x_axis_increment].max
+ @minimum_x_value = @minimum_x_value.floor
+ calculate_spread
+ normalize(true)
+
+ @marker_count = (@x_spread / @x_axis_increment).to_i
+ @x_increment = @x_axis_increment
+ end
+ @increment_x_scaled = @graph_width.to_f / (@x_spread / @x_increment)
+
+ # Draw vertical line markers and annotate with numbers
+ (0..@marker_x_count).each do |index|
+ x = @graph_left + @graph_width - index.to_f * @increment_x_scaled
+
+ # TODO Fix the vertical lines. Not pretty when they don't match up with top y-axis line
+ #~ @d = @d.stroke(@marker_color)
+ #~ @d = @d.stroke_width 1
+ #~ @d = @d.line(x, @graph_top, x, @graph_bottom)
+
+ unless @hide_line_numbers
+ marker_label = index * @x_increment + @minimum_x_value.to_f
+ y_offset = @graph_bottom + LABEL_MARGIN
+ x_offset = getXCoord(index.to_f, @increment_x_scaled, @graph_left)
+
+ @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,
+ 1.0, 1.0,
+ x_offset, y_offset,
+ label(marker_label), @scale)
+ end
+ end
+
+ @d = @d.stroke_antialias true
+ end
+
+private
+
+ def getXCoord(x_data_point, width, offset) #:nodoc:
+ return(x_data_point * width + offset)
+ end
+
+end # end Gruff::Scatter
233 test/test_scatter.rb
View
@@ -0,0 +1,233 @@
+#!/usr/bin/ruby
+
+require File.dirname(__FILE__) + "/gruff_test_case"
+
+class TestGruffScatter < Test::Unit::TestCase
+
+ def setup
+ @datasets = [
+ [:Chuck, [20,10,5,12,11,6,10,7], [5,10,19,6,9,1,14,8] ],
+ [:Brown, [5,10,20,6,9,12,14,8], [20,10,5,12,11,6,10,7] ],
+ [:Lucy, [19,9,6,11,12,7,15,8], [6,11,18,8,12,8,10,6] ]
+ ]
+ end
+
+ # Done
+ def test_scatter_graph
+ g = setup_basic_graph
+ g.title = "Basic Scatter Plot Test"
+ g.write("test/output/scatter_basic.png")
+ end
+
+ #~ # Done
+ def test_many_datapoints
+ g = Gruff::Scatter.new
+ g.title = "Many Datapoint Graph Test"
+ y_values = (0..50).collect {|i| rand(100) }
+ x_values = (0..50).collect {|i| rand(100) }
+ g.data('many points', y_values, x_values)
+
+ # Default theme
+ g.write("test/output/scatter_many.png")
+ end
+
+ # Done
+ def test_no_data
+ g = Gruff::Scatter.new(400)
+ g.title = "No Data"
+ # Default theme
+ g.write("test/output/scatter_no_data.png")
+
+ g = Gruff::Scatter.new(400)
+ g.title = "No Data Title"
+ g.no_data_message = 'There is no data'
+ g.write("test/output/scatter_no_data_msg.png")
+ end
+
+ # Done
+ def test_all_zeros
+ g = Gruff::Scatter.new(400)
+ g.title = "All Zeros"
+
+ g.data(:gus, [0,0,0,0], [0,0,0,0])
+
+ # Default theme
+ g.write("test/output/scatter_no_data_other.png")
+ end
+
+ # Done
+ def test_some_nil_points
+ g = Gruff::Scatter.new
+ g.title = "Some Nil Points"
+
+ @datasets = [
+ [:data1, [1, 2, 3, nil, 3, 5, 6], [5, nil, nil, nil, nil, 5, 7] ]
+ ]
+
+ @datasets.each do |data|
+ assert_raise ArgumentError do
+ g.data(*data)
+ end
+ end
+ end
+
+ # Done
+ def test_unequal_number_of_x_and_y_values
+ g = Gruff::Scatter.new
+ g.title = "Unequal number of X and Y values"
+
+ @datasets = [
+ [:data1, [1,2,3], [1,2]],
+ [:data2, [1,2,3,4,5], [1,2,3,4,5,6]],
+ ]
+
+ @datasets.each do |data|
+ assert_raise ArgumentError do
+ g.data(*data)
+ end
+ end
+ end
+
+ # Done
+ def test_empty_set_of_axis_values
+ g = Gruff::Scatter.new
+ g.title = "Missing Axis Values"
+
+ @datasets = [
+ [:data1, [1,2,3,4,5]]
+ ]
+
+ @datasets.each do |data|
+ assert_raise ArgumentError do
+ g.data(*data)
+ end
+ end
+ end
+
+ # Done
+ def test_no_title
+ g = Gruff::Scatter.new(400)
+ g.data(:data1, [1,2,3,4,5], [1,2,3,4,5])
+ g.write("test/output/scatter_no_title.png")
+ end
+
+ # Done
+ def test_no_line_markers
+ g = setup_basic_graph(400)
+ g.title = "No Line Markers"
+ g.hide_line_markers = true
+ g.write("test/output/scatter_no_line_markers.png")
+ end
+
+ # Done
+ def test_no_legend
+ g = setup_basic_graph(400)
+ g.title = "No Legend"
+ g.hide_legend = true
+ g.write("test/output/scatter_no_legend.png")
+ end
+
+ # Done
+ 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/scatter_nothing_but_the_graph.png")
+ end
+
+ # TODO Implement baselines on x and y axis
+ #~ 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/scatter_large_baseline.png")
+ #~ end
+
+ # Done
+ def test_wide_graph
+ g = setup_basic_graph('800x400')
+ g.title = "Wide Graph"
+ g.write("test/output/scatter_wide_graph.png")
+
+ g = setup_basic_graph('400x200')
+ g.title = "Wide Graph Small"
+ g.write("test/output/scatter_wide_graph_small.png")
+ end
+
+ # Done
+ def test_negative
+ g = setup_pos_neg(800)
+ g.write("test/output/scatter_pos_neg.png")
+
+ g = setup_pos_neg(400)
+ g.title = 'Pos/Neg Line Test Small'
+ g.write("test/output/scatter_pos_neg_400.png")
+ end
+
+ # Done
+ def test_all_negative
+ g = setup_all_neg(800)
+ g.write("test/output/scatter_all_neg.png")
+
+ g = setup_all_neg(400)
+ g.title = 'All Neg Line Test Small'
+ g.write("test/output/scatter_all_neg_400.png")
+ end
+
+ # Done
+ def test_no_hide_line_no_labels
+ g = Gruff::Scatter.new
+ g.title = "No Hide Line No Labels"
+ @datasets.each do |data|
+ g.data(data[0], data[1], data[2])
+ end
+ g.hide_line_markers = false
+ g.write('test/output/scatter_no_hide.png')
+ end
+
+ def test_no_set_labels
+ g = Gruff::Scatter.new
+ g.title = "Setting Labels Test"
+ g.labels = {
+ 0 => 'This',
+ 1 => 'should',
+ 2 => 'not',
+ 3 => 'show',
+ 4 => 'up'
+ }
+ @datasets.each do |data|
+ g.data(data[0], data[1], data[2])
+ end
+ g.write('test/output/scatter_no_labels.png')
+ end
+
+protected
+
+ def setup_basic_graph(size=800)
+ g = Gruff::Scatter.new(size)
+ g.title = "Rad Graph"
+ @datasets.each do |data|
+ g.data(data[0], data[1], data[2])
+ end
+ g
+ end
+
+ def setup_pos_neg(size=800)
+ g = Gruff::Scatter.new(size)
+ g.title = "Pos/Neg Scatter Graph Test"
+ g.data(:apples, [-1, 0, 4, -4], [-5, -1, 3, 4])
+ g.data(:peaches, [10, 8, 6, 3], [-1, 1, 3, 3])
+ return g
+ end
+
+ def setup_all_neg(size=800)
+ g = Gruff::Scatter.new(size)
+ g.title = "Neg Scatter Graph Test"
+ g.data(:apples, [-1, -1, -4, -4], [-5, -1, -3, -4])
+ g.data(:peaches, [-10, -8, -6, -3], [-1, -1, -3, -3])
+ return g
+ end
+
+end # end GruffTestCase
Please sign in to comment.
Something went wrong with that request. Please try again.