gruff 0.11.0-java → 0.14.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 (69) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +66 -0
  3. data/.gitignore +1 -0
  4. data/.rubocop.yml +24 -8
  5. data/.rubocop_todo.yml +57 -53
  6. data/CHANGELOG.md +34 -0
  7. data/README.md +15 -7
  8. data/assets/fonts/LICENSE.txt +202 -0
  9. data/assets/fonts/Roboto-Bold.ttf +0 -0
  10. data/assets/fonts/Roboto-Regular.ttf +0 -0
  11. data/gruff.gemspec +8 -5
  12. data/lib/gruff.rb +9 -3
  13. data/lib/gruff/accumulator_bar.rb +3 -3
  14. data/lib/gruff/area.rb +5 -12
  15. data/lib/gruff/bar.rb +43 -47
  16. data/lib/gruff/base.rb +267 -146
  17. data/lib/gruff/bezier.rb +4 -10
  18. data/lib/gruff/bullet.rb +13 -19
  19. data/lib/gruff/dot.rb +14 -19
  20. data/lib/gruff/font.rb +39 -0
  21. data/lib/gruff/helper/bar_conversion.rb +28 -13
  22. data/lib/gruff/helper/bar_value_label.rb +68 -0
  23. data/lib/gruff/helper/stacked_mixin.rb +1 -2
  24. data/lib/gruff/histogram.rb +9 -8
  25. data/lib/gruff/line.rb +59 -56
  26. data/lib/gruff/mini/bar.rb +10 -7
  27. data/lib/gruff/mini/legend.rb +19 -10
  28. data/lib/gruff/mini/pie.rb +10 -8
  29. data/lib/gruff/mini/side_bar.rb +10 -8
  30. data/lib/gruff/net.rb +13 -20
  31. data/lib/gruff/patch/rmagick.rb +22 -24
  32. data/lib/gruff/patch/string.rb +7 -4
  33. data/lib/gruff/pie.rb +24 -70
  34. data/lib/gruff/renderer/bezier.rb +11 -11
  35. data/lib/gruff/renderer/circle.rb +11 -11
  36. data/lib/gruff/renderer/dash_line.rb +12 -12
  37. data/lib/gruff/renderer/dot.rb +16 -16
  38. data/lib/gruff/renderer/ellipse.rb +11 -11
  39. data/lib/gruff/renderer/line.rb +12 -12
  40. data/lib/gruff/renderer/polygon.rb +13 -13
  41. data/lib/gruff/renderer/polyline.rb +11 -11
  42. data/lib/gruff/renderer/rectangle.rb +9 -9
  43. data/lib/gruff/renderer/renderer.rb +23 -50
  44. data/lib/gruff/renderer/text.rb +32 -29
  45. data/lib/gruff/scatter.rb +52 -76
  46. data/lib/gruff/scene.rb +15 -14
  47. data/lib/gruff/side_bar.rb +49 -52
  48. data/lib/gruff/side_stacked_bar.rb +29 -20
  49. data/lib/gruff/spider.rb +13 -22
  50. data/lib/gruff/stacked_area.rb +10 -16
  51. data/lib/gruff/stacked_bar.rb +29 -18
  52. data/lib/gruff/store/{base_data.rb → basic_data.rb} +5 -7
  53. data/lib/gruff/store/custom_data.rb +4 -6
  54. data/lib/gruff/store/store.rb +9 -12
  55. data/lib/gruff/store/xy_data.rb +6 -7
  56. data/lib/gruff/themes.rb +6 -6
  57. data/lib/gruff/version.rb +1 -1
  58. data/rails_generators/gruff/templates/controller.rb +1 -1
  59. metadata +24 -14
  60. data/.travis.yml +0 -26
  61. data/Rakefile +0 -47
  62. data/assets/plastik/blue.png +0 -0
  63. data/assets/plastik/green.png +0 -0
  64. data/assets/plastik/red.png +0 -0
  65. data/docker/Dockerfile +0 -14
  66. data/docker/build.sh +0 -4
  67. data/docker/launch.sh +0 -4
  68. data/lib/gruff/helper/bar_value_label_mixin.rb +0 -30
  69. data/lib/gruff/photo_bar.rb +0 -97
Binary file
Binary file
data/gruff.gemspec CHANGED
@@ -1,4 +1,3 @@
1
- # -*- encoding: utf-8 -*-
2
1
  # frozen_string_literal: true
3
2
 
4
3
  lib = File.expand_path('lib')
@@ -10,10 +9,11 @@ Gem::Specification.new do |s|
10
9
  s.name = 'gruff'
11
10
  s.version = Gruff::VERSION
12
11
  s.authors = ['Geoffrey Grosenbach', 'Uwe Kubosch']
13
- s.date = Date.today.to_s
14
12
  s.description = 'Beautiful graphs for one or multiple datasets. Can be used on websites or in documents.'
15
13
  s.email = 'boss@topfunky.com'
16
- s.files = `git ls-files`.split($/).reject { |f| f =~ /^test/ }
14
+ s.files = `git ls-files`.split.reject do |f|
15
+ f =~ /^test|^docker|^Rakefile/i
16
+ end
17
17
  s.homepage = 'https://github.com/topfunky/gruff'
18
18
  s.require_paths = %w[lib]
19
19
  s.summary = 'Beautiful graphs for one or multiple datasets.'
@@ -27,12 +27,15 @@ Gem::Specification.new do |s|
27
27
  s.add_dependency 'rmagick4j'
28
28
  else
29
29
  s.add_dependency 'rmagick'
30
- s.add_development_dependency 'rubocop', '~> 0.81.0'
30
+ s.add_development_dependency 'rubocop', '~> 1.12.1'
31
+ s.add_development_dependency 'rubocop-performance', '~> 1.10.2'
32
+ s.add_development_dependency 'rubocop-rake', '~> 0.5.1'
31
33
  end
