gruff 0.1.2 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,311 @@
1
+ # CocoaMagick.rb
2
+ # Tim Burks / RubyCocoa Resources http://www.rubycocoa.com
3
+ #
4
+ # This is a quick-and-dirty replacement for RMagic written specifically
5
+ # for Geoffrey Grosenbach's "gruff" graphing package.
6
+ # It uses RubyCocoa and the Cocoa API to draw the elements of gruff graphs.
7
+ # It is FAR from comprehensive, but works for nearly all of the test cases
8
+ # distributed with gruff. Tested with gruff 0.1.2.
9
+ #
10
+ # To use, change "require 'RMagick'"to "require 'CocoaMagick'" in gruff/base.rb.
11
+ # You also must have RubyCocoa installed, preferably the most recent version.
12
+ # This means you must also be running on a Mac, preferably with OS X 10.4 or later.
13
+ # Due to a Cocoa limitation, this script must either be run by the user currently
14
+ # logged into the console or as root.
15
+ #
16
+ # For help with RubyCocoa, visit "RubyCocoa Resources" at www.rubycocoa.com.
17
+ # For more on gruff, see http://nubyonrails.com/pages/gruff
18
+ #
19
+ require 'osx/cocoa'
20
+
21
+ def log(str)
22
+ puts str if false
23
+ end
24
+
25
+ class OSX::NSImage
26
+ def writePNG(filename)
27
+ bits = OSX::NSBitmapImageRep.alloc.initWithData(self.TIFFRepresentation)
28
+ data = bits.representationUsingType_properties(OSX::NSPNGFileType, nil)
29
+ data.writeToFile_atomically(filename, false)
30
+ end
31
+ end
32
+
33
+ class OSX::NSColor
34
+ def self.colorWithName(name)
35
+ log "colorWithName #{name}"
36
+ if name[0..0] == "#"
37
+ r = eval("0x"+name[1..2]) / 256.0
38
+ g = eval("0x"+name[3..4]) / 256.0
39
+ b = eval("0x"+name[5..6]) / 256.0
40
+ colorWithDeviceRed_green_blue_alpha(r,g,b,1)
41
+ elsif name == "transparent"
42
+ OSX::NSColor.blackColor.colorWithAlphaComponent(0.0)
43
+ elsif name == "grey"
44
+ OSX::NSColor.colorWithDeviceRed_green_blue_alpha(0.5,0.5,0.5,1)
45
+ else
46
+ eval("OSX::NSColor.#{name}Color")
47
+ end
48
+ end
49
+ end
50
+
51
+ module Magick
52
+ class Draw
53
+ attr_accessor :pointsize, :fill, :stroke, :font_weight, :gravity, :font,
54
+ :stroke_color, :fill_color, :stroke_width, :dasharray
55
+ def stroke_and_fill(path)
56
+ @stroke_color.colorWithAlphaComponent(@stroke_opacity).set
57
+ path.stroke
58
+ @fill_color.colorWithAlphaComponent(@fill_opacity).set
59
+ path.fill
60
+ end
61
+ def initialize
62
+ @fill = "white"
63
+ @pointsize = 18
64
+ @stroke_opacity = 1
65
+ @fill_opacity = 1
66
+ @stroke_width = 1
67
+ @stroke_color = OSX::NSColor.colorWithName("grey")
68
+ @fill_color = OSX::NSColor.colorWithName("grey")
69
+ @stack = []
70
+ OSX::NSBezierPath.setDefaultLineJoinStyle(OSX::NSRoundLineJoinStyle)
71
+ OSX::NSBezierPath.setDefaultLineWidth(1)
72
+ end
73
+ def push
74
+ log "Draw.push"
75
+ @stack.push({ # add more if needed
76
+ :fill_color => @fill_color,
77
+ :stroke_color => @stroke_color,
78
+ :dasharray => @dasharray,
79
+ :dashcount => @dashcount,
80
+ :stroke_width => @stroke_width
81
+ })
82
+ self
83
+ end
84
+ def pop
85
+ log "Draw.pop"
86
+ hash = @stack.pop
87
+ hash.keys.each {|key| self.instance_variable_set("@"+key.to_s, hash[key])}
88
+ self
89
+ end
90
+ def scale(x,y)
91
+ log "scale #{x} #{y}"
92
+ @scalex = x
93
+ @scaley = y
94
+ self
95
+ end
96
+ def string_attributes
97
+ attributes = OSX::NSMutableDictionary.alloc.initWithCapacity_(10)
98
+ attributes.setObject_forKey(OSX::NSColor.colorWithName(@fill),
99
+ OSX.NSForegroundColorAttributeName)
100
+ if @font_weight == BoldWeight
101
+ attributes.setObject_forKey(OSX::NSFont.boldSystemFontOfSize(@pointsize),
102
+ OSX.NSFontAttributeName)
103
+ else
104
+ attributes.setObject_forKey(OSX::NSFont.systemFontOfSize(@pointsize),
105
+ OSX.NSFontAttributeName)
106
+ end
107
+ attributes
108
+ end
109
+ def get_type_metrics(a,text)
110
+ log "get_type_metrics #{a} #{text.inspect}"
111
+ attributes = string_attributes()
112
+ metrics = TypeMetric.new
113
+ size = OSX::NSString.stringWithString(text).sizeWithAttributes(attributes)
114
+ metrics.width = size.width
115
+ metrics
116
+ end
117
+ def annotate(image,width,height,x,y,text)
118
+ log "annotate #{image} #{width} #{height} #{x} #{y} #{text}"
119
+ attributes = string_attributes()
120
+ string = OSX::NSString.stringWithString(text)
121
+ size = string.sizeWithAttributes(attributes)
122
+ if @gravity == EastGravity or @gravity == SouthEastGravity or @gravity == NorthEastGravity
123
+ x = x + width - size.width/2
124
+ elsif @gravity == CenterGravity or @gravity == NorthGravity or @gravity == SouthGravity
125
+ x = x + width/2 - size.width/2
126
+ end
127
+ if @gravity == NorthGravity or @gravity == NorthEastGravity or @gravity == NorthWestGravity
128
+ y = y + height - size.height
129
+ elsif @gravity == CenterGravity or @gravity == EastGravity or @gravity == WestGravity
130
+ y = y + height/2 - size.height/2
131
+ else
132
+ y = y - size.height/2
133
+ end
134
+ string.drawAtPoint_withAttributes([x,image.height-y-size.height], attributes)
135
+ self
136
+ end
137
+ def stroke(color)
138
+ log "stroke #{color}"
139
+ @stroke_color = OSX::NSColor.colorWithName(color)
140
+ self
141
+ end
142
+ def fill(color)
143
+ log "fill #{color}"
144
+ @fill_color = OSX::NSColor.colorWithName(color)
145
+ self
146
+ end
147
+ def rectangle(a,b,c,d)
148
+ log "rectangle #{a} #{b} #{c} #{d}"
149
+ @fill_color.set
150
+ OSX::NSRectFill([a*@scalex,$height-d*@scalex,(c-a)*@scalex,(d-b)*@scaley])
151
+ self
152
+ end
153
+ def stroke_width(a)
154
+ log "stroke_width #{a}"
155
+ @stroke_width = a
156
+ OSX::NSBezierPath.setDefaultLineWidth(a*@scalex)
157
+ self
158
+ end
159
+ def line(a,b,c,d)
160
+ log "line #{a} #{b} #{c} #{d}"
161
+ @stroke_color.set
162
+ path = OSX::NSBezierPath.bezierPath
163
+ path.setLineDash_count_phase(@dasharray, @dashcount, 0) if @dasharray
164
+ path.moveToPoint([a*@scalex,$height-b*@scaley])
165
+ path.lineToPoint([c*@scalex,$height-d*@scaley])
166
+ path.stroke
167
+ self
168
+ end
169
+ def stroke_opacity(opacity)
170
+ log "stroke_opacity #{opacity}"
171
+ @stroke_opacity = opacity
172
+ self
173
+ end
174
+ def stroke_color(color)
175
+ stroke(color)
176
+ self
177
+ end
178
+ def fill_opacity(opacity)
179
+ @fill_opacity = opacity
180
+ self
181
+ end
182
+ def stroke_dasharray(a, b)
183
+ log "stroke_dasharray #{a} #{b}"
184
+ @dashcount = 2
185
+ @dasharray = [a,b].pack('f2')
186
+ self
187
+ end
188
+ def circle(cx,cy,px,py)
189
+ log "circle #{cx} #{cy} #{px} #{py}"
190
+ r = Math::sqrt((cx-px)*(cx-px) + (cy-py)*(cy-py))
191
+ path = OSX::NSBezierPath.bezierPathWithOvalInRect(
192
+ [(cx-r)*@scalex,$height-(cy+r)*@scaley,2*r*@scalex,2*r*@scaley])
193
+ stroke_and_fill(path)
194
+ self
195
+ end
196
+ def ellipse(x, y, w, h, as, ae)
197
+ log "ellipse #{x} #{y} #{w} #{h} #{as} #{ae}"
198
+ push
199
+ stroke_width(1)
200
+ @fill_color = @stroke_color
201
+ path = OSX::NSBezierPath.bezierPath
202
+ center = [x*@scalex,$height-y*@scaley]
203
+ path.moveToPoint(center)
204
+ path.appendBezierPathWithArcWithCenter_radius_startAngle_endAngle(
205
+ center,2*w*@scalex,-ae,-as)
206
+ path.closePath
207
+ stroke_and_fill(path)
208
+ pop
209
+ self
210
+ end
211
+ def polyline(*args)
212
+ path = OSX::NSBezierPath.bezierPath
213
+ path.moveToPoint([args[0]*@scalex,$height-args[1]*@scaley])
214
+ i = 2
215
+ while(i < args.length)
216
+ path.lineToPoint([args[i]*@scalex,$height-args[i+1]*@scaley])
217
+ i = i + 2
218
+ end
219
+ stroke_and_fill(path)
220
+ self
221
+ end
222
+ def polygon(*args)
223
+ path = OSX::NSBezierPath.bezierPath
224
+ path.moveToPoint([args[0]*@scalex,$height-args[1]*@scaley])
225
+ i = 2
226
+ while(i < args.length)
227
+ path.lineToPoint([args[i]*@scalex,$height-args[i+1]*@scaley])
228
+ i = i + 2
229
+ end
230
+ path.closePath
231
+ stroke_and_fill(path)
232
+ self
233
+ end
234
+ def draw(a)
235
+ log "draw #{a}"
236
+ self
237
+ end
238
+ end
239
+ class Image
240
+ attr_accessor :image, :width, :height
241
+ def initialize(width, height, fill=nil)
242
+ log "Image.initialize #{width} #{height} #{fill}"
243
+ @width = width
244
+ @height = height
245
+ @image = OSX::NSImage.alloc.initWithSize([width,height])
246
+ @image.lockFocus
247
+ if fill == nil
248
+ OSX::NSColor.blackColor.set
249
+ OSX::NSRectFill([0,0,width,height])
250
+ elsif fill.class == Magick::GradientFill
251
+ gradient = fill.image
252
+ gradient.drawInRect_fromRect_operation_fraction([0,0,width,height],
253
+ [0,0,gradient.size.width,gradient.size.height], OSX::NSCompositeCopy, 1.0)
254
+ else
255
+ fill.drawInRect_fromRect_operation_fraction([0,0,width,height],
256
+ [0,0,fill.size.width,fill.size.height], OSX::NSCompositeCopy, 1.0)
257
+ end
258
+ $width = width
259
+ $height = height
260
+ self
261
+ end
262
+ def write(filename)
263
+ log "Image.write #{filename}"
264
+ @image.unlockFocus
265
+ @image.writePNG(filename)
266
+ self
267
+ end
268
+ def self.read(filename)
269
+ log "Image.read #{filename}"
270
+ image = OSX::NSImage.alloc.initWithContentsOfFile(filename)
271
+ [Image.new(800,600,image)]
272
+ end
273
+ end
274
+ class GradientFill
275
+ attr_accessor :image
276
+ def initialize(a,b,c,d,e,f)
277
+ log "GradientFill.initialize #{a} #{b} #{c} #{d} #{e} #{f}"
278
+ # There's no direct gradient fill support in Cocoa, so fake it
279
+ resolution = 100 # increase this to reduce striping and increase run time...
280
+ @image = OSX::NSImage.alloc.initWithSize([1,resolution])
281
+ @image.lockFocus
282
+ bottomColor = OSX::NSColor.colorWithName(f)
283
+ topColor = OSX::NSColor.colorWithName(e)
284
+ resolution.times {|i|
285
+ bottomColor.blendedColorWithFraction_ofColor(i/(resolution - 1.0), topColor).set
286
+ OSX::NSRectFill([0,i,1,i+1])
287
+ }
288
+ self
289
+ end
290
+ end
291
+ class TypeMetric
292
+ attr_accessor :width
293
+ def initialize
294
+ @width = 50
295
+ end
296
+ end
297
+ NormalWeight = 400
298
+ BoldWeight = 700
299
+ WestGravity = :west
300
+ EastGravity = :east
301
+ CenterGravity = :center
302
+ SouthGravity = :south
303
+ NorthGravity = :north
304
+ SouthWestGravity = :southwest
305
+ NorthWestGravity = :northwest
306
+ NorthEastGravity = :northeast
307
+ SouthEastGravity = :southeast
308
+ end
309
+
310
+ # this prevents a warning from AppKit
311
+ app = OSX::NSApplication.sharedApplication
@@ -16,11 +16,16 @@ class Gruff::Bar < Gruff::Base
16
16
 
