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,20 +1,28 @@
1
+ # frozen_string_literal: true
1
2
 
2
- require File.dirname(__FILE__) + '/base'
3
+ require 'gruff/base'
3
4
 
4
5
  # Experimental!!! See also the Net graph.
5
6
  #
6
- # Submitted by Kevin Clark http://glu.ttono.us/
7
+ # Here's how to set up a Gruff::Spider.
8
+ #
9
+ # g = Gruff::Spider.new(30)
10
+ # g.title = "Spider Graph"
11
+ # g.data :Strength, [10]
12
+ # g.data :Dexterity, [16]
13
+ # g.data :Constitution, [12]
14
+ # g.data :Intelligence, [12]
15
+ # g.data :Wisdom, [10]
16
+ # g.data 'Charisma', [16]
17
+ # g.write("spider.png")
18
+
7
19
  class Gruff::Spider < Gruff::Base
8
-
9
- # Hide all text
10
- attr_reader :hide_text
11
- attr_accessor :hide_axes
12
- attr_reader :transparent_background
13
- attr_accessor :rotation
14
-
20
+ # Hide all text.
21
+ attr_writer :hide_axes
22
+ attr_writer :rotation
23
+
15
24
  def transparent_background=(value)
16
- @transparent_background = value
17
- @base_image = render_transparent_background if value
25
+ Gruff::Renderer.setup_transparent_background(@columns, @rows) if value
18
26
  end
19
27
 
20
28
  def hide_text=(value)
@@ -24,39 +32,40 @@ class Gruff::Spider < Gruff::Base
24
32
  def initialize(max_value, target_width = 800)
25
33
  super(target_width)
26
34
  @max_value = max_value
27
- @hide_legend = true;
35
+ end
36
+
37
+ def initialize_ivars
38
+ super
39
+ @hide_legend = true
40
+ @hide_axes = false
41
+ @hide_text = false
28
42
  @rotation = 0
29
43
  end
30
-
44
+ private :initialize_ivars
45
+
31
46
  def draw
32
47
  @hide_line_markers = true
33
-
48
+
34
49
  super
35
50
 
36
- return unless @has_data
51
+ return unless data_given?
37
52
 
38
53
  # Setup basic positioning
39
- diameter = @graph_height
40
54
  radius = @graph_height / 2.0
41
- top_x = @graph_left + (@graph_width - diameter) / 2.0
42
55
  center_x = @graph_left + (@graph_width / 2.0)
43
56
  center_y = @graph_top + (@graph_height / 2.0) - 25 # Move graph up a bit
44
57
 
45
58
  @unit_length = radius / @max_value
46
-
47
- total_sum = sums_for_spider
48
- prev_degrees = 0.0
49
- additive_angle = (2 * Math::PI)/ @data.size
50
-
51
- current_angle = rotation * Math::PI / 180.0
59
+
60
+ additive_angle = (2 * Math::PI) / store.length
52
61
 
53
62
  # Draw axes
54
- draw_axes(center_x, center_y, radius, additive_angle) unless hide_axes
63
+ draw_axes(center_x, center_y, radius, additive_angle) unless @hide_axes
55
64
 
56
65
  # Draw polygon
57
66
  draw_polygon(center_x, center_y, additive_angle)
58
67
 
59
- @d.draw(@base_image)
68
+ Gruff::Renderer.finish
60
69
  end
61
70
 
62
71
  private
@@ -66,42 +75,30 @@ private
66
75
  end
67
76
 
68
77
  def draw_label(center_x, center_y, angle, radius, amount)
69
- r_offset = 50 # The distance out from the center of the pie to get point
78
+ r_offset = 50 # The distance out from the center of the pie to get point
70
79
  x_offset = center_x # The label points need to be tweaked slightly
71
80
  y_offset = center_y + 0 # This one doesn't though
72
81
  x = x_offset + ((radius + r_offset) * Math.cos(angle))
73
82
  y = y_offset + ((radius + r_offset) * Math.sin(angle))
74
83
 
75
84
  # Draw label
76
- @d.fill = @marker_color
77
- @d.font = @font if @font
78
- @d.pointsize = scale_fontsize(legend_font_size)
79
- @d.stroke = 'transparent'
80
- @d.font_weight = BoldWeight
81
- @d.gravity = CenterGravity
82
- @d.annotate_scaled( @base_image,
83
- 0, 0,
84
- x, y,
85
- amount, @scale)
85
+ text_renderer = Gruff::Renderer::Text.new(amount, font: @font, size: @legend_font_size, color: @marker_color, weight: Magick::BoldWeight)
86
+ text_renderer.add_to_render_queue(0, 0, x, y, Magick::CenterGravity)
86
87
  end