32
34
  s.add_dependency 'histogram'
33
- s.required_ruby_version = '>= 1.9.3'
35
+ s.required_ruby_version = '>= 2.4.0'
34
36
 
35
37
  s.add_development_dependency 'rake'
36
38
  s.add_development_dependency 'minitest-reporters'
39
+ s.add_development_dependency 'simplecov'
37
40
  s.add_development_dependency 'yard', '~> 0.9.25'
38
41
  end
data/lib/gruff.rb CHANGED
@@ -9,8 +9,14 @@ require 'gruff/version'
9
9
  patch/rmagick
10
10
  patch/string
11
11
 
12
- themes
12
+ font
13
13
  base
14
+
15
+ helper/bar_conversion.rb
16
+ helper/stacked_mixin
17
+ helper/bar_value_label
18
+
19
+ themes
14
20
  area
15
21
  bar
16
22
  bezier
@@ -22,10 +28,10 @@ require 'gruff/version'
22
28
  pie
23
29
  scatter
24
30
  spider
31
+ side_bar
25
32
  stacked_area
26
33
  stacked_bar
27
34
  side_stacked_bar
28
- side_bar
29
35
  accumulator_bar
30
36
 
31
37
  scene
@@ -43,7 +49,7 @@ require 'gruff/version'
43
49
  renderer/text
44
50
 
45
51
  store/store
46
- store/base_data
52
+ store/basic_data
47
53
  store/custom_data
48
54
  store/xy_data
49
55
 
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'gruff/base'
4
-
5
3
  #
6
4
  # Gruff::AccumulatorBar is a special bar graph that shows a
7
5
  # single dataset as a set of stacked bars.
@@ -16,7 +14,9 @@ require 'gruff/base'
16
14
  # g.write('accumulator_bar.png')
17
15
  #
18
16
  class Gruff::AccumulatorBar < Gruff::StackedBar
19
- def draw
17
+ private
18
+
19
+ def setup_data
20
20
  raise(Gruff::IncorrectNumberOfDatasetsException) unless store.length == 1
21
21
 
22
22
  accum_array = store.data.first.points[0..-2].reduce([0]) { |a, v| a << a.last + v }
data/lib/gruff/area.rb CHANGED
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'gruff/base'
4
-
5
3
  #
6
4
  # Gruff::Area provides an area graph which displays graphically
7
5
  # quantitative data.
@@ -22,19 +20,16 @@ class Gruff::Area < Gruff::Base
22
20
  # Specifies the stroke width in line around area graph. Default is +2.0+.
23
21
  attr_writer :stroke_width
24
22
 
25
- def initialize_ivars
23
+ private
24
+
25
+ def initialize_attributes
26
26
  super
27
27
  @sorted_drawing = true
28
28
  @fill_opacity = 0.85
29
29
  @stroke_width = 2.0
30
30
  end
31
- private :initialize_ivars
32
-
33
- def draw
34
- super
35
-
36
- return unless data_given?
37
31
 
32
+ def draw_graph
38
33
  x_increment = @graph_width / (column_count - 1).to_f
39
34
 
40
35
  store.norm_data.each do |data_row|
@@ -57,9 +52,7 @@ class Gruff::Area < Gruff::Base
57
52
  poly_points << @graph_left
58
53
  poly_points << @graph_bottom - 1
59
54
 
60
- Gruff::Renderer::Polygon.new(color: data_row.color, width: @stroke_width, opacity: @fill_opacity).render(poly_points)
55
+ Gruff::Renderer::Polygon.new(renderer, color: data_row.color, width: @stroke_width, opacity: @fill_opacity).render(poly_points)
61
56
  end
62
-
63
- Gruff::Renderer.finish
64
57
  end
65
58
  end
data/lib/gruff/bar.rb CHANGED
@@ -1,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'gruff/base'
4
- require 'gruff/helper/bar_conversion'
5
-
6
3
  #
7
4
  # Gruff::Bar provide a bar graph that presents categorical data
8
5
  # with rectangular bars.
@@ -25,7 +22,7 @@ class Gruff::Bar < Gruff::Base
25
22
  # Spacing factor applied between a group of bars belonging to the same label.
26
23
  attr_writer :group_spacing
27
24
 
28
- # Set the number output format for labels using sprintf.
25
+ # Set the number output format string or lambda.
29
26
  # Default is +"%.2f"+.
30
27
  attr_writer :label_formatting
31
28
 
@@ -33,42 +30,57 @@ class Gruff::Bar < Gruff::Base
33
30
  # Default is +false+.
34
31
  attr_writer :show_labels_for_bar_values
35
32
 
36
- def initialize_ivars
33
+ # Prevent drawing of column labels below a bar graph. Default is +false+.
34
+ attr_writer :hide_labels
35
+
36
+ # Can be used to adjust the spaces between the bars.
37
+ # Accepts values between 0.00 and 1.00 where 0.00 means no spacing at all
38
+ # and 1 means that each bars' width is nearly 0 (so each bar is a simple
39
+ # line with no x dimension).
40
+ #
41
+ # Default value is +0.9+.
42
+ def spacing_factor=(space_percent)
43
+ raise ArgumentError, 'spacing_factor must be between 0.00 and 1.00' unless (space_percent >= 0) && (space_percent <= 1)
44
+
45
+ @spacing_factor = (1 - space_percent)
46
+ end
47
+
48
+ private
49
+
50
+ def initialize_attributes
37
51
  super
38
52
  @spacing_factor = 0.9
39
53
  @group_spacing = 10
40
54
  @label_formatting = nil
41
55
  @show_labels_for_bar_values = false
56
+ @hide_labels = false
42
57
  end
43
- private :initialize_ivars
44
58
 
45
- def draw
59
+ def setup_drawing
46
60
  # Labels will be centered over the left of the bar if
47
61
  # there are more labels than columns. This is basically the same
48
62
  # as where it would be for a line graph.
