woodhull-gruff 0.3.5

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.
Files changed (80) hide show
  1. data/History.txt +111 -0
  2. data/MIT-LICENSE +21 -0
  3. data/Manifest.txt +79 -0
  4. data/README.txt +40 -0
  5. data/Rakefile +55 -0
  6. data/assets/bubble.png +0 -0
  7. data/assets/city_scene/background/0000.png +0 -0
  8. data/assets/city_scene/background/0600.png +0 -0
  9. data/assets/city_scene/background/2000.png +0 -0
  10. data/assets/city_scene/clouds/cloudy.png +0 -0
  11. data/assets/city_scene/clouds/partly_cloudy.png +0 -0
  12. data/assets/city_scene/clouds/stormy.png +0 -0
  13. data/assets/city_scene/grass/default.png +0 -0
  14. data/assets/city_scene/haze/true.png +0 -0
  15. data/assets/city_scene/number_sample/1.png +0 -0
  16. data/assets/city_scene/number_sample/2.png +0 -0
  17. data/assets/city_scene/number_sample/default.png +0 -0
  18. data/assets/city_scene/sky/0000.png +0 -0
  19. data/assets/city_scene/sky/0200.png +0 -0
  20. data/assets/city_scene/sky/0400.png +0 -0
  21. data/assets/city_scene/sky/0600.png +0 -0
  22. data/assets/city_scene/sky/0800.png +0 -0
  23. data/assets/city_scene/sky/1000.png +0 -0
  24. data/assets/city_scene/sky/1200.png +0 -0
  25. data/assets/city_scene/sky/1400.png +0 -0
  26. data/assets/city_scene/sky/1500.png +0 -0
  27. data/assets/city_scene/sky/1700.png +0 -0
  28. data/assets/city_scene/sky/2000.png +0 -0
  29. data/assets/pc306715.jpg +0 -0
  30. data/assets/plastik/blue.png +0 -0
  31. data/assets/plastik/green.png +0 -0
  32. data/assets/plastik/red.png +0 -0
  33. data/init.rb +2 -0
  34. data/lib/gruff.rb +27 -0
  35. data/lib/gruff/accumulator_bar.rb +27 -0
  36. data/lib/gruff/area.rb +58 -0
  37. data/lib/gruff/bar.rb +84 -0
  38. data/lib/gruff/bar_conversion.rb +46 -0
  39. data/lib/gruff/base.rb +1116 -0
  40. data/lib/gruff/bullet.rb +109 -0
  41. data/lib/gruff/deprecated.rb +39 -0
  42. data/lib/gruff/line.rb +105 -0
  43. data/lib/gruff/mini/bar.rb +32 -0
  44. data/lib/gruff/mini/legend.rb +77 -0
  45. data/lib/gruff/mini/pie.rb +36 -0
  46. data/lib/gruff/mini/side_bar.rb +35 -0
  47. data/lib/gruff/net.rb +142 -0
  48. data/lib/gruff/photo_bar.rb +100 -0
  49. data/lib/gruff/pie.rb +124 -0
  50. data/lib/gruff/scene.rb +209 -0
  51. data/lib/gruff/side_bar.rb +115 -0
  52. data/lib/gruff/side_stacked_bar.rb +74 -0
  53. data/lib/gruff/spider.rb +130 -0
  54. data/lib/gruff/stacked_area.rb +67 -0
  55. data/lib/gruff/stacked_bar.rb +54 -0
  56. data/lib/gruff/stacked_mixin.rb +23 -0
  57. data/rails_generators/gruff/gruff_generator.rb +63 -0
  58. data/rails_generators/gruff/templates/controller.rb +32 -0
  59. data/rails_generators/gruff/templates/functional_test.rb +24 -0
  60. data/test/gruff_test_case.rb +123 -0
  61. data/test/test_accumulator_bar.rb +50 -0
  62. data/test/test_area.rb +134 -0
  63. data/test/test_bar.rb +283 -0
  64. data/test/test_base.rb +8 -0
  65. data/test/test_bullet.rb +26 -0
  66. data/test/test_legend.rb +68 -0
  67. data/test/test_line.rb +513 -0
  68. data/test/test_mini_bar.rb +32 -0
  69. data/test/test_mini_pie.rb +20 -0
  70. data/test/test_mini_side_bar.rb +37 -0
  71. data/test/test_net.rb +230 -0
  72. data/test/test_photo.rb +41 -0
  73. data/test/test_pie.rb +154 -0
  74. data/test/test_scene.rb +100 -0
  75. data/test/test_side_bar.rb +12 -0
  76. data/test/test_sidestacked_bar.rb +89 -0
  77. data/test/test_spider.rb +216 -0
  78. data/test/test_stacked_area.rb +52 -0
  79. data/test/test_stacked_bar.rb +52 -0
  80. metadata +160 -0