17
17
  @d = @d.stroke_opacity 0.0
18
18
 
19
- # setup the BarConversion Object
19
+ # Setup the BarConversion Object
20
20
  conversion = Gruff::BarConversion.new()
21
21
  conversion.graph_height = @graph_height
22
22
  conversion.graph_top = @graph_top
23
- # set up the right mode [1,2,3] see BarConversion for further explains
23
+
24
+ # Labels will be centered over the left of the bar if
25
+ # there are more labels than columns.
26
+ center_labels_left = (@labels.keys.length > @column_count ? true : false)
27
+
28
+ # Set up the right mode [1,2,3] see BarConversion for further explanation
24
29
  if @minimum_value >= 0 then
25
30
  # all bars go from zero to positiv
26
31
  conversion.mode = 1
@@ -54,12 +59,18 @@ class Gruff::Bar < Gruff::Base
54
59
  @d = @d.rectangle(left_x, conv[0], right_x, conv[1])
55
60
 
56
61
  # Calculate center based on bar_width and current row
57
- label_center = @graph_left + (@data.length * @bar_width * point_index) + (@data.length * @bar_width / 2.0)
58
- draw_label(label_center, point_index)
62
+ label_center = @graph_left +
63
+ (@data.length * @bar_width * point_index) +
64
+ (@data.length * @bar_width / 2.0)
65
+ # Subtract half a bar width to center left if requested
66
+ draw_label(label_center - (center_labels_left ? @bar_width / 2.0 : 0.0), point_index)
59
67
  end