49
63
  @center_labels_over_point = (@labels.keys.length > column_count)
50
64
 
51
65
  super
52
- return unless data_given?
66
+ end
53
67
 
54
- draw_bars
68
+ def hide_labels?
69
+ @hide_labels
55
70
  end
56
71
 
57
- # Can be used to adjust the spaces between the bars.
58
- # Accepts values between 0.00 and 1.00 where 0.00 means no spacing at all
59
- # and 1 means that each bars' width is nearly 0 (so each bar is a simple
60
- # line with no x dimension).
61
- #
62
- # Default value is +0.9+.
63
- def spacing_factor=(space_percent)
64
- raise ArgumentError, 'spacing_factor must be between 0.00 and 1.00' unless (space_percent >= 0) && (space_percent <= 1)
72
+ def hide_left_label_area?
73
+ @hide_line_markers
74
+ end
65
75
 
66
- @spacing_factor = (1 - space_percent)
76
+ def hide_bottom_label_area?
77
+ hide_labels?
67
78
  end
68
79
 
69
- protected
80
+ # Value to avoid completely overwriting the coordinate axis
81
+ AXIS_MARGIN = 0.5
70
82
 
71
- def draw_bars
83
+ def draw_graph
72
84
  # Setup spacing.
73
85
  #
74
86
  # Columns sit side-by-side.
@@ -78,24 +90,10 @@ protected
78
90
  padding = (bar_width * (1 - @bar_spacing)) / 2
79
91
 
80
92
  # Setup the BarConversion Object
81
- conversion = Gruff::BarConversion.new
82
- conversion.graph_height = @graph_height
83
- conversion.graph_top = @graph_top
84
-
85
- # Set up the right mode [1,2,3] see BarConversion for further explanation
86
- if minimum_value >= 0
87
- # all bars go from zero to positive
88
- conversion.mode = 1
89
- elsif maximum_value <= 0
90
- # all bars go from 0 to negative
91
- conversion.mode = 2
92
- else
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
98
- end
93
+ conversion = Gruff::BarConversion.new(
94
+ top: @graph_top, bottom: @graph_bottom,
95
+ minimum_value: minimum_value, maximum_value: maximum_value, spread: @spread
96
+ )
99
97
 
100
98
  # iterate over all normalised data
101
99
  store.norm_data.each_with_index do |data_row, row_index|
@@ -107,11 +105,11 @@ protected
107
105
  left_x = @graph_left + (bar_width * (row_index + point_index + ((store.length - 1) * point_index))) + padding + group_spacing
108
106
  right_x = left_x + bar_width * @bar_spacing
109
107
  # y
110
- left_y, right_y = conversion.get_left_y_right_y_scaled(data_point)
108
+ left_y, right_y = conversion.get_top_bottom_scaled(data_point)
111
109
 
112
110
  # create new bar
113
- rect_renderer = Gruff::Renderer::Rectangle.new(color: data_row.color)
114
- rect_renderer.render(left_x, left_y, right_x, right_y)
111
+ rect_renderer = Gruff::Renderer::Rectangle.new(renderer, color: data_row.color)
112
+ rect_renderer.render(left_x, left_y - AXIS_MARGIN, right_x, right_y - AXIS_MARGIN)
115
113
 
116
114
  # Calculate center based on bar_width and current row
117
115
  label_center = @graph_left + group_spacing + (store.length * bar_width * point_index) + (store.length * bar_width / 2.0)
@@ -119,18 +117,16 @@ protected
119
117
  # Subtract half a bar width to center left if requested
120
118
  draw_label(label_center, point_index)
121
119
  if @show_labels_for_bar_values
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)
120
+ bar_value_label = Gruff::BarValueLabel::Bar.new([left_x, left_y, right_x, right_y], store.data[row_index].points[point_index])
121
+ bar_value_label.prepare_rendering(@label_formatting, bar_width) do |x, y, text|
122
+ draw_value_label(x, y, text)
123
+ end
126
124
  end
127
125
  end
128
126
  end
129
127
 
130
128
  # Draw the last label if requested
131
129
  draw_label(@graph_right, column_count, Magick::NorthWestGravity) if @center_labels_over_point
132
-
133
- Gruff::Renderer.finish
134
130
  end
135
131
 
136
132
  def calculate_spacing
data/lib/gruff/base.rb CHANGED
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'rmagick'
4
3
  require 'bigdecimal'
5
4
 
6
5
  ##
@@ -17,6 +16,9 @@ require 'bigdecimal'
17
16
  #
18
17
  # See {Gruff::Base#theme=} for setting themes.
19
18
  module Gruff
19
+ using String::GruffCommify
20
+
21
+ # A common base class inherited from class of drawing a graph.
20
22
  class Base
21
23
  # Space around text elements. Mostly used for vertical spacing.
22
24
  LEGEND_MARGIN = TITLE_MARGIN = 20.0
@@ -91,15 +93,6 @@ module Gruff
91
93
  # Set the large title of the graph displayed at the top.
92
94
  attr_writer :title
93
95
 
94
- # Same as {#font=} but for the title.
95
- attr_writer :title_font
96
-
97
- # Specifies whether to draw the title bolded or not. Default is +true+.
98
- attr_writer :bold_title
99
-
100
- # Specifies the text color.
101
- attr_writer :font_color
102
-
103
96
  # Prevent drawing of line markers. Default is +false+.
104
97
  attr_writer :hide_line_markers
105
98
 
@@ -116,21 +109,9 @@ module Gruff
116
109
  # to +"No Data."+.
117
110
  attr_writer :no_data_message
118
111
 
119
- # Set the font size of the large title at the top of the graph. Default is +36+.
120
- attr_writer :title_font_size
121
-
122
- # Optionally set the size of the font. Based on an 800x600px graph.
123
- # Default is +20+.
124
- #
125
- # Will be scaled down if the graph is smaller than 800px wide.
126
- attr_writer :legend_font_size
127
-
128
112
  # Display the legend under the graph. Default is +false+.
