umang-gruff 0.3.6

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