gruff 0.6.0-java → 0.11.0-java

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