129
113
  attr_writer :legend_at_bottom
130
114
 
131
- # The font size of the labels around the graph. Default is +21+.
132
- attr_writer :marker_font_size
133
-
134
115
  # Set the color of the auxiliary lines.
135
116
  attr_writer :marker_color
136
117
 
@@ -154,9 +135,11 @@ module Gruff
154
135
  # Will be scaled down if graph is smaller than 800px wide.
155
136
  attr_writer :legend_box_size
156
137
 
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
138
+ # Allow passing lambdas to format labels for x axis.
139
+ attr_writer :x_axis_label_format
140
+
141
+ # Allow passing lambdas to format labels for y axis.
142
+ attr_writer :y_axis_label_format
160
143
 
161
144
  # If one numerical argument is given, the graph is drawn at 4/3 ratio
162
145
  # according to the given width (+800+ results in 800x600, +400+ gives 400x300,
@@ -177,7 +160,7 @@ module Gruff
177
160
  @rows.freeze
178
161
 
179
162
  initialize_graph_scale
180
- initialize_ivars
163
+ initialize_attributes
181
164
  initialize_store
182
165
 
183
166
  self.theme = Themes::KEYNOTE
@@ -195,31 +178,27 @@ module Gruff
195
178
  protected :initialize_graph_scale
196
179
 
197
180
  def initialize_store
198
- @store = Gruff::Store.new(Gruff::Store::BaseData)
181
+ @store = Gruff::Store.new(Gruff::Store::BasicData)
199
182
  end
200
183
  protected :initialize_store
201
184
 
202
- # Initialize instance variable of attribures
185
+ # Initialize instance variable of attributes
203
186
  #
204
187
  # Subclasses can override this, call super, then set values separately.
205
188
  #
206
189
  # This makes it possible to set defaults in a subclass but still allow
207
190
  # developers to change this values in their program.
208
- def initialize_ivars
191
+ def initialize_attributes
209
192
  @marker_count = nil
210
193
  @maximum_value = @minimum_value = nil
211
194
  @labels = {}
212
195
  @sort = false
213
196
  @sorted_drawing = false
214
197
  @title = nil
215
- @title_font = nil
216
198
 
217
- @font = nil
218
- @bold_title = true
219
-
220
- @marker_font_size = 21.0
221
- @legend_font_size = 20.0
222
- @title_font_size = 36.0
199
+ @title_font = Gruff::Font.new(size: 36.0, bold: true)
200
+ @marker_font = Gruff::Font.new(size: 21.0)
201
+ @legend_font = Gruff::Font.new(size: 20.0)
223
202
 
224
203
  @top_margin = @bottom_margin = @left_margin = @right_margin = DEFAULT_MARGIN
225
204
  @legend_margin = LEGEND_MARGIN
@@ -236,12 +215,14 @@ module Gruff
236
215
  @label_max_size = 0
237
216
  @label_truncation_style = :absolute
238
217
 
239
- @use_data_label = false
240
218
  @x_axis_increment = nil
241
219
  @x_axis_label = @y_axis_label = nil
242
220
  @y_axis_increment = nil
221
+
222
+ @x_axis_label_format = nil
223
+ @y_axis_label_format = nil
243
224
  end
244
- protected :initialize_ivars
225
+ protected :initialize_attributes
245
226
 
246
227
  # Sets the top, bottom, left and right margins to +margin+.
247
228
  #
@@ -256,8 +237,62 @@ module Gruff
256
237
  # @param font_path [String] The path to font.
257
238
  #
258
239
  def font=(font_path)
259
- @font = font_path
260
- Gruff::Renderer.font = @font
240
+ @title_font.path = font_path unless @title_font.path
241
+ @marker_font.path = font_path
242
+ @legend_font.path = font_path
243
+ end
244
+
245
+ # Same as {#font=} but for the title.
246
+ #
247
+ # @param font_path [String] The path to font.
248
+ #
249
+ def title_font=(font_path)
250
+ @title_font.path = font_path
251
+ end
252
+
253
+ # Set the font size of the large title at the top of the graph. Default is +36+.
254
+ #
255
+ # @param value [Numeric] title font size
256
+ #
257
+ def title_font_size=(value)
258
+ @title_font.size = value
259
+ end
260
+
261
+ # The font size of the labels around the graph. Default is +21+.
262
+ #
263
+ # @param value [Numeric] marker font size
264
+ #
265
+ def marker_font_size=(value)
266
+ @marker_font.size = value
267
+ end
268
+
269
+ # Optionally set the size of the font. Based on an 800x600px graph.
270
+ # Default is +20+.
271
+ #
272
+ # Will be scaled down if the graph is smaller than 800px wide.
273
+ #
274
+ # @param value [Numeric] legend font size
275
+ #
276
+ def legend_font_size=(value)
277
+ @legend_font.size = value
278
+ end
279
+
280
+ # Specifies whether to draw the title bolded or not. Default is +true+.
281
+ #
282
+ # @param value [Boolean] specifies whether to draw the title bolded or not.
283
+ #
284
+ def bold_title=(value)
285
+ @title_font.bold = value
286
+ end
287
+
288
+ # Specifies the text color.
289
+ #
290
+ # @param value [String] color
291
+ #
292
+ def font_color=(value)
293
+ @title_font.color = value
294
+ @marker_font.color = value
295
+ @legend_font.color = value
261
296
  end
262
297
 
263
298
  # Add a color to the list of available colors for lines.
@@ -326,12 +361,13 @@ module Gruff
326
361
  }
327
362
  @theme_options = defaults.merge options
328
363
 
364
+ self.marker_color = @theme_options[:marker_color]
365
+ self.font_color = @theme_options[:font_color] || @marker_color
366
+
329
367
  @colors = @theme_options[:colors]
