gruff 0.8.0 → 0.9.0

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 (88) hide show
  1. checksums.yaml +4 -4
  2. data/.github/ISSUE_TEMPLATE.md +18 -0
  3. data/.gitignore +3 -0
  4. data/.rubocop.yml +93 -0
  5. data/.rubocop_todo.yml +23 -810
  6. data/.travis.yml +4 -4
  7. data/.yardopts +1 -0
  8. data/CHANGELOG.md +22 -0
  9. data/Gemfile +3 -1
  10. data/README.md +44 -21
  11. data/Rakefile +2 -206
  12. data/docker/Dockerfile +14 -0
  13. data/docker/build.sh +4 -0
  14. data/docker/launch.sh +4 -0
  15. data/gruff.gemspec +11 -8
  16. data/init.rb +2 -0
  17. data/lib/gruff.rb +23 -0
  18. data/lib/gruff/accumulator_bar.rb +6 -6
  19. data/lib/gruff/area.rb +13 -17
  20. data/lib/gruff/bar.rb +58 -41
  21. data/lib/gruff/base.rb +243 -566
  22. data/lib/gruff/bezier.rb +12 -14
  23. data/lib/gruff/bullet.rb +39 -57
  24. data/lib/gruff/dot.rb +25 -59
  25. data/lib/gruff/{bar_conversion.rb → helper/bar_conversion.rb} +13 -12
  26. data/lib/gruff/helper/bar_value_label_mixin.rb +30 -0
  27. data/lib/gruff/{stacked_mixin.rb → helper/stacked_mixin.rb} +7 -6
  28. data/lib/gruff/line.rb +95 -177
  29. data/lib/gruff/mini/bar.rb +6 -7
  30. data/lib/gruff/mini/legend.rb +16 -32
  31. data/lib/gruff/mini/pie.rb +6 -7
  32. data/lib/gruff/mini/side_bar.rb +4 -5
  33. data/lib/gruff/net.rb +37 -65
  34. data/lib/gruff/patch/rmagick.rb +33 -0
  35. data/lib/gruff/patch/string.rb +8 -0
  36. data/lib/gruff/photo_bar.rb +19 -19
  37. data/lib/gruff/pie.rb +22 -73
  38. data/lib/gruff/renderer/bezier.rb +21 -0
  39. data/lib/gruff/renderer/circle.rb +21 -0
  40. data/lib/gruff/renderer/dash_line.rb +22 -0
  41. data/lib/gruff/renderer/dot.rb +39 -0
  42. data/lib/gruff/renderer/ellipse.rb +21 -0
  43. data/lib/gruff/renderer/line.rb +34 -0
  44. data/lib/gruff/renderer/polygon.rb +23 -0
  45. data/lib/gruff/renderer/polyline.rb +21 -0
  46. data/lib/gruff/renderer/rectangle.rb +19 -0
  47. data/lib/gruff/renderer/renderer.rb +127 -0
  48. data/lib/gruff/renderer/text.rb +42 -0
  49. data/lib/gruff/scatter.rb +85 -156
  50. data/lib/gruff/scene.rb +22 -30
  51. data/lib/gruff/side_bar.rb +62 -58
  52. data/lib/gruff/side_stacked_bar.rb +47 -43
  53. data/lib/gruff/spider.rb +19 -36
  54. data/lib/gruff/stacked_area.rb +17 -21
  55. data/lib/gruff/stacked_bar.rb +50 -24
  56. data/lib/gruff/store/base_data.rb +34 -0
  57. data/lib/gruff/store/custom_data.rb +34 -0
  58. data/lib/gruff/store/store.rb +80 -0
  59. data/lib/gruff/store/xy_data.rb +55 -0
  60. data/lib/gruff/themes.rb +3 -3
  61. data/lib/gruff/version.rb +3 -1
  62. metadata +41 -30
  63. data/Manifest.txt +0 -81
  64. data/assets/bubble.png +0 -0
  65. data/assets/city_scene/background/0000.png +0 -0
  66. data/assets/city_scene/background/0600.png +0 -0
  67. data/assets/city_scene/background/2000.png +0 -0
  68. data/assets/city_scene/clouds/cloudy.png +0 -0
  69. data/assets/city_scene/clouds/partly_cloudy.png +0 -0
  70. data/assets/city_scene/clouds/stormy.png +0 -0
  71. data/assets/city_scene/grass/default.png +0 -0
  72. data/assets/city_scene/haze/true.png +0 -0
  73. data/assets/city_scene/number_sample/1.png +0 -0
  74. data/assets/city_scene/number_sample/2.png +0 -0
  75. data/assets/city_scene/number_sample/default.png +0 -0
  76. data/assets/city_scene/sky/0000.png +0 -0
  77. data/assets/city_scene/sky/0200.png +0 -0
  78. data/assets/city_scene/sky/0400.png +0 -0
  79. data/assets/city_scene/sky/0600.png +0 -0
  80. data/assets/city_scene/sky/0800.png +0 -0
  81. data/assets/city_scene/sky/1000.png +0 -0
  82. data/assets/city_scene/sky/1200.png +0 -0
  83. data/assets/city_scene/sky/1400.png +0 -0
  84. data/assets/city_scene/sky/1500.png +0 -0
  85. data/assets/city_scene/sky/1700.png +0 -0
  86. data/assets/city_scene/sky/2000.png +0 -0
  87. data/assets/pc306715.jpg +0 -0
  88. data/lib/gruff/deprecated.rb +0 -38
@@ -1,20 +1,21 @@
1
- require File.dirname(__FILE__) + '/base'
1
+ # frozen_string_literal: true
2
+
3
+ require 'gruff/base'
2
4
 
3
5
  class Gruff::Bezier < Gruff::Base
4
6
  def draw
5
7
  super
6
8
 
7
- return unless @has_data
9
+ return unless data_given?
8
10
 
