gruff 0.1.2 → 0.2.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+