330
- @marker_color = @theme_options[:marker_color]
331
368
  @marker_shadow_color = @theme_options[:marker_shadow_color]
332
- @font_color = @theme_options[:font_color] || @marker_color
333
369
 
334
- Gruff::Renderer.setup(@columns, @rows, @font, @scale, @theme_options)
370
+ @renderer = Gruff::Renderer.new(@columns, @rows, @scale, @theme_options)
335
371
  end
336
372
 
337
373
  # Apply Apple's keynote theme.
@@ -414,39 +450,65 @@ module Gruff
414
450
  # @example
415
451
  # write('graphs/my_pretty_graph.png')
416
452
  def write(file_name = 'graph.png')
417
- draw
418
- Gruff::Renderer.write(file_name)
453
+ to_image.write(file_name)
454
+ end
455
+
456
+ # Return a rendered graph image.
457
+ # This can use RMagick's methods to adjust the image before saving.
458
+ #
459
+ # @return [Magick::Image] The rendered image.
460
+ #
461
+ # @example
462
+ # g = Gruff::Line.new
463
+ # g.data :Jimmy, [25, 36, 86, 39, 25, 31, 79, 88]
464
+ # g.data :Charles, [80, 54, 67, 54, 68, 70, 90, 95]
465
+ # image = g.to_image
466
+ # image = image.resize(400, 300).quantize(128, Magick::RGBColorspace)
467
+ # image.write('test.png')
468
+ #
469
+ def to_image
470
+ @to_image ||= begin
471
+ draw
472
+ renderer.finish
473
+ renderer.image
474
+ end
419
475
  end
420
476
 
421
477
  # Return the graph as a rendered binary blob.
422
478
  #
423
479
  # @param image_format [String] The image format of binary blob.
480
+ #
481
+ # @deprecated Please use +to_image.to_blob+ instead.
424
482
  def to_blob(image_format = 'PNG')
425
- draw
426
- Gruff::Renderer.to_blob(image_format)
483
+ warn '#to_blob is deprecated. Please use `to_image.to_blob` instead'
484
+ to_image.to_blob do
485
+ self.format = image_format
486
+ end
427
487
  end
428
488
 
429
- protected
430
-
431
- # Overridden by subclasses to do the actual plotting of the graph.
432
- #
433
- # Subclasses should start by calling super() for this method.
489
+ # Draw a graph.
434
490
  def draw
491
+ setup_data
492
+
435
493
  # Maybe should be done in one of the following functions for more granularity.
436
494
  unless data_given?
437
495
  draw_no_data
438
496
  return
439
497
  end
440
498
 
441
- setup_data
442
499
  setup_drawing
443
500
 
444
501
  draw_legend
445
502
  draw_line_markers
446
503
  draw_axis_labels
447
504
  draw_title
505
+ draw_graph
448
506
  end
449
507
 
508
+ protected
509
+
510
+ attr_reader :renderer
511
+
450
512
  # Perform data manipulation before calculating chart measurements
451
513
  def setup_data # :nodoc:
452
514
  if @y_axis_increment && !@hide_line_markers
@@ -486,6 +548,18 @@ module Gruff
486
548
  store.columns
487
549
  end
488
550
 
551
+ def marker_count
552
+ @marker_count ||= begin
553
+ count = nil
554
+ (3..7).each do |lines|
555
+ if @spread.to_f % lines == 0.0
556
+ count = lines and break
557
+ end
558
+ end
559
+ count || 4
560
+ end
561
+ end
562
+
489
563
  # Make copy of data with values scaled between 0-100
490
564
  def normalize
491
565
  store.normalize(minimum: minimum_value, spread: @spread)
@@ -500,6 +574,18 @@ module Gruff
500
574
  @hide_title || @title.nil? || @title.empty?
501
575
  end
502
576
 
577
+ def hide_labels?
578
+ @hide_line_markers
579
+ end
580
+
581
+ def hide_left_label_area?
582
+ @hide_line_markers
583
+ end
584
+
585
+ def hide_bottom_label_area?
586
+ @hide_line_markers
587
+ end
588
+
503
589
  ##
504
590
  # Calculates size of drawable area, general font dimensions, etc.
505
591
 
@@ -527,13 +613,13 @@ module Gruff
527
613
  x_axis_label_y_coordinate = @graph_bottom + LABEL_MARGIN + @marker_caps_height
528
614
 
529
615
  # TODO: Center between graph area
530
- text_renderer = Gruff::Renderer::Text.new(@x_axis_label, font: @font, size: @marker_font_size, color: @font_color)
616
+ text_renderer = Gruff::Renderer::Text.new(renderer, @x_axis_label, font: @marker_font)
531
617
  text_renderer.add_to_render_queue(@raw_columns, 1.0, 0.0, x_axis_label_y_coordinate)
532
618
  end
533
619
 
534
620
  if @y_axis_label
535
621
  # Y Axis, rotated vertically
536
- text_renderer = Gruff::Renderer::Text.new(@y_axis_label, font: @font, size: @marker_font_size, color: @font_color, rotation: -90)
622
+ text_renderer = Gruff::Renderer::Text.new(renderer, @y_axis_label, font: @marker_font, rotation: -90)
537
623
  text_renderer.add_to_render_queue(1.0, @raw_rows, @left_margin + @marker_caps_height / 2.0, 0.0, Magick::CenterGravity)
538
624
  end
539
625
  end
@@ -545,28 +631,21 @@ module Gruff
545
631
  increment_scaled = @graph_height.to_f / (@spread / @increment)
546
632
 
547
633
  # Draw horizontal line markers and annotate with numbers
548
- (0..@marker_count).each do |index|
634
+ (0..marker_count).each do |index|
549
635
  y = @graph_top + @graph_height - index.to_f * increment_scaled
550
636
 