9
- @x_increment = @graph_width / (@column_count - 1).to_f
11
+ x_increment = @graph_width / (column_count - 1).to_f
10
12
 
11
- @norm_data.each do |data_row|
12
- poly_points = Array.new
13
- @d = @d.fill data_row[DATA_COLOR_INDEX]
13
+ store.norm_data.each do |data_row|
14
+ poly_points = []
14
15
 
15
16
  data_row[1].each_with_index do |data_point, index|
16
17
  # Use incremented x and scaled y
17
- new_x = @graph_left + (@x_increment * index)
18
+ new_x = @graph_left + (x_increment * index)
18
19
  new_y = @graph_top + (@graph_height - data_point * @graph_height)
19
20
 
20
21
  if index == 0 && RUBY_PLATFORM != 'java'
@@ -28,18 +29,15 @@ class Gruff::Bezier < Gruff::Base
28
29
  draw_label(new_x, index)
29
30
  end
30
31
 
31
- @d = @d.fill_opacity 0.0
32
- @d = @d.stroke data_row[DATA_COLOR_INDEX]
33
- @d = @d.stroke_width clip_value_if_greater_than(@columns / (@norm_data.first[1].size * 4), 5.0)
32
+ stroke_width = clip_value_if_greater_than(@columns / (store.norm_data.first[1].size * 4), 5.0)
34
33
 
35
34
  if RUBY_PLATFORM == 'java'
36
- @d = @d.polyline(*poly_points)
35
+ Gruff::Renderer::Polyline.new(color: data_row.color, width: stroke_width).render(poly_points)
37
36
  else
38
- @d = @d.bezier(*poly_points)
37
+ Gruff::Renderer::Bezier.new(color: data_row.color, width: stroke_width).render(poly_points)
39
38
  end
40
39
  end
41
40
 
42
- @d.draw(@base_image)
41
+ Gruff::Renderer.finish
43
42
  end
44
-
45
43
  end
@@ -1,11 +1,12 @@
1
- require File.dirname(__FILE__) + '/base'
1
+ # frozen_string_literal: true
2
+
3
+ require 'gruff/base'
2
4
  require 'gruff/themes'
3
5
 
4
6
  # http://en.wikipedia.org/wiki/Bullet_graph
5
7
  class Gruff::Bullet < Gruff::Base
6
-
7
- def initialize(target_width = "400x40")
8
- if not Numeric === target_width
8
+ def initialize(target_width = '400x40')
9
+ if target_width.is_a?(String)
9
10
  geometric_width, geometric_height = target_width.split('x')
10
11
  @columns = geometric_width.to_f
11
12
  @rows = geometric_height.to_f
@@ -13,6 +14,8 @@ class Gruff::Bullet < Gruff::Base
13
14
  @columns = target_width.to_f
14
15
  @rows = target_width.to_f / 5.0
15
16
  end
17
+ @columns.freeze
18
+ @rows.freeze
16
19
 
17
20
  initialize_ivars
18
21
 
@@ -23,30 +26,13 @@ class Gruff::Bullet < Gruff::Base
23
26
 
24
27
  def data(value, maximum_value, options = {})
25
28
  @value = value.to_f
26
- @maximum_value = maximum_value.to_f
29
+ self.maximum_value = maximum_value.to_f
27
30
  @options = options
28
- @options.map { |k, v| @options[k] = v.to_f if v === Numeric }
31
+ @options.map { |k, v| @options[k] = v.to_f if v.is_a?(Numeric) }
29
32
  end
30
33
 
31
- # def setup_drawing
32
- # # Maybe should be done in one of the following functions for more granularity.
33
- # unless @has_data
34
- # draw_no_data()
35
- # return
36
- # end
37
- #
38
- # normalize()
39
- # setup_graph_measurements()
40
- # sort_norm_data() if @sort # Sort norm_data with avg largest values set first (for display)
41
- #
42
- # draw_legend()
43
- # draw_line_markers()
44
- # draw_axis_labels()
45
- # draw_title
46
- # end
47
-
48
34
  def draw
49
- # TODO Left label
35
+ # TODO: Left label
50
36
  # TODO Bottom labels and markers
51
37
  # @graph_bottom
52
38
  # Calculations are off 800x???
@@ -55,54 +41,50 @@ class Gruff::Bullet < Gruff::Base
55
41
 
56
42
  draw_title
57
43
 
58
- @margin = 30.0
59
- @thickness = @raw_rows / 6.0
60
- @right_margin = @margin
61
- @graph_left = (@title && (@title_width * 1.3)) || @margin
62
- @graph_width = @raw_columns - @graph_left - @right_margin
63
- @graph_height = @thickness * 3.0
44
+ title_width = calculate_width(@title_font_size, @title)
45
+ margin = 30.0
46
+ thickness = @raw_rows / 6.0
47
+ right_margin = margin
48
+ graph_left = (@title && (title_width * 1.3)) || margin
49
+ graph_width = @raw_columns - graph_left - right_margin
50
+ graph_height = thickness * 3.0
64
51
 
65
52
  # Background
66
- @d = @d.fill @colors[0]
67
- @d = @d.rectangle(@graph_left, 0, @graph_left + @graph_width, @graph_height)
53
+ rect_renderer = Gruff::Renderer::Rectangle.new(color: @colors[0])
54
+ rect_renderer.render(graph_left, 0, graph_left + graph_width, graph_height)
68
55
 
69
56
  [:high, :low].each_with_index do |indicator, index|
70
- next unless @options.has_key?(indicator)
57
+ next unless @options.key?(indicator)
58
+
59
+ indicator_width_x = graph_left + graph_width * (@options[indicator] / maximum_value)
71
60
 