60
68
 
61
69
  end
62
70
 
71
+ # Draw the last label if requested
72
+ draw_label(@graph_right, @column_count) if center_labels_left
73
+
63
74
  @d.draw(@base_image)
64
75
  end
65
76
 
@@ -14,20 +14,33 @@
14
14
  # and a cast of thousands.
15
15
  #
16
16
 
17
+ require 'rubygems'
17
18
  require 'RMagick'
19
+ require File.dirname(__FILE__) + '/deprecated'
18
20
 
19
21
  module Gruff
20
22
 
21
- VERSION = '0.1.2'
23
+ VERSION = '0.2.3'
22
24
 
23
25
  class Base
24
26
 
25
27
  include Magick
28
+ include Deprecated
26
29
 
30
+ # Draw extra lines showing where the margins and text centers are
31
+ DEBUG = false
32
+
33
+ # Used for navigating the array of data to plot
27
34
  DATA_LABEL_INDEX = 0
28
35
  DATA_VALUES_INDEX = 1
29
36
  DATA_COLOR_INDEX = 2
30
37
 
38
+ # Blank space around the edges of the graph
39
+ TOP_MARGIN = BOTTOM_MARGIN = RIGHT_MARGIN = LEFT_MARGIN = 20.0
40
+
41
+ # Space around text elements. Mostly used for vertical spacing
42
+ LEGEND_MARGIN = TITLE_MARGIN = LABEL_MARGIN = 10.0
43
+
31
44
  # A hash of names for the individual columns, where the key is the array index for the column this label represents.