551
- line_renderer = Gruff::Renderer::Line.new(color: @marker_color, shadow_color: @marker_shadow_color)
637
+ line_renderer = Gruff::Renderer::Line.new(renderer, color: @marker_color, shadow_color: @marker_shadow_color)
552
638
  line_renderer.render(@graph_left, y, @graph_right, y)
553
639
 
554
640
  unless @hide_line_numbers
555
641
  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)
642
+ label = y_axis_label(marker_label, @increment)
643
+ text_renderer = Gruff::Renderer::Text.new(renderer, label, font: @marker_font)
558
644
  text_renderer.add_to_render_queue(@graph_left - LABEL_MARGIN, 1.0, 0.0, y, Magick::EastGravity)
559
645
  end
560
646
  end
561
647
  end
562
648
 
563
- # Return the sum of values in an array.
564
- #
565
- # Duplicated to not conflict with active_support in Rails.
566
- def sum(arr)
567
- arr.reduce(0) { |i, m| m + i }
568
- end
569
-
570
649
  # Return a calculation of center
571
650
  def center(size)
572
651
  (@raw_columns - size) / 2
@@ -581,10 +660,10 @@ module Gruff
581
660
  legend_square_width = @legend_box_size # small square with color of this item
582
661
  label_widths = calculate_legend_label_widths_for_each_line(legend_labels, legend_square_width)
583
662
 
584
- current_x_offset = center(sum(label_widths.first))
663
+ current_x_offset = center(label_widths.first.sum)
585
664
  current_y_offset = begin
586
665
  if @legend_at_bottom
587
- @graph_height + @title_margin
666
+ @graph_bottom + @legend_margin + @legend_caps_height + LABEL_MARGIN
588
667
  else
589
668
  hide_title? ? @top_margin + @title_margin : @top_margin + @title_margin + @title_caps_height
590
669
  end
@@ -594,30 +673,28 @@ module Gruff
594
673
  next if legend_label.empty?
595
674
 
596
675
  # Draw label
597
- text_renderer = Gruff::Renderer::Text.new(legend_label, font: @font, size: @legend_font_size, color: @font_color)
676
+ text_renderer = Gruff::Renderer::Text.new(renderer, legend_label, font: @legend_font)
598
677
  text_renderer.add_to_render_queue(@raw_columns, 1.0, current_x_offset + (legend_square_width * 1.7), current_y_offset, Magick::WestGravity)
599
678
 
600
679
  # Now draw box with color of this dataset
601
- rect_renderer = Gruff::Renderer::Rectangle.new(color: store.data[index].color)
680
+ rect_renderer = Gruff::Renderer::Rectangle.new(renderer, color: store.data[index].color)
602
681
  rect_renderer.render(current_x_offset,
603
682
  current_y_offset - legend_square_width / 2.0,
604
683
  current_x_offset + legend_square_width,
605
684
  current_y_offset + legend_square_width / 2.0)
606
685
 
607
- width = calculate_width(@legend_font_size, legend_label)
686
+ width = calculate_width(@legend_font, legend_label)
608
687
  current_x_offset += width + (legend_square_width * 2.7)
609
688
  label_widths.first.shift
610
689
 
611
690
  # Handle wrapping
612
691
  if label_widths.first.empty?
613
692
  label_widths.shift
614
- current_x_offset = center(sum(label_widths.first)) unless label_widths.empty?
693
+ current_x_offset = center(label_widths.first.sum) unless label_widths.empty?
615
694
  line_height = [@legend_caps_height, legend_square_width].max + @legend_margin
616
695
  unless label_widths.empty?
617
696
  # Wrap to next line and shrink available graph dimensions
618
697
  current_y_offset += line_height
619
- @graph_top += line_height
620
- @graph_height = @graph_bottom - @graph_top
621
698
  end
622
699
  end
623
700
  end
@@ -627,15 +704,12 @@ module Gruff
627
704
  def draw_title
628
705
  return if hide_title?
629
706
 
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)
707
+ metrics = Gruff::Renderer::Text.new(renderer, @title, font: @title_font).metrics
635
708
  if metrics.width > @raw_columns
636
- font_size = font_size * (@raw_columns / metrics.width) * 0.95
709
+ @title_font.size = @title_font.size * (@raw_columns / metrics.width) * 0.95
637
710
  end
638
- text_renderer = Gruff::Renderer::Text.new(@title, font: font, size: font_size, color: @font_color, weight: font_weight)
711
+
712
+ text_renderer = Gruff::Renderer::Text.new(renderer, @title, font: @title_font)
639
713
  text_renderer.add_to_render_queue(@raw_columns, 1.0, 0, @top_margin)
640
714
  end
641
715
 
@@ -651,17 +725,14 @@ module Gruff
651
725
  # TODO: See if index.odd? is the best stragegy
652
726
  y_offset += @label_stagger_height if index.odd?
653
727
 
654
- label_text = truncate_label_text(@labels[index].to_s)
655
-
656
728
  if x_offset >= @graph_left && x_offset <= @graph_right
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)
729
+ draw_label_at(1.0, 1.0, x_offset, y_offset, @labels[index], gravity)
659
730
  end
660
731
  end
661
732
  end
662
733
 
663
734
  def draw_unique_label(index)
664
- return if @hide_line_markers
735
+ return if hide_labels?
665
736
 
666
737
  @labels_seen ||= {}
667
738
  if !@labels[index].nil? && @labels_seen[index].nil?
@@ -670,20 +741,33 @@ module Gruff
670
741
  end
671
742
  end
672
743
 
744
+ def draw_label_at(width, height, x, y, text, gravity = Magick::NorthGravity)
745
+ label_text = truncate_label_text(text)
746
+ text_renderer = Gruff::Renderer::Text.new(renderer, label_text, font: @marker_font)
747
+ text_renderer.add_to_render_queue(width, height, x, y, gravity)
748
+ end
749
+
673
750
  # Draws the data value over the data point in bar graphs