72
- @d = @d.fill @colors[index + 1]
73
- indicator_width_x = @graph_left + @graph_width * (@options[indicator] / @maximum_value)
74
- @d = @d.rectangle(@graph_left, 0, indicator_width_x, @graph_height)
61
+ rect_renderer = Gruff::Renderer::Rectangle.new(color: @colors[index + 1])
62
+ rect_renderer.render(graph_left, 0, indicator_width_x, graph_height)
75
63
  end
76
64
 
77
- if @options.has_key?(:target)
78
- @d = @d.fill @font_color
79
- target_x = @graph_left + @graph_width * (@options[:target] / @maximum_value)
80
- half_thickness = @thickness / 2.0
81
- @d = @d.rectangle(target_x, half_thickness, target_x + half_thickness, @thickness * 2 + half_thickness)
65
+ if @options.key?(:target)
66
+ target_x = graph_left + graph_width * (@options[:target] / maximum_value)
67
+ half_thickness = thickness / 2.0
68
+
69
+ rect_renderer = Gruff::Renderer::Rectangle.new(color: @font_color)
70
+ rect_renderer.render(target_x, half_thickness, target_x + half_thickness, thickness * 2 + half_thickness)
82
71
  end
83
72
 
84
73
  # Value
85
- @d = @d.fill @font_color
86
- @d = @d.rectangle(@graph_left, @thickness, @graph_left + @graph_width * (@value / @maximum_value), @thickness * 2)
74
+ rect_renderer = Gruff::Renderer::Rectangle.new(color: @font_color)
75
+ rect_renderer.render(graph_left, thickness, graph_left + graph_width * (@value / maximum_value), thickness * 2)
87
76
 
88
- @d.draw(@base_image)
77
+ Gruff::Renderer.finish
89
78
  end
90
79
 
80
+ private
81
+
91
82
  def draw_title
92
83
  return unless @title
93
84
 
94
- @font_height = calculate_caps_height(scale_fontsize(@title_font_size))
95
- @title_width = calculate_width(@title_font_size, @title)
96
-
97
- @d.fill = @font_color
98
- @d.font = @font if @font
99
- @d.stroke('transparent')
100
- @d.font_weight = NormalWeight
101
- @d.pointsize = scale_fontsize(@title_font_size)
102
- @d.gravity = NorthWestGravity
103
- @d = @d.annotate_scaled(
104
- @base_image, 1.0, 1.0, @font_height / 2, @font_height / 2, @title, @scale
105
- )
106
- end
85
+ font_height = calculate_caps_height(scale_fontsize(@title_font_size))
107
86
 
87
+ text_renderer = Gruff::Renderer::Text.new(@title, font: @font, size: @title_font_size, color: @font_color)
88
+ text_renderer.render(1.0, 1.0, font_height / 2, font_height / 2, Magick::NorthWestGravity)
89
+ end
108
90
  end
@@ -1,50 +1,40 @@
1
- require File.dirname(__FILE__) + '/base'
1
+ # frozen_string_literal: true
2
+
3
+ require 'gruff/base'
2
4
 
3
- ##
4
5
  # Graph with dots and labels along a vertical access
5
6
  # see: 'Creating More Effective Graphs' by Robbins
6
-
7
7
  class Gruff::Dot < Gruff::Base
8
-
9
8
  def draw
10
9
  @has_left_labels = true
11
10
  super
12
11
 
13
- return unless @has_data
12
+ return unless data_given?
14
13
 
15
14
  # Setup spacing.
16
15
  #
17
16
  spacing_factor = 1.0
18
17
 
19
- @items_width = @graph_height / @column_count.to_f
20
- @item_width = @items_width * spacing_factor / @norm_data.size
21
- @d = @d.stroke_opacity 0.0
22
- padding = (@items_width * (1 - spacing_factor)) / 2
18
+ items_width = @graph_height / column_count.to_f
19
+ item_width = items_width * spacing_factor / store.length
20
+ padding = (items_width * (1 - spacing_factor)) / 2
23
21
 
24
- @norm_data.each_with_index do |data_row, row_index|
25
- data_row[DATA_VALUES_INDEX].each_with_index do |data_point, point_index|
22
+ store.norm_data.each_with_index do |data_row, row_index|
23
+ data_row.points.each_with_index do |data_point, point_index|
26
24
  x_pos = @graph_left + (data_point * @graph_width)
27
- y_pos = @graph_top + (@items_width * point_index) + padding + (@items_width.to_f / 2.0).round
25
+ y_pos = @graph_top + (items_width * point_index) + padding + (items_width.to_f / 2.0).round
28
26
 
29
27
  if row_index == 0
30
- @d = @d.stroke(@marker_color)
31
- @d = @d.fill(@marker_color)
32
- @d = @d.stroke_width 1.0
33
- @d = @d.stroke_opacity 0.1
34
- @d = @d.fill_opacity 0.1
35
- @d = @d.line(@graph_left, y_pos, @graph_left + @graph_width, y_pos)
36
- @d = @d.fill_opacity 1
28
+ Gruff::Renderer::Line.new(color: @marker_color).render(@graph_left, y_pos, @graph_left + @graph_width, y_pos)
37
29
  end
38
30
 
39
- @d = @d.fill data_row[DATA_COLOR_INDEX]
40
- @d = @d.stroke('transparent')
41
- @d = @d.circle(x_pos, y_pos, x_pos + (@item_width.to_f / 3.0).round, y_pos)
31
+ Gruff::Renderer::Circle.new(color: data_row.color).render(x_pos, y_pos, x_pos + (item_width.to_f / 3.0).round, y_pos)
42
32
 
43
33
  draw_label(y_pos, point_index)
44
34
  end
45
35
  end
46
36
 
47
- @d.draw(@base_image)
37
+ Gruff::Renderer.finish
48
38
  end
49
39
 
50
40
  protected
@@ -53,11 +43,7 @@ protected
53
43
  def draw_line_markers
54
44
  return if @hide_line_markers
55
45
 
