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 nil value. The graph will not make an assumption # as how to graph nil # 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