gruff 0.6.0-java → 0.11.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 (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