gruffy 0.0.2 → 0.0.3

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