87
88
 
88
89
  def draw_axes(center_x, center_y, radius, additive_angle, line_color = nil)
89
- return if hide_axes
90
+ return if @hide_axes
90
91
 
91
- current_angle = rotation * Math::PI / 180.0
92
-
93
- @data.each do |data_row|
94
- @d.stroke(line_color || data_row[DATA_COLOR_INDEX])
95
- @d.stroke_width 5.0
92
+ current_angle = @rotation * Math::PI / 180.0
96
93
 
94
+ store.data.each do |data_row|
97
95
  x_offset = radius * Math.cos(current_angle)
98
96
  y_offset = radius * Math.sin(current_angle)
99
97
 
100
- @d.line(center_x, center_y,
101
- center_x + x_offset,
102
- center_y + y_offset)
98
+ Gruff::Renderer::Line.new(color: line_color || data_row.color, width: 5.0)
99
+ .render(center_x, center_y, center_x + x_offset, center_y + y_offset)
103
100
 
104
- draw_label(center_x, center_y, current_angle, radius, data_row[DATA_LABEL_INDEX].to_s) unless hide_text
101
+ draw_label(center_x, center_y, current_angle, radius, data_row.label.to_s) unless @hide_text
105
102
 
106
103
  current_angle += additive_angle
107
104
  end
@@ -109,23 +106,18 @@ private
109
106
 
110
107
  def draw_polygon(center_x, center_y, additive_angle, color = nil)
111
108
  points = []
112
- current_angle = rotation * Math::PI / 180.0
109
+ current_angle = @rotation * Math::PI / 180.0
113
110
 
114
- @data.each do |data_row|
115
- points << center_x + normalize_points(data_row[DATA_VALUES_INDEX].first) * Math.cos(current_angle)
116
- points << center_y + normalize_points(data_row[DATA_VALUES_INDEX].first) * Math.sin(current_angle)
111
+ store.data.each do |data_row|
112
+ points << center_x + normalize_points(data_row.points.first) * Math.cos(current_angle)
113
+ points << center_y + normalize_points(data_row.points.first) * Math.sin(current_angle)
117
114
  current_angle += additive_angle
118
115
  end
119
116
 
120
- @d.stroke_width 1.0
121
- @d.stroke(color || @marker_color)
122
- @d.fill(color || @marker_color)
123
- @d.fill_opacity 0.4
124
- @d.polygon(*points)
117
+ Gruff::Renderer::Polygon.new(color: color || @marker_color, opacity: 0.4).render(points)
125
118
  end
126
119
 
127
120
  def sums_for_spider
128
- @data.inject(0.0) {|sum, data_row| sum += data_row[DATA_VALUES_INDEX].first}
121
+ store.data.reduce(0.0) { |sum, data_row| sum + data_row.points.first }
129
122
  end
130
-
131
123
  end
@@ -1,67 +1,75 @@
1
+ # frozen_string_literal: true
1
2
 
2
- require File.dirname(__FILE__) + '/base'
3
- require File.dirname(__FILE__) + '/stacked_mixin'
3
+ require 'gruff/base'
4
+ require 'gruff/helper/stacked_mixin'
4
5
 
6
+ #
7
+ # Here's how to set up a Gruff::StackedArea.
8
+ #
9
+ # g = Gruff::StackedArea.new
10
+ # g.title = 'StackedArea Graph'
11
+ # g.data :Jimmy, [25, 36, 86, 39, 25, 31, 79, 88]
12
+ # g.data :Charles, [80, 54, 67, 54, 68, 70, 90, 95]
13
+ # g.data :Julie, [22, 29, 35, 38, 36, 40, 46, 57]
14
+ # g.write('stacked_area.png')
15
+ #
5
16
  class Gruff::StackedArea < Gruff::Base
6
17
  include StackedMixin
7
- attr_accessor :last_series_goes_on_bottom
8
-
18
+ attr_writer :last_series_goes_on_bottom
19
+
20
+ def initialize_ivars
21
+ super
22
+ @last_series_goes_on_bottom = false
23
+ end
24
+ private :initialize_ivars
25
+
9
26
  def draw
10
- get_maximum_by_stack
27
+ calculate_maximum_by_stack
11
28
  super
12
29
 
13
- return unless @has_data
30
+ return unless data_given?
14
31
 
15
- @x_increment = @graph_width / (@column_count - 1).to_f
16
- @d = @d.stroke 'transparent'
32
+ x_increment = @graph_width / (column_count - 1).to_f
17
33
 
18
- height = Array.new(@column_count, 0)
34
+ height = Array.new(column_count, 0)
19
35
 