32
45
  #
33
46
  # Not all columns need to be named.
@@ -35,6 +48,17 @@ module Gruff
35
48
  # Example: 0 => 2005, 3 => 2006, 5 => 2007, 7 => 2008
36
49
  attr_accessor :labels
37
50
 
51
+ # A label for the bottom of the graph
52
+ attr_accessor :x_axis_label
53
+
54
+ # A label for the left side of the graph
55
+ attr_accessor :y_axis_label
56
+
57
+ # attr_accessor :x_axis_increment
58
+
59
+ # Manually set increment of the horizontal marking lines
60
+ attr_accessor :y_axis_increment
61
+
38
62
  # Get or set the list of colors that will be used to draw the bars or lines.
39
63
  attr_accessor :colors
40
64
 
@@ -64,6 +88,9 @@ module Gruff
64
88
 
65
89
  # The color of the auxiliary labels and lines
66
90
  attr_accessor :marker_color
91
+
92
+ # The number of horizontal lines shown for reference
93
+ attr_accessor :marker_count
67
94
 
68
95
  # The font size of the large title at the top of the graph
69
96
  attr_accessor :title_font_size
@@ -105,11 +132,13 @@ module Gruff
105
132
  @raw_columns = 800.0
106
133
  @raw_rows = 800.0 * (@rows/@columns)
107
134
  @column_count = 0
135
+ @marker_count = nil
108
136
  @maximum_value = @minimum_value = nil
109
137
  @has_data = false
110
138
  @data = Array.new
111
139
  @labels = Hash.new
112
140
  @labels_seen = Hash.new
141
+
113
142
  @scale = @columns / @raw_columns
114
143
 
115
144
  vera_font_path = File.expand_path('Vera.ttf', ENV['MAGICK_FONT_PATH'])
@@ -181,6 +210,11 @@ module Gruff
181
210
  end
182
211
  end
183
212
 
213
+ def font=(font_path)
214
+ @font = font_path
215
+ @d.font = @font
216
+ end
217
+
184
218
  # Add a color to the list of available colors for lines.
185
219
  #
186
220
  # Example:
@@ -200,6 +234,14 @@ module Gruff
200
234
 
201
235
  # A color scheme similar to the popular presentation software.
202
236
  def theme_keynote
237
+ # defaults = {
238
+ # :colors => ['black', 'white'],
239
+ # :additional_line_colors => ['grey'],
240
+ # :marker_color => 'white',
241
+ # :background_colors => nil,
242
+ # :background_image => nil
243
+ # }
244
+
203
245
  reset_themes()
204
246
  # Colors
205
247
  @blue = '#6886B4'
@@ -284,7 +326,8 @@ module Gruff
284
326
  # data("Bart S.", [95, 45, 78, 89, 88, 76], '#ffcc00')
285
327
  #
286
328
  def data(name, data_points=[], color=nil)
287
- @data << [name, data_points, color || increment_color]
329
+ data_points = Array(data_points) # make sure it's an array
330
+ @data << [name, data_points, (color || increment_color)]
288
331
  # Set column count if this is larger than previous counts
289
332
  @column_count = (data_points.length > @column_count) ? data_points.length : @column_count
290
333
 
@@ -298,15 +341,12 @@ module Gruff
298
341
  end
299
342
 
300
343
  # TODO Doesn't work with stacked bar graphs
301
- #Original: @maximum_value = larger_than_max?(data_point, index) ? max(data_point, index) : @maximum_value
344
+ # Original: @maximum_value = larger_than_max?(data_point, index) ? max(data_point, index) : @maximum_value
302
345
  @maximum_value = larger_than_max?(data_point) ? data_point : @maximum_value
