gruff 0.7.0-java → 0.12.0-java

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