674
- def draw_value_label(x_offset, y_offset, data_point, bar_value = false)
675
- return if @hide_line_markers && !bar_value
751
+ def draw_value_label(x_offset, y_offset, data_point)
752
+ return if @hide_line_markers
676
753
 
677
- text_renderer = Gruff::Renderer::Text.new(data_point, font: @font, size: @marker_font_size, color: @font_color)
754
+ text_renderer = Gruff::Renderer::Text.new(renderer, data_point, font: @marker_font)
678
755
  text_renderer.add_to_render_queue(1.0, 1.0, x_offset, y_offset)
679
756
  end
680
757
 
681
758
  # Shows an error message because you have no data.
682
759
  def draw_no_data
683
- text_renderer = Gruff::Renderer::Text.new(@no_data_message, font: @font, size: 80, color: @font_color)
760
+ font = @title_font.dup
761
+ font.size = 80
762
+ font.bold = false
763
+ text_renderer = Gruff::Renderer::Text.new(renderer, @no_data_message, font: font)
684
764
  text_renderer.render(@raw_columns, @raw_rows, 0, 0, Magick::CenterGravity)
685
765
  end
686
766
 
767
+ def draw_graph
768
+ raise 'Should implement this method at inherited class.'
769
+ end
770
+
687
771
  # Resets everything to defaults (except data).
688
772
  def reset_themes
689
773
  @theme_options = {}
@@ -746,15 +830,15 @@ module Gruff
746
830
  private
747
831
 
748
832
  def setup_marker_caps_height
749
- @hide_line_markers ? 0 : calculate_caps_height(@marker_font_size)
833
+ hide_bottom_label_area? ? 0 : calculate_caps_height(@marker_font)
750
834
  end
751
835
 
752
836
  def setup_title_caps_height
753
- hide_title? ? 0 : calculate_caps_height(@title_font_size) * @title.lines.to_a.size
837
+ hide_title? ? 0 : calculate_caps_height(@title_font) * @title.lines.to_a.size
754
838
  end
755
839
 
756
840
  def setup_legend_caps_height
757
- @hide_legend ? 0 : calculate_caps_height(@legend_font_size)
841
+ @hide_legend ? 0 : calculate_caps_height(@legend_font)
758
842
  end
759
843
 
760
844
  def graph_right_margin
@@ -765,19 +849,22 @@ module Gruff
765
849
  # Make space for half the width of the rightmost column label.
766
850
  # Might be greater than the number of columns if between-style bar markers are used.
767
851
  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
852
+ (last_label >= (column_count - 1) && @center_labels_over_point) ? calculate_width(@marker_font, @labels[last_label]) / 2.0 : 0
769
853
  end
770
854
 
771
855
  def setup_left_margin
772
- return @left_margin if @hide_line_markers
856
+ return @left_margin if hide_left_label_area?
773
857
 
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))
858
+ text = begin
859
+ if @has_left_labels
860
+ @labels.values.reduce('') { |value, memo| (value.to_s.length > memo.to_s.length) ? value : memo }
861
+ else
862
+ y_axis_label(maximum_value.to_f, @increment)
863
+ end
780
864
  end
865
+ longest_left_label_width = calculate_width(@marker_font, truncate_label_text(text))
866
+ longest_left_label_width *= 1.25 if @has_left_labels
867
+
781
868
  # Shift graph if left line numbers are hidden
782
869
  line_number_width = @hide_line_numbers && !@has_left_labels ? 0.0 : (longest_left_label_width + LABEL_MARGIN * 2)
783
870
 
@@ -785,17 +872,16 @@ module Gruff
785
872
  end
786
873
 
787
874
  def setup_top_margin
788
- return @top_margin if @legend_at_bottom
789
-
790
875
  # When @hide title, leave a title_margin space for aesthetics.
791
876
  # Same with @hide_legend
792
877
  @top_margin +
793
878
  (hide_title? ? @title_margin : @title_caps_height + @title_margin) +
794
- (@hide_legend ? @legend_margin : @legend_caps_height + @legend_margin)
879
+ ((@hide_legend || @legend_at_bottom) ? @legend_margin : calculate_legend_height + @legend_margin)
795
880
  end
796
881
 
797
882
  def setup_bottom_margin
798
- graph_bottom_margin = @hide_line_markers ? @bottom_margin : @bottom_margin + @marker_caps_height + LABEL_MARGIN
883
+ graph_bottom_margin = hide_bottom_label_area? ? @bottom_margin : @bottom_margin + @marker_caps_height + LABEL_MARGIN
884
+ graph_bottom_margin += (calculate_legend_height + @legend_margin) if @legend_at_bottom
799
885
 
800
886
  x_axis_label_height = @x_axis_label.nil? ? 0.0 : @marker_caps_height + LABEL_MARGIN
801
887
  # FIXME: Consider chart types other than bar
@@ -803,6 +889,7 @@ module Gruff
803
889
  end
804
890
 
805
891
  def truncate_label_text(text)
892
+ text = text.to_s
806
893
  return text if text.size <= @label_max_size
807
894
 
808
895
  if @label_truncation_style == :trailing_dots
@@ -817,44 +904,62 @@ module Gruff
817
904
  # Return a formatted string representing a number value that should be
818
905
  # printed as a label.
819
906
  def label(value, increment)