56
- @d = @d.stroke_antialias false
57
-
58
46
  # Draw horizontal line markers and annotate with numbers
59
- @d = @d.stroke(@marker_color)
60
- @d = @d.stroke_width 1
61
47
  if @y_axis_increment
62
48
  increment = @y_axis_increment
63
49
  number_of_lines = (@spread / @y_axis_increment).to_i
@@ -74,31 +60,21 @@ protected
74
60
  end
75
61
  @marker_count ||= 5
76
62
  end
77
- # TODO Round maximum marker value to a round number like 100, 0.1, 0.5, etc.
78
- @increment = (@spread > 0 && @marker_count > 0) ? significant(@spread / @marker_count) : 1
79
-
63
+ # TODO: Round maximum marker value to a round number like 100, 0.1, 0.5, etc.
64
+ increment = (@spread > 0 && @marker_count > 0) ? significant(@spread / @marker_count) : 1
80
65
  number_of_lines = @marker_count
81
- increment = @increment
82
66
  end
83
67
 
84
68
  (0..number_of_lines).each do |index|
85
- marker_label = @minimum_value + index * increment
86
- x = @graph_left + (marker_label - @minimum_value) * @graph_width / @spread
87
- @d = @d.line(x, @graph_bottom, x, @graph_bottom + 0.5 * LABEL_MARGIN)
69
+ marker_label = minimum_value + index * increment
70
+ x = @graph_left + (marker_label - minimum_value) * @graph_width / @spread
71
+ Gruff::Renderer::Line.new(color: @marker_color).render(x, @graph_bottom, x, @graph_bottom + 0.5 * LABEL_MARGIN)
88
72
 
89
73
  unless @hide_line_numbers
90
- @d.fill = @font_color
91
- @d.font = @font if @font
92
- @d.stroke = 'transparent'
93
- @d.pointsize = scale_fontsize(@marker_font_size)
94
- @d.gravity = CenterGravity
95
- # TODO Center text over line
96
- @d = @d.annotate_scaled(@base_image,
97
- 0, 0, # Width of box to draw text in
98
- x, @graph_bottom + (LABEL_MARGIN * 2.0), # Coordinates of text
99
- label(marker_label, increment), @scale)
100
- end # unless
101
- @d = @d.stroke_antialias true
74
+ label = label(marker_label, increment)
75
+ text_renderer = Gruff::Renderer::Text.new(label, font: @font, size: @marker_font_size, color: @font_color)
76
+ text_renderer.render(0, 0, x, @graph_bottom + (LABEL_MARGIN * 2.0), Magick::CenterGravity)
77
+ end
102
78
  end
103
79
  end
104
80
 
@@ -106,19 +82,9 @@ protected
106
82
  # Draw on the Y axis instead of the X
107
83
 
108
84
  def draw_label(y_offset, index)
109
- if !@labels[index].nil? && @labels_seen[index].nil?
110
- @d.fill = @font_color
111
- @d.font = @font if @font
112
- @d.stroke = 'transparent'
113
- @d.font_weight = NormalWeight
114
- @d.pointsize = scale_fontsize(@marker_font_size)
115
- @d.gravity = EastGravity
116
- @d = @d.annotate_scaled(@base_image,
117
- 1, 1,
118
- -@graph_left + LABEL_MARGIN * 2.0, y_offset,
119
- @labels[index], @scale)
120
- @labels_seen[index] = 1
85
+ draw_unique_label(index) do
86
+ text_renderer = Gruff::Renderer::Text.new(@labels[index], font: @font, size: @marker_font_size, color: @font_color)
87
+ text_renderer.render(1, 1, -@graph_left + LABEL_MARGIN * 2.0, y_offset, Magick::EastGravity)
121
88
  end
122
89
  end
123
-
124
90
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  ##
2
4
  # Original Author: David Stokar
3
5
  #
@@ -9,6 +11,7 @@
9
11
  # 2. Bars all go from zero to negative direction
10
12
  # 3. Bars either go from zero to positive or from zero to negative
11
13
  #
14
+ # @private
12
15
  class Gruff::BarConversion
13
16
  attr_writer :mode
14
17
  attr_writer :zero
@@ -17,30 +20,28 @@ class Gruff::BarConversion
17
20
  attr_writer :minimum_value
18
21
  attr_writer :spread
19
22
 
20
- def get_left_y_right_y_scaled(data_point, result)
23
+ def get_left_y_right_y_scaled(data_point)
24
+ result = []
25
+
21
26
  case @mode
22
27
  when 1 then # Case one
23
- # minimum value >= 0 ( only positiv values )
28
+ # minimum value >= 0 ( only positive values )
24
29
  result[0] = @graph_top + @graph_height * (1 - data_point) + 1
25
30
  result[1] = @graph_top + @graph_height - 1
26
31
  when 2 then # Case two
27
- # only negativ values
32
+ # only negative values
28
33
  result[0] = @graph_top + 1
29
34
  result[1] = @graph_top + @graph_height * (1 - data_point) - 1
30
35
  when 3 then # Case three
31
- # positiv and negativ values
36
+ # positive and negative values
32
37
  val = data_point - @minimum_value / @spread
33
- if data_point >= @zero
34
- result[0] = @graph_top + @graph_height * (1 - (val - @zero)) + 1
35
- result[1] = @graph_top + @graph_height * (1 - @zero) - 1
36
- else
37
- result[0] = @graph_top + @graph_height * (1 - (val - @zero)) + 1
38
- result[1] = @graph_top + @graph_height * (1 - @zero) - 1
39
- end
38
+ result[0] = @graph_top + @graph_height * (1 - (val - @zero)) + 1
39
+ result[1] = @graph_top + @graph_height * (1 - @zero) - 1
40
40
  else
41
41
  result[0] = 0.0
42
42
  result[1] = 0.0
43
43
  end