20
36
  data_points = nil
21
- iterator = last_series_goes_on_bottom ? :reverse_each : :each
22
- @norm_data.send(iterator) do |data_row|
37
+ iterator = @last_series_goes_on_bottom ? :reverse_each : :each
38
+ store.norm_data.public_send(iterator) do |data_row|
23
39
  prev_data_points = data_points
24
- data_points = Array.new
25
-
26
- @d = @d.fill data_row[DATA_COLOR_INDEX]
40
+ data_points = []
27
41
 
28
- data_row[DATA_VALUES_INDEX].each_with_index do |data_point, index|
42
+ data_row.points.each_with_index do |data_point, index|
29
43
  # Use incremented x and scaled y
30
- new_x = @graph_left + (@x_increment * index)
44
+ new_x = @graph_left + (x_increment * index)
31
45
  new_y = @graph_top + (@graph_height - data_point * @graph_height - height[index])
32
46
 
33
47
  height[index] += (data_point * @graph_height)
34
-
48
+
35
49
  data_points << new_x
36
50
  data_points << new_y
37
-
38
- draw_label(new_x, index)
39
51
 
52
+ draw_label(new_x, index)
40
53
  end
41
54
 
55
+ poly_points = data_points.dup
42
56
  if prev_data_points
43
- poly_points = data_points.dup
44
- (prev_data_points.length/2 - 1).downto(0) do |i|
45
- poly_points << prev_data_points[2*i]
46
- poly_points << prev_data_points[2*i+1]
57
+ (prev_data_points.length / 2 - 1).downto(0) do |i|
58
+ poly_points << prev_data_points[2 * i]
59
+ poly_points << prev_data_points[2 * i + 1]
47
60
  end
48
- poly_points << data_points[0]
49
- poly_points << data_points[1]
50
61
  else
51
- poly_points = data_points.dup
52
62
  poly_points << @graph_right
53
63
  poly_points << @graph_bottom - 1
54
64
  poly_points << @graph_left
55
65
  poly_points << @graph_bottom - 1
56
- poly_points << data_points[0]
57
- poly_points << data_points[1]
58
66
  end
59
- @d = @d.polyline(*poly_points)
67
+ poly_points << data_points[0]
68
+ poly_points << data_points[1]
60
69
 
70
+ Gruff::Renderer::Polygon.new(color: data_row.color).render(poly_points)
61
71
  end
62
72
 
63
- @d.draw(@base_image)
73
+ Gruff::Renderer.finish
64
74
  end
65
-
66
-
67
75
  end
@@ -1,61 +1,94 @@
1
+ # frozen_string_literal: true
1
2
 
2
- require File.dirname(__FILE__) + '/base'
3
- require File.dirname(__FILE__) + '/stacked_mixin'
3
+ require 'gruff/base'
4
+ require 'gruff/helper/stacked_mixin'
5
+ require 'gruff/helper/bar_value_label_mixin'
4
6
 
7
+ #
8
+ # Here's how to set up a Gruff::StackedBar.
9
+ #
10
+ # g = Gruff::StackedBar.new
11
+ # g.title = 'StackedBar Graph'
12
+ # g.data :Art, [0, 5, 8, 15]
13
+ # g.data :Philosophy, [10, 3, 2, 8]
14
+ # g.data :Science, [2, 15, 8, 11]
15
+ # g.write('stacked_bar.png')
16
+ #
5
17
  class Gruff::StackedBar < Gruff::Base
6
- include StackedMixin
7
-
8
- # Spacing factor applied between bars
9
- attr_accessor :bar_spacing
10
-
11
- # Number of pixels between bar segments
12
- attr_accessor :segment_spacing
13
-
14
- # Draws a bar graph, but multiple sets are stacked on top of each other.
15
- def draw
16
- get_maximum_by_stack
17
- super
18
- return unless @has_data
19
-
20
- # Setup spacing.
21
- #
22
- # Columns sit stacked.
23
- @bar_spacing ||= 0.9
24
- @segment_spacing ||= 1
25
- @bar_width = @graph_width / @column_count.to_f
26
- padding = (@bar_width * (1 - @bar_spacing)) / 2
27
-
28
- @d = @d.stroke_opacity 0.0
29
-
30
- height = Array.new(@column_count, 0)
31
-
32
- @norm_data.each_with_index do |data_row, row_index|
33
- data_row[DATA_VALUES_INDEX].each_with_index do |data_point, point_index|
34
- @d = @d.fill data_row[DATA_COLOR_INDEX]
35
-
36
- # Calculate center based on bar_width and current row
37
- label_center = @graph_left + (@bar_width * point_index) + (@bar_width * @bar_spacing / 2.0)
38
- draw_label(label_center, point_index)
39
-
40
- next if (data_point == 0)
41
- # Use incremented x and scaled y
42
- left_x = @graph_left + (@bar_width * point_index) + padding
43
- left_y = @graph_top + (@graph_height -
44
- data_point * @graph_height -
45
- height[point_index]) + @segment_spacing
46
- right_x = left_x + @bar_width * @bar_spacing
47
- right_y = @graph_top + @graph_height - height[point_index] - @segment_spacing
48
-
49
- # update the total height of the current stacked bar
50
- height[point_index] += (data_point * @graph_height )
51
-
52
- @d = @d.rectangle(left_x, left_y, right_x, right_y)
53
-
54
- end
18
+ include StackedMixin
19
+ include BarValueLabelMixin
55
20
 
