image_paradise 0.1.12

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of image_paradise might be problematic. Click here for more details.

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