44
- end
45
44
 
45
+ result
46
+ end
46
47
  end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @private
4
+ module Gruff::Base::BarValueLabelMixin
5
+ class BarValueLabel
6
+ attr_accessor :coordinates, :values
7
+
8
+ def initialize(size, bar_width)
9
+ @coordinates = Array.new(size)
10
+ @values = Hash.new(0)
11
+ @bar_width = bar_width
12
+ end
13
+
14
+ def prepare_rendering(format)
15
+ @coordinates.each_with_index do |(left_x, left_y, right_x, _right_y), index|
16
+ value = @values[index]
17
+ val = (format || '%.2f') % value
18
+ y = value >= 0 ? left_y - 30 : left_y + 12
19
+ yield left_x + (right_x - left_x) / 2, y, val.commify
20
+ end
21
+ end
22
+
23
+ def prepare_sidebar_rendering(format)
24
+ @coordinates.each_with_index do |(_left_x, _left_y, right_x, right_y), index|
25
+ val = (format || '%.2f') % @values[index]
26
+ yield right_x + 40, right_y - @bar_width / 2, val.commify
27
+ end
28
+ end
29
+ end
30
+ end
@@ -1,22 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @private
1
4
  module Gruff::Base::StackedMixin
2
5
  # Used by StackedBar and child classes.
3
6
  #
4
7
  # tsal: moved from Base 03 FEB 2007
5
- DATA_VALUES_INDEX = Gruff::Base::DATA_VALUES_INDEX
6
8
  def get_maximum_by_stack
7
9
  # Get sum of each stack
8
10
  max_hash = {}
9
- @data.each do |data_set|
10
- data_set[DATA_VALUES_INDEX].each_with_index do |data_point, i|
11
+ store.data.each do |data_set|
12
+ data_set.points.each_with_index do |data_point, i|
11
13
  max_hash[i] = 0.0 unless max_hash[i]
12
14
  max_hash[i] += data_point.to_f
13
15
  end
14
16
  end
15
17
 
16
- # @maximum_value = 0
17
18
  max_hash.each_key do |key|
18
- @maximum_value = max_hash[key] if max_hash[key] > @maximum_value
19
+ self.maximum_value = max_hash[key] if max_hash[key] > maximum_value
19
20
  end
20
- @minimum_value = 0
21
+ self.minimum_value = 0
21
22
  end
22
23
  end
@@ -1,6 +1,7 @@
1
- require File.dirname(__FILE__) + '/base'
1
+ # frozen_string_literal: true
2
+
3
+ require 'gruff/base'
2
4
 
3
- ##
4
5
  # Here's how to make a Line graph:
5
6
  #
6
7
  # g = Gruff::Line.new
@@ -9,79 +10,74 @@ require File.dirname(__FILE__) + '/base'
9
10
  # g.data 'Hamburgers', [50, 19, 99, 29]
10
11
  # g.write("test/output/line.png")
11
12
  #
12
- # There are also other options described below, such as #baseline_value, #baseline_color, #hide_dots, and #hide_lines.
13
-
13
+ # There are also other options described below, such as {#baseline_value}, {#baseline_color},
14
+ # {#hide_dots}, and {#hide_lines}.
14
15
  class Gruff::Line < Gruff::Base
15
-
16
- # Allow for reference lines ( which are like baseline ... just allowing for more & on both axes )
16
+ # Allow for reference lines ( which are like baseline ... just allowing for more & on both axes ).
17
17
  attr_accessor :reference_lines
18
18
  attr_accessor :reference_line_default_color
19
19
  attr_accessor :reference_line_default_width
20
20
 
21
- # Allow for vertical marker lines
21
+ # Allow for vertical marker lines.
22
22
  attr_accessor :show_vertical_markers
23
23
 
24
- # Dimensions of lines and dots; calculated based on dataset size if left unspecified
24
+ # Dimensions of lines and dots; calculated based on dataset size if left unspecified.
25
25
  attr_accessor :line_width
26
26
  attr_accessor :dot_radius
27
27
 
28
- # default is a circle, other options include square
28
+ # default is +'circle'+, other options include square.
29
29
  attr_accessor :dot_style
30
30
 
31
31
  # Hide parts of the graph to fit more datapoints, or for a different appearance.
32
32
  attr_accessor :hide_dots, :hide_lines
33
33
 
34
- #accessors for support of xy data
34
+ # accessors for support of xy data.
35
35
  attr_accessor :minimum_x_value
36
+
37
+ # accessors for support of xy data.
36
38
  attr_accessor :maximum_x_value
37
39
 
38
40
  # Get the value if somebody has defined it.
39
41
  def baseline_value
40
42
  if @reference_lines.key?(:baseline)
41
43
  @reference_lines[:baseline][:value]
42
- else
43
- nil
44
44
  end
45
45
  end
46
46
 
47
47
  # Set a value for a baseline reference line..
48
48
  def baseline_value=(new_value)
49
- @reference_lines[:baseline] ||= Hash.new
49
+ @reference_lines[:baseline] ||= {}
50
50
  @reference_lines[:baseline][:value] = new_value
51
51
  end
52
52
 
53
53
  def baseline_color
54
54
  if @reference_lines.key?(:baseline)
55
55
  @reference_lines[:baseline][:color]
56
- else
57
- nil
58
56
  end
59
57
  end
60
58
 
61
59
  def baseline_color=(new_value)
62
- @reference_lines[:baseline] ||= Hash.new
60
+ @reference_lines[:baseline] ||= {}
63
61
  @reference_lines[:baseline][:color] = new_value
64
62
  end
65
63
 
66
- # Call with target pixel width of graph (800, 400, 300), and/or 'false' to omit lines (points only).
64
+ # Call with target pixel width of graph (+800+, +400+, +300+), and/or +false+ to omit lines (points only).
67
65
  #