21
+ # Spacing factor applied between bars.
22
+ attr_writer :bar_spacing
23
+
24
+ # Number of pixels between bar segments.
25
+ attr_writer :segment_spacing
26
+
27
+ # Set the number output format for labels using sprintf.
28
+ # Default is +"%.2f"+.
29
+ attr_writer :label_formatting
30
+
31
+ # Output the values for the bars on a bar graph.
32
+ # Default is +false+.
33
+ attr_writer :show_labels_for_bar_values
34
+
35
+ def initialize_ivars
36
+ super
37
+ @bar_spacing = 0.9
38
+ @segment_spacing = 2
39
+ @label_formatting = nil
40
+ @show_labels_for_bar_values = false
41
+ end
42
+ private :initialize_ivars
43
+
44
+ # Draws a bar graph, but multiple sets are stacked on top of each other.
45
+ def draw
46
+ calculate_maximum_by_stack
47
+ super
48
+ return unless data_given?
49
+
50
+ # Setup spacing.
51
+ #
52
+ # Columns sit stacked.
53
+ bar_width = @graph_width / column_count.to_f
54
+ padding = (bar_width * (1 - @bar_spacing)) / 2
55
+
56
+ height = Array.new(column_count, 0)
57
+ bar_value_label = BarValueLabel.new(column_count, bar_width)
58
+
59
+ store.norm_data.each_with_index do |data_row, row_index|
60
+ data_row.points.each_with_index do |data_point, point_index|
61
+ next if data_point == 0
62
+
63
+ # Use incremented x and scaled y
64
+ left_x = @graph_left + (bar_width * point_index) + padding
65
+ left_y = @graph_top + (@graph_height -
66
+ data_point * @graph_height -
67
+ height[point_index]) + @segment_spacing
68
+ right_x = left_x + bar_width * @bar_spacing
69
+ right_y = @graph_top + @graph_height - height[point_index]
70
+
71
+ # update the total height of the current stacked bar
72
+ height[point_index] += (data_point * @graph_height)
73
+
74
+ rect_renderer = Gruff::Renderer::Rectangle.new(color: data_row.color)
75
+ rect_renderer.render(left_x, left_y, right_x, right_y)
76
+
77
+ # Calculate center based on bar_width and current row
78
+ label_center = left_x + bar_width * @bar_spacing / 2.0
79
+ draw_label(label_center, point_index)
80
+
81
+ bar_value_label.coordinates[point_index] = [left_x, left_y, right_x, right_y]
82
+ bar_value_label.values[point_index] += store.data[row_index].points[point_index]
83
+ end
84
+ end
85
+
86
+ if @show_labels_for_bar_values
87
+ bar_value_label.prepare_rendering(@label_formatting) do |x, y, text|
88
+ draw_value_label(x, y, text, true)
56
89
  end
57
-
58
- @d.draw(@base_image)
59
90
  end
60
91
 
92
+ Gruff::Renderer.finish
93
+ end
61
94
  end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gruff
4
+ # @private
5
+ class Store
6
+ class BaseData < Struct.new(:label, :points, :color)
7
+ def initialize(label, points, color)
8
+ self.label = label.to_s
9
+ self.points = Array(points)
10
+ self.color = color
11
+ end
12
+
13
+ def empty?
14
+ points.empty?
15
+ end
16
+
17
+ def columns
18
+ points.length
19
+ end
20
+
21
+ def min
22
+ points.compact.min
23
+ end
24
+
25
+ def max
26
+ points.compact.max
27
+ end
28
+
29
+ def normalize(args = {})
30
+ norm_points = points.map do |point|
31
+ point.nil? ? nil : (point.to_f - args[:minimum].to_f) / args[:spread]
32
+ end
33
+
34
+ self.class.new(label, norm_points, color)
35
+ end
36
+ end
37
+ end
38
+ end