gruffy 0.1.2 → 0.7.1.dev

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