68
- # g = Gruff::Line.new(400) # 400px wide with lines
66
+ # g = Gruff::Line.new(400) # 400px wide with lines
67
+ # g = Gruff::Line.new(400, false) # 400px wide, no lines (for backwards compatibility)
68
+ # g = Gruff::Line.new(false) # Defaults to 800px wide, no lines (for backwards compatibility)
69
69
  #
70
- # g = Gruff::Line.new(400, false) # 400px wide, no lines (for backwards compatibility)
71
- #
72
- # g = Gruff::Line.new(false) # Defaults to 800px wide, no lines (for backwards compatibility)
73
- #
74
- # The preferred way is to call hide_dots or hide_lines instead.
70
+ # The preferred way is to call {#hide_dots} or {#hide_lines} instead.
75
71
  def initialize(*args)
76
72
  raise ArgumentError, 'Wrong number of arguments' if args.length > 2
77
73
 
78
- if args.empty? || ((not Numeric === args.first) && (not String === args.first))
74
+ if args.empty? || (!args.first.is_a?(Numeric) && !args.first.is_a?(String))
79
75
  super()
80
76
  else
81
77
  super args.shift
82
78
  end
83
79
 
84
- @reference_lines = Hash.new
80
+ @reference_lines = {}
85
81
  @reference_line_default_color = 'red'
86
82
  @reference_line_default_width = 5
87
83
 
@@ -92,83 +88,67 @@ class Gruff::Line < Gruff::Base
92
88
  @dot_style = 'circle'
93
89
 
94
90
  @show_vertical_markers = false
91
+
92
+ @store = Gruff::Store.new(Gruff::Store::XYData)
95
93
  end
96
94
 
97
95
  # This method allows one to plot a dataset with both X and Y data.
98
96
  #
99
- # Parameters are as follows:
100
- # name: string, the title of the dataset
101
- # x_data_points: an array containing the x data points for the graph
102
- # y_data_points: an array containing the y data points for the graph
103
- # color: hex number indicating the line color as an RGB triplet
97
+ # @overload dataxy(name, x_data_points = [], y_data_points = [], color = nil)
98
+ # @param name [String] the title of the dataset.
99
+ # @param x_data_points [Array] an array containing the x data points for the graph.
100
+ # @param y_data_points [Array] an array containing the y data points for the graph.
101
+ # @param color [String] hex number indicating the line color as an RGB triplet.
104
102
  #
105
- # or
103
+ # @overload dataxy(name, xy_data_points = [], color = nil)
104
+ # @param name [String] the title of the dataset.
105
+ # @param xy_data_points [Array] an array containing both x and y data points for the graph.
106
+ # @param color [String] hex number indicating the line color as an RGB triplet.
106
107
  #
107
- # name: string, the title of the dataset
108
- # xy_data_points: an array containing both x and y data points for the graph
109
- # color: hex number indicating the line color as an RGB triplet
110
- #
111
- # Notes:
112
- # -if (x_data_points.length != y_data_points.length) an error is
108
+ # @note
109
+ # - if (x_data_points.length != y_data_points.length) an error is
113
110
  # returned.
114
- # -if the color argument is nil, the next color from the default theme will
111
+ # - if the color argument is nil, the next color from the default theme will
115
112
  # be used.
116
- # -if you want to use a preset theme, you must set it before calling
117
- # dataxy().
113
+ # - if you want to use a preset theme, you must set it before calling {#dataxy}.
118
114
  #
119
- # Example:
115
+ # @example
120
116
  # g = Gruff::Line.new
121
117
  # g.title = "X/Y Dataset"
122
118
  # g.dataxy("Apples", [1,3,4,5,6,10], [1, 2, 3, 4, 4, 3])
123
119
  # g.dataxy("Bapples", [1,3,4,5,7,9], [1, 1, 2, 2, 3, 3])
124
120
  # g.dataxy("Capples", [[1,1],[2,3],[3,4],[4,5],[5,7],[6,9]])
125
- # #you can still use the old data method too if you want:
121
+ #
122
+ # # you can still use the old data method too if you want:
126
123
  # g.data("Capples", [1, 1, 2, 2, 3, 3])
127
- # #labels will be drawn at the x locations of the keys passed in.
124
+ #
125
+ # # labels will be drawn at the x locations of the keys passed in.
128
126
  # In this example the lables are drawn at x positions 2, 4, and 6:
129
127
  # g.labels = {0 => '2003', 2 => '2004', 4 => '2005', 6 => '2006'}
130
- # The 0 => '2003' label will be ignored since it is outside the chart range.
128
+ # # The 0 => '2003' label will be ignored since it is outside the chart range.
131
129
  def dataxy(name, x_data_points = [], y_data_points = [], color = nil)
132
- raise ArgumentError, 'x_data_points is nil!' if x_data_points.length == 0
130
+ # make sure it's an array
131
+ x_data_points = Array(x_data_points)
132
+ y_data_points = Array(y_data_points)
133
+
134
+ raise ArgumentError, 'x_data_points is nil!' if x_data_points.empty?
133
135
 
134
136
  if x_data_points.all? { |p| p.is_a?(Array) && p.size == 2 }
135
- y_data_points = x_data_points.map { |p| p[1] }
136
- x_data_points = x_data_points.map { |p| p[0] }
137
+ x_data_points, y_data_points = x_data_points.transpose
137
138
  end
138
139
 
139
140
  raise ArgumentError, 'x_data_points.length != y_data_points.length!' if x_data_points.length != y_data_points.length
140
141
 