@@ -0,0 +1,84 @@
1
+ require File.dirname(__FILE__) + '/base'
2
+ require File.dirname(__FILE__) + '/bar_conversion'
3
+
4
+ class Gruff::Bar < Gruff::Base
5
+
6
+ def draw
7
+ # Labels will be centered over the left of the bar if
8
+ # there are more labels than columns. This is basically the same
9
+ # as where it would be for a line graph.
10
+ @center_labels_over_point = (@labels.keys.length > @column_count ? true : false)
11
+
12
+ super
13
+ return unless @has_data
14
+
15
+ draw_bars
16
+ end
17
+
18
+ protected
19
+
20
+ def draw_bars
21
+ # Setup spacing.
22
+ #
23
+ # Columns sit side-by-side.
24
+ spacing_factor = 0.9 # space between the bars
25
+ @bar_width = @graph_width / (@column_count * @data.length).to_f
26
+ padding = (@bar_width * (1 - spacing_factor)) / 2
27
+
28
+ @d = @d.stroke_opacity 0.0
29
+
30
+ # Setup the BarConversion Object
31
+ conversion = Gruff::BarConversion.new()
32
+ conversion.graph_height = @graph_height
33
+ conversion.graph_top = @graph_top
34
+
35
+ # Set up the right mode [1,2,3] see BarConversion for further explanation
36
+ if @minimum_value >= 0 then
37
+ # all bars go from zero to positiv
38
+ conversion.mode = 1
39
+ else
40
+ # all bars go from 0 to negativ
41
+ if @maximum_value <= 0 then
42
+ conversion.mode = 2
43
+ else
44
+ # bars either go from zero to negativ or to positiv
45
+ conversion.mode = 3
46
+ conversion.spread = @spread
47
+ conversion.minimum_value = @minimum_value
48
+ conversion.zero = -@minimum_value/@spread
49
+ end
50
+ end
51
+
52
+ # iterate over all normalised data
53
+ @norm_data.each_with_index do |data_row, row_index|
54
+
55
+ data_row[DATA_VALUES_INDEX].each_with_index do |data_point, point_index|
56
+ # Use incremented x and scaled y
57
+ # x
58
+ left_x = @graph_left + (@bar_width * (row_index + point_index + ((@data.length - 1) * point_index))) + padding
59
+ right_x = left_x + @bar_width * spacing_factor
60
+ # y
61
+ conv = []
62
+ conversion.getLeftYRightYscaled( data_point, conv )
63
+
64
+ # create new bar
65
+ @d = @d.fill data_row[DATA_COLOR_INDEX]
66
+ @d = @d.rectangle(left_x, conv[0], right_x, conv[1])
67
+
68
+ # Calculate center based on bar_width and current row
69
+ label_center = @graph_left +
70
+ (@data.length * @bar_width * point_index) +
71
+ (@data.length * @bar_width / 2.0)
72
+ # Subtract half a bar width to center left if requested
73
+ draw_label(label_center - (@center_labels_over_point ? @bar_width / 2.0 : 0.0), point_index)
74
+ end
75
+
76
+ end
77
+
78
+ # Draw the last label if requested
79
+ draw_label(@graph_right, @column_count) if @center_labels_over_point
80
+
81
+ @d.draw(@base_image)
82
+ end
83
+
84
+ end
@@ -0,0 +1,46 @@
1
+ ##
2
+ # Original Author: David Stokar
3
+ #
4
+ # This class perfoms the y coordinats conversion for the bar class.
5
+ #
6
+ # There are three cases:
7
+ #
8
+ # 1. Bars all go from zero in positive direction
9
+ # 2. Bars all go from zero to negative direction
10
+ # 3. Bars either go from zero to positive or from zero to negative
11
+ #
12
+ class Gruff::BarConversion
13
+ attr_writer :mode
14
+ attr_writer :zero
15
+ attr_writer :graph_top
16
+ attr_writer :graph_height
17
+ attr_writer :minimum_value
18
+ attr_writer :spread
19
+
20
+ def getLeftYRightYscaled( data_point, result )
21
+ case @mode
22
+ when 1 then # Case one
23
+ # minimum value >= 0 ( only positiv values )
24
+ result[0] = @graph_top + @graph_height*(1 - data_point) + 1
25
+ result[1] = @graph_top + @graph_height - 1
26
+ when 2 then # Case two
27
+ # only negativ values
28
+ result[0] = @graph_top + 1
29
+ result[1] = @graph_top + @graph_height*(1 - data_point) - 1
30
+ when 3 then # Case three
31
+ # positiv and negativ values
32
+ val = data_point-@minimum_value/@spread
33
+ if ( data_point >= @zero ) then
34
+ result[0] = @graph_top + @graph_height*(1 - (val-@zero)) + 1
35
+ result[1] = @graph_top + @graph_height*(1 - @zero) - 1
36
+ else
37
+ result[0] = @graph_top + @graph_height*(1 - (val-@zero)) + 1
38
+ result[1] = @graph_top + @graph_height*(1 - @zero) - 1
39
+ end
40
+ else
41
+ result[0] = 0.0
42
+ result[1] = 0.0
43
+ end
44
+ end
45
+
46
+ end
@@ -0,0 +1,1116 @@
1
+ require 'rubygems'
2
+ require 'RMagick'
3
+
4
+ require File.dirname(__FILE__) + '/deprecated'
5
+
6
+ ##
7
+ # = Gruff. Graphs.
8
+ #
9
+ # Author:: Geoffrey Grosenbach boss@topfunky.com
10
+ #
11
+ # Originally Created:: October 23, 2005
12
+ #
13
+ # Extra thanks to Tim Hunter for writing RMagick, and also contributions by
14
+ # Jarkko Laine, Mike Perham, Andreas Schwarz, Alun Eyre, Guillaume Theoret,
15
+ # David Stokar, Paul Rogers, Dave Woodward, Frank Oxener, Kevin Clark, Cies
16
+ # Breijs, Richard Cowin, and a cast of thousands.
17
+ #
18
+ # See Gruff::Base#theme= for setting themes.
19
+
20
+ module Gruff
21
+
22
+ # This is the version of Gruff you are using.
23
+ VERSION = '0.3.5'
24
+
25
+ class Base
26
+
27
+ include Magick
28
+ include Deprecated
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
34
+ DATA_LABEL_INDEX = 0
35
+ DATA_VALUES_INDEX = 1
36
+ DATA_COLOR_INDEX = 2
37
+
38
+ # Space around text elements. Mostly used for vertical spacing
39
+ LEGEND_MARGIN = TITLE_MARGIN = LABEL_MARGIN = 10.0
40
+
41
+ DEFAULT_TARGET_WIDTH = 800
42
+
43
+ # Blank space above the graph
44
+ attr_accessor :top_margin
45
+
46
+ # Blank space below the graph
47
+ attr_accessor :bottom_margin
48
+
49
+ # Blank space to the right of the graph
50
+ attr_accessor :right_margin
51
+
52
+ # Blank space to the left of the graph
53
+ attr_accessor :left_margin
54
+
55
+ # A hash of names for the individual columns, where the key is the array
56
+ # index for the column this label represents.
57
+ #
58
+ # Not all columns need to be named.
59
+ #
60
+ # Example: 0 => 2005, 3 => 2006, 5 => 2007, 7 => 2008
61
+ attr_accessor :labels
62
+
63
+ # Used internally for spacing.
64
+ #
65
+ # By default, labels are centered over the point they represent.
66
+ attr_accessor :center_labels_over_point
67
+
68
+ # Used internally for horizontal graph types.
69
+ attr_accessor :has_left_labels
70
+
71
+ # A label for the bottom of the graph
72
+ attr_accessor :x_axis_label
73
+
74
+ # A label for the left side of the graph
75
+ attr_accessor :y_axis_label
76
+
77
+ # attr_accessor :x_axis_increment
78
+
79
+ # Manually set increment of the horizontal marking lines
80
+ attr_accessor :y_axis_increment
81
+
82
+ # Get or set the list of colors that will be used to draw the bars or lines.
83
+ attr_accessor :colors
84
+
85
+ # The large title of the graph displayed at the top
86
+ attr_accessor :title
87
+
88
+ # Font used for titles, labels, etc. Works best if you provide the full
89
+ # path to the TTF font file. RMagick must be built with the Freetype
90
+ # libraries for this to work properly.
91
+ #
92
+ # Tries to find Bitstream Vera (Vera.ttf) in the location specified by
93
+ # ENV['MAGICK_FONT_PATH']. Uses default RMagick font otherwise.
94
+ #
95
+ # The font= method below fulfills the role of the writer, so we only need
96
+ # a reader here.
97
+ attr_reader :font
98
+
99
+ attr_accessor :font_color
100
+
101
+ # Prevent drawing of line markers
102
+ attr_accessor :hide_line_markers
103
+
104
+ # Prevent drawing of the legend
105
+ attr_accessor :hide_legend
106
+
107
+ # Prevent drawing of the title
108
+ attr_accessor :hide_title
109
+
110
+ # Prevent drawing of line numbers
111
+ attr_accessor :hide_line_numbers
112
+
113
+ # Message shown when there is no data. Fits up to 20 characters. Defaults
114
+ # to "No Data."
115
+ attr_accessor :no_data_message
116
+
117
+ # The font size of the large title at the top of the graph
118
+ attr_accessor :title_font_size
119
+
120
+ # The size of the line that the graph is drawn with.
121
+ attr_accessor :line_stroke_width
122
+
123
+ # Optionally set the size of the font. Based on an 800x600px graph.
124
+ # Default is 20.
125
+ #
126
+ # Will be scaled down if graph is smaller than 800px wide.
127
+ attr_accessor :legend_font_size
128
+
129
+ # The font size of the labels around the graph
130
+ attr_accessor :marker_font_size
131
+
132
+ # The color of the auxiliary lines
133
+ attr_accessor :marker_color
134
+
135
+ # The number of horizontal lines shown for reference
136
+ attr_accessor :marker_count
137
+
138
+ # You can manually set a minimum value instead of having the values
139
+ # guessed for you.
140
+ #
141
+ # Set it after you have given all your data to the graph object.
142
+ attr_accessor :minimum_value
143
+
144
+ # You can manually set a maximum value, such as a percentage-based graph
145
+ # that always goes to 100.
146
+ #
147
+ # If you use this, you must set it after you have given all your data to
148
+ # the graph object.
149
+ attr_accessor :maximum_value
150
+
151
+ # Set to false if you don't want the data to be sorted with largest avg
152
+ # values at the back.
153
+ attr_accessor :sort
154
+
155
+ # Experimental
156
+ attr_accessor :additional_line_values
157
+
158
+ # Experimental
159
+ attr_accessor :stacked
160
+
161
+ # Optionally set the size of the colored box by each item in the legend.
162
+ # Default is 20.0
163
+ #
164
+ # Will be scaled down if graph is smaller than 800px wide.
165
+ attr_accessor :legend_box_size
166
+
167
+ # If one numerical argument is given, the graph is drawn at 4/3 ratio
168
+ # according to the given width (800 results in 800x600, 400 gives 400x300,
169
+ # etc.).
170
+ #
171
+ # Or, send a geometry string for other ratios ('800x400', '400x225').
172
+ #
173
+ # Looks for Bitstream Vera as the default font. Expects an environment var
174
+ # of MAGICK_FONT_PATH to be set. (Uses RMagick's default font otherwise.)
175
+ def initialize(target_width=DEFAULT_TARGET_WIDTH)
176
+ @top_margin = @bottom_margin = @left_margin = @right_margin = 20.0
177
+
178
+ if not Numeric === target_width
179
+ geometric_width, geometric_height = target_width.split('x')
180
+ @columns = geometric_width.to_f
181
+ @rows = geometric_height.to_f
182
+ else
183
+ @columns = target_width.to_f
184
+ @rows = target_width.to_f * 0.75
185
+ end
186
+
187
+ initialize_ivars
188
+
189
+ reset_themes
190
+ theme_keynote
191
+ end
192
+
193
+ # Set instance variables for this object.
194
+ #
195
+ # Subclasses can override this, call super, then set values separately.
196
+ #
197
+ # This makes it possible to set defaults in a subclass but still allow
198
+ # developers to change this values in their program.
199
+ def initialize_ivars
200
+ # Internal for calculations
201
+ @raw_columns = 800.0
202
+ @raw_rows = 800.0 * (@rows/@columns)
203
+ @column_count = 0
204
+ @marker_count = nil
205
+ @maximum_value = @minimum_value = nil
206
+ @has_data = false
207
+ @data = Array.new
208
+ @labels = Hash.new
209
+ @labels_seen = Hash.new
210
+ @sort = true
211
+ @title = nil
212
+ @line_stroke_width = 5.0
213
+
214
+ @scale = @columns / @raw_columns
215
+
216
+ vera_font_path = File.expand_path('Vera.ttf', ENV['MAGICK_FONT_PATH'])
217
+ @font = File.exists?(vera_font_path) ? vera_font_path : nil
218
+
219
+ @marker_font_size = 21.0
220
+ @legend_font_size = 20.0
221
+ @title_font_size = 36.0
222
+
223
+ @legend_box_size = 20.0
224
+
225
+ @no_data_message = "No Data"
226
+
227
+ @hide_line_markers = @hide_legend = @hide_title = @hide_line_numbers = false
228
+ @center_labels_over_point = true
229
+ @has_left_labels = false
230
+
231
+ @additional_line_values = []
232
+ @additional_line_colors = []
233
+ @theme_options = {}
234
+
235
+ @x_axis_label = @y_axis_label = nil
236
+ @y_axis_increment = nil
237
+ @stacked = nil
238
+ @norm_data = nil
239
+ end
240
+
241
+ # Sets the top, bottom, left and right margins to +margin+.
242
+ def margins=(margin)
243
+ @top_margin = @left_margin = @right_margin = @bottom_margin = margin
244
+ end
245
+
246
+ # Sets the font for graph text to the font at +font_path+.
247
+ def font=(font_path)
248
+ @font = font_path
249
+ @d.font = @font
250
+ end
251
+
252
+ # Add a color to the list of available colors for lines.
253
+ #
254
+ # Example:
255
+ # add_color('#c0e9d3')
256
+ def add_color(colorname)
257
+ @colors << colorname
258
+ end
259
+
260
+ # Replace the entire color list with a new array of colors. You need to
261
+ # have one more color than the number of datasets you intend to draw. Also
262
+ # aliased as the colors= setter method.
263
+ #
264
+ # Example:
265
+ # replace_colors ['#cc99cc', '#d9e043', '#34d8a2']
266
+ def replace_colors(color_list=[])
267
+ @colors = color_list
268
+ end
269
+
270
+ # You can set a theme manually. Assign a hash to this method before you
271
+ # send your data.
272
+ #
273
+ # graph.theme = {
274
+ # :colors => %w(orange purple green white red),
275
+ # :marker_color => 'blue',
276
+ # :background_colors => %w(black grey)
277
+ # }
278
+ #
279
+ # :background_image => 'squirrel.png' is also possible.
280
+ #
281
+ # (Or hopefully something better looking than that.)
282
+ #
283
+ def theme=(options)
284
+ reset_themes()
285
+
286
+ defaults = {
287
+ :colors => ['black', 'white'],
288
+ :additional_line_colors => [],
289
+ :marker_color => 'white',
290
+ :font_color => 'black',
291
+ :background_colors => nil,
292
+ :background_image => nil
293
+ }
294
+ @theme_options = defaults.merge options
295
+
296
+ @colors = @theme_options[:colors]
297
+ @marker_color = @theme_options[:marker_color]
298
+ @font_color = @theme_options[:font_color] || @marker_color
299
+ @additional_line_colors = @theme_options[:additional_line_colors]
300
+
301
+ render_background
302
+ end
303
+
304
+ # A color scheme similar to the popular presentation software.
305
+ def theme_keynote
306
+ # Colors
307
+ @blue = '#6886B4'
308
+ @yellow = '#FDD84E'
309
+ @green = '#72AE6E'
310
+ @red = '#D1695E'
311
+ @purple = '#8A6EAF'
312
+ @orange = '#EFAA43'
313
+ @white = 'white'
314
+ @colors = [@yellow, @blue, @green, @red, @purple, @orange, @white]
315
+
316
+ self.theme = {
317
+ :colors => @colors,
318
+ :marker_color => 'white',
319
+ :font_color => 'white',
320
+ :background_colors => ['black', '#4a465a']
321
+ }
322
+ end
323
+
324
+ # A color scheme plucked from the colors on the popular usability blog.
325
+ def theme_37signals
326
+ # Colors
327
+ @green = '#339933'
328
+ @purple = '#cc99cc'
329
+ @blue = '#336699'
330
+ @yellow = '#FFF804'
331
+ @red = '#ff0000'
332
+ @orange = '#cf5910'
333
+ @black = 'black'
334
+ @colors = [@yellow, @blue, @green, @red, @purple, @orange, @black]
335
+
336
+ self.theme = {
337
+ :colors => @colors,
338
+ :marker_color => 'black',
339
+ :font_color => 'black',
340
+ :background_colors => ['#d1edf5', 'white']
341
+ }
342
+ end
343
+
344
+ # A color scheme from the colors used on the 2005 Rails keynote
345
+ # presentation at RubyConf.
346
+ def theme_rails_keynote
347
+ # Colors
348
+ @green = '#00ff00'
349
+ @grey = '#333333'
350
+ @orange = '#ff5d00'
351
+ @red = '#f61100'
352
+ @white = 'white'
353
+ @light_grey = '#999999'
354
+ @black = 'black'
355
+ @colors = [@green, @grey, @orange, @red, @white, @light_grey, @black]
356
+
357
+ self.theme = {
358
+ :colors => @colors,
359
+ :marker_color => 'white',
360
+ :font_color => 'white',
361
+ :background_colors => ['#0083a3', '#0083a3']
362
+ }
363
+ end
364
+
365
+ # A color scheme similar to that used on the popular podcast site.
366
+ def theme_odeo
367
+ # Colors
368
+ @grey = '#202020'
369
+ @white = 'white'
370
+ @dark_pink = '#a21764'
371
+ @green = '#8ab438'
372
+ @light_grey = '#999999'
373
+ @dark_blue = '#3a5b87'
374
+ @black = 'black'
375
+ @colors = [@grey, @white, @dark_blue, @dark_pink, @green, @light_grey, @black]
376
+
377
+ self.theme = {
378
+ :colors => @colors,
379
+ :marker_color => 'white',
380
+ :font_color => 'white',
381
+ :background_colors => ['#ff47a4', '#ff1f81']
382
+ }
383
+ end
384
+
385
+ # A pastel theme
386
+ def theme_pastel
387
+ # Colors
388
+ @colors = [
389
+ '#a9dada', # blue
390
+ '#aedaa9', # green
391
+ '#daaea9', # peach
392
+ '#dadaa9', # yellow
393
+ '#a9a9da', # dk purple
394
+ '#daaeda', # purple
395
+ '#dadada' # grey
396
+ ]
397
+
398
+ self.theme = {
399
+ :colors => @colors,
400
+ :marker_color => '#aea9a9', # Grey
401
+ :font_color => 'black',
402
+ :background_colors => 'white'
403
+ }
404
+ end
405
+
406
+ # A greyscale theme
407
+ def theme_greyscale
408
+ # Colors
409
+ @colors = [
410
+ '#282828', #
411
+ '#383838', #
412
+ '#686868', #
413
+ '#989898', #
414
+ '#c8c8c8', #
415
+ '#e8e8e8', #
416
+ ]
417
+
418
+ self.theme = {
419
+ :colors => @colors,
420
+ :marker_color => '#aea9a9', # Grey
421
+ :font_color => 'black',
422
+ :background_colors => 'white'
423
+ }
424
+ end
425
+
426
+ # Parameters are an array where the first element is the name of the dataset
427
+ # and the value is an array of values to plot.
428
+ #
429
+ # Can be called multiple times with different datasets for a multi-valued
430
+ # graph.
431
+ #
432
+ # If the color argument is nil, the next color from the default theme will
433
+ # be used.
434
+ #
435
+ # NOTE: If you want to use a preset theme, you must set it before calling
436
+ # data().
437
+ #
438
+ # Example:
439
+ # data("Bart S.", [95, 45, 78, 89, 88, 76], '#ffcc00')
440
+ def data(name, data_points=[], color=nil)
441
+ data_points = Array(data_points) # make sure it's an array
442
+ @data << [name, data_points, (color || increment_color)]
443
+ # Set column count if this is larger than previous counts
444
+ @column_count = (data_points.length > @column_count) ? data_points.length : @column_count
445
+
446
+ # Pre-normalize
447
+ data_points.each_with_index do |data_point, index|
448
+ next if data_point.nil?
449
+
450
+ # Setup max/min so spread starts at the low end of the data points
451
+ if @maximum_value.nil? && @minimum_value.nil?
452
+ @maximum_value = @minimum_value = data_point
453
+ end
454
+
455
+ # TODO Doesn't work with stacked bar graphs
456
+ # Original: @maximum_value = larger_than_max?(data_point, index) ? max(data_point, index) : @maximum_value
457
+ @maximum_value = larger_than_max?(data_point) ? data_point : @maximum_value
458
+ @has_data = true if @maximum_value > 0
459
+
460
+ @minimum_value = less_than_min?(data_point) ? data_point : @minimum_value
461
+ @has_data = true if @minimum_value < 0
462
+ end
463
+ end
464
+
465
+ # Writes the graph to a file. Defaults to 'graph.png'
466
+ #
467
+ # Example:
468
+ # write('graphs/my_pretty_graph.png')
469
+ def write(filename="graph.png")
470
+ draw()
471
+ @base_image.write(filename)
472
+ end
473
+
474
+ # Return the graph as a rendered binary blob.
475
+ def to_blob(fileformat='PNG')
476
+ draw()
477
+ return @base_image.to_blob do
478
+ self.format = fileformat
479
+ end
480
+ end
481
+
482
+
483
+
484
+ protected
485
+
486
+ # Overridden by subclasses to do the actual plotting of the graph.
487
+ #
488
+ # Subclasses should start by calling super() for this method.
489
+ def draw
490
+ make_stacked if @stacked
491
+ setup_drawing
492
+
493
+ debug {
494
+ # Outer margin
495
+ @d.rectangle( @left_margin, @top_margin,
496
+ @raw_columns - @right_margin, @raw_rows - @bottom_margin)
497
+ # Graph area box
498
+ @d.rectangle( @graph_left, @graph_top, @graph_right, @graph_bottom)
499
+ }
500
+ end
501
+
502
+ # Calculates size of drawable area and draws the decorations.
503
+ #
504
+ # * line markers
505
+ # * legend
506
+ # * title
507
+ def setup_drawing
508
+ # Maybe should be done in one of the following functions for more granularity.
509
+ unless @has_data
510
+ draw_no_data()
511
+ return
512
+ end
513
+
514
+ normalize()
515
+ setup_graph_measurements()
516
+ sort_norm_data() if @sort # Sort norm_data with avg largest values set first (for display)
517
+
518
+ draw_legend()
519
+ draw_line_markers()
520
+ draw_axis_labels()
521
+ draw_title
522
+ end
523
+
524
+ # Make copy of data with values scaled between 0-100
525
+ def normalize(force=false)
526
+ if @norm_data.nil? || force
527
+ @norm_data = []
528
+ return unless @has_data
529
+
530
+ calculate_spread
531
+
532
+ @data.each do |data_row|
533
+ norm_data_points = []
534
+ data_row[DATA_VALUES_INDEX].each do |data_point|
535
+ if data_point.nil?
536
+ norm_data_points << nil
537
+ else
538
+ norm_data_points << ((data_point.to_f - @minimum_value.to_f ) / @spread)
539
+ end
540
+ end
541
+ @norm_data << [data_row[DATA_LABEL_INDEX], norm_data_points, data_row[DATA_COLOR_INDEX]]
542
+ end
543
+ end
544
+ end
545
+
546
+ def calculate_spread # :nodoc:
547
+ @spread = @maximum_value.to_f - @minimum_value.to_f
548
+ @spread = @spread > 0 ? @spread : 1
549
+ end
550
+
551
+ ##
552
+ # Calculates size of drawable area, general font dimensions, etc.
553
+
554
+ def setup_graph_measurements
555
+ @marker_caps_height = @hide_line_markers ? 0 :
556
+ calculate_caps_height(@marker_font_size)
557
+ @title_caps_height = @hide_title ? 0 :
558
+ calculate_caps_height(@title_font_size)
559
+ @legend_caps_height = @hide_legend ? 0 :
560
+ calculate_caps_height(@legend_font_size)
561
+
562
+ if @hide_line_markers
563
+ (@graph_left,
564
+ @graph_right_margin,
565
+ @graph_bottom_margin) = [@left_margin, @right_margin, @bottom_margin]
566
+ else
567
+ longest_left_label_width = 0
568
+ if @has_left_labels
569
+ longest_left_label_width = calculate_width(@marker_font_size,
570
+ labels.values.inject('') { |value, memo| (value.to_s.length > memo.to_s.length) ? value : memo }) * 1.25
571
+ else
572
+ longest_left_label_width = calculate_width(@marker_font_size,
573
+ label(@maximum_value.to_f))
574
+ end
575
+
576
+ # Shift graph if left line numbers are hidden
577
+ line_number_width = @hide_line_numbers && !@has_left_labels ?
578
+ 0.0 :
579
+ (longest_left_label_width + LABEL_MARGIN * 2)
580
+
581
+ @graph_left = @left_margin +
582
+ line_number_width +
583
+ (@y_axis_label.nil? ? 0.0 : @marker_caps_height + LABEL_MARGIN * 2)
584
+
585
+ # Make space for half the width of the rightmost column label.
586
+ # Might be greater than the number of columns if between-style bar markers are used.
587
+ last_label = @labels.keys.sort.last.to_i
588
+ extra_room_for_long_label = (last_label >= (@column_count-1) && @center_labels_over_point) ?
589
+ calculate_width(@marker_font_size, @labels[last_label]) / 2.0 :
590
+ 0
591
+ @graph_right_margin = @right_margin + extra_room_for_long_label
592
+
593
+ @graph_bottom_margin = @bottom_margin +
594
+ @marker_caps_height + LABEL_MARGIN
595
+ end
596
+
597
+ @graph_right = @raw_columns - @graph_right_margin
598
+ @graph_width = @raw_columns - @graph_left - @graph_right_margin
599
+
600
+ # When @hide title, leave a TITLE_MARGIN space for aesthetics.
601
+ # Same with @hide_legend
602
+ @graph_top = @top_margin +
603
+ (@hide_title ? TITLE_MARGIN : @title_caps_height + TITLE_MARGIN * 2) +
604
+ (@hide_legend ? LEGEND_MARGIN : @legend_caps_height + LEGEND_MARGIN * 2)
605
+
606
+ x_axis_label_height = @x_axis_label.nil? ? 0.0 :
607
+ @marker_caps_height + LABEL_MARGIN
608
+ @graph_bottom = @raw_rows - @graph_bottom_margin - x_axis_label_height
609
+ @graph_height = @graph_bottom - @graph_top
610
+ end
611
+
612
+ # Draw the optional labels for the x axis and y axis.
613
+ def draw_axis_labels
614
+ unless @x_axis_label.nil?
615
+ # X Axis
616
+ # Centered vertically and horizontally by setting the
617
+ # height to 1.0 and the width to the width of the graph.
618
+ x_axis_label_y_coordinate = @graph_bottom + LABEL_MARGIN * 2 + @marker_caps_height
619
+
620
+ # TODO Center between graph area
621
+ @d.fill = @font_color
622
+ @d.font = @font if @font
623
+ @d.stroke('transparent')
624
+ @d.pointsize = scale_fontsize(@marker_font_size)
625
+ @d.gravity = NorthGravity
626
+ @d = @d.annotate_scaled( @base_image,
627
+ @raw_columns, 1.0,
628
+ 0.0, x_axis_label_y_coordinate,
629
+ @x_axis_label, @scale)
630
+ debug { @d.line 0.0, x_axis_label_y_coordinate, @raw_columns, x_axis_label_y_coordinate }
631
+ end
632
+
633
+ unless @y_axis_label.nil?
634
+ # Y Axis, rotated vertically
635
+ @d.rotation = 90.0
636
+ @d.gravity = CenterGravity
637
+ @d = @d.annotate_scaled( @base_image,
638
+ 1.0, @raw_rows,
639
+ @left_margin + @marker_caps_height / 2.0, 0.0,
640
+ @y_axis_label, @scale)
641
+ @d.rotation = -90.0
642
+ end
643
+ end
644
+
645
+ # Draws horizontal background lines and labels
646
+ def draw_line_markers
647
+ return if @hide_line_markers
648
+
649
+ @d = @d.stroke_antialias false
650
+
651
+ if @y_axis_increment.nil?
652
+ # Try to use a number of horizontal lines that will come out even.
653
+ #
654
+ # TODO Do the same for larger numbers...100, 75, 50, 25
655
+ if @marker_count.nil?
656
+ (3..7).each do |lines|
657
+ if @spread % lines == 0.0
658
+ @marker_count = lines
659
+ break
660
+ end
661
+ end
662
+ @marker_count ||= 4
663
+ end
664
+ @increment = (@spread > 0) ? significant(@spread / @marker_count) : 1
665
+ else
666
+ # TODO Make this work for negative values
667
+ @maximum_value = [@maximum_value.ceil, @y_axis_increment].max
668
+ @minimum_value = @minimum_value.floor
669
+ calculate_spread
670
+ normalize(true)
671
+
672
+ @marker_count = (@spread / @y_axis_increment).to_i
673
+ @increment = @y_axis_increment
674
+ end
675
+ @increment_scaled = @graph_height.to_f / (@spread / @increment)
676
+
677
+ # Draw horizontal line markers and annotate with numbers
678
+ (0..@marker_count).each do |index|
679
+ y = @graph_top + @graph_height - index.to_f * @increment_scaled
680
+
681
+ @d = @d.stroke(@marker_color)
682
+ @d = @d.stroke_width 1
683
+ @d = @d.line(@graph_left, y, @graph_right, y)
684
+
685
+ marker_label = index * @increment + @minimum_value.to_f
686
+
687
+ unless @hide_line_numbers
688
+ @d.fill = @font_color
689
+ @d.font = @font if @font
690
+ @d.stroke('transparent')
691
+ @d.pointsize = scale_fontsize(@marker_font_size)
692
+ @d.gravity = EastGravity
693
+
694
+ # Vertically center with 1.0 for the height
695
+ @d = @d.annotate_scaled( @base_image,
696
+ @graph_left - LABEL_MARGIN, 1.0,
697
+ 0.0, y,
698
+ label(marker_label), @scale)
699
+ end
700
+ end
701
+
702
+ # # Submitted by a contibutor...the utility escapes me
703
+ # i = 0
704
+ # @additional_line_values.each do |value|
705
+ # @increment_scaled = @graph_height.to_f / (@maximum_value.to_f / value)
706
+ #
707
+ # y = @graph_top + @graph_height - @increment_scaled
708
+ #
709
+ # @d = @d.stroke(@additional_line_colors[i])
710
+ # @d = @d.line(@graph_left, y, @graph_right, y)
711
+ #
712
+ #
713
+ # @d.fill = @additional_line_colors[i]
714
+ # @d.font = @font if @font
715
+ # @d.stroke('transparent')
716
+ # @d.pointsize = scale_fontsize(@marker_font_size)
717
+ # @d.gravity = EastGravity
718
+ # @d = @d.annotate_scaled( @base_image,
719
+ # 100, 20,
720
+ # -10, y - (@marker_font_size/2.0),
721
+ # "", @scale)
722
+ # i += 1
723
+ # end
724
+
725
+ @d = @d.stroke_antialias true
726
+ end
727
+
728
+ ##
729
+ # Return the sum of values in an array.
730
+ #
731
+ # Duplicated to not conflict with active_support in Rails.
732
+
733
+ def sum(arr)
734
+ arr.inject(0) { |i, m| m + i }
735
+ end
736
+
737
+ ##
738
+ # Return a calculation of center
739
+
740
+ def center(size)
741
+ (@raw_columns - size) / 2
742
+ end
743
+
744
+ ##
745
+ # Draws a legend with the names of the datasets matched
746
+ # to the colors used to draw them.
747
+
748
+ def draw_legend
749
+ return if @hide_legend
750
+
751
+ @legend_labels = @data.collect {|item| item[DATA_LABEL_INDEX] }
752
+
753
+ legend_square_width = @legend_box_size # small square with color of this item
754
+
755
+ # May fix legend drawing problem at small sizes
756
+ @d.font = @font if @font
757
+ @d.pointsize = @legend_font_size
758
+
759
+ label_widths = [[]] # Used to calculate line wrap
760
+ @legend_labels.each do |label|
761
+ metrics = @d.get_type_metrics(@base_image, label.to_s)
762
+ label_width = metrics.width + legend_square_width * 2.7
763
+ label_widths.last.push label_width
764
+
765
+ if sum(label_widths.last) > (@raw_columns * 0.9)
766
+ label_widths.push [label_widths.last.pop]
767
+ end
768
+ end
769
+
770
+ current_x_offset = center(sum(label_widths.first))
771
+ current_y_offset = @hide_title ?
772
+ @top_margin + LEGEND_MARGIN :
773
+ @top_margin + TITLE_MARGIN + @title_caps_height + LEGEND_MARGIN
774
+
775
+ @legend_labels.each_with_index do |legend_label, index|
776
+
777
+ # Draw label
778
+ @d.fill = @font_color
779
+ @d.font = @font if @font
780
+ @d.pointsize = scale_fontsize(@legend_font_size)
781
+ @d.stroke('transparent')
782
+ @d.font_weight = NormalWeight
783
+ @d.gravity = WestGravity
784
+ @d = @d.annotate_scaled( @base_image,
785
+ @raw_columns, 1.0,
786
+ current_x_offset + (legend_square_width * 1.7), current_y_offset,
787
+ legend_label.to_s, @scale)
788
+
789
+ # Now draw box with color of this dataset
790
+ @d = @d.stroke('transparent')
791
+ @d = @d.fill @data[index][DATA_COLOR_INDEX]
792
+ @d = @d.rectangle(current_x_offset,
793
+ current_y_offset - legend_square_width / 2.0,
794
+ current_x_offset + legend_square_width,
795
+ current_y_offset + legend_square_width / 2.0)
796
+
797
+ @d.pointsize = @legend_font_size
798
+ metrics = @d.get_type_metrics(@base_image, legend_label.to_s)
799
+ current_string_offset = metrics.width + (legend_square_width * 2.7)
800
+
801
+ # Handle wrapping
802
+ label_widths.first.shift
803
+ if label_widths.first.empty?
804
+ debug { @d.line 0.0, current_y_offset, @raw_columns, current_y_offset }
805
+
806
+ label_widths.shift
807
+ current_x_offset = center(sum(label_widths.first)) unless label_widths.empty?
808
+ line_height = [@legend_caps_height, legend_square_width].max + LEGEND_MARGIN
809
+ if label_widths.length > 0
810
+ # Wrap to next line and shrink available graph dimensions
811
+ current_y_offset += line_height
812
+ @graph_top += line_height
813
+ @graph_height = @graph_bottom - @graph_top
814
+ end
815
+ else
816
+ current_x_offset += current_string_offset
817
+ end
818
+ end
819
+ @color_index = 0
820
+ end
821
+
822
+ # Draws a title on the graph.
823
+ def draw_title
824
+ return if (@hide_title || @title.nil?)
825
+
826
+ @d.fill = @font_color
827
+ @d.font = @font if @font
828
+ @d.stroke('transparent')
829
+ @d.pointsize = scale_fontsize(@title_font_size)
830
+ @d.font_weight = BoldWeight
831
+ @d.gravity = NorthGravity
832
+ @d = @d.annotate_scaled( @base_image,
833
+ @raw_columns, 1.0,
834
+ 0, @top_margin,
835
+ @title, @scale)
836
+ end
837
+
838
+ # Draws column labels below graph, centered over x_offset
839
+ #--
840
+ # TODO Allow WestGravity as an option
841
+ def draw_label(x_offset, index)
842
+ return if @hide_line_markers
843
+
844
+ if !@labels[index].nil? && @labels_seen[index].nil?
845
+ y_offset = @graph_bottom + LABEL_MARGIN
846
+
847
+ @d.fill = @font_color
848
+ @d.font = @font if @font
849
+ @d.stroke('transparent')
850
+ @d.font_weight = NormalWeight
851
+ @d.pointsize = scale_fontsize(@marker_font_size)
852
+ @d.gravity = NorthGravity
853
+ @d = @d.annotate_scaled(@base_image,
854
+ 1.0, 1.0,
855
+ x_offset, y_offset,
856
+ @labels[index], @scale)
857
+ @labels_seen[index] = 1
858
+ debug { @d.line 0.0, y_offset, @raw_columns, y_offset }
859
+ end
860
+ end
861
+
862
+ # Shows an error message because you have no data.
863
+ def draw_no_data
864
+ @d.fill = @font_color
865
+ @d.font = @font if @font
866
+ @d.stroke('transparent')
867
+ @d.font_weight = NormalWeight
868
+ @d.pointsize = scale_fontsize(80)
869
+ @d.gravity = CenterGravity
870
+ @d = @d.annotate_scaled( @base_image,
871
+ @raw_columns, @raw_rows/2.0,
872
+ 0, 10,
873
+ @no_data_message, @scale)
874
+ end
875
+
876
+ # Finds the best background to render based on the provided theme options.
877
+ #
878
+ # Creates a @base_image to draw on.
879
+ def render_background
880
+ case @theme_options[:background_colors]
881
+ when Array
882
+ @base_image = render_gradiated_background(*@theme_options[:background_colors])
883
+ when String
884
+ @base_image = render_solid_background(@theme_options[:background_colors])
885
+ else
886
+ @base_image = render_image_background(*@theme_options[:background_image])
887
+ end
888
+ end
889
+
890
+ # Make a new image at the current size with a solid +color+.
891
+ def render_solid_background(color)
892
+ Image.new(@columns, @rows) {
893
+ self.background_color = color
894
+ }
895
+ end
896
+
897
+ # Use with a theme definition method to draw a gradiated background.
898
+ def render_gradiated_background(top_color, bottom_color)
899
+ Image.new(@columns, @rows,
900
+ GradientFill.new(0, 0, 100, 0, top_color, bottom_color))
901
+ end
902
+
903
+ # Use with a theme to use an image (800x600 original) background.
904
+ def render_image_background(image_path)
905
+ image = Image.read(image_path)
906
+ if @scale != 1.0
907
+ image[0].resize!(@scale) # TODO Resize with new scale (crop if necessary for wide graph)
908
+ end
909
+ image[0]
910
+ end
911
+
912
+ # Use with a theme to make a transparent background
913
+ def render_transparent_background
914
+ Image.new(@columns, @rows) do
915
+ self.background_color = 'transparent'
916
+ end
917
+ end
918
+
919
+ # Resets everything to defaults (except data).
920
+ def reset_themes
921
+ @color_index = 0
922
+ @labels_seen = {}
923
+ @theme_options = {}
924
+
925
+ @d = Draw.new
926
+ # Scale down from 800x600 used to calculate drawing.
927
+ @d = @d.scale(@scale, @scale)
928
+ end
929
+
930
+ def scale(value) # :nodoc:
931
+ value * @scale
932
+ end
933
+
934
+ # Return a comparable fontsize for the current graph.
935
+ def scale_fontsize(value)
936
+ new_fontsize = value * @scale
937
+ # return new_fontsize < 10.0 ? 10.0 : new_fontsize
938
+ return new_fontsize
939
+ end
940
+
941
+ def clip_value_if_greater_than(value, max_value) # :nodoc:
942
+ (value > max_value) ? max_value : value
943
+ end
944
+
945
+ # Overridden by subclasses such as stacked bar.
946
+ def larger_than_max?(data_point, index=0) # :nodoc:
947
+ data_point > @maximum_value
948
+ end
949
+
950
+ def less_than_min?(data_point, index=0) # :nodoc:
951
+ data_point < @minimum_value
952
+ end
953
+
954
+ # Overridden by subclasses that need it.
955
+ def max(data_point, index) # :nodoc:
956
+ data_point
957
+ end
958
+
959
+ # Overridden by subclasses that need it.
960
+ def min(data_point, index) # :nodoc:
961
+ data_point
962
+ end
963
+
964
+ def significant(inc) # :nodoc:
965
+ return 1.0 if inc == 0 # Keep from going into infinite loop
966
+ factor = 1.0
967
+ while (inc < 10)
968
+ inc *= 10
969
+ factor /= 10
970
+ end
971
+
972
+ while (inc > 100)
973
+ inc /= 10
974
+ factor *= 10
975
+ end
976
+
977
+ res = inc.floor * factor
978
+ if (res.to_i.to_f == res)
979
+ res.to_i
980
+ else
981
+ res
982
+ end
983
+ end
984
+
985
+ # Sort with largest overall summed value at front of array so it shows up
986
+ # correctly in the drawn graph.
987
+ def sort_norm_data
988
+ @norm_data.sort! { |a,b| sums(b[DATA_VALUES_INDEX]) <=> sums(a[DATA_VALUES_INDEX]) }
989
+ end
990
+
991
+ def sums(data_set) # :nodoc:
992
+ total_sum = 0
993
+ data_set.collect {|num| total_sum += num.to_f }
994
+ total_sum
995
+ end
996
+
997
+ # Used by StackedBar and child classes.
998
+ #
999
+ # May need to be moved to the StackedBar class.
1000
+ def get_maximum_by_stack
1001
+ # Get sum of each stack
1002
+ max_hash = {}
1003
+ @data.each do |data_set|
1004
+ data_set[DATA_VALUES_INDEX].each_with_index do |data_point, i|
1005
+ max_hash[i] = 0.0 unless max_hash[i]
1006
+ max_hash[i] += data_point.to_f
1007
+ end
1008
+ end
1009
+
1010
+ # @maximum_value = 0
1011
+ max_hash.keys.each do |key|
1012
+ @maximum_value = max_hash[key] if max_hash[key] > @maximum_value
1013
+ end
1014
+ @minimum_value = 0
1015
+ end
1016
+
1017
+ def make_stacked # :nodoc:
1018
+ stacked_values = Array.new(@column_count, 0)
1019
+ @data.each do |value_set|
1020
+ value_set[DATA_VALUES_INDEX].each_with_index do |value, index|
1021
+ stacked_values[index] += value
1022
+ end
1023
+ value_set[DATA_VALUES_INDEX] = stacked_values.dup
1024
+ end
1025
+ end
1026
+
1027
+ private
1028
+
1029
+ # Takes a block and draws it if DEBUG is true.
1030
+ #
1031
+ # Example:
1032
+ # debug { @d.rectangle x1, y1, x2, y2 }
1033
+ def debug
1034
+ if DEBUG
1035
+ @d = @d.fill 'transparent'
1036
+ @d = @d.stroke 'turquoise'
1037
+ @d = yield
1038
+ end
1039
+ end
1040
+
1041
+ # Uses the next color in your color list.
1042
+ def increment_color
1043
+ if @color_index == 0
1044
+ @color_index += 1
1045
+ return @colors[0]
1046
+ else
1047
+ if @color_index < @colors.length
1048
+ @color_index += 1
1049
+ return @colors[@color_index - 1]
1050
+ else
1051
+ # Start over
1052
+ @color_index = 0
1053
+ return @colors[-1]
1054
+ end
1055
+ end
1056
+ end
1057
+
1058
+ # Return a formatted string representing a number value that should be
1059
+ # printed as a label.
1060
+ def label(value)
1061
+ if (@spread.to_f % @marker_count.to_f == 0) || !@y_axis_increment.nil?
1062
+ return value.to_i.to_s
1063
+ end
1064
+
1065
+ if @spread > 10.0
1066
+ sprintf("%0i", value)
1067
+ elsif @spread >= 3.0
1068
+ sprintf("%0.2f", value)
1069
+ else
1070
+ value.to_s
1071
+ end
1072
+ end
1073
+
1074
+ # Returns the height of the capital letter 'X' for the current font and
1075
+ # size.
1076
+ #
1077
+ # Not scaled since it deals with dimensions that the regular scaling will
1078
+ # handle.
1079
+ def calculate_caps_height(font_size)
1080
+ @d.pointsize = font_size
1081
+ @d.get_type_metrics(@base_image, 'X').height
1082
+ end
1083
+
1084
+ # Returns the width of a string at this pointsize.
1085
+ #
1086
+ # Not scaled since it deals with dimensions that the regular
1087
+ # scaling will handle.
1088
+ def calculate_width(font_size, text)
1089
+ @d.pointsize = font_size
1090
+ @d.get_type_metrics(@base_image, text.to_s).width
1091
+ end
1092
+
1093
+ end # Gruff::Base
1094
+
1095
+ class IncorrectNumberOfDatasetsException < StandardError; end
1096
+
1097
+ end # Gruff
1098
+
1099
+ module Magick
1100
+
1101
+ class Draw
1102
+
1103
+ # Additional method to scale annotation text since Draw.scale doesn't.
1104
+ def annotate_scaled(img, width, height, x, y, text, scale)
1105
+ scaled_width = (width * scale) >= 1 ? (width * scale) : 1
1106
+ scaled_height = (height * scale) >= 1 ? (height * scale) : 1
1107
+
1108
+ self.annotate( img,
1109
+ scaled_width, scaled_height,
1110
+ x * scale, y * scale,
1111
+ text)
1112
+ end
1113
+
1114
+ end
1115
+
1116
+ end # Magick