303
- if @maximum_value > 0
304
- @has_data = true
305
- end
346
+ @has_data = true if @maximum_value > 0
347
+
306
348
  @minimum_value = less_than_min?(data_point) ? data_point : @minimum_value
307
- if @minimum_value < 0
308
- @has_data = true
309
- end
349
+ @has_data = true if @minimum_value < 0
310
350
  end
311
351
  end
312
352
 
@@ -326,51 +366,17 @@ module Gruff
326
366
  end
327
367
  end
328
368
 
329
- def scale_measurements
330
- setup_graph_measurements
331
- end
332
-
333
- def total_height
334
- @rows + 10
335
- end
336
-
337
- def graph_top
338
- @graph_top * @scale
339
- end
340
-
341
- def graph_height
342
- @graph_height * @scale
343
- end
344
-
345
- def graph_left
346
- @graph_left * @scale
347
- end
348
-
349
- def graph_width
350
- @graph_width * @scale
351
- end
352
-
353
-
354
369
  protected
355
370
 
356
-
357
- def make_stacked
358
- stacked_values = Array.new(@column_count, 0)
359
- @data.each do |value_set|
360
- value_set[1].each_with_index do |value, index|
361
- stacked_values[index] += value
362
- end
363
- value_set[1] = stacked_values.dup
364
- end
365
- end
366
-
367
-
368
371
  # Overridden by subclasses to do the actual plotting of the graph.
369
372
  #
370
373
  # Subclasses should start by calling super() for this method.
371
374
  def draw
372
375
  make_stacked if @stacked
373
376
  setup_drawing()
377
+
378
+ debug { @d.rectangle( LEFT_MARGIN, TOP_MARGIN,
379
+ @raw_columns - RIGHT_MARGIN, @raw_rows - BOTTOM_MARGIN) }
374
380
 
375
381
  # Subclasses will do some drawing here...
376
382
  #@d.draw(@base_image)
@@ -392,26 +398,26 @@ protected
392
398
  sort_norm_data() # Sort norm_data with avg largest values set first (for display)
393
399
 
394
400
  draw_legend()
395
- setup_graph_height()
396
401
  draw_line_markers()
402
+ draw_axis_labels()
397
403
  draw_title
398
404
  end
399
405
 
400
406
  # Make copy of data with values scaled between 0-100
401
- def normalize
402
- if @norm_data.nil?
407
+ def normalize(force=false)
408
+ if @norm_data.nil? || force
403
409
  @norm_data = []
404
410
  return unless @has_data
405
- @spread = @maximum_value.to_f - @minimum_value.to_f
406
- @spread = 20.0 if @spread == 0.0 # Protect from divide by zero
407
- min_val = @minimum_value.to_f
411
+
412
+ calculate_spread
413
+
408
414
  @data.each do |data_row|
409
415
  norm_data_points = []
410
416
  data_row[DATA_VALUES_INDEX].each do |data_point|
411
417
  if data_point.nil?
412
418
  norm_data_points << nil
413
419
  else
414
- norm_data_points << ((data_point.to_f - min_val ) / @spread)
420
+ norm_data_points << ((data_point.to_f - @minimum_value.to_f ) / @spread)
415
421
  end
416
422
  end
417
423
  @norm_data << [data_row[DATA_LABEL_INDEX], norm_data_points, data_row[DATA_COLOR_INDEX]]
@@ -419,28 +425,80 @@ protected
419
425
  end
420
426
  end
421
427
 
422
- def setup_graph_height
423
- @graph_height = @graph_bottom - @graph_top
428
+ def calculate_spread
429
+ @spread = @maximum_value.to_f - @minimum_value.to_f
430
+ @spread = @spread > 0 ? @spread : 1
424
431
  end
425
432
 
426
433
  def setup_graph_measurements
427
- # TODO Separate horizontal lines from line number labels so they can be shown or hidden independently
428
- # TODO Get width of longest left-hand vertical text label and space left margin accordingly
429
- unless @hide_line_markers
430
- @graph_left = 130.0 # TODO Calculate based on string width of labels
431
- @graph_right_margin = 80.0 # TODO see previous line
432
- @graph_bottom_margin = 80.0
434
+ @marker_caps_height = calculate_caps_height(@marker_font_size)
435
+ @title_caps_height = calculate_caps_height(@title_font_size)
436
+ @legend_caps_height = calculate_caps_height(@legend_font_size)
437
+
438
+ if @hide_line_markers
439
+ (@graph_left,
440
+ @graph_right_margin,
441
+ @graph_bottom_margin) = [LEFT_MARGIN, RIGHT_MARGIN, BOTTOM_MARGIN]
433
442
  else
434
- @graph_left = @graph_right_margin = @graph_bottom_margin = 40
443
+ @graph_left = LEFT_MARGIN +
444
+ calculate_width(@marker_font_size, label(@maximum_value.to_f)) +
445
+ LABEL_MARGIN * 2 +
446
+ (@y_axis_label.nil? ? 0.0 : @marker_caps_height + LABEL_MARGIN * 2)
447
+ # Make space for half the width of the rightmost column label.
448
+ # Might be greater than the number of columns if between-style bar markers are used.
449
+ last_label = @labels.keys.sort.last.to_i
450
+ @graph_right_margin = RIGHT_MARGIN +
451
+ (last_label >= (@column_count-1) ? calculate_width(@marker_font_size, @labels[last_label])/2.0 : 0)
452
+ @graph_bottom_margin = BOTTOM_MARGIN +
453
+ @marker_caps_height + LABEL_MARGIN
435
454
  end