141
- # call the existing data routine for the y data.
142
- self.data(name, y_data_points, color)
143
-
144
- x_data_points = Array(x_data_points) # make sure it's an array
145
- # append the x data to the last entry that was just added in the @data member
146
- @data.last[DATA_VALUES_X_INDEX] = x_data_points
147
-
148
- # Update the global min/max values for the x data
149
- x_data_points.each do |x_data_point|
150
- next if x_data_point.nil?
151
-
152
- # Setup max/min so spread starts at the low end of the data points
153
- if @maximum_x_value.nil? && @minimum_x_value.nil?
154
- @maximum_x_value = @minimum_x_value = x_data_point
155
- end
156
-
157
- @maximum_x_value = (x_data_point > @maximum_x_value) ?
158
- x_data_point : @maximum_x_value
159
- @minimum_x_value = (x_data_point < @minimum_x_value) ?
160
- x_data_point : @minimum_x_value
161
- end
142
+ # call the existing data routine for the x/y data.
143
+ store.add(name, y_data_points, color, x_data_points)
162
144
  end
163
145
 
164
146
  def draw_reference_line(reference_line, left, right, top, bottom)
165
- @d = @d.push
166
- @d.stroke_color(reference_line[:color] || @reference_line_default_color)
167
- @d.fill_opacity 0.0
168
- @d.stroke_dasharray(10, 20)
169
- @d.stroke_width(reference_line[:width] || @reference_line_default_width)
170
- @d.line(left, top, right, bottom)
171
- @d = @d.pop
147
+ config = {
148
+ color: reference_line[:color] || @reference_line_default_color,
149
+ width: reference_line[:width] || @reference_line_default_width
150
+ }
151
+ Gruff::Renderer::DashLine.new(config).render(left, top, right, bottom)
172
152
  end
173
153
 
174
154
  def draw_horizontal_reference_line(reference_line)
@@ -184,10 +164,10 @@ class Gruff::Line < Gruff::Base
184
164
  def draw
185
165
  super
186
166
 
187
- return unless @has_data
167
+ return unless data_given?
188
168
 
189
169
  # Check to see if more than one datapoint was given. NaN can result otherwise.
190
- @x_increment = (@column_count > 1) ? (@graph_width / (@column_count - 1).to_f) : @graph_width
170
+ @x_increment = (column_count > 1) ? (@graph_width / (column_count - 1).to_f) : @graph_width
191
171
 
192
172
  @reference_lines.each_value do |curr_reference_line|
193
173
  draw_horizontal_reference_line(curr_reference_line) if curr_reference_line.key?(:norm_value)
@@ -195,70 +175,51 @@ class Gruff::Line < Gruff::Base
195
175
  end
196
176
 
197
177
  if @show_vertical_markers
198
- (0..@column_count).each do |column|
178
+ (0..column_count).each do |column|
199
179
  x = @graph_left + @graph_width - column.to_f * @x_increment
200
180
 
201
- @d = @d.fill(@marker_color)
202
-
203
- # FIXME(uwe): Workaround for Issue #66
204
- # https://github.com/topfunky/gruff/issues/66
205
- # https://github.com/rmagick/rmagick/issues/82
206
- # Remove if the issue gets fixed.
207
- x += 0.001 unless defined?(JRUBY_VERSION)
208
- # EMXIF
209
-
210
- @d = @d.line(x, @graph_bottom, x, @graph_top)
181
+ Gruff::Renderer::Line.new(color: @marker_color).render(x, @graph_bottom, x, @graph_top)
211
182
  #If the user specified a marker shadow color, draw a shadow just below it
212
- unless @marker_shadow_color.nil?
213
- @d = @d.fill(@marker_shadow_color)
214
- @d = @d.line(x + 1, @graph_bottom, x + 1, @graph_top)
183
+ if @marker_shadow_color
184
+ Gruff::Renderer::Line.new(color: @marker_shadow_color).render(x + 1, @graph_bottom, x + 1, @graph_top)
215
185
  end
216
186
  end
217
187
  end
218
188
 
219
- @norm_data.each do |data_row|
189
+ store.norm_data.each do |data_row|
220
190
  prev_x = prev_y = nil
221
191
 
222
- @one_point = contains_one_point_only?(data_row)
192
+ one_point = contains_one_point_only?(data_row)
223
193
 
224
- data_row[DATA_VALUES_INDEX].each_with_index do |data_point, index|
225
- x_data = data_row[DATA_VALUES_X_INDEX]
226
- if x_data == nil
194
+ data_row.coordinates.each_with_index do |(x_data, y_data), index|
195
+ if x_data.nil?
227
196
  #use the old method: equally spaced points along the x-axis
228
197
  new_x = @graph_left + (@x_increment * index)
229
198
  draw_label(new_x, index)
230
199
  else
231
- new_x = get_x_coord(x_data[index], @graph_width, @graph_left)
200
+ new_x = get_x_coord(x_data, @graph_width, @graph_left)
232
201
  @labels.each do |label_pos, _|
233
202
  draw_label(@graph_left + ((label_pos - @minimum_x_value) * @graph_width) / (@maximum_x_value - @minimum_x_value), label_pos)
234
203
  end
235
204
  end
236
- unless data_point # we can't draw a line for a null data point, we can still label the axis though
205
+ unless y_data # we can't draw a line for a null data point, we can still label the axis though
237
206
  prev_x = prev_y = nil
238
207
  next
239
208
  end
240
209
 
241
- new_y = @graph_top + (@graph_height - data_point * @graph_height)
210
+ new_y = @graph_top + (@graph_height - y_data * @graph_height)
242
211
 
243
212
  # Reset each time to avoid thin-line errors
