pfsc_gruff 0.3.6

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