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