244
- @d = @d.stroke data_row[DATA_COLOR_INDEX]
245
- @d = @d.fill data_row[DATA_COLOR_INDEX]
246
- @d = @d.stroke_opacity 1.0
247
- @d = @d.stroke_width line_width ||
248
- clip_value_if_greater_than(@columns / (@norm_data.first[DATA_VALUES_INDEX].size * 4), 5.0)
249
-
250
- circle_radius = dot_radius ||
251
- clip_value_if_greater_than(@columns / (@norm_data.first[DATA_VALUES_INDEX].size * 2.5), 5.0)
252
-
253
- if !@hide_lines && !prev_x.nil? && !prev_y.nil?
254
- @d = @d.line(prev_x, prev_y, new_x, new_y)
255
- elsif @one_point
256
- # Show a circle if there's just one_point
257
- @d = DotRenderers.renderer(@dot_style).render(@d, new_x, new_y, circle_radius)
213
+ stroke_width = line_width || clip_value_if_greater_than(@columns / (store.norm_data.first.y_points.size * 4), 5.0)
214
+ circle_radius = dot_radius || clip_value_if_greater_than(@columns / (store.norm_data.first.y_points.size * 2.5), 5.0)
215
+
216
+ if !@hide_lines && prev_x && prev_y
217
+ Gruff::Renderer::Line.new(color: data_row.color, width: stroke_width)
218
+ .render(prev_x, prev_y, new_x, new_y)
258
219
  end
259
220
 
260
- unless @hide_dots
261
- @d = DotRenderers.renderer(@dot_style).render(@d, new_x, new_y, circle_radius)
221
+ if one_point || !@hide_dots
222
+ Gruff::Renderer::Dot.new(@dot_style, color: data_row.color, width: stroke_width).render(new_x, new_y, circle_radius)
262
223
  end
263
224
 
264
225
  prev_x = new_x
@@ -266,13 +227,19 @@ class Gruff::Line < Gruff::Base
266
227
  end
267
228
  end
268
229
 
269
- @d.draw(@base_image)
230
+ Gruff::Renderer.finish
270
231
  end
271
232
 
233
+ private
234
+
272
235
  def setup_data
236
+ # Update the global min/max values for the x data
237
+ @maximum_x_value ||= store.max_x
238
+ @minimum_x_value ||= store.min_x
239
+
273
240
  # Deal with horizontal reference line values that exceed the existing minimum & maximum values.
274
- possible_maximums = [@maximum_value.to_f]
275
- possible_minimums = [@minimum_value.to_f]
241
+ possible_maximums = [maximum_value.to_f]
242
+ possible_minimums = [minimum_value.to_f]
276
243
 
277
244
  @reference_lines.each_value do |curr_reference_line|
278
245
  if curr_reference_line.key?(:value)
@@ -281,37 +248,28 @@ class Gruff::Line < Gruff::Base
281
248
  end
282
249
  end
283
250
 
284
- @maximum_value = possible_maximums.max
285
- @minimum_value = possible_minimums.min
251
+ self.maximum_value = possible_maximums.max
252
+ self.minimum_value = possible_minimums.min
286
253
 
287
254
  super
288
255
  end
289
256
 
290
- def normalize(force = false)
291
- super(force)
257
+ def normalize
258
+ return unless data_given?
259
+
260
+ spread_x = @maximum_x_value.to_f - @minimum_x_value.to_f
261
+ store.normalize(minimum_x: @minimum_x_value, spread_x: spread_x, minimum_y: minimum_value, spread_y: @spread)
292
262
 
293
263
  @reference_lines.each_value do |curr_reference_line|
294
264
  # We only care about horizontal markers ... for normalization.
295
265
  # Vertical markers won't have a :value, they will have an :index
296
266
 
297
- curr_reference_line[:norm_value] = ((curr_reference_line[:value].to_f - @minimum_value) / @spread.to_f) if curr_reference_line.key?(:value)
298
- end
299
-
300
- #normalize the x data if it is specified
301
- @data.each_with_index do |data_row, index|
302
- norm_x_data_points = []
303
- if data_row[DATA_VALUES_X_INDEX] != nil
304
- data_row[DATA_VALUES_X_INDEX].each do |x_data_point|
305
- norm_x_data_points << ((x_data_point.to_f - @minimum_x_value.to_f) /
306
- (@maximum_x_value.to_f - @minimum_x_value.to_f))
307
- end
308
- @norm_data[index] << norm_x_data_points
309
- end
267
+ curr_reference_line[:norm_value] = ((curr_reference_line[:value].to_f - minimum_value) / @spread.to_f) if curr_reference_line.key?(:value)
310
268
  end
311
269
  end
312
270
 
313
271
  def sort_norm_data
314
- super unless @data.any? { |d| d[DATA_VALUES_X_INDEX] }
272
+ super unless store.data.any?(&:x_points)
315
273
  end
316
274
 
317
275
  def get_x_coord(x_data_point, width, offset)
@@ -319,46 +277,6 @@ class Gruff::Line < Gruff::Base
319
277
  end
320
278
 
321
279
  def contains_one_point_only?(data_row)
322
- # Spin through data to determine if there is just one_value present.
323
- one_point = false
324
- data_row[DATA_VALUES_INDEX].each do |data_point|
325
- unless data_point.nil?
326
- if one_point
327
- # more than one point, bail
328
- return false
329
- end
330
-
331
- # there is at least one data point
332
- one_point = true
333
- end
334
- end
335
- one_point
336
- end
337
-
338
- module DotRenderers
339
- class Circle
340
- def render(d, new_x, new_y, circle_radius)
341
- d.circle(new_x, new_y, new_x - circle_radius, new_y)
342
- end
343
- end
344
-
345
- class Square
346
- def render(d, new_x, new_y, circle_radius)
347
- offset = (circle_radius * 0.8).to_i
348
- corner_1 = new_x - offset
349
- corner_2 = new_y - offset
350
- corner_3 = new_x + offset
351
- corner_4 = new_y + offset
352
- d.rectangle(corner_1, corner_2, corner_3, corner_4)
353
- end
354
- end
355
-
356
- def self.renderer(style)
357
- if style.to_s == 'square'
358
- Square.new
359
- else
360
- Circle.new
361
- end
362
- end
280
+ data_row.y_points.compact.count == 1
363
281
  end
364
282
  end