436
455
 
437
456
  @graph_right = @raw_columns - @graph_right_margin
438
-
439
457
  @graph_width = @raw_columns - @graph_left - @graph_right_margin
440
458
 
441
- @graph_top = 150.0
442
- @graph_bottom = @raw_rows - @graph_bottom_margin
443
- setup_graph_height()
459
+ # When @hide title, leave a TITLE_MARGIN space for aesthetics.
460
+ # Same with @hide_legend
461
+ @graph_top = TOP_MARGIN +
462
+ (@hide_title ? TITLE_MARGIN : @title_caps_height + TITLE_MARGIN * 2) +
463
+ (@hide_legend ? LEGEND_MARGIN : @legend_caps_height + LEGEND_MARGIN * 2)
464
+
465
+ @graph_bottom = @raw_rows - @graph_bottom_margin -
466
+ (@x_axis_label.nil? ? 0.0 : @marker_caps_height + LABEL_MARGIN)
467
+
468
+ @graph_height = @graph_bottom - @graph_top
469
+ end
470
+
471
+ # Draw the optional labels for the x axis and y axis.
472
+ def draw_axis_labels
473
+ unless @x_axis_label.nil?
474
+ # X Axis
475
+ # Centered vertically and horizontally by setting the
476
+ # height to 1.0 and the width to the width of the graph.
477
+ x_axis_label_y_coordinate = @graph_bottom + LABEL_MARGIN * 2 + @marker_caps_height
478
+
479
+ # TODO Center between graph area
480
+ @d.fill = @marker_color
481
+ @d.font = @font if @font
482
+ @d.stroke = 'transparent'
483
+ @d.pointsize = scale_fontsize(@marker_font_size)
484
+ @d.gravity = NorthGravity
485
+ @d = @d.annotate_scaled( @base_image,
486
+ @raw_columns, 1.0,
487
+ 0.0, x_axis_label_y_coordinate,
488
+ @x_axis_label, @scale)
489
+ debug { @d.line 0.0, x_axis_label_y_coordinate, @raw_columns, x_axis_label_y_coordinate }
490
+ end
491
+
492
+ unless @y_axis_label.nil?
493
+ # Y Axis, rotated vertically
494
+ @d.rotation = 90.0
495
+ @d.gravity = CenterGravity
496
+ @d = @d.annotate_scaled( @base_image,
497
+ 1.0, @raw_rows,
498
+ LEFT_MARGIN + @marker_caps_height / 2.0, 0.0,
499
+ @y_axis_label, @scale)
500
+ @d.rotation = -90.0
501
+ end
444
502
  end
445
503
 
446
504
  # Draws horizontal background lines and labels
@@ -449,20 +507,39 @@ protected
449
507
 
450
508
  # Draw horizontal line markers and annotate with numbers
451
509
  @d = @d.stroke(@marker_color)
452
- @d = @d.stroke_width 1
453
- number_of_lines = 4
454
-
455
- # TODO Round maximum marker value to a round number like 100, 0.1, 0.5, etc.
456
- spread = @maximum_value.to_f - @minimum_value.to_f
457
- spread = spread > 0 ? spread : 1
458
- increment = (spread > 0) ? significant(spread / number_of_lines) : 1
459
- inc_graph = @graph_height.to_f / (spread / increment)
510
+ @d = @d.stroke_width 1
511
+
512
+ if @y_axis_increment.nil?
513
+ # Try to use a number of horizontal lines that will come out even.
514
+ #
515
+ # TODO Do the same for larger numbers...100, 75, 50, 25
516
+ if @marker_count.nil?
517
+ (3..7).each do |lines|
518
+ if @spread % lines == 0.0
519
+ @marker_count = lines
520
+ break
521
+ end
522
+ end
523
+ @marker_count ||= 4
524
+ end
525
+ @increment = (@spread > 0) ? significant(@spread / @marker_count) : 1
526
+ else
527
+ # TODO Make this work for negative values
528
+ @maximum_value = [@maximum_value.ceil, @y_axis_increment].max
529
+ @minimum_value = @minimum_value.floor
530
+ calculate_spread
531
+ normalize(true)
532
+
533
+ @marker_count = (@spread / @y_axis_increment).to_i
534
+ @increment = @y_axis_increment
535
+ end
536
+ @increment_scaled = @graph_height.to_f / (@spread / @increment)
460
537
 
461
- (0..number_of_lines).each do |index|
462
- y = @graph_top + @graph_height - index.to_f * inc_graph
538
+ (0..@marker_count).each do |index|
539
+ y = @graph_top + @graph_height - index.to_f * @increment_scaled
463
540
  @d = @d.line(@graph_left, y, @graph_right, y)
464
541
 
