File: scatter.rb

package info (click to toggle)
ruby-gruff 0.6.0-2
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, sid
  • size: 852 kB
  • sloc: ruby: 4,929; makefile: 3
file content (264 lines) | stat: -rw-r--r-- 9,008 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
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 setup_drawing
    # TODO Need to get x-axis labels working. Current behavior will be to not allow.
    @labels = {}

    super

    # Translate our values so that we can use the base methods for drawing
    # the standard chart stuff
    @column_count = @x_spread
  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_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|      
      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 = get_x_coord(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)
      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
    last_elem = @data.length()-1
    @data[last_elem] << 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|

      # TODO Fix the vertical lines.  Not pretty when they don't match up with top y-axis line
      # x = @graph_left + @graph_width - index.to_f * @increment_x_scaled
      # @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 = get_x_coord(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, @x_increment), @scale)
      end
    end
    
    @d = @d.stroke_antialias true
  end
  
private
  
  def get_x_coord(x_data_point, width, offset) #:nodoc:
    x_data_point * width + offset
  end
  
end # end Gruff::Scatter