topfunky-gruff 0.3.2

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