465
- marker_label = index * increment + @minimum_value.to_f
542
+ marker_label = index * @increment + @minimum_value.to_f
466
543
 
467
544
  @d.fill = @marker_color
468
545
  @d.font = @font if @font
@@ -470,16 +547,19 @@ protected
470
547
  @d.pointsize = scale_fontsize(@marker_font_size)
471
548
  @d.gravity = EastGravity
472
549
 
550
+ # Vertically center with 1.0 for the height
473
551
  @d = @d.annotate_scaled( @base_image,
474
- 100, 20,
475
- -10, y - (@marker_font_size/2.0),
476
- marker_label.to_s, @scale)
552
+ @graph_left - LABEL_MARGIN, 1.0,
553
+ 0.0, y,
554
+ label(marker_label), @scale)
477
555
  end
556
+
557
+ # Submitted by a contibutor...the utility escapes me
478
558
  i = 0
479
559
  @additional_line_values.each do |value|
480
- inc_graph = @graph_height.to_f / (@maximum_value.to_f / value)
560
+ @increment_scaled = @graph_height.to_f / (@maximum_value.to_f / value)
481
561
 
482
- y = @graph_top + @graph_height - inc_graph
562
+ y = @graph_top + @graph_height - @increment_scaled
483
563
 
484
564
  @d = @d.stroke(@additional_line_colors[i])
485
565
  @d = @d.line(@graph_left, y, @graph_right, y)
@@ -498,13 +578,15 @@ protected
498
578
  end
499
579
  end
500
580
 
501
- # Draws a legend with the names of the datasets matched to the colors used to draw them.
581
+ # Draws a legend with the names of the datasets
582
+ # matched to the colors used to draw them.
502
583
  def draw_legend
503
584
  return if @hide_legend
504
585
 
505
586
  @legend_labels = @data.collect {|item| item[DATA_LABEL_INDEX] }
506
587
 
507
588
  legend_square_width = 20 # small square with color of this item
589
+ legend_square_margin = 4
508
590
 
509
591
  # May fix legend drawing problem at small sizes
510
592
  @d.font = @font if @font
@@ -512,12 +594,22 @@ protected
512
594
 
513
595
  metrics = @d.get_type_metrics(@base_image, @legend_labels.join(''))
514
596
  legend_text_width = metrics.width
515
- legend_width = legend_text_width + (@legend_labels.length * legend_square_width * 2.7)
597
+ legend_width = legend_text_width +
598
+ (@legend_labels.length * legend_square_width * 2.7)
516
599
  legend_left = (@raw_columns - legend_width) / 2
517
600
  legend_increment = legend_width / @legend_labels.length.to_f
518
601
 
519
602
  current_x_offset = legend_left
603
+ current_y_offset = @hide_title ?
604
+ TOP_MARGIN + LEGEND_MARGIN :
605
+ TOP_MARGIN +
606
+ TITLE_MARGIN + @title_caps_height +
607
+ LEGEND_MARGIN
608
+
609
+ debug { @d.line 0.0, current_y_offset, @raw_columns, current_y_offset }
610
+
520
611
  @legend_labels.each_with_index do |legend_label, index|
612
+
521
613
  # Draw label
522
614
  @d.fill = @marker_color
523
615
  @d.font = @font if @font
@@ -526,16 +618,17 @@ protected
526
618
  @d.font_weight = NormalWeight
527
619
  @d.gravity = WestGravity
528
620
  @d = @d.annotate_scaled( @base_image,
529
- @raw_columns, 24,
530
- current_x_offset + (legend_square_width * 1.7), 70,
621
+ @raw_columns, 1.0,
622
+ current_x_offset + (legend_square_width * 1.7), current_y_offset,
531
623
  legend_label.to_s, @scale)
532
624
 
533
625
  # Now draw box with color of this dataset
534
- legend_box_y_offset = 2 # Move box down slightly to center
535
626
  @d = @d.stroke 'transparent'
536
627
  @d = @d.fill @data[index][DATA_COLOR_INDEX]
537
- @d = @d.rectangle(current_x_offset, 70 + legend_box_y_offset,
538
- current_x_offset + legend_square_width, 70 + legend_square_width + legend_box_y_offset)
628
+ @d = @d.rectangle(current_x_offset,
629
+ current_y_offset - legend_square_width / 2.0,
630
+ current_x_offset + legend_square_width,
631
+ current_y_offset + legend_square_width / 2.0)
539
632
 
540
633
  @d.pointsize = @legend_font_size
541
634
  metrics = @d.get_type_metrics(@base_image, legend_label.to_s)
@@ -553,30 +646,36 @@ protected
553
646
  @d.stroke = 'transparent'
554
647
  @d.pointsize = scale_fontsize(@title_font_size)
555
648
  @d.font_weight = BoldWeight
556
- @d.gravity = CenterGravity
649
+ @d.gravity = NorthGravity
557
650
  @d = @d.annotate_scaled( @base_image,
558
- @raw_columns, 50,
559
- 0, 10,
651
+ @raw_columns, 1.0,
652
+ 0, TOP_MARGIN,
560
653
  @title, @scale)
561
654
  end
562
655
 
563
656
  ##
