gruffy 0.1.2 → 0.7.1.dev

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.
@@ -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