jslade-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 +29 -0
  35. data/lib/gruff/accumulator_bar.rb +27 -0
  36. data/lib/gruff/area.rb +58 -0
  37. data/lib/gruff/bar.rb +105 -0
  38. data/lib/gruff/bar_conversion.rb +46 -0
  39. data/lib/gruff/base.rb +925 -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 +118 -0
  52. data/lib/gruff/side_stacked_bar.rb +76 -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 +66 -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 +302 -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 +548 -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 +14 -0
  76. data/test/test_sidestacked_bar.rb +91 -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 +78 -0
  80. metadata +160 -0
data/lib/gruff/bar.rb ADDED
@@ -0,0 +1,105 @@
1
+ require File.dirname(__FILE__) + '/base'
2
+ require File.dirname(__FILE__) + '/bar_conversion'
3
+ require File.dirname(__FILE__) + '/ldata_mixin'
4
+
5
+ class Gruff::Bar < Gruff::Base
6
+ include LdataMixin
7
+
8
+ def draw
9
+ # Labels will be centered over the left of the bar if
10
+ # there are more labels than columns. This is basically the same
11
+ # as where it would be for a line graph.
12
+ @center_labels_over_point = (@labels.keys.length > @column_count ? true : false)
13
+
14
+ super
15
+ draw_bars if @has_data
16
+ draw_ldata if @has_ldata
17
+ end
18
+
19
+ protected
20
+
21
+ def draw_bars
22
+ # Setup spacing.
23
+ #
24
+ # Columns sit side-by-side.
25
+ spacing_factor = 0.9 # space between the bars
26
+ @bar_width = @graph_width / (@column_count * @data.length).to_f
27
+ padding = (@bar_width * (1 - spacing_factor)) / 2
28
+
29
+ @d = @d.stroke_opacity 0.0
30
+
31
+ # Setup the BarConversion Object
32
+ conversion = Gruff::BarConversion.new()
33
+ conversion.graph_height = @graph_height
34
+ conversion.graph_top = @graph_top
35
+
36
+ # Set up the right mode [1,2,3] see BarConversion for further explanation
37
+ if @minimum_value >= 0 then
38
+ # all bars go from zero to positiv
39
+ conversion.mode = 1
40
+ else
41
+ # all bars go from 0 to negativ
42
+ if @maximum_value <= 0 then
43
+ conversion.mode = 2
44
+ else
45
+ # bars either go from zero to negativ or to positiv
46
+ conversion.mode = 3
47
+ conversion.spread = @spread
48
+ conversion.minimum_value = @minimum_value
49
+ conversion.zero = -@minimum_value/@spread
50
+ end
51
+ end
52
+
53
+ # iterate over all normalised data
54
+ @norm_data.each_with_index do |data_row, row_index|
55
+
56
+ data_row[DATA_VALUES_INDEX].each_with_index do |data_point, point_index|
57
+ # Use incremented x and scaled y
58
+ # x
59
+ left_x = @graph_left + (@bar_width * (row_index + point_index + ((@data.length - 1) * point_index))) + padding
60
+ right_x = left_x + @bar_width * spacing_factor
61
+ # y
62
+ conv = []
63
+ conversion.getLeftYRightYscaled( data_point, conv )
64
+
65
+ # create new bar
66
+ @d = @d.fill data_row[DATA_COLOR_INDEX]
67
+ @d = @d.rectangle(left_x, conv[0], right_x, conv[1])
68
+
69
+ # Calculate center based on bar_width and current row
70
+ label_center = @graph_left +
71
+ (@data.length * @bar_width * point_index) +
72
+ (@data.length * @bar_width / 2.0)
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
+
76
+ if @has_ldata
77
+ @ldata_offset_and_increment[row_index] ||=
78
+ [ label_center, @bar_width * @norm_data.size ]
79
+ end
80
+ end
81
+
82
+ end
83
+
84
+ # Draw the last label if requested
85
+ draw_label(@graph_right, @column_count) if @center_labels_over_point
86
+
87
+ @d.draw(@base_image)
88
+ end
89
+
90
+
91
+ def calc_ldata_on_bar row_index, point_index, label_center, bar_width
92
+ # Save the x mid point of the first bar and the x distance between 2
93
+ # bars of the same data set to be used to position ldata points.
94
+ line_info = @ldata_offset_and_increment[row_index] ||= Array.new
95
+ if point_index == 0
96
+ line_info[0] = label_center
97
+ else
98
+ if line_info[1].nil?
99
+ line_info[1] = label_center - line_info[0]
100
+ end
101
+ end
102
+ end
103
+
104
+
105
+ 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,925 @@
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
+ DATA_ATTRS_INDEX = 3
38
+
39
+ # Space around text elements. Mostly used for vertical spacing
40
+ LEGEND_MARGIN = TITLE_MARGIN = LABEL_MARGIN = 10.0
41
+
42
+ DEFAULT_TARGET_WIDTH = 800
43
+
44
+ # Blank space above the graph
45
+ attr_accessor :top_margin
46
+
47
+ # Blank space below the graph
48
+ attr_accessor :bottom_margin
49
+
50
+ # Blank space to the right of the graph
51
+ attr_accessor :right_margin
52
+
53
+ # Blank space to the left of the graph
54
+ attr_accessor :left_margin
55
+
56
+ # A hash of names for the individual columns, where the key is the array
57
+ # index for the column this label represents.
58
+ #
59
+ # Not all columns need to be named.
60
+ #
61
+ # Example: 0 => 2005, 3 => 2006, 5 => 2007, 7 => 2008
62
+ attr_accessor :labels
63
+
64
+ # Labels can be rotated from 0-90 degrees
65
+ attr_accessor :label_rotation
66
+
67
+ # Used internally for spacing.
68
+ #
69
+ # By default, labels are centered over the point they represent.
70
+ attr_accessor :center_labels_over_point
71
+
72
+ # Used internally for horizontal graph types.
73
+ attr_accessor :has_left_labels
74
+
75
+ # A label for the bottom of the graph
76
+ attr_accessor :x_axis_label
77
+
78
+ # A label for the left side of the graph
79
+ attr_accessor :y_axis_label
80
+
81
+ # attr_accessor :x_axis_increment
82
+
83
+ # Manually set increment of the horizontal marking lines
84
+ attr_accessor :y_axis_increment
85
+
86
+ # Get or set the list of colors that will be used to draw the bars or lines.
87
+ attr_accessor :colors
88
+
89
+ # The large title of the graph displayed at the top
90
+ attr_accessor :title
91
+
92
+ # Font used for titles, labels, etc. Works best if you provide the full
93
+ # path to the TTF font file. RMagick must be built with the Freetype
94
+ # libraries for this to work properly.
95
+ #
96
+ # Tries to find Bitstream Vera (Vera.ttf) in the location specified by
97
+ # ENV['MAGICK_FONT_PATH']. Uses default RMagick font otherwise.
98
+ #
99
+ # The font= method below fulfills the role of the writer, so we only need
100
+ # a reader here.
101
+ attr_reader :font
102
+
103
+ attr_accessor :font_color
104
+
105
+ # Prevent drawing of line markers
106
+ attr_accessor :hide_line_markers
107
+
108
+ # Prevent drawing of the legend
109
+ attr_accessor :hide_legend
110
+
111
+ # Prevent drawing of the title
112
+ attr_accessor :hide_title
113
+
114
+ # Prevent drawing of line numbers
115
+ attr_accessor :hide_line_numbers
116
+
117
+ # Message shown when there is no data. Fits up to 20 characters. Defaults
118
+ # to "No Data."
119
+ attr_accessor :no_data_message
120
+
121
+ # The font size of the large title at the top of the graph
122
+ attr_accessor :title_font_size
123
+
124
+ # Optionally set the size of the font. Based on an 800x600px graph.
125
+ # Default is 20.
126
+ #
127
+ # Will be scaled down if graph is smaller than 800px wide.
128
+ attr_accessor :legend_font_size
129
+
130
+ # The font size of the labels around the graph
131
+ attr_accessor :marker_font_size
132
+
133
+ # The color of the auxiliary lines
134
+ attr_accessor :marker_color
135
+
136
+ # The number of horizontal lines shown for reference
137
+ attr_accessor :marker_count
138
+
139
+ # You can manually set a minimum value instead of having the values
140
+ # guessed for you.
141
+ #
142
+ # Set it after you have given all your data to the graph object.
143
+ attr_accessor :minimum_value
144
+
145
+ # You can manually set a maximum value, such as a percentage-based graph
146
+ # that always goes to 100.
147
+ #
148
+ # If you use this, you must set it after you have given all your data to
149
+ # the graph object.
150
+ attr_accessor :maximum_value
151
+
152
+ # Set to false if you don't want the data to be sorted with largest avg
153
+ # values at the back.
154
+ attr_accessor :sort
155
+
156
+ # Experimental
157
+ attr_accessor :additional_line_values
158
+
159
+ # Experimental
160
+ attr_accessor :stacked
161
+
162
+ # Optionally set the size of the colored box by each item in the legend.
163
+ # Default is 20.0
164
+ #
165
+ # Will be scaled down if graph is smaller than 800px wide.
166
+ attr_accessor :legend_box_size
167
+
168
+ # If one numerical argument is given, the graph is drawn at 4/3 ratio
169
+ # according to the given width (800 results in 800x600, 400 gives 400x300,
170
+ # etc.).
171
+ #
172
+ # Or, send a geometry string for other ratios ('800x400', '400x225').
173
+ #
174
+ # Looks for Bitstream Vera as the default font. Expects an environment var
175
+ # of MAGICK_FONT_PATH to be set. (Uses RMagick's default font otherwise.)
176
+ def initialize(target_width=DEFAULT_TARGET_WIDTH)
177
+ @top_margin = @bottom_margin = @left_margin = @right_margin = 20.0
178
+
179
+ if not Numeric === target_width
180
+ geometric_width, geometric_height = target_width.split('x')
181
+ @columns = geometric_width.to_f
182
+ @rows = geometric_height.to_f
183
+ else
184
+ @columns = target_width.to_f
185
+ @rows = target_width.to_f * 0.75
186
+ end
187
+
188
+ # Call any other initializers -- open-ended to allow for
189
+ # subclass extensions
190
+ self.class.instance_methods.
191
+ select{|m| m.to_s =~ /^initialize_/o}.each do |initializer|
192
+ self.send(initializer)
193
+ end
194
+
195
+ reset_themes
196
+ theme_keynote
197
+ end
198
+
199
+ # Set instance variables for this object.
200
+ #
201
+ # Subclasses can override this, call super, then set values separately.
202
+ #
203
+ # This makes it possible to set defaults in a subclass but still allow
204
+ # developers to change this values in their program.
205
+ def initialize_ivars
206
+ # Internal for calculations
207
+ @raw_columns = 800.0
208
+ @raw_rows = 800.0 * (@rows/@columns)
209
+ @column_count = 0
210
+ @marker_count = nil
211
+ @maximum_value = @minimum_value = nil
212
+ @has_data = false
213
+ @data = Array.new
214
+ @labels = Hash.new
215
+ @labels_seen = Hash.new
216
+ @sort = true
217
+ @title = nil
218
+
219
+ @scale = @columns / @raw_columns
220
+
221
+ vera_font_path = File.expand_path('Vera.ttf', ENV['MAGICK_FONT_PATH'])
222
+ @font = File.exists?(vera_font_path) ? vera_font_path : nil
223
+
224
+ @marker_font_size = 21.0
225
+ @legend_font_size = 20.0
226
+ @title_font_size = 36.0
227
+
228
+ @legend_box_size = 20.0
229
+
230
+ @no_data_message = "No Data"
231
+
232
+ @hide_line_markers = @hide_legend = @hide_title = @hide_line_numbers = false
233
+ @center_labels_over_point = true
234
+ @has_left_labels = false
235
+
236
+ @label_rotation = 0
237
+
238
+ @additional_line_values = []
239
+ @additional_line_colors = []
240
+ @theme_options = {}
241
+
242
+ @x_axis_label = @y_axis_label = nil
243
+ @y_axis_increment = nil
244
+ @stacked = nil
245
+ @norm_data = nil
246
+
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. You need to
269
+ # have one more color than the number of datasets you intend to draw. Also
270
+ # aliased as the colors= setter method.
271
+ #
272
+ # Example:
273
+ # replace_colors ['#cc99cc', '#d9e043', '#34d8a2']
274
+ def replace_colors(color_list=[])
275
+ @colors = color_list
276
+ end
277
+
278
+ # Parameters are an array where the first element is the name of the dataset
279
+ # and the value is an array of values to plot.
280
+ #
281
+ # Can be called multiple times with different datasets for a multi-valued
282
+ # graph.
283
+ #
284
+ # If the color argument is nil, the next color from the default theme will
285
+ # be used.
286
+ #
287
+ # NOTE: If you want to use a preset theme, you must set it before calling
288
+ # data().
289
+ #
290
+ # Example:
291
+ # data("Bart S.", [95, 45, 78, 89, 88, 76], '#ffcc00')
292
+ def data(name, data_points=[], color=nil)
293
+ data_points = Array(data_points) # make sure it's an array
294
+ @data << [name, data_points, (color || increment_color)]
295
+ # Set column count if this is larger than previous counts
296
+ @column_count = (data_points.length > @column_count) ? data_points.length : @column_count
297
+
298
+ # Pre-normalize
299
+ data_points.each_with_index do |data_point, index|
300
+ next if data_point.nil?
301
+
302
+ # Setup max/min so spread starts at the low end of the data points
303
+ if @maximum_value.nil? && @minimum_value.nil?
304
+ @maximum_value = @minimum_value = data_point
305
+ end
306
+
307
+ # TODO Doesn't work with stacked bar graphs
308
+ # Original: @maximum_value = larger_than_max?(data_point, index) ? max(data_point, index) : @maximum_value
309
+ @maximum_value = larger_than_max?(data_point) ? data_point : @maximum_value
310
+ @has_data = true if @maximum_value > 0
311
+
312
+ @minimum_value = less_than_min?(data_point) ? data_point : @minimum_value
313
+ @has_data = true if @minimum_value < 0
314
+ end
315
+ end
316
+
317
+ # Writes the graph to a file. Defaults to 'graph.png'
318
+ #
319
+ # Example:
320
+ # write('graphs/my_pretty_graph.png')
321
+ def write(filename="graph.png")
322
+ draw()
323
+ @base_image.write(filename)
324
+ end
325
+
326
+ # Return the graph as a rendered binary blob.
327
+ def to_blob(fileformat='PNG')
328
+ draw()
329
+ return @base_image.to_blob do
330
+ self.format = fileformat
331
+ end
332
+ end
333
+
334
+
335
+
336
+ protected
337
+
338
+ # Overridden by subclasses to do the actual plotting of the graph.
339
+ #
340
+ # Subclasses should start by calling super() for this method.
341
+ def draw
342
+ make_stacked if @stacked
343
+ setup_drawing
344
+
345
+ debug {
346
+ # Outer margin
347
+ @d.rectangle( @left_margin, @top_margin,
348
+ @raw_columns - @right_margin, @raw_rows - @bottom_margin)
349
+ # Graph area box
350
+ @d.rectangle( @graph_left, @graph_top, @graph_right, @graph_bottom)
351
+ }
352
+ end
353
+
354
+ # Calculates size of drawable area and draws the decorations.
355
+ #
356
+ # * line markers
357
+ # * legend
358
+ # * title
359
+ def setup_drawing
360
+ # Maybe should be done in one of the following functions for more granularity.
361
+ unless @has_data
362
+ draw_no_data()
363
+ return
364
+ end
365
+
366
+ normalize()
367
+ setup_graph_measurements()
368
+ sort_norm_data() if @sort # Sort norm_data with avg largest values set first (for display)
369
+
370
+ draw_legend()
371
+ draw_line_markers()
372
+ draw_axis_labels()
373
+ draw_title
374
+ end
375
+
376
+ # Make copy of data with values scaled between 0-100
377
+ def normalize(force=false)
378
+ if @norm_data.nil? || force
379
+ @norm_data = []
380
+ return unless @has_data
381
+
382
+ calculate_spread
383
+
384
+ @data.each do |data_row|
385
+ norm_data_points = []
386
+ data_row[DATA_VALUES_INDEX].each do |data_point|
387
+ if data_point.nil?
388
+ norm_data_points << nil
389
+ else
390
+ norm_data_points << ((data_point.to_f - @minimum_value.to_f ) / @spread)
391
+ end
392
+ end
393
+ @norm_data << [data_row[DATA_LABEL_INDEX], norm_data_points, data_row[DATA_COLOR_INDEX]]
394
+ end
395
+ end
396
+ end
397
+
398
+ def calculate_spread # :nodoc:
399
+ @spread = @maximum_value.to_f - @minimum_value.to_f
400
+ @spread = @spread > 0 ? @spread : 1
401
+ end
402
+
403
+ # Draw the optional labels for the x axis and y axis.
404
+ def draw_axis_labels
405
+ draw_x_axis_label
406
+ draw_y_axis_label
407
+ end
408
+
409
+ def draw_x_axis_label
410
+ unless @x_axis_label.nil?
411
+ # X Axis
412
+ # Centered vertically and horizontally by setting the
413
+ # height to 1.0 and the width to the width of the graph.
414
+ x_axis_label_y_coordinate = @raw_rows - @x_axis_label_margin
415
+
416
+ # TODO Center between graph area
417
+ @d.fill = @font_color
418
+ @d.font = @font if @font
419
+ @d.stroke('transparent')
420
+ @d.pointsize = scale_fontsize(@marker_font_size)
421
+ @d.gravity = NorthGravity
422
+ @d = @d.annotate_scaled( @base_image,
423
+ @raw_columns, 1.0,
424
+ 0.0, x_axis_label_y_coordinate,
425
+ @x_axis_label, @scale)
426
+ debug { @d.line 0.0, x_axis_label_y_coordinate, @raw_columns, x_axis_label_y_coordinate }
427
+ end
428
+ end
429
+
430
+ def draw_y_axis_label
431
+ unless @y_axis_label.nil?
432
+ # Y Axis, rotated vertically
433
+ @d.rotation = 90.0
434
+ @d.gravity = CenterGravity
435
+ @d = @d.annotate_scaled( @base_image,
436
+ 1.0, @raw_rows,
437
+ @left_margin + @marker_caps_height / 2.0, 0.0,
438
+ @y_axis_label, @scale)
439
+ @d.rotation = -90.0
440
+ end
441
+ end
442
+
443
+ # Draws horizontal background lines and labels
444
+ def draw_line_markers
445
+ return if @hide_line_markers
446
+
447
+ @d = @d.stroke_antialias false
448
+
449
+ if @y_axis_increment.nil?
450
+ # Try to use a number of horizontal lines that will come out even.
451
+ #
452
+ # TODO Do the same for larger numbers...100, 75, 50, 25
453
+ if @marker_count.nil?
454
+ (3..7).each do |lines|
455
+ if @spread % lines == 0.0
456
+ @marker_count = lines
457
+ break
458
+ end
459
+ end
460
+ @marker_count ||= 4
461
+ end
462
+ @increment = (@spread > 0) ? significant(@spread / @marker_count) : 1
463
+ else
464
+ # TODO Make this work for negative values
465
+ @maximum_value = [@maximum_value.ceil, @y_axis_increment].max
466
+ @minimum_value = @minimum_value.floor
467
+ calculate_spread
468
+ normalize(true)
469
+
470
+ @marker_count = (@spread / @y_axis_increment).to_i
471
+ @increment = @y_axis_increment
472
+ end
473
+ @increment_scaled = @graph_height.to_f / (@spread / @increment)
474
+
475
+ # Draw horizontal line markers and annotate with numbers
476
+ (0..@marker_count).each do |index|
477
+ y = @graph_top + @graph_height - index.to_f * @increment_scaled
478
+
479
+ @d = @d.stroke(@marker_color)
480
+ @d = @d.stroke_width 1
481
+ @d = @d.line(@graph_left, y, @graph_right, y)
482
+
483
+ marker_label = index * @increment + @minimum_value.to_f
484
+
485
+ unless @hide_line_numbers
486
+ @d.fill = @font_color
487
+ @d.font = @font if @font
488
+ @d.stroke('transparent')
489
+ @d.pointsize = scale_fontsize(@marker_font_size)
490
+ @d.gravity = EastGravity
491
+
492
+ # Vertically center with 1.0 for the height
493
+ @d = @d.annotate_scaled( @base_image,
494
+ @graph_left - LABEL_MARGIN, 1.0,
495
+ 0.0, y,
496
+ label(marker_label), @scale)
497
+ end
498
+ end
499
+
500
+ # # Submitted by a contibutor...the utility escapes me
501
+ # i = 0
502
+ # @additional_line_values.each do |value|
503
+ # @increment_scaled = @graph_height.to_f / (@maximum_value.to_f / value)
504
+ #
505
+ # y = @graph_top + @graph_height - @increment_scaled
506
+ #
507
+ # @d = @d.stroke(@additional_line_colors[i])
508
+ # @d = @d.line(@graph_left, y, @graph_right, y)
509
+ #
510
+ #
511
+ # @d.fill = @additional_line_colors[i]
512
+ # @d.font = @font if @font
513
+ # @d.stroke('transparent')
514
+ # @d.pointsize = scale_fontsize(@marker_font_size)
515
+ # @d.gravity = EastGravity
516
+ # @d = @d.annotate_scaled( @base_image,
517
+ # 100, 20,
518
+ # -10, y - (@marker_font_size/2.0),
519
+ # "", @scale)
520
+ # i += 1
521
+ # end
522
+
523
+ @d = @d.stroke_antialias true
524
+ end
525
+
526
+ ##
527
+ # Return the sum of values in an array.
528
+ #
529
+ # Duplicated to not conflict with active_support in Rails.
530
+
531
+ def sum(arr)
532
+ arr.inject(0) { |i, m| m + i }
533
+ end
534
+
535
+ ##
536
+ # Return a calculation of center
537
+
538
+ def center(size)
539
+ (@raw_columns - size) / 2
540
+ end
541
+
542
+ ##
543
+ # Draws a legend with the names of the datasets matched
544
+ # to the colors used to draw them.
545
+
546
+ def draw_legend
547
+ return if @hide_legend
548
+ setup_legend_labels
549
+ setup_legend_label_measurements
550
+ draw_legend_labels
551
+ end
552
+
553
+ def setup_legend_labels
554
+ @legend_labels = @data.collect {|item| item[DATA_LABEL_INDEX] }
555
+ end
556
+
557
+ def setup_legend_label_measurements
558
+ # May fix legend drawing problem at small sizes
559
+ @d.font = @font if @font
560
+ @d.pointsize = @legend_font_size
561
+
562
+ @legend_label_widths = [[]] # Used to calculate line wrap
563
+ @legend_labels.each do |label|
564
+ metrics = @d.get_type_metrics(@base_image, label.to_s)
565
+ label_width = metrics.width + @legend_box_size * 2.7
566
+ @legend_label_widths.last.push label_width
567
+
568
+ if sum(@legend_label_widths.last) > (@raw_columns * 0.9)
569
+ @legend_label_widths.push [@legend_label_widths.last.pop]
570
+ end
571
+ end
572
+ end
573
+
574
+ def draw_legend_labels
575
+ current_x_offset = center(sum(@legend_label_widths.first))
576
+ current_y_offset = @hide_title ?
577
+ @top_margin + LEGEND_MARGIN :
578
+ @top_margin + TITLE_MARGIN + @title_caps_height + LEGEND_MARGIN
579
+
580
+ @legend_labels.each_with_index do |legend_label, index|
581
+ draw_legend_label(legend_label,index,
582
+ current_x_offset,current_y_offset)
583
+ current_x_offset, current_y_offset =
584
+ step_legend_position(current_x_offset,current_y_offset,legend_label)
585
+ end
586
+ @color_index = 0
587
+ end
588
+
589
+ def draw_legend_label(legend_label, index,
590
+ current_x_offset,current_y_offset)
591
+ draw_legend_label_text(legend_label, index,
592
+ current_x_offset, current_y_offset)
593
+ draw_legend_label_box(index, current_x_offset, current_y_offset)
594
+ end
595
+
596
+ def draw_legend_label_text(legend_label, index,
597
+ current_x_offset,current_y_offset)
598
+ @d.fill = @font_color
599
+ @d.font = @font if @font
600
+ @d.pointsize = scale_fontsize(@legend_font_size)
601
+ @d.stroke('transparent')
602
+ @d.font_weight = NormalWeight
603
+ @d.gravity = WestGravity
604
+ @d = @d.annotate_scaled(@base_image,
605
+ @raw_columns, 1.0,
606
+ current_x_offset + (@legend_box_size * 1.7),
607
+ current_y_offset,
608
+ legend_label.to_s, @scale)
609
+ end
610
+
611
+ def draw_legend_label_box(index,current_x_offset,current_y_offset)
612
+ # Now draw box with color of this dataset
613
+ @d = @d.stroke('transparent')
614
+ @d = @d.fill @data[index][DATA_COLOR_INDEX]
615
+ @d = @d.rectangle(current_x_offset,
616
+ current_y_offset - @legend_box_size / 2.0,
617
+ current_x_offset + @legend_box_size,
618
+ current_y_offset + @legend_box_size / 2.0)
619
+ end
620
+
621
+ def step_legend_position(current_x_offset,current_y_offset,legend_label)
622
+ # Handle wrapping
623
+ first = @legend_label_widths.first
624
+ first.shift if first
625
+ if first and first.empty?
626
+ debug { @d.line 0.0, current_y_offset, @raw_columns, current_y_offset }
627
+
628
+ @legend_label_widths.shift # next row
629
+ current_x_offset = center(sum(@legend_label_widths.first)) unless @legend_label_widths.empty?
630
+ line_height = [@legend_caps_height, @legend_box_size].max + LEGEND_MARGIN
631
+ if @legend_label_widths.length > 0
632
+ # Wrap to next line and shrink available graph dimensions
633
+ current_y_offset += line_height
634
+ @graph_top += line_height
635
+ @graph_height = @graph_bottom - @graph_top
636
+ end
637
+ else
638
+ @d.pointsize = @legend_font_size
639
+ metrics = @d.get_type_metrics(@base_image, legend_label.to_s)
640
+ current_string_offset = metrics.width + (@legend_box_size * 2.7)
641
+ current_x_offset += current_string_offset
642
+ end
643
+
644
+ [ current_x_offset, current_y_offset ]
645
+ end
646
+
647
+
648
+ # Draws a title on the graph.
649
+ def draw_title
650
+ return if (@hide_title || @title.nil?)
651
+
652
+ @d.fill = @font_color
653
+ @d.font = @font if @font
654
+ @d.stroke('transparent')
655
+ @d.pointsize = scale_fontsize(@title_font_size)
656
+ @d.font_weight = BoldWeight
657
+ @d.gravity = NorthGravity
658
+ @d = @d.annotate_scaled( @base_image,
659
+ @raw_columns, 1.0,
660
+ 0, @top_margin,
661
+ @title, @scale)
662
+ end
663
+
664
+ # Draws column labels below graph, centered over x_offset
665
+ #--
666
+ # TODO Allow WestGravity as an option
667
+ def draw_label(x_offset, index)
668
+ return if @hide_line_markers
669
+
670
+ if !@labels[index].nil? && @labels_seen[index].nil?
671
+ y_offset = @graph_bottom + LABEL_MARGIN
672
+
673
+ @d.fill = @font_color
674
+ @d.font = @font if @font
675
+ @d.stroke('transparent')
676
+ @d.rotation = @label_rotation
677
+ @d.font_weight = NormalWeight
678
+ @d.pointsize = scale_fontsize(@marker_font_size)
679
+ @d.gravity = @label_rotation == 0 ? NorthGravity : NorthWestGravity
680
+ @d = @d.annotate_scaled(@base_image,
681
+ 1.0, 1.0,
682
+ x_offset, y_offset,
683
+ @labels[index], @scale)
684
+ @d.rotation = -@label_rotation
685
+ @labels_seen[index] = 1
686
+ debug { @d.line 0.0, y_offset, @raw_columns, y_offset }
687
+ end
688
+ end
689
+
690
+ # Shows an error message because you have no data.
691
+ def draw_no_data
692
+ @d.fill = @font_color
693
+ @d.font = @font if @font
694
+ @d.stroke('transparent')
695
+ @d.font_weight = NormalWeight
696
+ @d.pointsize = scale_fontsize(80)
697
+ @d.gravity = CenterGravity
698
+ @d = @d.annotate_scaled( @base_image,
699
+ @raw_columns, @raw_rows/2.0,
700
+ 0, 10,
701
+ @no_data_message, @scale)
702
+ end
703
+
704
+ # Finds the best background to render based on the provided theme options.
705
+ #
706
+ # Creates a @base_image to draw on.
707
+ def render_background
708
+ case @theme_options[:background_colors]
709
+ when Array
710
+ @base_image = render_gradiated_background(*@theme_options[:background_colors])
711
+ when String
712
+ @base_image = render_solid_background(@theme_options[:background_colors])
713
+ else
714
+ @base_image = render_image_background(*@theme_options[:background_image])
715
+ end
716
+ end
717
+
718
+ # Make a new image at the current size with a solid +color+.
719
+ def render_solid_background(color)
720
+ Image.new(@columns, @rows) {
721
+ self.background_color = color
722
+ }
723
+ end
724
+
725
+ # Use with a theme definition method to draw a gradiated background.
726
+ def render_gradiated_background(top_color, bottom_color)
727
+ Image.new(@columns, @rows,
728
+ GradientFill.new(0, 0, 100, 0, top_color, bottom_color))
729
+ end
730
+
731
+ # Use with a theme to use an image (800x600 original) background.
732
+ def render_image_background(image_path)
733
+ image = Image.read(image_path)
734
+ if @scale != 1.0
735
+ image[0].resize!(@scale) # TODO Resize with new scale (crop if necessary for wide graph)
736
+ end
737
+ image[0]
738
+ end
739
+
740
+ # Use with a theme to make a transparent background
741
+ def render_transparent_background
742
+ Image.new(@columns, @rows) do
743
+ self.background_color = 'transparent'
744
+ end
745
+ end
746
+
747
+ # Resets everything to defaults (except data).
748
+ def reset_themes
749
+ @color_index = 0
750
+ @labels_seen = {}
751
+ @theme_options = {}
752
+
753
+ @d = Draw.new
754
+ # Scale down from 800x600 used to calculate drawing.
755
+ @d = @d.scale(@scale, @scale)
756
+ end
757
+
758
+ def scale(value) # :nodoc:
759
+ value * @scale
760
+ end
761
+
762
+ # Return a comparable fontsize for the current graph.
763
+ def scale_fontsize(value)
764
+ new_fontsize = value * @scale
765
+ # return new_fontsize < 10.0 ? 10.0 : new_fontsize
766
+ return new_fontsize
767
+ end
768
+
769
+ def clip_value_if_greater_than(value, max_value) # :nodoc:
770
+ (value > max_value) ? max_value : value
771
+ end
772
+
773
+ # Overridden by subclasses such as stacked bar.
774
+ def larger_than_max?(data_point, index=0) # :nodoc:
775
+ data_point > @maximum_value
776
+ end
777
+
778
+ def less_than_min?(data_point, index=0) # :nodoc:
779
+ data_point < @minimum_value
780
+ end
781
+
782
+ # Overridden by subclasses that need it.
783
+ def max(data_point, index) # :nodoc:
784
+ data_point
785
+ end
786
+
787
+ # Overridden by subclasses that need it.
788
+ def min(data_point, index) # :nodoc:
789
+ data_point
790
+ end
791
+
792
+ def significant(inc) # :nodoc:
793
+ return 1.0 if inc == 0 # Keep from going into infinite loop
794
+ factor = 1.0
795
+ while (inc < 10)
796
+ inc *= 10
797
+ factor /= 10
798
+ end
799
+
800
+ while (inc > 100)
801
+ inc /= 10
802
+ factor *= 10
803
+ end
804
+
805
+ res = inc.floor * factor
806
+ if (res.to_i.to_f == res)
807
+ res.to_i
808
+ else
809
+ res
810
+ end
811
+ end
812
+
813
+ # Sort with largest overall summed value at front of array so it shows up
814
+ # correctly in the drawn graph.
815
+ def sort_norm_data
816
+ @norm_data.sort! { |a,b| sums(b[DATA_VALUES_INDEX]) <=> sums(a[DATA_VALUES_INDEX]) }
817
+ end
818
+
819
+ def sums(data_set) # :nodoc:
820
+ total_sum = 0
821
+ data_set.collect {|num| total_sum += num.to_f }
822
+ total_sum
823
+ end
824
+
825
+ # Used by StackedBar and child classes.
826
+ #
827
+ # May need to be moved to the StackedBar class.
828
+ def get_maximum_by_stack
829
+ # Get sum of each stack
830
+ max_hash = {}
831
+ @data.each do |data_set|
832
+ data_set[DATA_VALUES_INDEX].each_with_index do |data_point, i|
833
+ max_hash[i] = 0.0 unless max_hash[i]
834
+ max_hash[i] += data_point.to_f
835
+ end
836
+ end
837
+
838
+ # @maximum_value = 0
839
+ max_hash.keys.each do |key|
840
+ @maximum_value = max_hash[key] if max_hash[key] > @maximum_value
841
+ end
842
+ @minimum_value = 0
843
+ end
844
+
845
+ def make_stacked # :nodoc:
846
+ stacked_values = Array.new(@column_count, 0)
847
+ @data.each do |value_set|
848
+ value_set[DATA_VALUES_INDEX].each_with_index do |value, index|
849
+ stacked_values[index] += value
850
+ end
851
+ value_set[DATA_VALUES_INDEX] = stacked_values.dup
852
+ end
853
+ end
854
+
855
+ private
856
+
857
+ # Takes a block and draws it if DEBUG is true.
858
+ #
859
+ # Example:
860
+ # debug { @d.rectangle x1, y1, x2, y2 }
861
+ def debug
862
+ if DEBUG
863
+ @d = @d.fill 'transparent'
864
+ @d = @d.stroke 'turquoise'
865
+ @d = yield
866
+ end
867
+ end
868
+
869
+ # Uses the next color in your color list.
870
+ def increment_color
871
+ if @color_index == 0
872
+ @color_index += 1
873
+ return @colors[0]
874
+ else
875
+ if @color_index < @colors.length
876
+ @color_index += 1
877
+ return @colors[@color_index - 1]
878
+ else
879
+ # Start over
880
+ @color_index = 0
881
+ return @colors[-1]
882
+ end
883
+ end
884
+ end
885
+
886
+ # Return a formatted string representing a number value that should be
887
+ # printed as a label.
888
+ def label(value)
889
+ if (@spread.to_f % @marker_count.to_f == 0) || !@y_axis_increment.nil?
890
+ return value.to_i.to_s
891
+ end
892
+
893
+ if @spread > 10.0
894
+ sprintf("%0i", value)
895
+ elsif @spread >= 3.0
896
+ sprintf("%0.2f", value)
897
+ else
898
+ value.to_s
899
+ end
900
+ end
901
+
902
+ end # Gruff::Base
903
+
904
+ class IncorrectNumberOfDatasetsException < StandardError; end
905
+
906
+ end # Gruff
907
+
908
+ module Magick
909
+
910
+ class Draw
911
+
912
+ # Additional method to scale annotation text since Draw.scale doesn't.
913
+ def annotate_scaled(img, width, height, x, y, text, scale)
914
+ scaled_width = (width * scale) >= 1 ? (width * scale) : 1
915
+ scaled_height = (height * scale) >= 1 ? (height * scale) : 1
916
+
917
+ self.annotate( img,
918
+ scaled_width, scaled_height,
919
+ x * scale, y * scale,
920
+ text)
921
+ end
922
+
923
+ end
924
+
925
+ end # Magick