564
657
  # Draws column labels below graph, centered over x_offset
658
+ #
659
+ # TODO Allow WestGravity as an option
660
+
565
661
  def draw_label(x_offset, index)
566
662
  return if @hide_line_markers
567
663
 
568
664
  if !@labels[index].nil? && @labels_seen[index].nil?
665
+ y_offset = @graph_bottom + LABEL_MARGIN
666
+
569
667
  @d.fill = @marker_color
570
668
  @d.font = @font if @font
571
669
  @d.stroke = 'transparent'
572
670
  @d.font_weight = NormalWeight
573
671
  @d.pointsize = scale_fontsize(@marker_font_size)
574
- @d.gravity = CenterGravity
672
+ @d.gravity = NorthGravity
575
673
  @d = @d.annotate_scaled(@base_image,
576
- 1, 1,
577
- x_offset, @raw_rows - (@graph_bottom_margin - 30),
674
+ 1.0, 1.0,
675
+ x_offset, y_offset,
578
676
  @labels[index], @scale)
579
677
  @labels_seen[index] = 1
678
+ debug { @d.line 0.0, y_offset, @raw_columns, y_offset }
580
679
  end
581
680
  end
582
681
 
@@ -621,7 +720,6 @@ protected
621
720
 
622
721
  @d = Draw.new
623
722
  # Scale down from 800x600 used to calculate drawing.
624
- # NOTE: Font annotation is now affected and has to be done manually.
625
723
  @d = @d.scale(@scale, @scale)
626
724
  end
627
725
 
@@ -629,9 +727,10 @@ protected
629
727
  value * @scale
630
728
  end
631
729
 
730
+ # Return a comparable fontsize for the current graph.
632
731
  def scale_fontsize(value)
633
732
  new_fontsize = value * @scale
634
- #return 10 if new_fontsize < 10
733
+ # return new_fontsize < 10.0 ? 10.0 : new_fontsize
635
734
  return new_fontsize
636
735
  end
637
736
 
@@ -648,10 +747,14 @@ protected
648
747
  data_point < @minimum_value
649
748
  end
650
749
 
750
+ ##
751
+ # Overridden by subclasses that need it.
651
752
  def max(data_point, index)
652
753
  data_point
653
754
  end
654
755
 
756
+ ##
757
+ # Overridden by subclasses that need it.
655
758
  def min(data_point, index)
656
759
  data_point
657
760
  end
@@ -706,9 +809,30 @@ protected
706
809
  @minimum_value = 0
707
810
  end
708
811
 
812
+ def make_stacked
813
+ stacked_values = Array.new(@column_count, 0)
814
+ @data.each do |value_set|
815
+ value_set[1].each_with_index do |value, index|
816
+ stacked_values[index] += value
817
+ end
818
+ value_set[1] = stacked_values.dup
819
+ end
820
+ end
709
821
 
710
822
  private
711
823
 
824
+ # Takes a block and draws it if DEBUG is true.
825
+ #
826
+ # debug { @d.rectangle x1, y1, x2, y2 }
827
+ #
828
+ def debug
829
+ if DEBUG
830
+ @d = @d.fill 'transparent'
831
+ @d = @d.stroke 'turquoise'
832
+ @d = yield
833
+ end
834
+ end
835
+
712
836
  def increment_color
713
837
  if @color_index == 0
714
838
  @color_index += 1
@@ -726,14 +850,51 @@ private
726
850
  end
727
851
  end
728
852
 
729
- end
853
+ def label(value)
854
+ if (@spread.to_f % @marker_count.to_f == 0) || !@y_axis_increment.nil?
855
+ return value.to_i.to_s
856
+ end
857
+
858
+ if @spread > 10.0
859
+ sprintf("%0i", value)
860
+ elsif @spread >= 3.0
861
+ sprintf("%0.2f", value)
862
+ else
863
+ value.to_s
864
+ end
865
+ end
866
+
867
+ ##
868
+ # Returns the height of the capital letter 'X' for the current font and size.
869
+ #
870
+ # Not scaled since it deals with dimensions that the regular
871
+ # scaling will handle.
872
+
873
+ def calculate_caps_height(font_size)
874
+ @d.pointsize = font_size
875
+ @d.get_type_metrics(@base_image, 'X').height
876
+ end
877
+
878
+ ##
879
+ # Returns the width of a string at this pointsize.
880
+ #
881
+ # Not scaled since it deals with dimensions that the regular
882
+ # scaling will handle.
883
+
884
+ def calculate_width(font_size, text)
885
+ @d.pointsize = font_size
886
+ @d.get_type_metrics(@base_image, text.to_s).width
887
+ end
888
+
889
+ end # Gruff::Base
730
890
 
731
891
  class ColorlistExhaustedException < StandardError; end
732
892
 
733
- end
893
+ end # Gruff
734
894
 
735
895
 
736
896
  module Magick
897
+
737
898
  class Draw
738
899
 
739
900
  # Additional method since Draw.scale doesn't affect annotations.
@@ -748,4 +909,6 @@ module Magick
748
909
  end
749
910
 
750
911
  end
751
- end
912
+
913
+ end # Magick
914
+