820
- label = if increment
821
- if increment >= 10 || (increment * 1) == (increment * 1).to_i.to_f
822
- sprintf('%0i', value)
823
- elsif increment >= 1.0 || (increment * 10) == (increment * 10).to_i.to_f
824
- sprintf('%0.1f', value)
825
- elsif increment >= 0.1 || (increment * 100) == (increment * 100).to_i.to_f
826
- sprintf('%0.2f', value)
827
- elsif increment >= 0.01 || (increment * 1000) == (increment * 1000).to_i.to_f
828
- sprintf('%0.3f', value)
829
- elsif increment >= 0.001 || (increment * 10000) == (increment * 10000).to_i.to_f
830
- sprintf('%0.4f', value)
831
- else
832
- value.to_s
833
- end
834
- elsif (@spread.to_f % (@marker_count.to_f == 0 ? 1 : @marker_count.to_f) == 0) || !@y_axis_increment.nil?
835
- value.to_i.to_s
836
- elsif @spread > 10.0
837
- sprintf('%0i', value)
838
- elsif @spread >= 3.0
839
- sprintf('%0.2f', value)
840
- else
841
- value.to_s
842
- end
907
+ label = begin
908
+ if increment
909
+ if increment >= 10 || (increment * 1) == (increment * 1).to_i.to_f
910
+ sprintf('%0i', value)
911
+ elsif increment >= 1.0 || (increment * 10) == (increment * 10).to_i.to_f
912
+ sprintf('%0.1f', value)
913
+ elsif increment >= 0.1 || (increment * 100) == (increment * 100).to_i.to_f
914
+ sprintf('%0.2f', value)
915
+ elsif increment >= 0.01 || (increment * 1000) == (increment * 1000).to_i.to_f
916
+ sprintf('%0.3f', value)
917
+ elsif increment >= 0.001 || (increment * 10000) == (increment * 10000).to_i.to_f
918
+ sprintf('%0.4f', value)
919
+ else
920
+ value.to_s
921
+ end
922
+ elsif (@spread.to_f % (marker_count.to_f == 0 ? 1 : marker_count.to_f) == 0) || !@y_axis_increment.nil?
923
+ value.to_i.to_s
924
+ elsif @spread > 10.0
925
+ sprintf('%0i', value)
926
+ elsif @spread >= 3.0
927
+ sprintf('%0.2f', value)
928
+ else
929
+ value.to_s
930
+ end
931
+ end
843
932
 
844
933
  parts = label.split('.')
845
934
  parts[0] = parts[0].commify
846
935
  parts.join('.')
847
936
  end
848
937
 
938
+ def x_axis_label(value, increment)
939
+ if @x_axis_label_format
940
+ @x_axis_label_format.call(value)
941
+ else
942
+ label(value, increment)
943
+ end
944
+ end
945
+
946
+ def y_axis_label(value, increment)
947
+ if @y_axis_label_format
948
+ @y_axis_label_format.call(value)
949
+ else
950
+ label(value, increment)
951
+ end
952
+ end
953
+
849
954
  def calculate_legend_label_widths_for_each_line(legend_labels, legend_square_width)
850
955
  # May fix legend drawing problem at small sizes
851
956
  label_widths = [[]] # Used to calculate line wrap
852
957
  legend_labels.each do |label|
853
- width = calculate_width(@legend_font_size, label)
958
+ width = calculate_width(@legend_font, label)
854
959
  label_width = width + legend_square_width * 2.7
855
960
  label_widths.last.push label_width
856
961
 
857
- if sum(label_widths.last) > (@raw_columns * 0.9)
962
+ if label_widths.last.sum > (@raw_columns * 0.9)
858
963
  label_widths.push [label_widths.last.pop]
859
964
  end
860
965
  end
@@ -862,25 +967,50 @@ module Gruff
862
967
  label_widths
863
968
  end
864
969
 
970
+ def calculate_legend_height
971
+ return 0.0 if @hide_legend
972
+
973
+ legend_labels = store.data.map(&:label)
974
+ legend_square_width = @legend_box_size
975
+ label_widths = calculate_legend_label_widths_for_each_line(legend_labels, legend_square_width)
976
+ legend_height = 0.0
977
+
978
+ legend_labels.each_with_index do |legend_label, _index|
979
+ next if legend_label.empty?
980
+
981
+ label_widths.first.shift
982
+ if label_widths.first.empty?
983
+ label_widths.shift
984
+ line_height = [@legend_caps_height, legend_square_width].max + @legend_margin
985
+ unless label_widths.empty?
986
+ # Wrap to next line and shrink available graph dimensions
987
+ legend_height += line_height
988
+ end
989
+ end
990
+ end
991
+
992
+ legend_height + @legend_caps_height
993
+ end
994
+
865
995
  # Returns the height of the capital letter 'X' for the current font and
866
996
  # size.
867
997
  #
868
998
  # Not scaled since it deals with dimensions that the regular scaling will
869
999
  # handle.
870
- def calculate_caps_height(font_size)
871
- metrics = Renderer::Text.metrics('X', font_size)
1000
+ def calculate_caps_height(font)
1001
+ metrics = Gruff::Renderer::Text.new(renderer, 'X', font: font).metrics
872
1002
  metrics.height
873
1003
  end
874
1004
 
875
- # Returns the width of a string at this pointsize.
1005
+ # Returns the width of a string at this point size.
876
1006
  #
877
1007
  # Not scaled since it deals with dimensions that the regular
878
1008
  # scaling will handle.
879
- def calculate_width(font_size, text)
1009
+ def calculate_width(font, text)
880
1010
  text = text.to_s
881
1011
  return 0 if text.empty?
882
1012
 
883
- metrics = Renderer::Text.metrics(text, font_size)
1013
+ metrics = Gruff::Renderer::Text.new(renderer, text, font: font).metrics
884
1014
  metrics.width
885
1015
  end
886
1016
 
@@ -889,19 +1019,10 @@ module Gruff
889
1019
  # Try to use a number of horizontal lines that will come out even.
890
1020
  #
891
1021
  # 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
1022
+ @increment = (@spread > 0 && marker_count > 0) ? significant(@spread / marker_count) : 1
902
1023
  else
903
1024
  # TODO: Make this work for negative values
904
- @marker_count = (@spread / @y_axis_increment).to_i
1025
+ self.marker_count = (@spread / @y_axis_increment).to_i
905
1026
  @increment = @y_axis_increment
906
1027
  end
907
1028
  end